@@ -445,6 +445,8 @@ class EnvironmentInitialEmbedding(NativeOP):
445445 Random seed for reproducibility.
446446 """
447447
448+ CONFIG_DERIVED_ARRAYS = ("spin_mask" ,)
449+
448450 def __init__ (
449451 self ,
450452 * ,
@@ -488,7 +490,10 @@ def __init__(
488490 # plus, for the native spin scheme, the 3 envelope-gated neighbor-spin
489491 # components, so the inner product ``D = M^T M`` yields the neighbor
490492 # spin-spin invariants alongside the geometric ones.
491- self .coord_dim = 4 + (3 if self .spin_flags is not None else 0 )
493+ self .geometry_coord_dim = 4
494+ self .coord_dim = self .geometry_coord_dim + (
495+ 3 if self .spin_flags is not None else 0
496+ )
492497
493498 # === RBF projection: n_radial -> rbf_out_dim (two-layer MLP) ===
494499 # rbf_out_dim = max(32, embed_dim - 2*type_dim) to align G-network width to embed_dim
@@ -566,19 +571,18 @@ def __init__(
566571 dtype = PRECISION_DICT [self .precision .lower ()],
567572 )
568573
569- # === Native spin: per-type mask and isotropic channel scale ===
574+ # === Native spin: per-type mask and post-quadratic activation gate ===
570575 # The mask gates the neighbor-spin channel by source type, so a
571576 # non-magnetic neighbor contributes zero and (critically) carries zero
572- # magnetic force ``-dE/ds``. The single scalar scale (shared across
573- # x/y/z) keeps the spin coordinates transforming with the geometry, so
574- # the env-matrix invariant stays SO(3)-invariant; ``output_proj`` is
575- # zero-initialized, so the spin contribution starts neutral regardless.
577+ # magnetic force ``-dE/ds``. ``spin_scale`` multiplies the spin-only
578+ # contribution after the environment quadratic form, providing a
579+ # linear gate that can start from exactly zero.
576580 if self .spin_flags is not None :
577581 self .spin_mask = np .array (
578582 [1.0 if flag else 0.0 for flag in self .spin_flags ],
579583 dtype = PRECISION_DICT [self .precision .lower ()],
580584 )
581- self .spin_scale = np .ones (
585+ self .spin_scale = np .zeros (
582586 (1 ,), dtype = PRECISION_DICT [self .precision .lower ()]
583587 )
584588
@@ -648,11 +652,7 @@ def call(
648652 xp .take (xp .astype (atype_flat , xp .int64 ), src_i , axis = 0 ),
649653 axis = 0 ,
650654 )[:, None ] # (E, 1)
651- spin_scale = xp .astype (
652- xp_asarray_nodetach (xp , self .spin_scale [...], device = device ),
653- r_tilde .dtype ,
654- )
655- spin_chan = edge_env * spin_scale * spin_src * mask # (E, 3)
655+ spin_chan = edge_env * spin_src * mask # (E, 3)
656656 else :
657657 spin_chan = xp .zeros (
658658 (r_tilde .shape [0 ], 3 ), dtype = r_tilde .dtype , device = device
@@ -720,9 +720,26 @@ def call(
720720 # Summing over the coordinate axis makes D invariant to a joint rotation
721721 # of the geometry and the spin channels; with the spin channels present,
722722 # D additionally carries the neighbor spin-spin invariants.
723- env_agg_t = xp .permute_dims (env_agg , (0 , 2 , 1 )) # (N, embed_dim, coord_dim)
724- env_agg_axis = env_agg [:, :, : self .axis_dim ] # (N, coord_dim, axis_dim)
725- D = xp .matmul (env_agg_t , env_agg_axis ) # (N, embed_dim, axis_dim)
723+ if self .spin_flags is None :
724+ env_agg_t = xp .permute_dims (env_agg , (0 , 2 , 1 ))
725+ env_agg_axis = env_agg [:, :, : self .axis_dim ]
726+ D = xp .matmul (env_agg_t , env_agg_axis )
727+ else :
728+ geometry_agg = env_agg [:, : self .geometry_coord_dim , :]
729+ spin_agg = env_agg [:, self .geometry_coord_dim :, :]
730+ D_geometry = xp .matmul (
731+ xp .permute_dims (geometry_agg , (0 , 2 , 1 )),
732+ geometry_agg [:, :, : self .axis_dim ],
733+ )
734+ D_spin = xp .matmul (
735+ xp .permute_dims (spin_agg , (0 , 2 , 1 )),
736+ spin_agg [:, :, : self .axis_dim ],
737+ )
738+ spin_scale = xp .astype (
739+ xp_asarray_nodetach (xp , self .spin_scale [...], device = device ),
740+ D_spin .dtype ,
741+ )
742+ D = D_geometry + spin_scale * D_spin
726743
727744 # === Step 6. Output projection for FiLM logits ===
728745 D_flat = xp .reshape (
@@ -994,6 +1011,8 @@ class SpinEmbedding(NativeOP):
9941011 Whether parameters are trainable.
9951012 """
9961013
1014+ CONFIG_DERIVED_ARRAYS = ("spin_mask" ,)
1015+
9971016 def __init__ (
9981017 self ,
9991018 * ,
@@ -1020,8 +1039,9 @@ def __init__(
10201039 self .spin_flags = [bool (flag ) for flag in use_spin ]
10211040
10221041 # === Per-type spin gate ===
1023- # Non-persistent: rebuilt from config on construction and moved with the
1024- # module, so the deterministic mask never enters the serialized state.
1042+ # Configuration-derived (hence ``CONFIG_DERIVED_ARRAYS``): rebuilt on
1043+ # construction and moved with the module, so the deterministic mask
1044+ # never enters the serialized state.
10251045 self .spin_mask = np .array (
10261046 [1.0 if bool (flag ) else 0.0 for flag in use_spin ], dtype = prec
10271047 )
@@ -1053,23 +1073,26 @@ def __init__(
10531073 seed = child_seed (seed_scalar , 1 ),
10541074 trainable = self .trainable ,
10551075 )
1076+ self .mag_layer2 .w = np .zeros (
1077+ (self .channels , self .channels ),
1078+ dtype = prec ,
1079+ )
10561080
10571081 # === l=1 per-type per-channel weight ===
10581082 # ``adam_`` prefix routes the table to Adam in HybridMuon, matching the
10591083 # type-embedding treatment for per-type lookup parameters.
1060- init_std = 1.0 / math .sqrt (float (self .ntypes + self .channels ))
1061- rng_vec = np .random .default_rng (child_seed (seed , 1 ))
1062- self .adam_spin_vec_weight = rng_vec .normal (
1063- 0.0 , init_std , size = (self .ntypes , self .channels )
1064- ).astype (prec )
1084+ self .adam_spin_vec_weight = np .zeros (
1085+ (self .ntypes , self .channels ),
1086+ dtype = prec ,
1087+ )
10651088
10661089 # === l=1 per-source-type per-channel weight for neighbor aggregation ===
10671090 # Separate from the on-site weight: this scales the neighbor's spin
10681091 # direction before it is aggregated into the center node's l=1 seed.
1069- rng_nbr = np .random . default_rng ( child_seed ( seed , 2 ))
1070- self .adam_spin_nbr_weight = rng_nbr . normal (
1071- 0.0 , init_std , size = ( self . ntypes , self . channels )
1072- ). astype ( prec )
1092+ self . adam_spin_nbr_weight = np .zeros (
1093+ ( self .ntypes , self . channels ),
1094+ dtype = prec ,
1095+ )
10731096
10741097 def call (self , spin : Any , atype : Any ) -> tuple [Any , Any ]:
10751098 """
0 commit comments