33
44import copy
55import json
6+ import operator
67
7- from ._native import NativeVectorEnv
8+ import numpy as np
9+
10+ from ._native import NativeEnv , NativeVectorEnv
11+ from . import curriculum
12+ from .positions import PositionSet
813
914
1015class VectorEnv :
@@ -15,10 +20,18 @@ class VectorEnv:
1520 """
1621
1722 def __init__ (self , game , n , * , config = None , seed = 0 , threads = 0 ):
18- self .native = NativeVectorEnv (game , n , json .dumps (config or {}), seed , threads )
23+ config_json = json .dumps (config or {})
24+ self .native = NativeVectorEnv (game , n , config_json , seed , threads )
1925 self .n = self .native .n
2026 self .num_players = self .native .num_players
2127 self .action_space_size = self .native .action_space_size
28+ self ._prototype = NativeEnv (game , config_json )
29+ self ._curricula = [(None , None )] * self .n
30+
31+ @property
32+ def origins (self ):
33+ """Public dataset identities per row; puzzle answers are excluded."""
34+ return [curriculum .origin_info (dataset , position_id ) for dataset , position_id in self ._curricula ]
2235
2336 def frame (self , seats = None ):
2437 """Return owned numeric arrays; select a view per row with seats."""
@@ -28,28 +41,119 @@ def step(self, actions):
2841 """Apply a whole batch atomically; None is required for finished rows."""
2942 return self .native .step (list (actions ))
3043
31- def reset (self , seed = 0 , * , positions = None ):
32- """Reset every row using seed + row, or an explicit list of seeds."""
33- seeds = [seed + index for index in range (self .n )] if isinstance (seed , int ) else list (seed )
34- return self .native .reset_at (
35- list (range (self .n )), seeds , [None ] * self .n if positions is None else list (positions )
36- )
44+ def reset (self , seed = 0 , * , positions = None , options = None ):
45+ """Reset all rows with seed + row, or an explicit list of seeds."""
46+ if isinstance (seed , (int , np .integer )):
47+ if isinstance (seed , (bool , np .bool_ )):
48+ raise ValueError ("seed must be an unsigned 64-bit integer" )
49+ seeds = [int (seed ) + index for index in range (self .n )]
50+ else :
51+ seeds = list (seed )
52+ return self .reset_at (range (self .n ), seeds , positions = positions , options = options )
3753
38- def reset_at (self , indices , seeds , * , positions = None ):
39- """Reset selected rows without discarding other episodes."""
40- indices = list (indices )
41- return self .native .reset_at (
42- indices , list (seeds ), [None ] * len (indices ) if positions is None else list (positions )
43- )
54+ def reset_at (self , indices , seeds , * , positions = None , options = None ):
55+ """Reset selected rows, optionally sampling a pinned position set."""
56+ indices , seeds = list (indices ), list (seeds )
57+ options = dict (options or {})
58+ if set (options ) - {"position" , "position_set" }:
59+ raise ValueError ("supported reset options: position, position_set" )
60+ if positions is not None and options :
61+ raise ValueError ("choose positions or reset options, not both" )
62+ if len (seeds ) != len (indices ):
63+ raise ValueError ("reset seeds must match indices" )
64+ normalized = []
65+ for index , seed in zip (indices , seeds ):
66+ if isinstance (index , (bool , np .bool_ )) or isinstance (seed , (bool , np .bool_ )):
67+ raise ValueError ("indices and seeds must be integers" )
68+ try :
69+ index , seed = operator .index (index ), operator .index (seed )
70+ except TypeError as error :
71+ raise ValueError ("indices and seeds must be integers" ) from error
72+ if not 0 <= index < self .n or not 0 <= seed < 2 ** 64 :
73+ raise ValueError ("reset index or seed is out of range" )
74+ normalized .append ((index , seed ))
75+ if len ({index for index , _ in normalized }) != len (indices ):
76+ raise ValueError ("reset indices must be unique" )
77+ indices = [index for index , _ in normalized ]
78+ seeds = [seed for _ , seed in normalized ]
79+ positions = None if positions is None else list (positions )
80+ if positions is not None and len (positions ) != len (indices ):
81+ raise ValueError ("reset positions must match indices" )
82+ if isinstance (options .get ("position_set" ), str ):
83+ options ["position_set" ] = PositionSet .load (options ["position_set" ])
84+ starts , origins = [], self ._curricula [:]
85+ for offset , (index , seed ) in enumerate (normalized ):
86+ row_options = {"position" : positions [offset ]} if positions is not None else options
87+ position , dataset , position_id = curriculum .resolve_start (
88+ self ._prototype , row_options , np .random .default_rng (seed ), self ._curricula [index ][0 ]
89+ )
90+ starts .append (position )
91+ origins [index ] = dataset , position_id
92+ result = self .native .reset_at (indices , seeds , starts )
93+ self ._curricula = origins
94+ return result
4495
4596 def clone (self ):
4697 result = copy .copy (self )
4798 result .native = self .native .clone ()
99+ result ._curricula = self ._curricula [:]
48100 return result
49101
50102 def get_state (self ):
51- """Full private checkpoints for trusted training code."""
52- return self .native .get_state ()
103+ """Private checkpoints; curriculum datasets are stored once per batch."""
104+ native = self .native .get_state ()
105+ if not any (dataset is not None for dataset , _ in self ._curricula ):
106+ return native
107+ datasets , rows = {}, []
108+ for dataset , position_id in self ._curricula :
109+ if dataset is None :
110+ rows .append (None )
111+ else :
112+ key = dataset .identifier + ":" + dataset .sha256
113+ datasets [key ] = dataset .checkpoint ()
114+ rows .append ({"dataset" : key , "position_id" : position_id })
115+ return {"version" : 2 , "native" : native , "datasets" : datasets , "rows" : rows }
53116
54117 def set_state (self , snapshots ):
55- self .native .set_state (list (snapshots ))
118+ """Restore native state and per-row curricula as one atomic operation."""
119+ origins = [(None , None )] * self .n
120+ if isinstance (snapshots , dict ):
121+ if (
122+ set (snapshots ) != {"version" , "native" , "datasets" , "rows" }
123+ or type (snapshots ["version" ]) is not int or snapshots ["version" ] != 2
124+ or not isinstance (snapshots ["datasets" ], dict )
125+ or len (snapshots ["datasets" ]) > self .n
126+ or not isinstance (snapshots ["rows" ], list ) or len (snapshots ["rows" ]) != self .n
127+ ):
128+ raise ValueError ("invalid vector curriculum checkpoint" )
129+ datasets = {}
130+ for key , payload in snapshots ["datasets" ].items ():
131+ if not isinstance (payload , dict ) or set (payload ) != {"manifest" , "data" }:
132+ raise ValueError ("invalid vector position-set checkpoint" )
133+ dataset = PositionSet .from_jsonl (payload ["manifest" ], payload ["data" ])
134+ dataset .validate_for (self ._prototype )
135+ if key != dataset .identifier + ":" + dataset .sha256 :
136+ raise ValueError ("vector checkpoint dataset identity mismatch" )
137+ datasets [key ] = dataset
138+ used = set ()
139+ for index , row in enumerate (snapshots ["rows" ]):
140+ if row is None :
141+ continue
142+ if (
143+ not isinstance (row , dict ) or set (row ) != {"dataset" , "position_id" }
144+ or not isinstance (row ["dataset" ], str ) or row ["dataset" ] not in datasets
145+ ):
146+ raise ValueError ("invalid vector checkpoint curriculum row" )
147+ dataset = datasets [row ["dataset" ]]
148+ if row ["position_id" ] not in {entry .id for entry in dataset .entries }:
149+ raise ValueError ("unknown vector checkpoint position" )
150+ origins [index ] = dataset , row ["position_id" ]
151+ used .add (row ["dataset" ])
152+ if used != set (datasets ):
153+ raise ValueError ("unused vector checkpoint dataset" )
154+ snapshots = snapshots ["native" ]
155+ if not isinstance (snapshots , (list , tuple )):
156+ raise ValueError ("vector native checkpoints must be a list" )
157+ candidate = self .native .clone ()
158+ candidate .set_state (list (snapshots ))
159+ self .native , self ._curricula = candidate , origins
0 commit comments