# -*- 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..`, see `stage_field_key`). New keys here that do not fit a single flat name follow the same shape: `design_criteria.` for one-off Settings/Project fields (e.g. `design_criteria.analysis_beta_d`), and `load_combo...include` / `load_combo..min_earth_factor` for the Load Combinations grid, mirroring `stage_field_key`'s `..` 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 " 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