From a4e69596625527cfc03fc1e57fb5d704df22817a Mon Sep 17 00:00:00 2001 From: xywei Date: Tue, 7 Apr 2026 09:50:42 -0500 Subject: [PATCH] feat(formdsl): add DGSEM multipatch compatibility diagnostics Keep strict DGSEM multipatch behavior unchanged while adding deterministic compatibility-profile diagnostics in permissive mode. Capability checks now report explicit DGSEM diagnostic codes for reversed interface orientation usage and per-interface penalty controls when multipatch descriptors are provided. Adds regression coverage for strict/permissive DGSEM multipatch diagnostics, updates docs with the new compatibility-only diagnostic codes, and marks the OpenSpec task/plan checklist complete. Validated with: uv run pytest tests/formdsl/test_formdsl_assembly.py -k multipatch, uv run pytest tests/formdsl/test_formdsl_assembly.py, openspec validate formdsl-dgsem-multipatch-compatibility-semantics --strict, and make dev. --- ...gsem-multipatch-compatibility-semantics.md | 6 +- docs/ufl-backend-support.md | 6 ++ .../tasks.md | 8 +-- src/cutkit/formdsl/capabilities.py | 56 ++++++++++++++++ tests/formdsl/test_formdsl_assembly.py | 65 ++++++++++++++++++- 5 files changed, 133 insertions(+), 8 deletions(-) diff --git a/docs/exec-plans/active/20260407-formdsl-dgsem-multipatch-compatibility-semantics.md b/docs/exec-plans/active/20260407-formdsl-dgsem-multipatch-compatibility-semantics.md index 54f0d55..51073bd 100644 --- a/docs/exec-plans/active/20260407-formdsl-dgsem-multipatch-compatibility-semantics.md +++ b/docs/exec-plans/active/20260407-formdsl-dgsem-multipatch-compatibility-semantics.md @@ -30,9 +30,9 @@ keeping DG-SEM multipatch execution unsupported. ## Implementation Checklist - [x] Add OpenSpec proposal/design/spec/tasks artifacts. -- [ ] Implement DG-SEM compatibility diagnostics for multipatch descriptors. -- [ ] Add strict/permissive regression tests. -- [ ] Update docs and progress tracking. +- [x] Implement DG-SEM compatibility diagnostics for multipatch descriptors. +- [x] Add strict/permissive regression tests. +- [x] Update docs and progress tracking. ## Risks / Open Questions diff --git a/docs/ufl-backend-support.md b/docs/ufl-backend-support.md index 0f65830..c78afd2 100644 --- a/docs/ufl-backend-support.md +++ b/docs/ufl-backend-support.md @@ -115,6 +115,12 @@ Common diagnostic codes: - `unsupported_value_shape` - `unsupported_multipatch_interface` +DG-SEM multipatch compatibility-only diagnostics (permissive mode): + +- `dgsem_multipatch_compatibility_profile` +- `dgsem_unsupported_multipatch_orientation` +- `dgsem_unsupported_multipatch_penalty_control` + ## DG-SEM Prerequisite The `dgsem` backend requires an explicit `MeshmodeCutOverlay` payload from diff --git a/openspec/changes/formdsl-dgsem-multipatch-compatibility-semantics/tasks.md b/openspec/changes/formdsl-dgsem-multipatch-compatibility-semantics/tasks.md index 8404ac4..b1f6d45 100644 --- a/openspec/changes/formdsl-dgsem-multipatch-compatibility-semantics/tasks.md +++ b/openspec/changes/formdsl-dgsem-multipatch-compatibility-semantics/tasks.md @@ -5,12 +5,12 @@ ## 2. Capability Diagnostics -- [ ] 2.1 Keep strict unsupported behavior and add deterministic compatibility +- [x] 2.1 Keep strict unsupported behavior and add deterministic compatibility profile diagnostics in permissive mode. -- [ ] 2.2 Emit explicit DG-SEM diagnostics for orientation variants and +- [x] 2.2 Emit explicit DG-SEM diagnostics for orientation variants and per-interface penalty controls. ## 3. Validation + Docs -- [ ] 3.1 Add regression tests for strict/permissive diagnostics stability. -- [ ] 3.2 Update docs and execution-plan progress. +- [x] 3.1 Add regression tests for strict/permissive diagnostics stability. +- [x] 3.2 Update docs and execution-plan progress. diff --git a/src/cutkit/formdsl/capabilities.py b/src/cutkit/formdsl/capabilities.py index 72e62fd..00f62ba 100644 --- a/src/cutkit/formdsl/capabilities.py +++ b/src/cutkit/formdsl/capabilities.py @@ -27,6 +27,60 @@ } +def _dgsem_multipatch_compatibility_diagnostics( + form_ir: WeakFormIR, +) -> tuple[CapabilityDiagnostic, ...]: + multipatch = form_ir.multipatch + if multipatch is None: + return () + + diagnostics: list[CapabilityDiagnostic] = [ + CapabilityDiagnostic( + code="dgsem_multipatch_compatibility_profile", + backend="dgsem", + detail=( + "dgsem multipatch compatibility is diagnostics-only " + f"(patches={len(multipatch.patch_ids)}, interfaces={len(multipatch.interfaces)})" + ), + ) + ] + + reversed_count = sum( + 1 for interface in multipatch.interfaces if interface.orientation == "reversed" + ) + if reversed_count > 0: + diagnostics.append( + CapabilityDiagnostic( + code="dgsem_unsupported_multipatch_orientation", + backend="dgsem", + detail=( + "dgsem multipatch compatibility does not support " + f"reversed orientation interfaces (count={reversed_count})" + ), + alternatives=("aligned",), + ) + ) + + controlled_penalties = sum( + 1 for interface in multipatch.interfaces if interface.penalty is not None + ) + if controlled_penalties > 0: + diagnostics.append( + CapabilityDiagnostic( + code="dgsem_unsupported_multipatch_penalty_control", + backend="dgsem", + detail=( + "dgsem multipatch compatibility does not support " + "per-interface penalty controls " + f"(count={controlled_penalties})" + ), + alternatives=("multipatch_penalty",), + ) + ) + + return tuple(diagnostics) + + def _is_supported_value_shape(value_shape: tuple[int, ...], *, backend: str) -> bool: if value_shape == (): return True @@ -155,6 +209,8 @@ def check_support( alternatives=alternatives, ) ) + if backend == "dgsem": + diagnostics.extend(_dgsem_multipatch_compatibility_diagnostics(form_ir)) if strict and diagnostics: raise CapabilityError(diagnostics[0]) diff --git a/tests/formdsl/test_formdsl_assembly.py b/tests/formdsl/test_formdsl_assembly.py index 62e1a7d..de65d64 100644 --- a/tests/formdsl/test_formdsl_assembly.py +++ b/tests/formdsl/test_formdsl_assembly.py @@ -1529,7 +1529,70 @@ def test_dgsem_permissive_reports_multipatch_interface_diagnostic() -> None: strict=False, ) - assert any(d.code == "unsupported_multipatch_interface" for d in result.diagnostics) + codes = {d.code for d in result.diagnostics} + assert "unsupported_multipatch_interface" in codes + assert "dgsem_multipatch_compatibility_profile" in codes + + +def test_dgsem_permissive_reports_reversed_orientation_compatibility_diagnostic() -> ( + None +): + form = _base_form() + form["multipatch"] = { + "patch_ids": ["patch-a", "patch-b"], + "interfaces": [ + { + "plus_patch": "patch-b", + "minus_patch": "patch-a", + "plus_boundary": "right", + "minus_boundary": "top", + "orientation": "reversed", + } + ], + } + + result = assemble_form( + form, + backend="dgsem", + overlay_payload=_overlay_contract(), + strict=False, + ) + + diagnostics_by_code = {d.code: d for d in result.diagnostics} + assert "dgsem_unsupported_multipatch_orientation" in diagnostics_by_code + assert diagnostics_by_code[ + "dgsem_unsupported_multipatch_orientation" + ].alternatives == ("aligned",) + + +def test_dgsem_permissive_reports_penalty_control_compatibility_diagnostic() -> None: + form = _base_form() + form["multipatch"] = { + "patch_ids": ["patch-a", "patch-b"], + "interfaces": [ + { + "plus_patch": "patch-b", + "minus_patch": "patch-a", + "plus_boundary": "right", + "minus_boundary": "top", + "orientation": "aligned", + "penalty": 1.5, + } + ], + } + + result = assemble_form( + form, + backend="dgsem", + overlay_payload=_overlay_contract(), + strict=False, + ) + + diagnostics_by_code = {d.code: d for d in result.diagnostics} + assert "dgsem_unsupported_multipatch_penalty_control" in diagnostics_by_code + assert diagnostics_by_code[ + "dgsem_unsupported_multipatch_penalty_control" + ].alternatives == ("multipatch_penalty",) def test_iga_rejects_vector_value_shape() -> None: