Skip to content

Commit 206102d

Browse files
author
Han Wang
committed
fix(dpmodel): reject an intensive/extensive composition at construction
get_intensive aggregated with all(), so a composition mixing an intensive with an extensive child quietly reported False -- the same plausible-default failure mode as the capability gaps fixed alongside it. Such a sum is not physically meaningful, so the composition must not exist at all: LinearAtomicModel.__init__ now validates that every child agrees, next to the existing mixed-type check, and raises naming the offenders. The accessor is then a plain read of an invariant rather than a vote. Regression asserts CONSTRUCTION raises, with an anti-vacuity check that agreeing children still compose and report their value.
1 parent 963b284 commit 206102d

2 files changed

Lines changed: 36 additions & 3 deletions

File tree

deepmd/dpmodel/atomic_model/linear_atomic_model.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ def __init__(
8484
f"LinearAtomicModel only supports AtomicModel of mixed type, the following models are not mixed type: {model_mixed_type}."
8585
)
8686

87+
# Fail fast: a sum mixing an intensive with an extensive term is not
88+
# physically meaningful, so such a composition must not exist.
89+
intensive_flags = {m.get_intensive() for m in models}
90+
if len(intensive_flags) > 1:
91+
raise ValueError(
92+
"LinearAtomicModel cannot combine intensive and extensive "
93+
"sub-models: "
94+
+ ", ".join(f"{type(m).__name__}={m.get_intensive()}" for m in models)
95+
)
96+
8797
self.models = models
8898
self.type_map = type_map
8999
self._rebuild_mapping_state()
@@ -613,10 +623,11 @@ def has_chg_spin_ebd(self) -> bool:
613623
return any(model.has_chg_spin_ebd() for model in self.models)
614624

615625
def get_intensive(self) -> bool:
616-
"""Intensive iff EVERY child is: a sum cannot be intensive while one
617-
child scales with system size.
626+
"""Whether the composed property is intensive.
627+
628+
All children agree by construction (validated in ``__init__``).
618629
"""
619-
return all(model.get_intensive() for model in self.models)
630+
return self.models[0].get_intensive() if self.models else False
620631

621632
def get_compute_stats_distinguish_types(self) -> bool:
622633
"""Needed if ANY child needs them; the stricter rule is safe for

source/tests/common/dpmodel/test_zbl_bridging.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,28 @@ class TestCompositionForwardsStatCapabilities:
499499
charge-spin and pair-exclusion accessors.
500500
"""
501501

502+
def test_intensive_mixture_is_rejected_at_construction(self) -> None:
503+
"""An intensive/extensive mixture must not be CONSTRUCTIBLE.
504+
505+
Rejected in ``__init__`` rather than at query time: such a
506+
composition is not physically meaningful, so it should never exist
507+
rather than exist and answer a plausible-looking default.
508+
"""
509+
zbl_a = InterPotentialAtomicModel(type_map=["Ni", "O"], rcut=4.0, sel=[8])
510+
zbl_b = InterPotentialAtomicModel(type_map=["Ni", "O"], rcut=4.0, sel=[8])
511+
# anti-vacuity: matching children compose fine and report their value
512+
assert zbl_a.get_intensive() is False
513+
assert (
514+
LinearEnergyAtomicModel(
515+
[zbl_a, zbl_b], type_map=["Ni", "O"], weights="sum"
516+
).get_intensive()
517+
is False
518+
)
519+
520+
zbl_b.get_intensive = lambda: True # type: ignore[method-assign]
521+
with pytest.raises(ValueError, match="intensive and extensive"):
522+
LinearEnergyAtomicModel([zbl_a, zbl_b], type_map=["Ni", "O"], weights="sum")
523+
502524
def test_forwarded_from_children(self) -> None:
503525
bridged = get_model(copy.deepcopy(ZBL_CONFIG))
504526
children = bridged.atomic_model.models

0 commit comments

Comments
 (0)