8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
945 lines
46 KiB
Python
945 lines
46 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""The Design Criteria section of the Panel Analysis input form.
|
||
|
||
WHY THIS FILE EXISTS: the owner rejected the old Panel Analysis form three
|
||
times running -- "太丑了" (too ugly), "你这么输入用户上哪儿能看懂" (input like
|
||
this, where would a user even understand it), "现在这个panel analysis做的也太丑
|
||
了" (even now it is still too ugly) -- and then gave the fix in one sentence:
|
||
"全部给我做成普通人能看懂的样子", make every part of it something an ordinary
|
||
person can understand. The form is being rebuilt to look like Eriksson Wall's
|
||
own printed pages, split one Eriksson section per file so eight agents can
|
||
build it at once without stepping on each other. This file owns exactly one
|
||
of those sections: **Design Criteria**, which on Eriksson's own printout is
|
||
four blocks stacked on one page -- Project Information, Material Properties,
|
||
Load Combinations, Settings -- not four separate sections. `page()` below
|
||
returns all four, in that order, as one flat block list.
|
||
|
||
`page()` NEVER RAISES. A form button that throws mid-render leaves the owner
|
||
looking at a stack trace instead of a form, which is worse than an ugly form
|
||
-- so every lookup here is defensive, and anything this module could not lay
|
||
out is named in the `problems` list it returns instead.
|
||
|
||
THE SHAPE OF THE OUTPUT is the block vocabulary the caller's generic WPF
|
||
renderer understands (`heading`, `columns`, `field`, `kv`, `sentence`,
|
||
`table`, `lc_table`, `rule`, `space`) and nothing else -- an unknown `kind` renders as
|
||
nothing, so a typo here is a silently missing block, not an error. There is
|
||
no test for "the renderer draws this correctly" (there is no renderer to run
|
||
against on host CPython, and Revit is not running for this module to check
|
||
against) -- only for "every block this function returns is a well-formed
|
||
member of that vocabulary."
|
||
|
||
WHERE THE FIELD TEXT COMES FROM. This file authors its own `label` / `symbol`
|
||
/ `hint` for every field, rather than importing them from
|
||
`vibe.panelanalysisform`. Two reasons, both load-bearing:
|
||
|
||
1. Another agent is restructuring `panelanalysisform.FIELDS` while this file
|
||
is being written, so importing text from it ties this module's stability
|
||
to a moving target -- exactly what "the block list is stable" (tested
|
||
below) is supposed to rule out.
|
||
2. Roughly half of what Eriksson's own Design Criteria page prints has NO
|
||
key in `panelanalysisform.FIELDS` at all yet (project identity, concrete
|
||
elastic modulus, deflection-limit checkboxes, stress-limit checkboxes,
|
||
resistance-factor splits, development multipliers by strand type, ...).
|
||
Those need a label and a hint regardless of whether the backing field
|
||
exists, so authoring locally is the only option that covers both cases the
|
||
same way.
|
||
|
||
`panelanalysisform` IS still consulted, but only for one yes/no fact per
|
||
field: does this key already exist in `FIELDS`. That answer decides EXISTS
|
||
vs. MISSING for the `problems` list, per the owner's instruction that a field
|
||
whose key does not exist yet degrades to an empty editor plus a named
|
||
problem, never to a vanished block. The import is wrapped in `try/except
|
||
Exception` (not just `ImportError`) because a sibling agent editing that file
|
||
right now can leave it syntactically broken for a moment, and this module
|
||
must keep rendering through that -- "could not judge whether the key exists"
|
||
degrades to "treat it as missing", the same discipline CLAUDE.md's
|
||
`same_face()` example applies to an unrecognised abbreviation.
|
||
|
||
WHY THE 3-COLUMN CONCRETE GRID AND THE REBAR/MESH/STRAND BLOCK ARE BUILT WITH
|
||
`columns`, NOT `table`. Eriksson prints both as grids, and the block
|
||
vocabulary's `table` kind was the first thing tried -- but a `table` cell is
|
||
only ever a plain string or a `{'editor': 'check', ...}` tick box; there is no
|
||
way to put a live numeric/text editor inside one. Every value in those two
|
||
grids (f′ci, f′c, E′ci, E′c, unit weight, concrete type, fy, Es, fpu, Ep) is
|
||
something the user can actually type, so they are laid out as `columns` of
|
||
`field` blocks that mirror Eriksson's rows and column groupings instead. The
|
||
Load Combinations tables used `table` for exactly that reason, back when the
|
||
only editable thing per row was the Include tick. They no longer do: the owner
|
||
ruled (2026-09-03) that the ACI factors themselves are editable and that a user
|
||
can add a combination of their own, so they emit `lc_table` -- the same grid,
|
||
but every factor cell carries a live numeric editor and the block names its
|
||
factor columns so the renderer can build an added row. The DEFAULT factors are
|
||
public ACI 318 code text (not this job's data) and now live in
|
||
`panelanalysisform.LOAD_COMBO_ROWS`, beside the keys they are stored under,
|
||
rather than in this file.
|
||
|
||
UPDATE 2026-09-04, RESISTANCE FACTORS IS THE ONE EXCEPTION TO ALL OF THAT. A
|
||
headless render of this page (WPA655) showed every phi editor squeezed to zero
|
||
width: that block was a `columns` of two sub-columns ("Developed" |
|
||
"Transferring"), each holding `field` blocks, nested inside `col_c` of the
|
||
Settings section's own three-way split -- so each phi row's `_two_col_row` (the
|
||
renderer's fixed-pixel `label* | symbol(66) | editor(138) | annot(150)` grid,
|
||
see `script.py`) needed >450px it never had once two levels of column-halving
|
||
were done to it. `_render_table`'s cell handling was generalised for `lc_table`
|
||
(any editor kind, not just a tick, keyed per cell) and that generalisation
|
||
reads for a plain `table` too -- its grid columns are ordinary (unconstrained)
|
||
`ColumnDefinition`s, not `_two_col_row`'s fixed pixels, so a cell's editor gets
|
||
real room regardless of how many columns the table has. Resistance Factors is
|
||
now a `table`: rows Bending/Compression/Shear, columns Developed/Transferring,
|
||
built by `_cell(F(key))` so the existing missing-field `problems` bookkeeping
|
||
in `_field()` still runs even though the field block itself is only used for
|
||
its `editor`/`key`/`choices`, never rendered directly -- which does mean these
|
||
five phi keys carry no Notes-panel hint any more (`_render_field` is what
|
||
files a field's hint into the notes; a bare table cell dict has no hook for
|
||
that). The Concrete / Rebar-Mesh-Strand grids above stay on `columns`
|
||
deliberately -- they are not being squeezed the way Resistance Factors was.
|
||
|
||
PSI VS. KSI. `panelanalysisform.FIELDS` keeps concrete/rebar/strand strength
|
||
keys in **psi** (`fc_psi`, `fci_psi`, `rebar_fy_ksi` is the one already in
|
||
ksi, `ps_fpu_ksi` likewise, `strand_ep_ksi` likewise -- the module is not
|
||
perfectly consistent about it, and this file does not try to fix that, only
|
||
to read it correctly field by field). Eriksson prints everything in **ksi**.
|
||
Per the assignment: keep the psi keys as the source of truth and put the ksi
|
||
reading in the field's `symbol` or `hint` instead of introducing a second,
|
||
parallel set of ksi keys nobody would keep in sync. Concretely: `fc_psi` and
|
||
`fci_psi` keep their psi keys and their `hint` says "Eriksson prints this
|
||
divided by 1000, in ksi" so the two reconcile by eye against a printed sheet;
|
||
the keys that are already ksi (`rebar_fy_ksi`, `rebar_es_ksi`, `ps_fpu_ksi`,
|
||
`strand_ep_ksi`) need no such note.
|
||
|
||
WHY EXTERIOR SOIL / INTERIOR SOIL ARE `sentence` BLOCKS, NOT `field` BLOCKS.
|
||
On Eriksson's own page these are not something the user types on THIS page at
|
||
all -- they are a computed caption ("Design contains no exterior soil loads")
|
||
that reads back whatever soil-load rows exist elsewhere in the model. Nothing
|
||
in this codebase collects soil-load rows anywhere yet, so there is no field
|
||
for this section to degrade to an "empty editor" for -- there is no editor to
|
||
have. Rendering it as a static `sentence` instead of inventing a text field
|
||
that would silently claim to collect something it cannot is the same
|
||
discipline as `knowledge.same_face()` returning `None` rather than `False`:
|
||
"cannot judge, and does not pretend to." One `problems` entry names this once
|
||
for both columns, so the gap is visible without repeating the same sentence
|
||
twice.
|
||
|
||
THE R-VALUE / MINIMUM-REINFORCEMENT PAIRS mirror Eriksson's own pattern: a
|
||
checkbox that turns a calculation on, printed beside the sentence that
|
||
applies when it is off ("R-Value not being calculated"). Both checkbox keys
|
||
are new (`design_criteria.rvalue_calc_enabled`,
|
||
`design_criteria.min_reinf_checked` -- see the naming note below) and both
|
||
degrade the same way every other missing key does: an empty `check` editor
|
||
plus a named `problems` entry, with the off-state sentence always shown
|
||
underneath since "unchecked" is this page's honest default until the field
|
||
exists to say otherwise.
|
||
|
||
NAMING STYLE FOR NEW KEYS. `panelanalysisform` already has a dotted
|
||
namespacing convention for one composite input (`stage.<stage_slug>.<opt>`,
|
||
see `stage_field_key`). New keys here that do not fit a single flat name
|
||
follow the same shape: `design_criteria.<thing>` for one-off Settings/Project
|
||
fields (e.g. `design_criteria.analysis_beta_d`), and
|
||
`load_combo.<strength|service>.<n>.include` /
|
||
`load_combo.<strength|service>.min_earth_factor` for the Load Combinations
|
||
grid, mirroring `stage_field_key`'s `<group>.<slug>.<opt>` shape exactly.
|
||
|
||
IRONPYTHON 2.7: no f-strings, no walrus, no typing syntax; string formatting
|
||
is `'{0}'.format(...)`; every literal that carries the prime character or a
|
||
non-ASCII letter is a `u'...'` literal with the coding cookie above declaring
|
||
the file UTF-8, the same convention `panelanalysisform.py` uses throughout.
|
||
Imports NOTHING beyond the standard library and, defensively,
|
||
`vibe.panelanalysisform` for key-existence checks only -- no WPF, no Revit,
|
||
no `panelsketch`, so this loads and runs on plain CPython the way
|
||
`tests/conftest.py` expects.
|
||
"""
|
||
|
||
# ------------------------------------------------------------- key existence
|
||
|
||
def _existing_keys():
|
||
"""The set of keys `panelanalysisform.FIELDS` already declares, or None.
|
||
|
||
None means "could not judge" -- the import failed, or the module's shape
|
||
changed out from under us mid-refactor -- and every caller below treats
|
||
None the same as "key not found": every field on this page degrades to
|
||
an empty editor plus a `problems` entry rather than silently assuming
|
||
the other agent's in-flight edit added the key. Broad `except Exception`
|
||
on purpose: a sibling agent saving a half-written `panelanalysisform.py`
|
||
can raise a SyntaxError, not just ImportError, and this page must keep
|
||
rendering through that.
|
||
"""
|
||
try:
|
||
from vibe import panelanalysisform as pf
|
||
return set(f['key'] for f in pf.FIELDS)
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
# ------------------------------------------------------------------- fields
|
||
#
|
||
# One entry per field this page can lay out: key -> (label, symbol, hint,
|
||
# required, editor, choices). Authored locally -- see the module docstring's
|
||
# "WHERE THE FIELD TEXT COMES FROM" for why. `required` is True only for the
|
||
# two keys `panelanalysisform` itself already gates the Run button on
|
||
# (`fc_psi`, `unit_weight_pcf`); every other field here is optional, matching
|
||
# the rest of Design Criteria in that module.
|
||
_F = {
|
||
# --------------------------------------------------- Project Information
|
||
'project_name': (
|
||
'Project name', '',
|
||
'The name of the project this panel belongs to, for the record on '
|
||
'this printout. Leave blank if the project has no name yet.',
|
||
False, 'text', None),
|
||
'client_name': (
|
||
'Client name', '',
|
||
"The client this panel's project is for. Leave blank if unknown.",
|
||
False, 'text', None),
|
||
'project_description': (
|
||
'Project description', '',
|
||
'A short free-text description of the project. Leave blank if '
|
||
'there is nothing to add.',
|
||
False, 'text', None),
|
||
'project_location': (
|
||
'Project location', '',
|
||
'Where the project is built -- city, state, or site name. Leave '
|
||
'blank if unknown.',
|
||
False, 'text', None),
|
||
'engineer_initials': (
|
||
'Engineer of record', '',
|
||
'Who is responsible for this design. Leave blank if not yet '
|
||
'assigned.',
|
||
False, 'text', None),
|
||
'project_date': (
|
||
'Design date', '',
|
||
'The date this design applies to. Leave blank to leave it unset.',
|
||
False, 'text', None),
|
||
'job_number': (
|
||
'Job number', '',
|
||
"This job's internal tracking number, if it has one. Leave blank "
|
||
'if none has been assigned yet.',
|
||
False, 'text', None),
|
||
'design_spec': (
|
||
'Governing design code', '',
|
||
'Which code edition this design is checked against. Leave blank '
|
||
'to leave it unstated.',
|
||
False, 'choice', ('ACI 318-19', 'ACI 318-14', 'ACI 318-11')),
|
||
# ------------------------------------------- Material Properties/Concrete
|
||
'fci_psi': (
|
||
'Concrete strength at release', u"f′ci (psi)",
|
||
u"How strong the concrete must be when the strand is cut or the "
|
||
u"panel is stripped from the bed -- usually lower than the 28-day "
|
||
u"strength. Eriksson prints this divided by 1000, in ksi. Leave "
|
||
u"blank if this panel is not prestressed.",
|
||
False, 'num', None),
|
||
'fc_psi': (
|
||
'Concrete strength at 28 days', u"f′c (psi)",
|
||
u"The concrete's design strength once fully cured; every stress "
|
||
u"limit on this page is a multiple of this number. Eriksson prints "
|
||
u"this divided by 1000, in ksi. Required -- the solve cannot run "
|
||
u"without it.",
|
||
True, 'num', None),
|
||
# Both moduli are DERIVED (`panelanalysisform.DERIVED_FIELDS`, ACI 318
|
||
# 19.2.2, Ec = 33 wc^1.5 sqrt(f'c)) and the page keeps them live as
|
||
# f'c / f'ci / unit weight are edited until the user types into one
|
||
# (issue #22, item 1). The old text here said a rule was still to be
|
||
# agreed; it had been in the field model since 2026-09-03. The solve
|
||
# still does not read either -- `panellift` computes its own -- and the
|
||
# hint says so, so the page mutes them honestly.
|
||
'conc_eci_psi': (
|
||
'Concrete elastic modulus at release', u"E′ci (psi)",
|
||
u"How stiff the concrete is at release strength. Computed by ACI "
|
||
u"318 from f′ci and the unit weight (Ec = 33 wc^1.5 sqrt(f′ci)) and "
|
||
u"kept up to date as you edit those; type a value to override it. "
|
||
u"Not yet used by the solve.",
|
||
False, 'num', None),
|
||
'conc_ec_psi': (
|
||
'Concrete elastic modulus, in-place', u"E′c (psi)",
|
||
u"How stiff the concrete is once fully cured. Computed by ACI 318 "
|
||
u"from f′c and the unit weight (Ec = 33 wc^1.5 sqrt(f′c)) and kept "
|
||
u"up to date as you edit those; type a value to override it. Not "
|
||
u"yet used by the solve.",
|
||
False, 'num', None),
|
||
'unit_weight_pcf': (
|
||
'Concrete unit weight', 'pcf',
|
||
"How much the concrete weighs per cubic foot; the panel's own "
|
||
'weight is computed from this. Required.',
|
||
True, 'num', None),
|
||
'concrete_type': (
|
||
'Concrete type', '',
|
||
'What kind of concrete this panel is cast from (normal weight, '
|
||
'lightweight, ...). Reference only -- nothing computes from it '
|
||
'yet.',
|
||
False, 'choice', ('Normalweight', 'Lightweight', 'Sand-Lightweight')),
|
||
# ----------------------------------------------- Material Properties/Bar
|
||
'rebar_fy_ksi': (
|
||
'Rebar yield strength', 'fy (ksi)',
|
||
'The stress at which ordinary (non-prestressed) reinforcing bar '
|
||
'starts to yield. Shown for the record; not yet used by the '
|
||
'solve.',
|
||
False, 'num', None),
|
||
'rebar_es_ksi': (
|
||
'Rebar elastic modulus', 'Es (ksi)',
|
||
'How stiff ordinary reinforcing steel is. Shown for the record; '
|
||
'not yet used by the solve.',
|
||
False, 'num', None),
|
||
'ps_fpu_ksi': (
|
||
u'Strand ultimate strength', u'fₚᵤ (ksi)',
|
||
'The breaking strength of the prestressing strand. Leave blank if '
|
||
'this panel is not prestressed.',
|
||
False, 'num', None),
|
||
'strand_ep_ksi': (
|
||
'Strand elastic modulus', 'Ep (ksi)',
|
||
'How stiff the prestressing strand is. Shown for the record; not '
|
||
'yet used by the solve.',
|
||
False, 'num', None),
|
||
# ----------------------------------------------- Material Properties/Mesh
|
||
'mesh_fy_ksi': (
|
||
'Mesh yield strength', 'fy (ksi)',
|
||
'The yield strength of the welded wire mesh. Defaults to the '
|
||
'common 80 ksi. Shown for the record; not yet used by the solve.',
|
||
False, 'num', None),
|
||
'mesh_es_ksi': (
|
||
'Mesh elastic modulus', 'Es (ksi)',
|
||
'How stiff the welded wire mesh is. Defaults to 29000 ksi. Shown '
|
||
'for the record; not yet used by the solve.',
|
||
False, 'num', None),
|
||
# -------------------------------------------------------- Load combos
|
||
'design_criteria.min_earth_factor_strength': (
|
||
'Minimum earth factor, strength combinations', '',
|
||
'The smallest lateral-earth-pressure factor applied across the '
|
||
'strength combinations below. Leave blank to use none.',
|
||
False, 'num', None),
|
||
'design_criteria.min_earth_factor_service': (
|
||
'Minimum earth factor, service combinations', '',
|
||
'The smallest lateral-earth-pressure factor applied across the '
|
||
'service combinations below. Leave blank to use none.',
|
||
False, 'num', None),
|
||
# --------------------------------------------------- Settings/Deflection
|
||
'deflection_limit': (
|
||
'Typical deflection limit', 'L / n',
|
||
'How much the panel is allowed to bow or sag, as a fraction of its '
|
||
'span, e.g. "150" for L/150. Read into the in-place service check; '
|
||
'blank leaves that check NOT JUDGED.',
|
||
False, 'text', None),
|
||
'design_criteria.deflection_add_bow': (
|
||
'Add an initial bow to reported deflections?', '',
|
||
'Whether the panel is assumed to already be slightly bowed before '
|
||
'any load is applied, and that starting bow is added to every '
|
||
'reported deflection. Leave unchecked to assume none.',
|
||
False, 'check', None),
|
||
'design_criteria.deflection_additional_bow_in': (
|
||
'Additional bow assumed', 'in',
|
||
'How much starting bow to add, if the box above is checked. Leave '
|
||
'blank for none.',
|
||
False, 'num', None),
|
||
'design_criteria.deflection_check_cantilever': (
|
||
'Check deflections on cantilevers?', '',
|
||
'Whether cantilevered portions of the panel get their own '
|
||
'deflection check, separate from the typical limit above. Leave '
|
||
'unchecked to skip it.',
|
||
False, 'check', None),
|
||
'design_criteria.deflection_cantilever_limit': (
|
||
'Cantilever deflection limit', 'L / n',
|
||
'The separate deflection limit for cantilevered portions, if the '
|
||
'box above is checked.',
|
||
False, 'choice', ('L/75', 'L/120', 'L/150')),
|
||
# ---------------------------------------------- Settings/Structural Anal.
|
||
'second_order': (
|
||
'2nd-order effects considered?', '',
|
||
'Whether P-delta / second-order bending is considered for this '
|
||
'panel. Not yet used by the solve.',
|
||
False, 'text', None),
|
||
'cracking_assumption': (
|
||
'Cracked or uncracked section assumed', '',
|
||
'Whether the stress check assumes the concrete section is cracked '
|
||
'or uncracked. Read into the in-place service check as the '
|
||
'cracking class.',
|
||
False, 'choice', ('Cracked', 'Uncracked')),
|
||
'design_criteria.analysis_beta_d': (
|
||
u'Sway condition for 2nd-order analysis', u'βd',
|
||
'Whether the 2nd-order analysis above treats this panel as braced '
|
||
'(non-sway) or unbraced (sway). Leave blank if not applicable.',
|
||
False, 'choice', ('Non-Sway', 'Sway')),
|
||
'design_criteria.cracking_method': (
|
||
'Cracking method', '',
|
||
'Which published method decides whether the section is treated as '
|
||
'cracked. Leave blank if not applicable.',
|
||
False, 'choice', ('Mast Method', 'Uncracked', 'Cracked')),
|
||
'design_criteria.cracking_limit_sqrt_fc': (
|
||
u'Cracking stress limit', u"n·√f′c",
|
||
u"The stress, as a multiple of √f′c, at which the "
|
||
u"section is assumed to crack. Leave blank to use the built-in "
|
||
u"default.",
|
||
False, 'num', None),
|
||
'design_criteria.rvalue_calc_enabled': (
|
||
'Calculate an R-value for this panel?', '',
|
||
'Whether an insulation R-value is calculated for this panel. Leave '
|
||
'unchecked if it is not being calculated -- that is the default.',
|
||
False, 'check', None),
|
||
'design_criteria.min_reinf_checked': (
|
||
'Check minimum reinforcement?', '',
|
||
'Whether the ACI minimum-steel requirement is checked for this '
|
||
'panel. Leave unchecked if it is not being checked -- that is the '
|
||
'default.',
|
||
False, 'check', None),
|
||
# -------------------------------------------------- Settings/Stress opts
|
||
'design_criteria.stress_failure_if_cracked': (
|
||
'Fail the panel if it cracks?', '',
|
||
'Whether a cracked section is treated as an outright failure '
|
||
'rather than just a flag. Leave unchecked to only flag it.',
|
||
False, 'check', None),
|
||
'design_criteria.stress_ignore_lateral_ecc': (
|
||
'Ignore lateral strand eccentricity?', '',
|
||
'Whether side-to-side strand placement is ignored when computing '
|
||
'stress. Leave unchecked to include it.',
|
||
False, 'check', None),
|
||
'design_criteria.stress_use_principal_axis': (
|
||
'Use the principal axis for stress?', '',
|
||
'Whether stress is computed about the section\'s principal axis '
|
||
'rather than its geometric one. Leave unchecked to use the '
|
||
'geometric axis.',
|
||
False, 'check', None),
|
||
# Eriksson prints these as three rows under one "In-Place Stress Limits"
|
||
# heading -- Tension (uncracked), Tension (transition / modulus of
|
||
# rupture), Compression -- and the review on 2026-09-04 (WPA655) found
|
||
# this page missing the first of the three (`inplace_tension_sqrt` is a
|
||
# real `panelanalysisform.FIELDS` key, just never surfaced here) and
|
||
# labelling the other two by what they compute rather than by Eriksson's
|
||
# own row names. All three keep the naming scheme now.
|
||
'inplace_tension_sqrt': (
|
||
u'Tension, uncracked', u"n·√f′c",
|
||
u"The concrete's tensile stress limit once erected, while the "
|
||
u"section is still assumed uncracked, as a multiple of √f′c. "
|
||
u"Leave blank to use the built-in default.",
|
||
False, 'num', None),
|
||
'design_criteria.inplace_modulus_rupture_sqrt_fc': (
|
||
u'Tension, transition (modulus of rupture)', u"n·√f′c",
|
||
u"The concrete's tensile cracking stress once erected, as a "
|
||
u"multiple of √f′c. Leave blank to use the built-in "
|
||
u"default.",
|
||
False, 'num', None),
|
||
'inplace_comp_ratio': (
|
||
u'Compression', u"n·f′c",
|
||
u"The compression limit for the panel once it is standing in "
|
||
u"place, as a fraction of f′c. Not yet used by the solve.",
|
||
False, 'num', None),
|
||
'handling_tension_sqrt': (
|
||
'Tension allowed while handling', u"n·√f′ci",
|
||
u"How much the concrete may be pulled apart during stripping / "
|
||
u"handling, as a multiple of √f′ci. Blank uses the "
|
||
u"built-in default.",
|
||
False, 'num', None),
|
||
'handling_comp_ratio': (
|
||
'Compression allowed while handling', u"n·f′ci",
|
||
u"How much the concrete may be squeezed during handling, as a "
|
||
u"fraction of f′ci. Blank uses the built-in default.",
|
||
False, 'num', None),
|
||
'design_criteria.flexure_method': (
|
||
'Flexure calculation method', '',
|
||
'Which method computes bending capacity. Leave blank to use the '
|
||
'built-in default.',
|
||
False, 'choice', ('Whitney Stress Block', 'Strain Compatibility')),
|
||
'design_criteria.flexure_ignored_end_distance_in': (
|
||
'Ignored end distance', 'in',
|
||
'A length at each end of the panel excluded from the flexure '
|
||
'check (strand not yet developed there). Leave blank for none.',
|
||
False, 'num', None),
|
||
'design_criteria.flexure_cracking_check': (
|
||
'Flexural cracking check', '',
|
||
'Which cracking check applies to the flexure calculation, if any. '
|
||
'Leave blank for none.',
|
||
False, 'choice', ('None', '1.2 Mcr', '1.33 Mu')),
|
||
# ------------------------------------------------ Settings/Resist. Fact.
|
||
'phi_flexure': (
|
||
'Bending strength reduction factor, developed', u'φ, tension',
|
||
"The ACI safety factor applied to the panel's bending strength "
|
||
'once the strand is fully developed. Blank uses the built-in '
|
||
'default (0.90).',
|
||
False, 'num', None),
|
||
'design_criteria.phi_compression': (
|
||
'Compression strength reduction factor, developed',
|
||
u'φ, compression',
|
||
"The ACI safety factor applied to the panel's compression "
|
||
'strength once the strand is fully developed. Leave blank to use '
|
||
'the built-in default.',
|
||
False, 'num', None),
|
||
'phi_shear': (
|
||
'Shear strength reduction factor, developed', u'φ, shear',
|
||
"The ACI safety factor applied to the panel's shear strength once "
|
||
'the strand is fully developed. Blank uses the built-in default '
|
||
'(0.75).',
|
||
False, 'num', None),
|
||
'design_criteria.phi_tension_transfer': (
|
||
'Bending strength reduction factor, transferring',
|
||
u'φ, tension',
|
||
'The same safety factor as above, but while the strand is still '
|
||
'transferring its force into the concrete. Leave blank if not '
|
||
'applicable.',
|
||
False, 'num', None),
|
||
'design_criteria.phi_shear_transfer': (
|
||
'Shear strength reduction factor, transferring', u'φ, shear',
|
||
'The same safety factor as above, but while the strand is still '
|
||
'transferring its force into the concrete. Leave blank if not '
|
||
'applicable.',
|
||
False, 'num', None),
|
||
'dev_mult': (
|
||
'Strand development multiplier, normal', '',
|
||
'A multiplier on the strand development length for ordinary '
|
||
'(non-debonded, uncut) strand. Blank uses the built-in default '
|
||
'(1.0).',
|
||
False, 'num', None),
|
||
'design_criteria.dev_mult_debonded': (
|
||
'Strand development multiplier, debonded', '',
|
||
'The same multiplier, for strand that is deliberately debonded '
|
||
'over part of its length. Leave blank to use the built-in '
|
||
'default.',
|
||
False, 'num', None),
|
||
'design_criteria.dev_mult_cut': (
|
||
'Strand development multiplier, cut', '',
|
||
'The same multiplier, for strand that is field-cut rather than '
|
||
'released normally. Leave blank to use the built-in default.',
|
||
False, 'num', None),
|
||
'design_criteria.dev_mult_mild': (
|
||
'Development multiplier, mild reinforcement', '',
|
||
'The same idea applied to ordinary (non-prestressed) reinforcing '
|
||
'bar. Leave blank to use the built-in default.',
|
||
False, 'num', None),
|
||
'design_criteria.shear_treat_as_column': (
|
||
'Treat this panel as a column for shear?', '',
|
||
'Whether the shear check uses column rules rather than wall '
|
||
'rules. Leave unchecked to use wall rules.',
|
||
False, 'check', None),
|
||
'design_criteria.axial_ignore_tensile_failures': (
|
||
'Ignore axial tensile-strength failures?', '',
|
||
'Whether the axial check is allowed to pass even when the '
|
||
"concrete's tensile strength alone is exceeded. Leave unchecked "
|
||
'to flag them.',
|
||
False, 'check', None),
|
||
'design_criteria.axial_ignore_buckling_failures': (
|
||
'Ignore buckling failures?', '',
|
||
'Whether the axial check is allowed to pass even when the panel '
|
||
'would buckle. Leave unchecked to flag them.',
|
||
False, 'check', None),
|
||
}
|
||
|
||
# Prestress-loss group: shown as a sentence when every one of these is blank
|
||
# (see `_group_or_sentence`), matching Eriksson's own "nothing entered here"
|
||
# style for Exterior/Interior Soil, applied to the one other Design Criteria
|
||
# group that is entirely optional and entirely empty on a fresh panel.
|
||
_PRESTRESS_LOSS_FIELDS = (
|
||
'ps_loss_method', 'ps_loss_humidity_pct',
|
||
'ps_loss_release_hours', 'ps_loss_erection_days',
|
||
)
|
||
_F['ps_loss_method'] = (
|
||
'Prestress loss method', '',
|
||
'Which published method (PCI, AASHTO, ...) the assumed loss below '
|
||
'comes from. Reference only.',
|
||
False, 'text', None)
|
||
_F['ps_loss_humidity_pct'] = (
|
||
'Ambient humidity assumed', '%',
|
||
'The relative humidity the loss method uses -- losses are larger in '
|
||
'drier air. Not yet used by the solve.',
|
||
False, 'num', None)
|
||
_F['ps_loss_release_hours'] = (
|
||
'Hours from casting to release', '',
|
||
'How many hours after casting the strand is cut / the panel is '
|
||
'stripped. Not yet used by the solve.',
|
||
False, 'num', None)
|
||
_F['ps_loss_erection_days'] = (
|
||
'Days from casting to erection', '',
|
||
'How many days after casting the panel is erected in place. Not yet '
|
||
'used by the solve.',
|
||
False, 'num', None)
|
||
|
||
|
||
def _field(key, problems, seen, existing):
|
||
"""One `field` block for `key`, plus a `problems` entry if it is missing.
|
||
|
||
`seen` de-dupes: several groups below reuse a couple of keys (none do
|
||
today, but the guard costs nothing and keeps a future edit from quietly
|
||
filing the same problem twice). `existing` is `_existing_keys()`'s
|
||
result, threaded through rather than re-read per field so one broken
|
||
import during a render is reported once, not dozens of times.
|
||
"""
|
||
spec = _F.get(key)
|
||
if spec is None:
|
||
# Programming error inside this file, not a missing form key --
|
||
# still degrade rather than raise, and say so plainly.
|
||
problems.append(
|
||
"Design Criteria has no text authored for the key '{0}' -- "
|
||
'this is a bug in pa_criteria.py, not a missing form field.'
|
||
.format(key))
|
||
return {'kind': 'field', 'key': key, 'label': key, 'symbol': None,
|
||
'hint': None, 'required': False, 'editor': 'text',
|
||
'choices': None}
|
||
label, symbol, hint, required, editor, choices = spec
|
||
if existing is None or key not in existing:
|
||
if key not in seen:
|
||
seen.add(key)
|
||
problems.append(
|
||
"'{0}' ({1}) has no backing field in panelanalysisform.FIELDS "
|
||
'yet -- shown as an empty editor until it is added.'
|
||
.format(label, key))
|
||
return {'kind': 'field', 'key': key, 'label': label,
|
||
'symbol': symbol or None, 'hint': hint, 'required': required,
|
||
'editor': editor, 'choices': list(choices) if choices else None}
|
||
|
||
|
||
def _heading(text, level):
|
||
return {'kind': 'heading', 'text': text, 'level': level}
|
||
|
||
|
||
def _cols(columns):
|
||
return {'kind': 'columns', 'columns': columns}
|
||
|
||
|
||
def _sentence(text):
|
||
return {'kind': 'sentence', 'text': text}
|
||
|
||
|
||
def _rule():
|
||
return {'kind': 'rule'}
|
||
|
||
|
||
def _space(h=1):
|
||
return {'kind': 'space', 'h': h}
|
||
|
||
|
||
def _table(headers, rows, header_groups=None):
|
||
return {'kind': 'table', 'headers': list(headers),
|
||
'header_groups': header_groups, 'rows': [list(r) for r in rows]}
|
||
|
||
|
||
def _cell(field_block):
|
||
"""A `table` cell dict for one `_field()` result -- the Resistance
|
||
Factors table (see the module docstring's 2026-09-04 update) reuses
|
||
`_field()` only for its `editor` / `key` / `choices` and the
|
||
missing-backing-field `problems` entry it already files, never for the
|
||
`field` block itself (a bare table cell has nowhere to put a label or a
|
||
hint, so those are simply not repeated here)."""
|
||
return {'editor': field_block['editor'], 'key': field_block['key'],
|
||
'choices': field_block['choices']}
|
||
|
||
|
||
def _group_or_sentence(values, keys, empty_text, build_filled):
|
||
"""[blocks] -- a `sentence` when every key in `keys` is blank, else
|
||
whatever `build_filled()` returns.
|
||
|
||
This is the "nothing here vs. never asked" split applied at group
|
||
granularity: `keys` all being blank in `values` is a real, filled-in
|
||
answer ("nothing entered"), not the absence of a form -- so it is shown
|
||
as a plain sentence instead of a wall of empty boxes, the same way
|
||
Eriksson's own printout shows a caption instead of blank inputs when
|
||
there is nothing to report. `values` is read with `.get(...)`, never
|
||
indexed, and never mutated.
|
||
|
||
`build_filled` is a zero-argument CALLABLE, not a plain list -- the
|
||
filled-in branch typically calls `F(key)`, which files a `problems`
|
||
entry for any key still missing from `panelanalysisform.FIELDS`, and
|
||
that side effect must only happen for fields actually being shown. A
|
||
plain list argument would be built eagerly by the caller before this
|
||
function ever looks at `keys`, filing a phantom "missing field" problem
|
||
for a field the empty-sentence branch never renders.
|
||
"""
|
||
for k in keys:
|
||
v = values.get(k)
|
||
if v is not None and str(v).strip():
|
||
return build_filled()
|
||
return [_sentence(empty_text)]
|
||
|
||
|
||
# --------------------------------------------------------- load combinations
|
||
#
|
||
# Standard ACI 318-14 basic strength/service combinations -- public code
|
||
# text, the same eight/ten rows any ACI-compliant tool would print, not this
|
||
# job's data.
|
||
#
|
||
# THESE ARE NOW A FALLBACK, not the source. The live copy is
|
||
# `panelanalysisform.LOAD_COMBO_ROWS`, because the factors became editable form
|
||
# VALUES and a value needs a key, which is that module's business. These stay
|
||
# because this file's whole discipline is that a sibling module mid-edit must
|
||
# not take the page down: if the import fails, the table still prints the ACI
|
||
# defaults (as plain text, with a named problem saying it cannot be edited)
|
||
# instead of vanishing. Keep the two in step -- a host test compares them.
|
||
_STRENGTH_COMBOS = (
|
||
('1', '1.4', '0', '0', '0', '0', '1', '1.6'),
|
||
('2', '1.2', '1.6', '0.5', '0', '0', '1', '1.6'),
|
||
('3', '1.2', '1', '1.6', '0', '0', '1', '1.6'),
|
||
('4', '1.2', '0', '1.6', '0.5', '0', '1', '1.6'),
|
||
('5', '1.2', '1', '0.5', '1', '0', '1', '1.6'),
|
||
('6', '1.2', '1', '0.2', '0', '1', '1', '1.6'),
|
||
('7', '0.9', '0', '0', '1', '0', '1', '1.6'),
|
||
('8', '0.9', '0', '0', '0', '1', '1', '1.6'),
|
||
)
|
||
_SERVICE_COMBOS = (
|
||
('1', '1', '0', '0', '0', '0', '0.75', '1'),
|
||
('2', '1', '1', '0', '0', '0', '0.75', '1'),
|
||
('3', '1', '0', '1', '0', '0', '0.75', '1'),
|
||
('4', '1', '0.75', '0.75', '0', '0', '0.75', '1'),
|
||
('5', '1', '0', '0', '0.6', '0', '0.75', '1'),
|
||
('6', '1', '0', '0', '0', '0.7', '0.75', '1'),
|
||
('7', '1', '0.75', '0.75', '0.45', '0', '0.75', '1'),
|
||
('8', '1', '0.75', '0.75', '0', '0.525', '0.75', '1'),
|
||
('9', '0.6', '0', '0', '0.6', '0', '0.75', '1'),
|
||
('10', '0.6', '0', '0', '0', '0.7', '0.75', '1'),
|
||
)
|
||
_COMBO_HEADERS = ['#', 'D', 'L', 'R', 'W', 'E', 'T', 'H', 'Include']
|
||
|
||
|
||
def _combo_table(name, values, problems):
|
||
"""One EDITABLE load-combination table, as an `lc_table` block.
|
||
|
||
Not a plain `table` any more. The owner ruled (2026-09-03) that the load
|
||
factors are the user's to change and that a row can be added, and a `table`
|
||
cell is only ever a string or a tick box -- so this emits `lc_table`, whose
|
||
factor cells each carry an `{'editor': 'num', 'key': ...}` the renderer
|
||
turns into a live box, plus the `factors` list the renderer's "Add LC"
|
||
button needs to build a new row's cells.
|
||
|
||
The FACTORS themselves come from `panelanalysisform.LOAD_COMBO_ROWS`, which
|
||
is the one place that knows both the ACI default and the key it is stored
|
||
under. The local tuples stay as a fallback for the case this file already
|
||
handles everywhere else -- the sibling module mid-edit and unimportable --
|
||
so a broken import degrades this page to the code defaults with no live
|
||
keys rather than to no table at all.
|
||
|
||
`values` is accepted (and read only for the user's own added rows) rather
|
||
than the row tuples the old signature took: the rows are no longer this
|
||
file's to decide.
|
||
"""
|
||
values = values if isinstance(values, dict) else {}
|
||
fallback = {'strength': _STRENGTH_COMBOS, 'service': _SERVICE_COMBOS}
|
||
factors = list(_COMBO_HEADERS[1:-1])
|
||
try:
|
||
from vibe import panelanalysisform as paf
|
||
factors = list(paf.LOAD_COMBO_FACTORS)
|
||
row_defaults = dict((r[0], r) for r in paf.LOAD_COMBO_ROWS.get(name, ()))
|
||
row_ids = paf.load_combo_row_ids(name, values)
|
||
factor_key = paf.load_combo_factor_key
|
||
include_key = paf.load_combo_include_key
|
||
except Exception:
|
||
problems.append(
|
||
'could not read panelanalysisform for the {0} load-combination '
|
||
'factors; the table below shows the ACI defaults as plain text '
|
||
'and cannot be edited until that module loads.'.format(name))
|
||
row_defaults = dict((r[0], r) for r in fallback.get(name, ()))
|
||
row_ids = [r[0] for r in fallback.get(name, ())]
|
||
|
||
def factor_key(nm, rid, col):
|
||
return 'load_combo.{0}.{1}.{2}'.format(nm, rid, col)
|
||
|
||
def include_key(nm, rid):
|
||
return 'load_combo.{0}.{1}.include'.format(nm, rid)
|
||
|
||
table_rows = []
|
||
for rid in row_ids:
|
||
cells = [str(rid)]
|
||
default = row_defaults.get(rid)
|
||
for i, col in enumerate(factors):
|
||
cells.append({'editor': 'num', 'width': 44,
|
||
'key': factor_key(name, rid, col),
|
||
'default': (default[i + 1] if default and
|
||
i + 1 < len(default) else None)})
|
||
cells.append({'editor': 'check', 'key': include_key(name, rid)})
|
||
table_rows.append(cells)
|
||
return {'kind': 'lc_table', 'name': name,
|
||
'headers': list(_COMBO_HEADERS), 'header_groups': None,
|
||
'rows': table_rows, 'factors': factors, 'row_key': None}
|
||
|
||
|
||
# --------------------------------------------------------------------- page
|
||
|
||
def page(values, reads=None):
|
||
"""(blocks, problems) for the Design Criteria section.
|
||
|
||
`values` is the form's current string-valued dict, read defensively --
|
||
every lookup goes through `.get(...)` and a key this module expects but
|
||
does not find is treated as blank, never as an error, because another
|
||
agent is restructuring `panelanalysisform.FIELDS` at the same time this
|
||
is being written. `values` is never mutated. `reads` is optional and, on
|
||
this section, only used for the one-line "Panel <mark>" context caption
|
||
at the top -- everything else on Design Criteria is job-level or
|
||
material data, not something read off the framing element.
|
||
|
||
Never raises: every lookup that could fail (the `panelanalysisform`
|
||
import, a missing dict key) degrades to a `problems` entry instead.
|
||
"""
|
||
values = values if isinstance(values, dict) else {}
|
||
reads = reads if isinstance(reads, dict) else {}
|
||
problems = []
|
||
seen = set()
|
||
existing = _existing_keys()
|
||
if existing is None:
|
||
problems.append(
|
||
'could not read panelanalysisform.FIELDS to check which keys '
|
||
'already exist (the module may be mid-edit); every field on '
|
||
'this page is shown, and is reported as missing until this is '
|
||
'confirmed.')
|
||
|
||
def F(key):
|
||
return _field(key, problems, seen, existing)
|
||
|
||
blocks = []
|
||
blocks.append(_heading('Design Criteria', 1))
|
||
|
||
mark = (reads.get('mark') or '').strip()
|
||
design_number = (reads.get('design_number') or '').strip()
|
||
if mark or design_number:
|
||
blocks.append({'kind': 'kv', 'rows': [
|
||
('Panel', mark or '(mark not read)'),
|
||
('Design number', design_number or '(not read)'),
|
||
]})
|
||
|
||
# ----------------------------------------------------- Project Information
|
||
blocks.append(_heading('Project Information', 2))
|
||
blocks.append(_cols([
|
||
[F('project_name'), F('client_name'), F('project_description')],
|
||
[F('project_location'), F('engineer_initials')],
|
||
[F('project_date'), F('job_number'), F('design_spec')],
|
||
]))
|
||
blocks.append(_rule())
|
||
|
||
# ------------------------------------------------------ Material Properties
|
||
blocks.append(_heading('Material Properties', 2))
|
||
blocks.append(_heading('Concrete', 3))
|
||
blocks.append(_cols([
|
||
[F('fci_psi'), F('fc_psi')],
|
||
[F('conc_eci_psi'), F('conc_ec_psi')],
|
||
[F('unit_weight_pcf'), F('concrete_type')],
|
||
]))
|
||
blocks.append(_space(1))
|
||
blocks.append(_cols([
|
||
[_heading('Rebar', 3), F('rebar_fy_ksi'), F('rebar_es_ksi')],
|
||
[_heading('Mesh', 3), F('mesh_fy_ksi'), F('mesh_es_ksi')],
|
||
[_heading('Strand', 3), F('ps_fpu_ksi'), F('strand_ep_ksi')],
|
||
]))
|
||
blocks.append(_rule())
|
||
|
||
# ---------------------------------------------------------- Prestress Loss
|
||
#
|
||
# Two columns, not the one wide column this used to be laid out in: a
|
||
# single-column `_cols` hands `_two_col_row`'s star label a full page
|
||
# width to stretch into, which is what the 2026-09-04 review (WPA655)
|
||
# reported as the '%' symbol on `ps_loss_humidity_pct` "floating mid-row
|
||
# far from its editor" -- the symbol and editor are still adjacent (see
|
||
# `script.py`'s `_two_col_row`), but a page-wide row puts that pair a
|
||
# long way from the label a reader's eye is anchored to. Splitting the
|
||
# four fields into two narrower columns, the same way every other group
|
||
# on this page already does, keeps the whole row -- label, symbol, and
|
||
# editor together -- inside one eye span.
|
||
blocks.append(_heading('Prestress Losses', 2))
|
||
blocks.extend(_group_or_sentence(
|
||
values, _PRESTRESS_LOSS_FIELDS,
|
||
'No prestress loss assumptions entered for this panel.',
|
||
lambda: [_cols([
|
||
[F('ps_loss_method'), F('ps_loss_humidity_pct')],
|
||
[F('ps_loss_release_hours'), F('ps_loss_erection_days')],
|
||
])]))
|
||
blocks.append(_rule())
|
||
|
||
# -------------------------------------------------------------- Soil loads
|
||
#
|
||
# `sentence`, not `field` -- see the module docstring's "WHY EXTERIOR
|
||
# SOIL / INTERIOR SOIL ARE `sentence` BLOCKS" for why there is nothing
|
||
# to degrade to an editor for here.
|
||
problems.append(
|
||
'soil loads are not collected anywhere in this form yet; Exterior '
|
||
'Soil and Interior Soil always read as "no loads" until a later '
|
||
'stage adds that input.')
|
||
blocks.append(_cols([
|
||
[_heading('Exterior Soil', 2),
|
||
_sentence('Design contains no exterior soil loads.')],
|
||
[_heading('Interior Soil', 2),
|
||
_sentence('Design contains no interior soil loads.')],
|
||
]))
|
||
blocks.append(_rule())
|
||
|
||
# ----------------------------------------------------------- Load Combos
|
||
blocks.append(_heading('Load Combinations', 1))
|
||
blocks.append(_cols([
|
||
[_heading('Strength Combinations', 2),
|
||
_combo_table('strength', values, problems),
|
||
F('design_criteria.min_earth_factor_strength')],
|
||
[_heading('Service Combinations', 2),
|
||
_combo_table('service', values, problems),
|
||
F('design_criteria.min_earth_factor_service')],
|
||
]))
|
||
blocks.append(_rule())
|
||
|
||
# --------------------------------------------------------------- Settings
|
||
blocks.append(_heading('Settings', 1))
|
||
col_a = [
|
||
_heading('Deflection Limits', 2),
|
||
F('design_criteria.deflection_add_bow'),
|
||
F('design_criteria.deflection_additional_bow_in'),
|
||
F('deflection_limit'),
|
||
F('design_criteria.deflection_check_cantilever'),
|
||
F('design_criteria.deflection_cantilever_limit'),
|
||
_heading('Structural Analysis', 2),
|
||
F('second_order'),
|
||
F('cracking_assumption'),
|
||
F('design_criteria.analysis_beta_d'),
|
||
F('design_criteria.cracking_method'),
|
||
F('design_criteria.cracking_limit_sqrt_fc'),
|
||
F('design_criteria.rvalue_calc_enabled'),
|
||
] + _group_or_sentence(
|
||
values, ('design_criteria.rvalue_calc_enabled',),
|
||
'R-value is not being calculated for this panel.', lambda: []) + [
|
||
F('design_criteria.min_reinf_checked'),
|
||
] + _group_or_sentence(
|
||
values, ('design_criteria.min_reinf_checked',),
|
||
'Minimum steel is not being checked for this panel.', lambda: [])
|
||
|
||
col_b = [
|
||
_heading('Stress Options', 2),
|
||
F('design_criteria.stress_failure_if_cracked'),
|
||
F('design_criteria.stress_ignore_lateral_ecc'),
|
||
F('design_criteria.stress_use_principal_axis'),
|
||
_heading('In-Place Stress Limits', 2),
|
||
F('inplace_tension_sqrt'),
|
||
F('design_criteria.inplace_modulus_rupture_sqrt_fc'),
|
||
F('inplace_comp_ratio'),
|
||
_heading('Handling Stress Limits', 2),
|
||
F('handling_tension_sqrt'),
|
||
F('handling_comp_ratio'),
|
||
_heading('Flexure Options', 2),
|
||
F('design_criteria.flexure_method'),
|
||
F('design_criteria.flexure_ignored_end_distance_in'),
|
||
F('design_criteria.flexure_cracking_check'),
|
||
]
|
||
|
||
col_c = [
|
||
_heading('Resistance Factors', 2),
|
||
# A `table`, not the `columns`-of-`field`s this used to be -- the
|
||
# 2026-09-04 review (WPA655) found every phi editor rendered with
|
||
# zero width. That old layout was two `columns` sub-columns
|
||
# ("Developed" | "Transferring") of `field` blocks, nested inside
|
||
# `col_c` itself one of three columns -- two levels of halving, each
|
||
# `field` needing the renderer's fixed ~450px `_two_col_row` it never
|
||
# had a fraction of. `_render_table` puts a cell's editor in an
|
||
# ordinary (unconstrained) grid column instead, so there is always
|
||
# room. See the module docstring's 2026-09-04 update for why `_cell`
|
||
# still routes every key through `F()` first (it is what keeps the
|
||
# existing "no backing field yet" `problems` bookkeeping honest).
|
||
_table(
|
||
[u'φ', 'Developed', 'Transferring'],
|
||
[
|
||
['Bending', _cell(F('phi_flexure')),
|
||
_cell(F('design_criteria.phi_tension_transfer'))],
|
||
['Compression', _cell(F('design_criteria.phi_compression')),
|
||
'Not applicable once the strand has fully transferred.'],
|
||
['Shear', _cell(F('phi_shear')),
|
||
_cell(F('design_criteria.phi_shear_transfer'))],
|
||
],
|
||
),
|
||
_heading('Development Multipliers', 2),
|
||
F('dev_mult'),
|
||
F('design_criteria.dev_mult_debonded'),
|
||
F('design_criteria.dev_mult_cut'),
|
||
F('design_criteria.dev_mult_mild'),
|
||
_heading('Shear Options', 2),
|
||
F('design_criteria.shear_treat_as_column'),
|
||
_heading('Axial Options', 2),
|
||
F('design_criteria.axial_ignore_tensile_failures'),
|
||
F('design_criteria.axial_ignore_buckling_failures'),
|
||
]
|
||
|
||
blocks.append(_cols([col_a, col_b, col_c]))
|
||
|
||
return blocks, problems
|