Validate semantics.py
Jump to navigation
Jump to search
- !/usr/bin/env python3
import math import sys from pathlib import Path from typing import Any
import yaml
def load_yaml(path: Path) -> Any:
with path.open("r", encoding="utf-8") as f:
return yaml.safe_load(f)
def is_number(value: Any) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool)
def nominal_to_actual_lumber(nominal: str) -> tuple[float, float]:
"""
Returns (actual_thickness_in, actual_depth_in)
for nominal lumber like '2x4', '2x6', etc.
"""
mapping = {
"2x2": (1.5, 1.5),
"2x3": (1.5, 2.5),
"2x4": (1.5, 3.5),
"2x6": (1.5, 5.5),
"2x8": (1.5, 7.25),
"2x10": (1.5, 9.25),
"2x12": (1.5, 11.25),
}
if nominal not in mapping:
raise ValueError(f"Unsupported nominal lumber size: {nominal}")
return mapping[nominal]
def get_param(instance: dict[str, Any], name: str) -> Any:
return instance.get("parameters", {}).get(name)
def validate_instance_semantics(instance: dict[str, Any]) -> list[str]:
errors: list[str] = []
instance_id = instance.get("id", "<unknown>")
params = instance.get("parameters", {})
if not isinstance(params, dict):
return [f"{instance_id}: parameters must be a mapping"]
width_ft = get_param(instance, "nominal_width_ft") height_ft = get_param(instance, "nominal_height_ft") stud_nominal = get_param(instance, "stud_lumber_nominal") spacing_oc_in = get_param(instance, "stud_spacing_oc_in") osb_thickness_in = get_param(instance, "osb_thickness_in") exterior_face = get_param(instance, "exterior_face") house_orientation = get_param(instance, "reference_house_orientation")
required = {
"nominal_width_ft": width_ft,
"nominal_height_ft": height_ft,
"stud_lumber_nominal": stud_nominal,
"stud_spacing_oc_in": spacing_oc_in,
"osb_thickness_in": osb_thickness_in,
"exterior_face": exterior_face,
"reference_house_orientation": house_orientation,
}
for key, value in required.items():
if value is None:
errors.append(f"{instance_id}: missing parameter '{key}'")
if errors:
return errors
if not is_number(width_ft):
errors.append(f"{instance_id}: nominal_width_ft must be numeric")
return errors
if not is_number(height_ft):
errors.append(f"{instance_id}: nominal_height_ft must be numeric")
return errors
if not is_number(spacing_oc_in):
errors.append(f"{instance_id}: stud_spacing_oc_in must be numeric")
return errors
if not is_number(osb_thickness_in):
errors.append(f"{instance_id}: osb_thickness_in must be numeric")
return errors
width_in = float(width_ft) * 12.0 height_in = float(height_ft) * 12.0 spacing_oc_in = float(spacing_oc_in)
try:
stud_thickness_in, stud_depth_in = nominal_to_actual_lumber(str(stud_nominal))
except ValueError as e:
errors.append(f"{instance_id}: {e}")
return errors
# 1. Width must at least accommodate the extreme filler case:
# two studs fastened together.
min_filler_width_in = 3.25
if width_in < min_filler_width_in:
errors.append(
f"{instance_id}: width {width_in:.3f} in is below minimum meaningful filler module width "
f"of {min_filler_width_in:.3f} in"
)
# 2. Height must be positive and practically meaningful.
if height_in <= 0:
errors.append(f"{instance_id}: height must be greater than zero")
# 3. Exterior face must be meaningful in context of house orientation.
if house_orientation == "faces_south" and exterior_face not in {"north", "south", "east", "west"}:
errors.append(
f"{instance_id}: exterior_face must be one of north/south/east/west"
)
# 4. Compute a simple stud-layout model: # Assume one stud at each end; interior studs at requested OC where possible. # # Clear space available between the two end studs: clear_span_in = width_in - 2.0 * stud_thickness_in
if clear_span_in < 0:
errors.append(
f"{instance_id}: width {width_in:.3f} in is less than two stud thicknesses "
f"({2.0 * stud_thickness_in:.3f} in)"
)
return errors
# 5. Distinguish filler modules from standard framed panels.
#
# If the width is too small to contain any interior stud bay, this is allowed,
# but it is a filler module only.
if clear_span_in < spacing_oc_in:
# Valid filler/special module.
is_filler_module = True
stud_count = 2
interior_stud_count = 0
else:
is_filler_module = False
interior_stud_count = math.floor(clear_span_in / spacing_oc_in)
stud_count = 2 + interior_stud_count
# 6. Standard framed panel sanity: if width is not tiny, require at least one meaningful bay
# when the geometry suggests it should support standard layout.
#
# This avoids nonsense wide-enough panels that somehow do not produce a sensible stud layout.
if width_in >= (2.0 * stud_thickness_in + spacing_oc_in) and stud_count < 3:
errors.append(
f"{instance_id}: width {width_in:.3f} in should support at least one interior stud "
f"at {spacing_oc_in:.1f} in OC, but derived stud count was {stud_count}"
)
# 7. Check derived overall thickness.
overall_panel_thickness_in = stud_depth_in + float(osb_thickness_in)
if overall_panel_thickness_in <= stud_depth_in:
errors.append(
f"{instance_id}: overall panel thickness computation is invalid"
)
# 8. Practical warning-level checks converted to semantic errors only where clearly nonsensical.
# A very tall but ultra-narrow wall is likely not meaningful as a standard wall module,
# but filler modules are allowed. Here we only reject widths that are between filler and
# standard wall but still fail to produce a coherent framing layout.
if not is_filler_module and stud_count < 2:
errors.append(
f"{instance_id}: derived stud count {stud_count} is not meaningful for a wall panel"
)
return errors
def main() -> int:
if len(sys.argv) != 2:
print("Usage: python3 validate_semantics.py <instances.yaml>")
return 2
instances_path = Path(sys.argv[1])
if not instances_path.exists():
print(f"Error: instances file not found: {instances_path}")
return 2
try:
instances_doc = load_yaml(instances_path)
except yaml.YAMLError as e:
print(f"YAML parse error: {e}")
return 2
except Exception as e:
print(f"File load error: {e}")
return 2
if not isinstance(instances_doc, dict):
print("Instances file root must be a mapping")
return 1
if "instances" not in instances_doc:
print("Instances file must contain top-level key 'instances'")
return 1
instances = instances_doc["instances"]
if not isinstance(instances, list):
print("Instances file 'instances' must be a list")
return 1
all_errors: list[str] = []
for idx, instance in enumerate(instances):
if not isinstance(instance, dict):
all_errors.append(f"instances[{idx}] must be a mapping")
continue
all_errors.extend(validate_instance_semantics(instance))
if all_errors:
print("Semantic validation failed:")
for err in all_errors:
print(f" - {err}")
return 1
print(f"Semantic validation passed: {len(instances)} instance(s) validated successfully.")
return 0
if __name__ == "__main__":
raise SystemExit(main())