@dataclass(frozen=True)
class GraphFieldSpec:
mode: Mode
dtype: DType
dim: int = 1
cat_dim: int = 0
inc: Inc = field(default_factory=Inc.none)
def __post_init__(self) -> None:
if self.dim <= 0:
raise ValueError("GraphFieldSpec.dim must be > 0")
if self.dtype in (DType.PYOBJ, DType.STR):
raise ValueError(
"GraphFieldSpec dtype is only supported for Python-side "
"batch_encodings collate_spec, not native HGraph dynamic fields"
)
normalized_cat_dim = 1 if self.cat_dim == -1 else int(self.cat_dim)
if self.mode in (Mode.CAT, Mode.RAGGED_CAT):
if normalized_cat_dim not in (0, 1):
raise ValueError(
"GraphFieldSpec.cat_dim must be 0, 1, or -1 for CAT/RAGGED_CAT"
)
elif normalized_cat_dim != 0:
raise ValueError("GraphFieldSpec.cat_dim must be 0 for STACK/CONST")
object.__setattr__(self, "cat_dim", normalized_cat_dim)
if self.inc.kind == "node_offset" and self.dtype is not DType.I64:
raise ValueError("NODE_OFFSET increment requires dtype=DType.I64")
if self.inc.kind == "field_offset" and self.dtype is not DType.I64:
raise ValueError("FIELD_OFFSET increment requires dtype=DType.I64")
def to_core_dict(self) -> dict[str, Any]:
return {
"mode": self.mode.value,
"dtype": self.dtype.value,
"dim": int(self.dim),
"cat_dim": int(self.cat_dim),
"inc": self.inc.to_core_dict(),
}
@classmethod
def from_spec(cls, spec: "GraphFieldSpec | Mapping[str, Any]") -> "GraphFieldSpec":
if isinstance(spec, cls):
return spec
if not isinstance(spec, Mapping):
raise TypeError(
f"Graph field spec must be GraphFieldSpec or mapping, got {type(spec)!r}"
)
mode_raw = spec.get("mode")
dtype_raw = spec.get("dtype")
if mode_raw is None or dtype_raw is None:
raise ValueError("Graph field spec mapping requires 'mode' and 'dtype'")
mode = _normalize_mode(mode_raw)
dtype = _normalize_dtype(dtype_raw)
dim = int(spec.get("dim", 1))
cat_dim = int(spec.get("cat_dim", 0))
inc = _normalize_inc(spec.get("inc", {"kind": "none"}))
return cls(mode=mode, dtype=dtype, dim=dim, cat_dim=cat_dim, inc=inc)