Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ while preserving global fallback semantics.
## Implementation Checklist

- [x] Add OpenSpec proposal/design/spec/tasks artifacts.
- [ ] Implement descriptor and validation support for optional interface
- [x] Implement descriptor and validation support for optional interface
penalties.
- [ ] Implement IGA precedence and metadata exposure for effective penalties.
- [ ] Add regression tests and docs updates.
- [x] Implement IGA precedence and metadata exposure for effective penalties.
- [x] Add regression tests and docs updates.

## Risks / Open Questions

Expand Down
3 changes: 2 additions & 1 deletion docs/formdsl-parity-benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Common options:
- `--manifest-path <path>` for machine-readable output
- `--allow-fail` to return zero regardless of tolerance outcome
- `--include-multipatch-stress` to run deterministic IGA multipatch stress
fixtures (3-patch mixed orientations + patch-local selector overrides)
fixtures (3-patch mixed orientations + patch-local selector overrides +
per-interface penalty controls)

## What Is Reported

Expand Down
4 changes: 4 additions & 0 deletions docs/ufl-backend-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ penalty-style interface coupling matrix contributions. Optional metadata key
`multipatch_penalty` may be provided as a finite positive scalar (defaults to
`1.0`).

Each interface descriptor may also include optional `penalty` (finite positive
float). When present, interface `penalty` overrides global `multipatch_penalty`
for that interface only.

If multiple interfaces would otherwise resolve to the same selector pair,
provide patch-local selector overrides via metadata keys
`multipatch_boundary:<patch_id>:<boundary>` mapping to one of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@

## 2. Descriptor + Validation

- [ ] 2.1 Add optional per-interface penalty field to multipatch interface
- [x] 2.1 Add optional per-interface penalty field to multipatch interface
descriptors in IR/parser.
- [ ] 2.2 Add deterministic validation for malformed per-interface penalties.
- [x] 2.2 Add deterministic validation for malformed per-interface penalties.

## 3. IGA Coupling Integration

- [ ] 3.1 Apply per-interface penalty override precedence in IGA interface
- [x] 3.1 Apply per-interface penalty override precedence in IGA interface
coupling assembly.
- [ ] 3.2 Expose effective interface penalty values in lowering metadata.
- [x] 3.2 Expose effective interface penalty values in lowering metadata.

## 4. Validation + Docs

- [ ] 4.1 Add regression tests for precedence behavior and malformed values.
- [ ] 4.2 Update docs and execution-plan progress.
- [x] 4.1 Add regression tests for precedence behavior and malformed values.
- [x] 4.2 Update docs and execution-plan progress.
4 changes: 4 additions & 0 deletions src/cutkit/evals/formdsl_benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,15 @@ def multipatch_stress_fixtures() -> tuple[FormDslMultipatchStressFixture, ...]:
"plus_boundary": "marker:b-right",
"minus_boundary": "marker:a-top",
"orientation": "aligned",
"penalty": 0.75,
},
{
"plus_patch": "patch-c",
"minus_patch": "patch-a",
"plus_boundary": "marker:c-top",
"minus_boundary": "marker:a-right",
"orientation": "reversed",
"penalty": 1.5,
},
],
},
Expand Down Expand Up @@ -351,13 +353,15 @@ def multipatch_stress_fixtures() -> tuple[FormDslMultipatchStressFixture, ...]:
"plus_boundary": "marker:b-right",
"minus_boundary": "marker:a-top",
"orientation": "aligned",
"penalty": 0.75,
},
{
"plus_patch": "patch-c",
"minus_patch": "patch-a",
"plus_boundary": "marker:c-top",
"minus_boundary": "marker:a-right",
"orientation": "reversed",
"penalty": 1.5,
},
],
},
Expand Down
68 changes: 67 additions & 1 deletion src/cutkit/formdsl/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from collections.abc import Mapping
from math import isfinite
from numbers import Integral as IntegralNumber
from typing import Any

Expand Down Expand Up @@ -76,16 +77,59 @@ def _normalize_interface_boundary(raw_boundary: object, *, context: str) -> str:

def _multipatch_interface_sort_key(
descriptor: MultipatchInterfaceDescriptor,
) -> tuple[str, str, str, str, str]:
) -> tuple[str, str, str, str, str, bool, float]:
penalty = descriptor.penalty if descriptor.penalty is not None else 0.0
return (
descriptor.plus_patch,
descriptor.minus_patch,
descriptor.plus_boundary,
descriptor.minus_boundary,
descriptor.orientation,
descriptor.penalty is None,
penalty,
)


def _canonical_interface_descriptor_key(
descriptor: MultipatchInterfaceDescriptor,
) -> tuple[str, str, str, str, str]:
if descriptor.plus_patch <= descriptor.minus_patch:
return (
descriptor.plus_patch,
descriptor.minus_patch,
descriptor.plus_boundary,
descriptor.minus_boundary,
descriptor.orientation,
)
return (
descriptor.minus_patch,
descriptor.plus_patch,
descriptor.minus_boundary,
descriptor.plus_boundary,
descriptor.orientation,
)


def _normalize_interface_penalty(raw_penalty: object, *, context: str) -> float | None:
if raw_penalty is None:
return None
if isinstance(raw_penalty, bool):
raise ValueError(f"{context} penalty must be finite positive float")
if isinstance(raw_penalty, (int, float, str)):
try:
penalty = float(raw_penalty)
except ValueError as exc:
raise ValueError(
f"{context} penalty must be finite positive float"
) from exc
else:
raise ValueError(f"{context} penalty must be finite positive float")

if not isfinite(penalty) or penalty <= 0.0:
raise ValueError(f"{context} penalty must be finite positive float")
return penalty


def _parse_multipatch_descriptor(
raw_descriptor: object,
*,
Expand Down Expand Up @@ -168,6 +212,10 @@ def _parse_multipatch_descriptor(
raise ValueError(
f"{context} multipatch interfaces[{index}] orientation {orientation!r} is unsupported"
)
penalty = _normalize_interface_penalty(
raw_interface.get("penalty"),
context=f"{context} multipatch interfaces[{index}]",
)

interfaces.append(
MultipatchInterfaceDescriptor(
Expand All @@ -176,6 +224,7 @@ def _parse_multipatch_descriptor(
plus_boundary=plus_boundary,
minus_boundary=minus_boundary,
orientation=orientation,
penalty=penalty,
)
)

Expand Down Expand Up @@ -214,6 +263,7 @@ def _validate_multipatch_descriptor(
raise ValueError(f"{context} interfaces must be non-empty")

patch_id_set = set(canonical_patch_ids)
canonical_interface_keys: set[tuple[str, str, str, str, str]] = set()
for index, interface in enumerate(multipatch.interfaces):
if interface.plus_patch == interface.minus_patch:
raise ValueError(
Expand All @@ -238,6 +288,22 @@ def _validate_multipatch_descriptor(
raise ValueError(
f"{context} interfaces[{index}] orientation {interface.orientation!r} is unsupported"
)
if interface.penalty is not None and not isinstance(
interface.penalty, (int, float)
):
raise ValueError(
f"{context} interfaces[{index}] penalty must be finite positive float"
)
_normalize_interface_penalty(
interface.penalty,
context=f"{context} interfaces[{index}]",
)
interface_key = _canonical_interface_descriptor_key(interface)
if interface_key in canonical_interface_keys:
raise ValueError(
f"{context} interfaces contain duplicate canonical descriptors"
)
canonical_interface_keys.add(interface_key)

canonical_interfaces = tuple(
sorted(multipatch.interfaces, key=_multipatch_interface_sort_key)
Expand Down
21 changes: 15 additions & 6 deletions src/cutkit/formdsl/iga_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class IGAInterfaceLowering:
minus_boundary: str
orientation: str
orientation_sign: int
coupling_penalty: float = 1.0


def _term_scalar(form_ir: WeakFormIR, kind: str) -> float:
Expand Down Expand Up @@ -88,6 +89,8 @@ def _lower_multipatch_interfaces(
if multipatch is None:
return ()

default_penalty = _parse_multipatch_penalty(form_ir.metadata)

lowered: list[tuple[tuple[str, str, str, str, str], IGAInterfaceLowering]] = []
for interface in multipatch.interfaces:
key = _interface_canonical_key(interface)
Expand All @@ -101,6 +104,11 @@ def _lower_multipatch_interfaces(
minus_boundary=interface.minus_boundary,
orientation=interface.orientation,
orientation_sign=_orientation_sign(interface.orientation),
coupling_penalty=(
interface.penalty
if interface.penalty is not None
else default_penalty
),
),
)
)
Expand Down Expand Up @@ -498,7 +506,7 @@ def _paired_interface_segments(
def _add_multipatch_interface_coupling(
matrix_rows: list[dict[int, float]],
*,
form_ir: WeakFormIR,
metadata: dict[str, str],
panel: TrimmedPanel2D,
resolution: int,
spline_degree: int,
Expand All @@ -514,7 +522,6 @@ def _add_multipatch_interface_coupling(
if not interface_lowering:
return

penalty = _parse_multipatch_penalty(form_ir.metadata)
nodes_1d, weights_1d = pg.gauss_legendre_01(
max(quadrature_order, spline_degree + 1)
)
Expand All @@ -524,12 +531,12 @@ def _add_multipatch_interface_coupling(
plus_selector = _resolve_interface_selector(
patch_id=interface.plus_patch,
boundary=interface.plus_boundary,
metadata=form_ir.metadata,
metadata=metadata,
)
minus_selector = _resolve_interface_selector(
patch_id=interface.minus_patch,
boundary=interface.minus_boundary,
metadata=form_ir.metadata,
metadata=metadata,
)
selector_pair = (plus_selector, minus_selector)
if selector_pair in used_selector_pairs:
Expand Down Expand Up @@ -589,7 +596,9 @@ def _add_multipatch_interface_coupling(
nurbs_weights=nurbs_weights,
)

interface_weight = penalty * weight_1d * segment_length
interface_weight = (
interface.coupling_penalty * weight_1d * segment_length
)
for row_index, row_value, _row_gx, _row_gy in plus_terms:
row = matrix_rows[row_index]
for col_index, col_value, _col_gx, _col_gy in plus_terms:
Expand Down Expand Up @@ -824,7 +833,7 @@ def assemble_iga(

_add_multipatch_interface_coupling(
matrix_rows,
form_ir=form_ir,
metadata=form_ir.metadata,
panel=panel,
resolution=resolution,
spline_degree=spline_degree,
Expand Down
1 change: 1 addition & 0 deletions src/cutkit/formdsl/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class MultipatchInterfaceDescriptor:
plus_boundary: str
minus_boundary: str
orientation: str
penalty: float | None = None


@dataclass(frozen=True)
Expand Down
Loading
Loading