<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.opensourceecology.org/index.php?action=history&amp;feed=atom&amp;title=Validate_semantics.py</id>
	<title>Validate semantics.py - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.opensourceecology.org/index.php?action=history&amp;feed=atom&amp;title=Validate_semantics.py"/>
	<link rel="alternate" type="text/html" href="https://wiki.opensourceecology.org/index.php?title=Validate_semantics.py&amp;action=history"/>
	<updated>2026-05-04T00:30:32Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.39.13</generator>
	<entry>
		<id>https://wiki.opensourceecology.org/index.php?title=Validate_semantics.py&amp;diff=321127&amp;oldid=prev</id>
		<title>Marcin: Created page with &quot;#!/usr/bin/env python3  import math import sys from pathlib import Path from typing import Any  import yaml   def load_yaml(path: Path) -&gt; Any:     with path.open(&quot;r&quot;, encoding=&quot;utf-8&quot;) as f:         return yaml.safe_load(f)   def is_number(value: Any) -&gt; bool:     return isinstance(value, (int, float)) and not isinstance(value, bool)   def nominal_to_actual_lumber(nominal: str) -&gt; tuple[float, float]:     &quot;&quot;&quot;     Returns (actual_thickness_in, actual_depth_in)     for no...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.opensourceecology.org/index.php?title=Validate_semantics.py&amp;diff=321127&amp;oldid=prev"/>
		<updated>2026-03-11T01:16:01Z</updated>

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