ifc-structural 0.4.0

Structural analysis model: members, connections, actions, reactions, loads.
Documentation
//! Load-group and action authoring/projection coverage, including
//! `IfcStructuralLoadGroup` semantics and dangling-reference rejection.

mod support;

use ifc_model::{EntityId, Value};
use ifc_schema::ifc4;
use ifc_structural::{ActionKind, CoordinateSystem, StructuralError, StructuralView};

use support::{enumeration, model, named, text, GUID};

#[test]
fn load_and_result_groups_expose_declared_semantics() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let load_group = model.push(named(
        schema,
        "IfcStructuralLoadGroup",
        &[
            ("GlobalId", text(GUID)),
            ("PredefinedType", enumeration("LOAD_CASE")),
            ("ActionType", enumeration("VARIABLE_Q")),
            ("ActionSource", enumeration("WIND_W")),
            ("Coefficient", Value::Real(1.35)),
            ("Purpose", text("ULS")),
        ],
    ));
    let result_group = model.push(named(
        schema,
        "IfcStructuralResultGroup",
        &[
            ("GlobalId", text("1O2Fr$t4X7Zf8NOew3FLOH")),
            ("TheoryType", enumeration("FIRST_ORDER_THEORY")),
            ("ResultForLoadGroup", Value::Ref(load_group)),
            ("IsLinear", Value::Bool(true)),
        ],
    ));

    let view = StructuralView::new(&model, schema);
    let load = view.load_group(load_group).unwrap();
    assert_eq!(load.predefined_type().unwrap(), "LOAD_CASE");
    assert_eq!(load.action_type().unwrap(), "VARIABLE_Q");
    assert_eq!(load.action_source().unwrap(), "WIND_W");
    assert_eq!(load.coefficient().unwrap(), Some(1.35));
    assert_eq!(load.purpose().unwrap(), Some("ULS"));

    let no_coefficient = model.push(named(
        schema,
        "IfcStructuralLoadGroup",
        &[
            ("GlobalId", text("0kVyZQY5P1M8Q8tb8gMCvI")),
            ("PredefinedType", enumeration("LOAD_GROUP")),
            ("ActionType", enumeration("PERMANENT_G")),
            ("ActionSource", enumeration("DEAD_LOAD_G")),
        ],
    ));
    assert_eq!(
        StructuralView::new(&model, schema)
            .load_group(no_coefficient)
            .unwrap()
            .coefficient()
            .unwrap(),
        None
    );

    let result = StructuralView::new(&model, schema)
        .result_group(result_group)
        .unwrap();
    assert_eq!(result.theory_type().unwrap(), "FIRST_ORDER_THEORY");
    assert_eq!(result.result_for_load_group().unwrap(), Some(load_group));
    assert!(result.is_linear().unwrap());
}

#[test]
fn point_action_validates_applied_load_and_coordinate_semantics() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let load = model.push(named(
        schema,
        "IfcStructuralLoadSingleForce",
        &[("ForceZ", Value::Real(-12.0))],
    ));
    let action = model.push(named(
        schema,
        "IfcStructuralPointAction",
        &[
            ("GlobalId", text(GUID)),
            ("AppliedLoad", Value::Ref(load)),
            ("GlobalOrLocal", enumeration("GLOBAL_COORDS")),
            ("DestabilizingLoad", Value::Bool(false)),
        ],
    ));

    let view = StructuralView::new(&model, schema);
    let action = view.action(action).unwrap();
    assert_eq!(action.kind(), ActionKind::Point);
    assert_eq!(action.applied_load().unwrap(), load);
    assert_eq!(
        action.coordinate_system().unwrap(),
        CoordinateSystem::Global
    );
    assert_eq!(action.destabilizing_load().unwrap(), Some(false));
    assert_eq!(action.projected_or_true().unwrap(), None);

    let curve = model.push(named(
        schema,
        "IfcStructuralCurveAction",
        &[
            ("GlobalId", text("1O2Fr$t4X7Zf8NOew3FLOH")),
            ("AppliedLoad", Value::Ref(load)),
            ("GlobalOrLocal", enumeration("LOCAL_COORDS")),
            ("ProjectedOrTrue", enumeration("TRUE_LENGTH")),
            ("PredefinedType", enumeration("CONST")),
        ],
    ));
    let curve = StructuralView::new(&model, schema).action(curve).unwrap();
    assert_eq!(curve.kind(), ActionKind::Curve);
    assert_eq!(curve.projected_or_true().unwrap(), Some("TRUE_LENGTH"));
    assert_eq!(curve.predefined_type().unwrap(), Some("CONST"));
}

#[test]
fn forged_enum_tokens_are_rejected_against_the_canonical_schema() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let group = model.push(named(
        schema,
        "IfcStructuralLoadGroup",
        &[
            ("GlobalId", text(GUID)),
            ("PredefinedType", enumeration("BOGUS")),
            ("ActionType", enumeration("PERMANENT_G")),
            ("ActionSource", enumeration("DEAD_LOAD_G")),
        ],
    ));
    assert!(StructuralView::new(&model, schema)
        .load_group(group)
        .unwrap()
        .predefined_type()
        .is_err());

    let load = model.push(named(
        schema,
        "IfcStructuralLoadLinearForce",
        &[("LinearForceX", Value::Real(1.0))],
    ));
    let action = model.push(named(
        schema,
        "IfcStructuralCurveAction",
        &[
            ("GlobalId", text(GUID)),
            ("AppliedLoad", Value::Ref(load)),
            ("GlobalOrLocal", enumeration("GLOBAL_COORDS")),
            ("ProjectedOrTrue", enumeration("BOGUS")),
            ("PredefinedType", enumeration("CONST")),
        ],
    ));
    assert!(StructuralView::new(&model, schema)
        .action(action)
        .unwrap()
        .projected_or_true()
        .is_err());
}

#[test]
fn userdefined_groups_require_object_type_in_ifc4_but_not_ifc2x3() {
    for (schema, token, must_reject) in [
        (ifc_schema::ifc2x3(), "IFC2X3", false),
        (ifc4(), "IFC4", true),
    ] {
        let mut model = model(token);
        let group = model.push(named(
            schema,
            "IfcStructuralLoadGroup",
            &[
                ("GlobalId", text(GUID)),
                ("PredefinedType", enumeration("USERDEFINED")),
                ("ActionType", enumeration("VARIABLE_Q")),
                ("ActionSource", enumeration("WIND_W")),
            ],
        ));
        let result = StructuralView::new(&model, schema)
            .load_group(group)
            .unwrap()
            .predefined_type();
        assert_eq!(result.is_err(), must_reject);
    }

    let schema = ifc4();
    let mut model = model("IFC4");
    let result = model.push(named(
        schema,
        "IfcStructuralResultGroup",
        &[
            ("GlobalId", text(GUID)),
            ("TheoryType", enumeration("USERDEFINED")),
            ("IsLinear", Value::Bool(true)),
        ],
    ));
    assert!(matches!(
        StructuralView::new(&model, schema)
            .result_group(result)
            .unwrap()
            .theory_type(),
        Err(StructuralError::SemanticViolation { .. })
    ));
}

fn curve_action_is_rejected(global_or_local: &str, projected: &str, predefined: &str) -> bool {
    let schema = ifc4();
    let mut model = model("IFC4");
    let load = model.push(named(
        schema,
        "IfcStructuralLoadLinearForce",
        &[("LinearForceX", Value::Real(1.0))],
    ));
    let action = model.push(named(
        schema,
        "IfcStructuralCurveAction",
        &[
            ("GlobalId", text(GUID)),
            ("AppliedLoad", Value::Ref(load)),
            ("GlobalOrLocal", enumeration(global_or_local)),
            ("ProjectedOrTrue", enumeration(projected)),
            ("PredefinedType", enumeration(predefined)),
        ],
    ));
    StructuralView::new(&model, schema)
        .action(action)
        .unwrap()
        .predefined_type()
        .is_err()
}

#[test]
fn projected_curve_action_requires_global_coordinates() {
    assert!(curve_action_is_rejected(
        "LOCAL_COORDS",
        "PROJECTED_LENGTH",
        "CONST"
    ));
}

#[test]
fn equidistant_curve_action_is_rejected() {
    assert!(curve_action_is_rejected(
        "GLOBAL_COORDS",
        "TRUE_LENGTH",
        "EQUIDISTANT"
    ));
}

#[test]
fn userdefined_curve_action_requires_object_type() {
    assert!(curve_action_is_rejected(
        "GLOBAL_COORDS",
        "TRUE_LENGTH",
        "USERDEFINED"
    ));
}

#[test]
fn destabilizing_load_requiredness_tracks_schema_version() {
    for (schema, token, required) in [
        (ifc_schema::ifc2x3(), "IFC2X3", true),
        (ifc4(), "IFC4", false),
    ] {
        let mut model = model(token);
        let load = model.push(named(
            schema,
            "IfcStructuralLoadSingleForce",
            &[("ForceX", Value::Real(1.0))],
        ));
        let action = model.push(named(
            schema,
            "IfcStructuralPointAction",
            &[
                ("GlobalId", text(GUID)),
                ("AppliedLoad", Value::Ref(load)),
                ("GlobalOrLocal", enumeration("GLOBAL_COORDS")),
            ],
        ));
        let result = StructuralView::new(&model, schema)
            .action(action)
            .unwrap()
            .destabilizing_load();
        if required {
            assert!(result.is_err());
        } else {
            assert_eq!(result.unwrap(), None);
        }
    }
}

#[test]
fn action_rejects_dangling_applied_load() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let action = model.push(named(
        schema,
        "IfcStructuralPointAction",
        &[
            ("GlobalId", text(GUID)),
            ("AppliedLoad", Value::Ref(EntityId(999))),
            ("GlobalOrLocal", enumeration("LOCAL_COORDS")),
        ],
    ));
    let error = StructuralView::new(&model, schema)
        .action(action)
        .unwrap()
        .applied_load()
        .unwrap_err();
    assert!(matches!(error, StructuralError::DanglingReference { .. }));
}