ifc-structural 0.4.0

Structural analysis model: members, connections, actions, reactions, loads.
Documentation
//! `StructuralView` projection coverage for members, connections, actions
//! and loads across IFC2X3, IFC4 and IFC4X3.

mod support;

use ifc_model::{Entity, EntityId, Value};
use ifc_schema::{ifc2x3, ifc4, ifc4x3};
use ifc_structural::{
    AnalysisModelType, ConnectionKind, LoadKind, MemberKind, StructuralError, StructuralView,
};

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

#[test]
fn selects_the_canonical_schema_from_the_file_header() {
    let model = model("IFC4X3_ADD2");
    let view = StructuralView::for_model(&model).unwrap();
    assert_eq!(view.schema().name(), "IFC4X3_ADD2");
}

#[test]
fn rejects_missing_ambiguous_and_unknown_schema_headers() {
    let empty = ifc_model::Model::new();
    assert!(matches!(
        StructuralView::for_model(&empty),
        Err(StructuralError::MissingSchema)
    ));

    let mut ambiguous = model("IFC4");
    ambiguous.header_mut().schema.push("IFC2X3".into());
    assert!(matches!(
        StructuralView::for_model(&ambiguous),
        Err(StructuralError::AmbiguousSchema { .. })
    ));

    let unknown = model("IFC5");
    assert!(matches!(
        StructuralView::for_model(&unknown),
        Err(StructuralError::UnsupportedSchema { .. })
    ));
}

#[test]
fn analysis_model_resolves_groups_and_version_specific_shared_placement() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let load = model.push(named(
        schema,
        "IfcStructuralLoadGroup",
        &[
            ("GlobalId", text(GUID)),
            ("PredefinedType", enumeration("LOAD_CASE")),
            ("ActionType", enumeration("VARIABLE_Q")),
            ("ActionSource", enumeration("LIVE_LOAD_Q")),
            ("Coefficient", Value::Real(1.5)),
        ],
    ));
    let result = model.push(named(
        schema,
        "IfcStructuralResultGroup",
        &[
            ("GlobalId", text("1O2Fr$t4X7Zf8NOew3FLOH")),
            ("TheoryType", enumeration("FIRST_ORDER_THEORY")),
            ("IsLinear", Value::Bool(true)),
        ],
    ));
    let placement = model.push(named(schema, "IfcLocalPlacement", &[]));
    let analysis = model.push(named(
        schema,
        "IfcStructuralAnalysisModel",
        &[
            ("GlobalId", text("0O2Fr$t4X7Zf8NOew3FLOH")),
            ("Name", text("Frame")),
            ("PredefinedType", enumeration("LOADING_3D")),
            ("LoadedBy", refs(&[load])),
            ("HasResults", refs(&[result])),
            ("SharedPlacement", Value::Ref(placement)),
        ],
    ));

    let projection = StructuralView::new(&model, schema)
        .analysis_model(analysis)
        .unwrap();
    assert_eq!(projection.name().unwrap(), Some("Frame"));
    assert_eq!(
        projection.predefined_type().unwrap(),
        AnalysisModelType::Loading3d
    );
    assert_eq!(projection.loaded_by().unwrap(), vec![load]);
    assert_eq!(projection.result_groups().unwrap(), vec![result]);
    assert_eq!(projection.shared_placement().unwrap(), Some(placement));
}

#[test]
fn ifc2x3_omits_shared_placement_without_shifting_other_fields() {
    let schema = ifc2x3();
    let mut model = model("IFC2X3");
    let analysis = model.push(named(
        schema,
        "IfcStructuralAnalysisModel",
        &[
            ("GlobalId", text(GUID)),
            ("OwnerHistory", Value::Ref(EntityId(99))),
            ("PredefinedType", enumeration("LOADING_3D")),
        ],
    ));
    let projection = StructuralView::new(&model, schema)
        .analysis_model(analysis)
        .unwrap();
    assert_eq!(projection.shared_placement().unwrap(), None);
}

#[test]
fn analysis_model_rejects_wrong_group_reference_types() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let wall = model.push(named(schema, "IfcWall", &[("GlobalId", text(GUID))]));
    let analysis = model.push(named(
        schema,
        "IfcStructuralAnalysisModel",
        &[
            ("GlobalId", text("1O2Fr$t4X7Zf8NOew3FLOH")),
            ("PredefinedType", enumeration("LOADING_3D")),
            ("LoadedBy", refs(&[wall])),
        ],
    ));
    let err = StructuralView::new(&model, schema)
        .analysis_model(analysis)
        .unwrap()
        .loaded_by()
        .unwrap_err();
    assert!(matches!(err, StructuralError::WrongReferenceType { .. }));
}

#[test]
fn user_defined_analysis_model_requires_nonblank_object_type() {
    let schema = ifc4x3();
    let mut model = model("IFC4X3_ADD2");
    let analysis = model.push(named(
        schema,
        "IfcStructuralAnalysisModel",
        &[
            ("GlobalId", text(GUID)),
            ("ObjectType", text("  ")),
            ("PredefinedType", enumeration("USERDEFINED")),
        ],
    ));
    assert!(matches!(
        StructuralView::new(&model, schema)
            .analysis_model(analysis)
            .unwrap()
            .predefined_type(),
        Err(StructuralError::SemanticViolation { .. })
    ));
}

#[test]
fn member_and_connection_views_resolve_schema_drift() {
    let schema = ifc4x3();
    let mut model = model("IFC4X3_ADD2");
    let axis = model.push(named(
        schema,
        "IfcDirection",
        &[(
            "DirectionRatios",
            Value::List(vec![Value::Real(1.0), Value::Real(0.0), Value::Real(0.0)]),
        )],
    ));
    let curve = model.push(named(
        schema,
        "IfcStructuralCurveMember",
        &[
            ("GlobalId", text(GUID)),
            ("PredefinedType", enumeration("RIGID_JOINED_MEMBER")),
            ("Axis", Value::Ref(axis)),
        ],
    ));
    let connection = model.push(named(
        schema,
        "IfcStructuralCurveConnection",
        &[
            ("GlobalId", text("1O2Fr$t4X7Zf8NOew3FLOH")),
            ("AxisDirection", Value::Ref(axis)),
        ],
    ));
    let view = StructuralView::new(&model, schema);
    let member = view.member(curve).unwrap();
    assert_eq!(member.kind(), MemberKind::Curve);
    assert_eq!(member.axis().unwrap(), Some(axis));
    let connection = view.connection(connection).unwrap();
    assert_eq!(connection.kind(), ConnectionKind::Curve);
    assert_eq!(connection.axis().unwrap(), Some(axis));
}

#[test]
fn member_schema_conditions_are_enforced() {
    let schema = ifc4();

    let mut missing_axis_model = model("IFC4");
    let missing_axis = missing_axis_model.push(named(
        schema,
        "IfcStructuralCurveMember",
        &[
            ("GlobalId", text(GUID)),
            ("PredefinedType", enumeration("RIGID_JOINED_MEMBER")),
        ],
    ));
    assert!(StructuralView::new(&missing_axis_model, schema)
        .member(missing_axis)
        .unwrap()
        .axis()
        .is_err());

    let mut user_defined_model = model("IFC4");
    let axis = user_defined_model.push(Entity::new(
        "IFCDIRECTION",
        vec![Value::List(vec![Value::Real(1.0), Value::Real(0.0)])],
    ));
    let user_defined = user_defined_model.push(named(
        schema,
        "IfcStructuralCurveMember",
        &[
            ("GlobalId", text(GUID)),
            ("PredefinedType", enumeration("USERDEFINED")),
            ("Axis", Value::Ref(axis)),
        ],
    ));
    assert!(matches!(
        StructuralView::new(&user_defined_model, schema)
            .member(user_defined)
            .unwrap()
            .predefined_type(),
        Err(StructuralError::SemanticViolation { .. })
    ));

    let mut thickness_model = model("IFC4");
    let surface = thickness_model.push(named(
        schema,
        "IfcStructuralSurfaceMember",
        &[
            ("GlobalId", text(GUID)),
            ("PredefinedType", enumeration("SHELL")),
            ("Thickness", Value::Real(0.0)),
        ],
    ));
    assert!(matches!(
        StructuralView::new(&thickness_model, schema)
            .member(surface)
            .unwrap()
            .thickness(),
        Err(StructuralError::SemanticViolation { .. })
    ));
}

#[test]
fn curve_connection_axis_and_numeric_measures_are_strict() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let connection = model.push(named(
        schema,
        "IfcStructuralCurveConnection",
        &[("GlobalId", text(GUID))],
    ));
    assert!(StructuralView::new(&model, schema)
        .connection(connection)
        .unwrap()
        .axis()
        .is_err());

    let load = model.push(named(
        schema,
        "IfcStructuralLoadSingleForce",
        &[("ForceX", Value::Real(f64::NAN))],
    ));
    assert!(StructuralView::new(&model, schema)
        .load(load)
        .unwrap()
        .components()
        .is_err());
}

#[test]
fn core_static_loads_expose_components_and_temperature_name_drift() {
    for (schema, token, names) in [
        (
            ifc2x3(),
            "IFC2X3",
            ["DeltaT_Constant", "DeltaT_Y", "DeltaT_Z"],
        ),
        (ifc4(), "IFC4", ["DeltaTConstant", "DeltaTY", "DeltaTZ"]),
        (
            ifc4x3(),
            "IFC4X3_ADD2",
            ["DeltaTConstant", "DeltaTY", "DeltaTZ"],
        ),
    ] {
        let mut model = model(token);
        let temperature = model.push(named(
            schema,
            "IfcStructuralLoadTemperature",
            &[
                (names[0], Value::Real(10.0)),
                (names[1], Value::Real(2.0)),
                (names[2], Value::Real(-1.0)),
            ],
        ));
        let load = StructuralView::new(&model, schema)
            .load(temperature)
            .unwrap();
        assert_eq!(load.kind(), LoadKind::Temperature);
        assert_eq!(
            load.components().unwrap(),
            vec![Some(10.0), Some(2.0), Some(-1.0)]
        );
    }
}

#[test]
fn rejects_forged_entity_type_even_if_slots_look_compatible() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let fake = model.push(Entity::new("IFCWALL", vec![Value::Null; 9]));
    assert!(matches!(
        StructuralView::new(&model, schema).member(fake),
        Err(StructuralError::WrongType { .. })
    ));
}

#[test]
fn member_predefined_type_is_required() {
    let schema = ifc4();
    let mut model = model("IFC4");
    let member = model.push(named(
        schema,
        "IfcStructuralCurveMember",
        &[("GlobalId", text(GUID))],
    ));
    let error = StructuralView::new(&model, schema)
        .member(member)
        .unwrap()
        .predefined_type()
        .unwrap_err();
    assert!(matches!(
        error,
        StructuralError::InvalidValue {
            attribute: "PredefinedType",
            ..
        }
    ));
}