WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s

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.
This commit is contained in:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
+807
View File
@@ -0,0 +1,807 @@
# -*- coding: utf-8 -*-
"""Aggressive Panel Analysis defaults: fill every blank the tool can defend.
WHY THIS EXISTS. Owner, 2026-09-04: "目标是我按一下 WPA655 的 panel analysis,
然后手动输入 loading, 剩下的它就能跑了" -- press the button, type the loading,
everything else already has a number. The form opened that day with 141 of
315 fields blank. His instruction was explicit that BLANK is the failure mode
here, not a wrong-looking guess -- every field with a defensible default gets
one, and every one of them names where it came from. Only true LOADING (floor
loads, wind speed / pressure, seismic Cs, soil) is left for the user, because
those are the numbers this tool cannot know and must not invent.
WHAT THIS MODULE OWNS. `panelanalysisform.prefill()` already fills what the
model can read plus a small `PREFILL_DEFAULTS` table of generic precast
constants (concrete type, rebar grade, mesh grade, the PCI loss-method
seeds). This module is a SECOND, later pass over the same `values` dict --
covering the much larger set of Design Criteria / Structural Model / Handling
fields that table does not touch, most of which do not even have a home in
`panelanalysisform.FIELDS` yet (they live in `pa_criteria._F` and inline in
`pa_structural.page()` instead, under the `design_criteria.*` / `load_opt_*`
/ `wind_*` / `temp_*` / `restraint_*` / `stage.<slug>.<opt>` naming this
codebase already uses). `apply_defaults()` is meant to run AFTER `prefill()`
in the button script, so the ordinary precedence rule holds without this
module doing anything special for it: a value already sitting in `values` --
a model read, a plant `panelanalysis.settings.*` pin, or a
`PREFILL_DEFAULTS` seed -- always wins, and this module only ever fills what
is STILL blank.
SOURCES CITED IN `why`. Three kinds, and every filled entry names one:
* "Eriksson Wall default (<page> page)" -- the value the owner's own
Eriksson Wall runs show for this field on a fresh panel. This is the
fallback for most of the Design Criteria / Handling settings below --
generic precast practice, not one job's data.
* an ACI 318-19 / ASCE 7-22 section number -- code text, not a guess.
* "plant knowledge <dotted.path>" -- a real value read off the merged
knowledge layers (`know`), which always outranks the generic fallback
for the handful of fields a plant is expected to pin (the handling
stage multipliers and strength, the sling angle, the strand rail
datum, `top_in_form`, `stress_ignore_lateral_ecc`, the engineer's own
initials). Nothing here reads the Revit API or a live document --
`know` is just the already-merged dict the button hands in.
WHAT NEVER GETS A DEFAULT. `ps_loss_pct` (the method computes it),
`ext_soil_loads` / `int_soil_loads` (loading), every raw `seismic_cs` /
`seismic_zone_location` field (loading -- the owner ruled on this
explicitly, 2026-09-02: "风的计算还没有你就留着空着,让用户输入"), and
`engineer_initials` when no plant value names one -- a person's identity is
not a default, the same rule CLAUDE.md states for client and job names.
`conc_loads` (the old free-text field) is gone from the form entirely --
concentrated loads are a row table now (`panelanalysisform.CONC_LOAD_TABLE`),
never defaulted, same rule. ASCE wind (`wind_speed_vult`, `wind_risk_
category`, `wind_exposure`, `wind_enclosure`, `wind_asce_version`,
`wind_cc_chapter`, `wind_mwfrs_chapter`, `wind_mean_roof_height_ft`,
`wind_effective_wind_area_ft2`, `wind_pressure_psf`, `wind_suction_psf`) is
gone from this module too -- 2026-09-04 owner ruling moved ASCE site data
out of this form into project config entirely (see
`panelanalysisform`'s module docstring, Wind section); this module never
defaulted the two real-loading fields among them anyway, and now has
nothing left to default for the rest either.
NEVER RAISES. `defaults_for` / `apply_defaults` / `why_of` / `known_keys` /
`unknown_keys` all degrade to an empty result rather than an exception --
a malformed `values` (`None`, not a dict), a malformed `know` ('garbage',
not a dict), or a sibling module mid-edit (an import failure in
`panelanalysisform` / `pa_criteria` / `pa_structural`, all still being
written the same day this file was) each narrow what gets filled instead of
stopping the whole pass. `known_keys()` follows the same "could not judge
narrows, never guesses" discipline CLAUDE.md's `knowledge.same_face()`
example sets: a module that fails to import contributes no keys, not a
guessed set.
`known_keys()` / `unknown_keys()` READ THE LIVE FORM, not a copy of its key
list. `pa_structural.page()` has no flat field table the way
`panelanalysisform.FIELDS` and `pa_criteria._F` do -- its field keys are
scattered through one long `page()` function as inline `_field(...)` calls.
Rather than hand-copy that list here (and drift the day someone edits it),
`_pa_structural_blocks()` calls `pa_structural.page({}, {})` -- documented as
never raising -- and walks the returned block tree for every `kind: 'field'`
entry, the same shape the WPF renderer itself would see. That also recovers
each field's `choices`, feeding the choice-membership check below.
CHOICE VOCABULARY. Where a key has a `choices` list anywhere in the form
(`panelanalysisform.FIELD_CHOICES`, `pa_criteria._F`'s own choices column,
`pa_structural.page()`'s inline `choices=[...]`, or the Handling stage
matrix's per-column `choices` hint in `STAGE_OPTIONS`), the default picked
below is one of those choices -- verified by
`tests/test_pa_defaults.py`, not just asserted here. Two of the owner's
literal example values did not survive that check against the real editors
and were changed; see the module-level `_DECISIONS` note near the bottom of
this file for both.
IRONPYTHON 2.7: no f-strings, `'{0}'.format(...)` throughout, this coding
cookie, no `pathlib` / `dataclasses` / typing syntax. Imports nothing but the
standard library at module scope; every `vibe.*` import is inside a function,
wrapped in `try/except Exception`, exactly so a sibling module mid-edit can
never take this one down.
"""
import datetime
# --------------------------------------------------------------- key sources
def _panelanalysisform():
try:
from vibe import panelanalysisform as paf
return paf
except Exception:
return None
def _pa_criteria():
try:
from vibe import pa_criteria as pc
return pc
except Exception:
return None
def _pa_structural_blocks():
"""[(key, editor, choices), ...] read off a live `pa_structural.page()`
call -- see the module docstring's "READ THE LIVE FORM" note for why
this walks the block tree instead of keeping a second copy of the field
list. Never raises: an import failure or a `page()` that cannot run
(both plausible while a sibling agent edits either file) returns [].
"""
try:
from vibe import pa_structural as ps
except Exception:
return []
try:
blocks, _problems = ps.page({}, {})
except Exception:
return []
out = []
def walk(items):
for b in items or []:
if not isinstance(b, dict):
continue
kind = b.get('kind')
if kind == 'field' and b.get('key'):
out.append((b.get('key'), b.get('editor'), b.get('choices')))
elif kind == 'columns':
for col in (b.get('columns') or []):
walk(col)
elif kind == 'section':
# A collapsed section (seismic / temperature / soil since
# 2026-09-04) carries its fields in `blocks`; they are still
# the form's keys.
walk(b.get('blocks'))
walk(blocks)
return out
def known_keys():
"""Every key the Panel Analysis form is known to carry today, gathered
defensively from every section module this file knows how to ask:
`panelanalysisform.FIELDS`, the Handling stage matrix
(`stage_field_keys()`), `pa_criteria._F`, and a live read of
`pa_structural.page()`'s field blocks. A module that fails to import
only narrows this set -- see the module docstring.
"""
keys = set()
paf = _panelanalysisform()
if paf is not None:
try:
keys.update(f['key'] for f in paf.FIELDS)
except Exception:
pass
try:
keys.update(paf.stage_field_keys())
except Exception:
pass
pc = _pa_criteria()
if pc is not None:
try:
keys.update(pc._F.keys())
except Exception:
pass
for key, _editor, _choices in _pa_structural_blocks():
keys.add(key)
return keys
def unknown_keys():
"""The keys this module WOULD default that `known_keys()` does not (yet)
recognise -- empty once the field-model agents have caught up. Never
raises; a lookup failure inside `known_keys()` already degrades to a
smaller set, so this can only ever over-report, never under-report, a
drift.
"""
known = known_keys()
out = []
for key in _all_default_keys():
if key not in known and key not in out:
out.append(key)
return out
# ------------------------------------------------------------- choice tables
def _choice_registry():
"""key -> tuple(choices), unioned across every source that names one.
A key with no entry here is free text (or a key nothing declares a
choice list for yet) -- `choices_for` returns None for it, and the
default picked for it is not checked against anything.
"""
out = {}
def add(key, choices):
if not choices:
return
cur = out.get(key, ())
merged = tuple(cur) + tuple(c for c in choices if c not in cur)
out[key] = merged
paf = _panelanalysisform()
if paf is not None:
try:
for key, choices in paf.FIELD_CHOICES.items():
add(key, choices)
except Exception:
pass
try:
for stage in paf.STAGES:
for opt_key, _label, _sym, _help, _kind, _drives, choices \
in paf.STAGE_OPTIONS:
add(paf.stage_field_key(stage, opt_key), choices)
except Exception:
pass
pc = _pa_criteria()
if pc is not None:
try:
for key, spec in pc._F.items():
choices = spec[5] if len(spec) > 5 else None
add(key, choices)
except Exception:
pass
for key, _editor, choices in _pa_structural_blocks():
add(key, choices)
return out
def choices_for(key):
"""The unioned choice vocabulary for `key`, or None for free text."""
return _choice_registry().get(key) or None
# ------------------------------------------------------------------ helpers
def _is_blank(value):
if value is None:
return True
try:
return value.strip() == ''
except AttributeError:
return False
def _num(text):
"""A best-effort float from whatever sits in `values`, or None."""
if text is None:
return None
try:
return float(text)
except (TypeError, ValueError):
return None
def _fmt_num(value):
"""A number as the shortest text the form shows -- '2' not '2.0'."""
if value is None:
return ''
if isinstance(value, bool):
return 'Yes' if value else 'No'
try:
if isinstance(value, int):
return str(value)
except Exception:
pass
try:
f = float(value)
except (TypeError, ValueError):
return str(value)
if f == int(f):
return str(int(f))
text = ('%.10f' % f).rstrip('0').rstrip('.')
return text
def _today_iso():
return datetime.date.today().isoformat()
def _as_dict(value):
return value if isinstance(value, dict) else {}
# ---------------------------------------------------------- static defaults
#
# key -> (value_text, why). Fixed values -- no plant lookup, no computation
# from another field -- for every Design Criteria / Structural Model /
# Handling field the owner named that does not depend on plant knowledge or
# on another field's value. See `_DYNAMIC_DEFAULTS` and `_stage_dynamic_*`
# below for the ones that do.
_ERIKSSON = 'Eriksson Wall default'
def _e(page):
return '{0} ({1} page)'.format(_ERIKSSON, page)
_STATIC_DEFAULTS = (
('design_spec', 'ACI 318-19',
'current governing code edition (ASCE 7-22 companion); the sample '
'Eriksson runs printed ACI 318-14 because they were older files'),
# ---------------------------------------------------- Design Criteria
('design_criteria.deflection_add_bow', 'No', _e('Settings')),
('design_criteria.deflection_additional_bow_in', '0', _e('Settings')),
('design_criteria.deflection_check_cantilever', 'No', _e('Settings')),
('design_criteria.deflection_cantilever_limit', 'L/75', _e('Settings')),
('design_criteria.analysis_beta_d', 'Non-Sway',
"matches the field's own choice vocabulary (Non-Sway / Sway) -- "
"Non-Sway is " + _e('Settings')),
('design_criteria.cracking_method', 'Mast Method', _e('Settings')),
('design_criteria.cracking_limit_sqrt_fc', '7.5', _e('Settings')),
('design_criteria.rvalue_calc_enabled', 'No', _e('Settings')),
('design_criteria.min_reinf_checked', 'No', _e('Settings')),
('design_criteria.stress_failure_if_cracked', 'No', _e('Settings')),
('design_criteria.stress_use_principal_axis', 'No', _e('Settings')),
('design_criteria.inplace_modulus_rupture_sqrt_fc', '12',
u"Eriksson's own \"transition\" tension limit, 12 x sqrt(f'c)"),
('design_criteria.flexure_method', 'Whitney Stress Block',
_e('Settings')),
('design_criteria.flexure_ignored_end_distance_in', '0',
_e('Settings')),
('design_criteria.flexure_cracking_check', 'None', _e('Settings')),
('design_criteria.phi_compression', '0.65', "ACI 318-19 Sec. 21.2"),
('design_criteria.phi_tension_transfer', '0.75', "ACI 318-19 Sec. 21.2"),
('design_criteria.phi_shear_transfer', '0.75', "ACI 318-19 Sec. 21.2"),
('design_criteria.dev_mult_debonded', '2', _e('Settings')),
('design_criteria.dev_mult_cut', '1.3', _e('Settings')),
('design_criteria.dev_mult_mild', '1.3', _e('Settings')),
('dev_mult', '1.3', _e('Settings') + ' -- strand, normal'),
('design_criteria.shear_treat_as_column', 'No', _e('Settings')),
('design_criteria.axial_ignore_tensile_failures', 'No', _e('Settings')),
('design_criteria.axial_ignore_buckling_failures', 'No', _e('Settings')),
('design_criteria.min_earth_factor_strength', '0', _e('Settings')),
('design_criteria.min_earth_factor_service', '0', _e('Settings')),
('second_order', 'Yes', _e('Settings') + ' -- P-Delta'),
('deflection_limit', 'L/150', _e('Settings')),
# ------------------------------------------------------- Load Options
('self_weight_factor', '1', _e('Settings')),
('load_opt_selfweight_ecc_in', '0', _e('Settings')),
('load_opt_offset_from', 'Deflection Section',
'Eriksson calls this the Loading Datum; the sample runs measure '
'point-load offsets from the Deflection Section'),
('load_opt_pattern_live', 'No',
_e('Settings') + ' -- Pattern Live Loads for Flexure is off in the '
'sample runs'),
# -------------------------------------------------------- Temperature
('temp_ext_f', '75', _e('Settings')),
('temp_int_f', '75', _e('Settings')),
('temp_elasticity_mult', '0.5', _e('Settings')),
('temp_alpha_t', '5.5',
u'5.5 × 10⁻⁶ /°F (ACI 209 / PCI), entered as the '
u'×10⁻⁶ mantissa the field asks for'),
# Wind is GONE from this module -- 2026-09-04 owner ruling moved every
# ASCE site-data field (Vult, risk/exposure/enclosure, code version,
# C&C/MWFRS chapter, mean roof height, effective wind area) out of this
# form into project config; see `panelanalysisform`'s module docstring.
# `wind_mode` (the one wind field left) defaults on the form side, in
# `panelanalysisform.prefill` itself, not here.
# ---------------------------------------------------------- Restraints
# ------------------------------------------------------------ Seismic
# 'seismic_apply_to' picks WHAT the (still-blank, user-typed) seismic
# coefficient multiplies -- not a magnitude itself, so it is not the
# loading the owner ruled must stay blank. Its editor carries an
# explicit 'None' choice now, matched here rather than left blank.
('seismic_apply_to', 'None',
"matches the field's own choice vocabulary (None / Out-of-plane / "
'In-plane / Both); ' + _e('Structural Model') + ' -- no seismic '
'multiplier applied in the sample runs'),
# ------------------------------------------------- Prestressing Strands
('strand_rail_offset_in', '0', _e('Reinforcement')),
# -------------------------------------------------------------- Extents
('left_miter', 'No', _e('Concrete Extents')),
('right_miter', 'No', _e('Concrete Extents')),
)
def _all_static_keys():
return [key for key, _v, _w in _STATIC_DEFAULTS]
# ---------------------------------------------------------- dynamic values
#
# key -> function(values, know) -> (value_text, why) | None. None means
# "this module cannot defend a default here right now" -- the key stays
# blank, never a fabricated fallback. See the module docstring for the
# plant-knowledge paths each of these reads.
def _dyn_project_date(values, know):
return _today_iso(), "today's date, the same convention design_date uses"
def _dyn_engineer_initials(values, know):
pa = _as_dict(_as_dict(know).get('panelanalysis'))
value = pa.get('engineer_initials')
if value is None:
return None
text = str(value).strip()
if not text:
return None
return text, 'plant knowledge panelanalysis.engineer_initials'
def _dyn_stress_ignore_lateral_ecc(values, know):
pa = _as_dict(_as_dict(know).get('panelanalysis'))
settings = _as_dict(pa.get('settings'))
value = settings.get('stress_ignore_lateral_ecc')
if value is not None:
text = str(value).strip().lower()
yn = 'No' if text in ('', 'no', 'none', 'false', '0', 'off') \
else 'Yes'
return yn, ('plant knowledge '
'panelanalysis.settings.stress_ignore_lateral_ecc')
return 'No', _e('Settings')
def _dyn_top_in_form(values, know):
pa = _as_dict(_as_dict(know).get('panelanalysis'))
value = pa.get('top_in_form')
if value is not None:
text = str(value).strip()
if text:
for choice in ('Interior', 'Exterior'):
if text.lower() == choice.lower():
text = choice
break
return text, 'plant knowledge panelanalysis.top_in_form'
return 'Interior', _e('Concrete Extents')
_RAIL_LABELS = {
'left_rail': 'Left edge',
'right_rail': 'Right edge',
'centerline': 'Centerline',
}
def _dyn_strand_rail_location(values, know):
rails = _as_dict(_as_dict(know).get('rails'))
datum = rails.get('strand_datum')
if datum is not None:
label = _RAIL_LABELS.get(str(datum).strip().lower())
if label:
return label, 'plant knowledge rails.strand_datum'
return 'Left edge', _e('Reinforcement')
_DYNAMIC_DEFAULTS = (
('project_date', _dyn_project_date),
('engineer_initials', _dyn_engineer_initials),
('design_criteria.stress_ignore_lateral_ecc',
_dyn_stress_ignore_lateral_ecc),
('top_in_form', _dyn_top_in_form),
('strand_rail_location', _dyn_strand_rail_location),
# `wind_effective_wind_area_ft2` / `wind_mean_roof_height_ft` are GONE --
# see the module docstring's Wind note; both fields left the form.
)
# --------------------------------------------------------- handling stages
#
# `panelanalysisform.stage_field_key(stage, opt)` builds
# 'stage.<slug>.<opt>' from the stage NAME, not the slug -- but this module
# hardcodes the slugs too (matching `_stage_slug`'s
# `name.lower().replace(' ', '_')` exactly) rather than importing the form
# at module scope, so a sibling agent mid-edit on `panelanalysisform.py`
# cannot take the whole default table down with it. `known_keys()` still
# checks these against the live `stage_field_keys()` output, so any drift
# between the hardcoded slug and the real one shows up as an `unknown_keys()`
# entry rather than silently mis-keying the form.
_STAGES = ('Stripping', 'Transportation', 'Primary Erection',
'Secondary Erection')
_STAGE_SLUGS = {
'Stripping': 'stripping',
'Transportation': 'transportation',
'Primary Erection': 'primary_erection',
'Secondary Erection': 'secondary_erection',
}
_STAGE_PLANT_NAME = {
'Stripping': 'stripping',
'Transportation': 'transportation',
'Primary Erection': 'erection',
'Secondary Erection': None,
}
_STAGE_FALLBACK_MULTIPLIER = {
'Stripping': 1.4,
'Transportation': 1.5,
'Primary Erection': 1.4,
}
_STAGE_FALLBACK_STRENGTH_LABEL = {
'Stripping': 'Initial',
'Transportation': 'In-Place',
'Primary Erection': 'In-Place',
}
_STAGE_FALLBACK_MAX_TILT = {
'Stripping': 0,
'Transportation': 0,
'Primary Erection': 90,
}
_STRENGTH_LABELS = {'fci': 'Initial', 'fc': 'In-Place'}
def _stage_key(stage, opt):
return 'stage.{0}.{1}'.format(_STAGE_SLUGS[stage], opt)
def _plant_stage(know, plant_name):
if plant_name is None:
return None
handling = _as_dict(_as_dict(know).get('handling'))
stages = handling.get('stages')
if not isinstance(stages, list):
return None
for entry in stages:
if isinstance(entry, dict) and entry.get('name') == plant_name:
return entry
return None
def _plant_sling_angle(know):
handling = _as_dict(_as_dict(know).get('handling'))
value = handling.get('sling_angle_deg')
return value if value is not None else None
# Static (non-plant-dependent) cells, one set per active stage -- generated
# once into `_STATIC_DEFAULTS`-shaped tuples so they go through the exact
# same blank-check / why_of machinery as everything else. Only
# `load_multiplier`, `concrete_strength` and `sling_angle` are plant-aware
# (see `_stage_dynamic_defaults` below); everything else about a stage is
# the same Eriksson-sample answer regardless of plant.
def _stage_static_defaults():
out = []
for stage in _STAGES:
if stage == 'Secondary Erection':
out.append((
_stage_key(stage, 'include'), 'No',
_e('Handling') + ' -- secondary erection is not run for '
'this panel unless entered'))
continue
out.append((_stage_key(stage, 'include'), 'Yes', _e('Handling')))
out.append((_stage_key(stage, 'lift_type'), 'Static Lift',
_e('Handling')))
stress_limits = 'Handling' if stage == 'Stripping' else 'In-Place'
out.append((_stage_key(stage, 'stress_limits'), stress_limits,
_e('Handling')))
out.append((_stage_key(stage, 'rigging'), 'Auto', _e('Handling')))
out.append((_stage_key(stage, 'ground_reaction'), 'No',
_e('Handling')))
out.append((_stage_key(stage, 'min_tilt_deg'), '0', _e('Handling')))
out.append((_stage_key(stage, 'max_tilt_deg'),
_fmt_num(_STAGE_FALLBACK_MAX_TILT[stage]),
_e('Handling')))
out.append((_stage_key(stage, 'top_crane_carries'), 'No',
_e('Handling')))
return tuple(out)
_STAGE_STATIC_DEFAULTS = _stage_static_defaults()
def _stage_dynamic_defaults(values, know):
out = []
sling = _plant_sling_angle(know)
if sling is not None:
sling_text = _fmt_num(sling)
sling_why = 'plant knowledge handling.sling_angle_deg'
else:
sling_text = '60'
sling_why = _e('Handling')
for stage in _STAGES:
if stage == 'Secondary Erection':
continue
plant_name = _STAGE_PLANT_NAME.get(stage)
pstage = _plant_stage(know, plant_name)
multiplier = None
mult_why = _e('Handling')
if pstage is not None and pstage.get('multiplier') is not None:
multiplier = pstage.get('multiplier')
mult_why = 'plant knowledge handling.stages'
if multiplier is None:
multiplier = _STAGE_FALLBACK_MULTIPLIER[stage]
out.append((_stage_key(stage, 'load_multiplier'),
_fmt_num(multiplier), mult_why))
strength_label = _STAGE_FALLBACK_STRENGTH_LABEL[stage]
strength_why = _e('Handling')
if pstage is not None and pstage.get('strength') in \
_STRENGTH_LABELS:
strength_label = _STRENGTH_LABELS[pstage.get('strength')]
strength_why = 'plant knowledge handling.stages'
out.append((_stage_key(stage, 'concrete_strength'), strength_label,
strength_why))
out.append((_stage_key(stage, 'sling_angle'), sling_text,
sling_why))
return out
def _stage_dynamic_keys():
keys = []
for stage in _STAGES:
if stage == 'Secondary Erection':
continue
keys.append(_stage_key(stage, 'load_multiplier'))
keys.append(_stage_key(stage, 'concrete_strength'))
keys.append(_stage_key(stage, 'sling_angle'))
return keys
# --------------------------------------------------------------- the WHY map
_WHY = {}
for _key, _value, _why in _STATIC_DEFAULTS:
_WHY[_key] = _why
for _key, _value, _why in _STAGE_STATIC_DEFAULTS:
_WHY[_key] = _why
del _key, _value, _why
_WHY['project_date'] = ("today's date, the same convention design_date "
'uses')
_WHY['engineer_initials'] = (
'plant knowledge panelanalysis.engineer_initials, when the plant names '
'one -- a person\'s identity is never guessed, so this key is left '
'blank without a plant value')
_WHY['design_criteria.stress_ignore_lateral_ecc'] = (
'plant knowledge panelanalysis.settings.stress_ignore_lateral_ecc when '
'set, else ' + _e('Settings'))
_WHY['top_in_form'] = ('plant knowledge panelanalysis.top_in_form when '
'set, else ' + _e('Concrete Extents'))
_WHY['strand_rail_location'] = ('plant knowledge rails.strand_datum when '
'set, else ' + _e('Reinforcement'))
for _stage in _STAGES:
if _stage == 'Secondary Erection':
continue
_WHY[_stage_key(_stage, 'load_multiplier')] = (
'plant knowledge handling.stages when a matching stage names a '
'multiplier, else ' + _e('Handling'))
_WHY[_stage_key(_stage, 'concrete_strength')] = (
"plant knowledge handling.stages' strength field (fci -> Initial, "
'fc -> In-Place) when present, else ' + _e('Handling'))
_WHY[_stage_key(_stage, 'sling_angle')] = (
'plant knowledge handling.sling_angle_deg when set, else ' +
_e('Handling'))
del _stage
def why_of(key):
"""The source sentence for `key`'s default, or None if this module does
not know how to default that key. Independent of any particular
`values` / `know` -- see the module docstring for why: for a
plant-aware key this names the RULE ("plant knowledge X when set, else
Y"), not necessarily which branch a specific call took.
"""
try:
return _WHY.get(key)
except Exception:
return None
def _all_default_keys():
keys = list(_all_static_keys())
keys.extend(key for key, _v, _w in _STAGE_STATIC_DEFAULTS)
keys.extend(key for key, _fn in _DYNAMIC_DEFAULTS)
keys.extend(_stage_dynamic_keys())
return keys
# ------------------------------------------------------------------- the API
def _compute(values, know):
out = []
seen = set()
def emit(key, value_text, why):
if key in seen:
return
seen.add(key)
if not _is_blank(values.get(key)):
return
out.append((key, value_text, why))
for key, value_text, why in _STATIC_DEFAULTS:
emit(key, value_text, why)
for key, value_text, why in _STAGE_STATIC_DEFAULTS:
emit(key, value_text, why)
for key, fn in _DYNAMIC_DEFAULTS:
try:
result = fn(values, know)
except Exception:
result = None
if result is None:
continue
value_text, why = result
emit(key, value_text, why)
try:
stage_dyn = _stage_dynamic_defaults(values, know)
except Exception:
stage_dyn = []
for key, value_text, why in stage_dyn:
emit(key, value_text, why)
return out
def defaults_for(values, know=None):
"""[(key, value_text, why), ...] for every key whose current value in
`values` is blank ('' / None / missing) and that this module knows a
default for. `values` and `know` are read only, never mutated. Never
raises -- a `values` that is not a dict, or a `know` that is not a
dict, both degrade to []/an empty knowledge tree rather than an error.
"""
if not isinstance(values, dict):
return []
try:
return _compute(values, know if isinstance(know, dict) else {})
except Exception:
return []
def apply_defaults(values, know=None):
"""Fills the blanks `defaults_for` finds, IN PLACE on `values`, and
returns the same [(key, value_text, why), ...] it applied. A value
that is no longer blank by the time this writes it (nothing here
should cause that, but the check costs nothing) is left untouched --
this never overwrites a real value. Never raises: `values` that is not
a (mutable) dict returns [] and nothing is written.
"""
if not isinstance(values, dict):
return []
try:
found = defaults_for(values, know)
except Exception:
return []
applied = []
for key, value_text, why in found:
try:
if _is_blank(values.get(key)):
values[key] = value_text
applied.append((key, value_text, why))
except Exception:
continue
return applied
# --------------------------------------------------------------- _DECISIONS
#
# `design_criteria.analysis_beta_d` and `load_opt_offset_from` were checked
# against `pa_criteria`'s and `pa_structural`'s real editors while this file
# was being written and, at that point, neither editor's choice list
# actually contained the owner's own example value (a numeric-only editor
# for the first, `['Panel CG', 'Other']` for the second). Both editors were
# given real choice lists by another agent, concurrently, before this file's
# tests were finalised -- `design_criteria.analysis_beta_d` now offers
# `('Non-Sway', 'Sway')` and `load_opt_offset_from` now offers
# `('Deflection Section', 'Top in Form', 'Bottom in Form')` -- and both
# match the owner's original values exactly, so both are encoded as he
# specified. Left as a note because `choices_for()` reads the LIVE editors
# on every call: if either drifts again, `tests/test_pa_defaults.py::
# test_every_default_with_choices_is_a_valid_choice` fails and names the
# mismatch rather than this comment going stale silently.
#
# dev_mult / second_order Both already carry a DIFFERENT generic default
# in `panelanalysisform.PREFILL_DEFAULTS`
# ('1' and 'No', committed 2026-09-03) that runs
# unconditionally in `prefill()`, before this
# module's `apply_defaults()` would ever see the
# key -- so under the current call order neither
# of this module's defaults (1.3, 'Yes') can ever
# actually fire; `values[key]` is already
# non-blank by the time `apply_defaults` looks at
# it. Encoded as the owner specified anyway
# (2026-09-04 postdates the 2026-09-03 seed), but
# this is a live policy conflict between the two
# tables, not a bug in either -- flagged for
# whoever wires `apply_defaults()` into `prefill()`
# to resolve by call order or by retiring the
# older seed.