2core/cluster_identifier.py — Abstract cluster state-space primitives (Layer 1).
4Host-independent. Implements the indexed cluster state space of
5Ghoniem (2026), *A Generalized Graph-Based Cluster Dynamics Framework
6for Irradiated Materials*, Section 2.1.
8A cluster identifier is the four-tuple
10 sigma = (chi, n, p, c)
12 chi : polarity index in {-1, +1} (-1 vacancy-type, +1 SIA-type)
13 n : size >= 1, the count of host Frenkel-pair sites of that polarity
14 carried by the cluster
15 p : population label (a :class:`Population`)
16 c : composition vector, the counts of trapped non-host species
17 (solutes / gases), e.g. (c_He, c_H)
19The polarity index classifies a cluster by the sign of its net
20Frenkel-pair content with respect to the host lattice. It is unaffected
21by solute trapping: a helium-vacancy cluster He_j V_n retains chi = -1
24Nothing here is host-specific. A host material declares *which*
25populations and *which* (chi, n, p, c) vertices are admissible; this
26module only provides the vocabulary.
28from __future__
import annotations
30from dataclasses
import dataclass, replace
31from enum
import IntEnum
32from typing
import Tuple
36 """Sign of a cluster's net Frenkel-pair content w.r.t. the host lattice.
38 The polarity index splits the state space into two sub-spaces without
39 a host-specific taxonomy. It also serves as the *species-class*
40 discriminator of the paper (C_V for vacancy-type, C_I for SIA-type).
48 return "vacancy" if self
is Polarity.VACANCY
else "sia"
52 """Signed lattice-defect content per unit size (-1 vacancy, +1 SIA)."""
57 return Polarity.SIA
if self
is Polarity.VACANCY
else Polarity.VACANCY
60@dataclass(frozen=True, order=True)
62 """A defect population within one polarity layer (paper Section 2.1/3.1).
64 A population is a subcollection of clusters of the same polarity that
65 share a mobility law, orientation state, microstructural environment,
66 or sink coupling. Populations are physically distinct *phases* of the
67 cluster (different binding energies, mobility, energetic stability),
68 not mere spatial coordinates; transport between populations is itself
69 a CD reaction (an inter-population edge).
71 The host material declares its population set, e.g.
72 vacancy: {bulk, GB, disl, ppt}
73 bcc SIA: {bulk-111, bulk-100, disl-trapped}
74 The abstract core never enumerates populations itself.
84 raise ValueError(
"Population.name must be non-empty")
87 f
"Population {self.name!r}: n_min must be >= 1 (got {self.n_min})")
90 f
"Population {self.name!r}: mobile_max must be >= 0 "
91 f
"(got {self.mobile_max})")
94 """True if size ``n`` is admissible for this population."""
95 return n >= self.
n_min
98 """True if a size-``n`` cluster of this population is mobile."""
102@dataclass(frozen=True)
104 """The four-tuple sigma = (chi, n, p, c) identifying one cluster vertex.
106 Instances are immutable and hashable, so they can be used directly as
107 keys of the reaction admissibility graph's vertex set.
112 population: Population
113 composition: Tuple[int, ...] = ()
118 f
"polarity {self.polarity.label!r} inconsistent with "
119 f
"population {self.population.name!r} "
120 f
"(polarity {self.population.polarity.label!r})")
122 raise ValueError(f
"cluster size must be >= 1 (got {self.size})")
125 f
"composition counts must be >= 0 (got {self.composition})")
130 """True for a bare point-defect monomer (size 1, no trapped solute)."""
135 """Signed lattice-defect content q(sigma) = chi * n (paper Eq. 49).
137 Used by the host conservation laws. A trapped gas atom carries
138 zero signed defect, so the composition does not enter here.
144 """Total number of trapped non-host atoms across all species."""
149 """Return the same identifier at a different size ``n``."""
150 return replace(self, size=n)
153 """Return the same identifier with a different composition vector."""
154 return replace(self, composition=tuple(int(x)
for x
in c))
157 """Return the same cluster moved to a different population ``p``."""
160 "cannot move a cluster to a population of opposite polarity")
161 return replace(self, population=p)
164 return (f
"<{self.polarity.label} n={self.size} "
165 f
"{self.population.name} c={self.composition}>")
"ClusterIdentifier" with_size(self, int n)
"ClusterIdentifier" with_composition(self, c)
"ClusterIdentifier" with_population(self, Population p)
"Polarity" opposite(self)
bool is_mobile(self, int n)
bool admits_size(self, int n)