morphtree 2.1.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. morphtree/__init__.py +28 -0
  2. morphtree/__main__.py +3 -0
  3. morphtree/_experimental/__init__.py +1 -0
  4. morphtree/_experimental/admission.py +364 -0
  5. morphtree/_experimental/base.py +215 -0
  6. morphtree/_experimental/component_cache_v2.py +263 -0
  7. morphtree/_experimental/evolution_report.py +307 -0
  8. morphtree/_experimental/execution_v2.py +697 -0
  9. morphtree/_experimental/facade.py +289 -0
  10. morphtree/_experimental/integrated.py +748 -0
  11. morphtree/_experimental/learning.py +418 -0
  12. morphtree/_experimental/manifest.json +204 -0
  13. morphtree/_experimental/numeric_fixture.py +21 -0
  14. morphtree/_experimental/parameter_contract.py +38 -0
  15. morphtree/_experimental/postfit.py +51 -0
  16. morphtree/_experimental/preprocessing.py +133 -0
  17. morphtree/_experimental/processes.py +191 -0
  18. morphtree/_experimental/producer.py +542 -0
  19. morphtree/_experimental/product.py +156 -0
  20. morphtree/_experimental/product_cli.py +43 -0
  21. morphtree/_experimental/roundtrip_v3.py +311 -0
  22. morphtree/_experimental/runtime_io.py +65 -0
  23. morphtree/_experimental/sampling.py +26 -0
  24. morphtree/_experimental/support.py +281 -0
  25. morphtree/_experimental/target.py +59 -0
  26. morphtree/_experimental/training.py +53 -0
  27. morphtree/_experimental/worker.py +682 -0
  28. morphtree/_installed.py +17 -0
  29. morphtree/_native/__init__.py +1 -0
  30. morphtree/_native/bin/win_amd64/morphtree_kernels.dll +0 -0
  31. morphtree/_native/buffers.py +82 -0
  32. morphtree/_native/loader.py +39 -0
  33. morphtree/_native/manifest.json +29 -0
  34. morphtree/_native/runtime.py +48 -0
  35. morphtree/_native/src/histogram.c +61 -0
  36. morphtree/_native/src/traversal.c +51 -0
  37. morphtree/api.py +388 -0
  38. morphtree/api_compat.py +65 -0
  39. morphtree/backend.py +162 -0
  40. morphtree/booster.py +252 -0
  41. morphtree/cli.py +52 -0
  42. morphtree/config.py +262 -0
  43. morphtree/control.py +20 -0
  44. morphtree/data.py +300 -0
  45. morphtree/diagnostics.py +158 -0
  46. morphtree/estimators.py +156 -0
  47. morphtree/examples.py +25 -0
  48. morphtree/exporting.py +224 -0
  49. morphtree/legacy_artifacts.py +32 -0
  50. morphtree/metrics.py +156 -0
  51. morphtree/model.py +1010 -0
  52. morphtree/mutations.py +205 -0
  53. morphtree/native_lab_backend.py +79 -0
  54. morphtree/numpy_backend.py +180 -0
  55. morphtree/optuna_tuner.py +154 -0
  56. morphtree/persistence.py +414 -0
  57. morphtree/prediction_output.py +35 -0
  58. morphtree/prepared_bins.py +82 -0
  59. morphtree/public.py +106 -0
  60. morphtree/resource_plan.py +170 -0
  61. morphtree/scalable.py +781 -0
  62. morphtree/scalable_backend.py +74 -0
  63. morphtree/scalable_checkpoint.py +129 -0
  64. morphtree/scalable_data.py +255 -0
  65. morphtree/scalable_receipts.py +92 -0
  66. morphtree/scalable_target.py +68 -0
  67. morphtree/scalable_variants.py +133 -0
  68. morphtree/sklearn_view.py +79 -0
  69. morphtree/source_markers.py +25 -0
  70. morphtree/species.py +47 -0
  71. morphtree/temporary.py +65 -0
  72. morphtree/training_fragment.py +130 -0
  73. morphtree/tree.py +231 -0
  74. morphtree/tuning.py +150 -0
  75. morphtree-2.1.0.dist-info/METADATA +212 -0
  76. morphtree-2.1.0.dist-info/RECORD +79 -0
  77. morphtree-2.1.0.dist-info/WHEEL +4 -0
  78. morphtree-2.1.0.dist-info/entry_points.txt +2 -0
  79. morphtree-2.1.0.dist-info/licenses/LICENSE +21 -0
morphtree/scalable.py ADDED
@@ -0,0 +1,781 @@
1
+ """Private columnar evolutionary facade, not a change to published 2.0.0.
2
+
3
+ Same native boosters, two genuine native families, current-parent OOF diagnosis,
4
+ two executable operators, verified SEARCH acceptance, final frozen-program
5
+ Optuna and explicit all-input refit. No source dataset JSON materialization.
6
+ """
7
+ from array import array
8
+ from collections import Counter
9
+ import copy
10
+ from dataclasses import asdict,replace
11
+ import gc
12
+ import hashlib
13
+ import json
14
+ import math
15
+ from pathlib import Path
16
+ import statistics
17
+ import time
18
+ from .booster import BinaryHistogramBooster
19
+ from .config import AlgorithmGenome,HyperParams
20
+ from .persistence import decode_json_bytes,LoadLimits,decode_booster,decode_multiclass_booster,atomic_text,regular_file_size,read_regular_bytes
21
+ from .scalable_data import Input,Table,Encoder,Resources,scalar
22
+ from .scalable_backend import ScalableBackend
23
+ from .scalable_receipts import Ledger,verify as verify_receipts,data_identity,encoder_digest
24
+ from .prepared_bins import BinCache
25
+ from .resource_plan import fit_plan,memory_allowance,inference_seconds,io_seconds,memory_snapshot,numeric_capacity_plan
26
+ from .scalable_target import detect_target,label_scalar,compact_oof_train_indices
27
+ from .source_markers import controlled_source
28
+ from ._experimental.target import _split
29
+ from ._experimental.sampling import bootstrap,oof_folds
30
+
31
+ _PACKAGE=Path(__file__).resolve().parent
32
+ _ENGINE_SHA256=hashlib.sha256((_PACKAGE/'_experimental/manifest.json').read_bytes()).hexdigest()
33
+
34
+
35
+ def verify_source_identity():
36
+ raw=(_PACKAGE/'_experimental/manifest.json').read_bytes()
37
+ if hashlib.sha256(raw).hexdigest()!=_ENGINE_SHA256:raise ImportError('Changed source manifest since import')
38
+ m=json.loads(raw)
39
+ actual={p.relative_to(_PACKAGE).as_posix() for p in _PACKAGE.rglob('*') if p.is_file() and '__pycache__' not in p.parts}
40
+ if actual!=set(m['files'])|{'_experimental/manifest.json'}:raise ImportError('Missing/unexpected private runtime source')
41
+ for n,h in m['files'].items():
42
+ if hashlib.sha256((_PACKAGE/n).read_bytes()).hexdigest()!=h:raise ImportError('Changed private engine source')
43
+
44
+
45
+ def digest(v):return hashlib.sha256(json.dumps(v,sort_keys=True,allow_nan=False,separators=(',',':')).encode()).hexdigest()
46
+ def point_loss(task,y,p):
47
+ # Invalid internal predictions must stop selection, not become an apparently
48
+ # excellent loss through NaN comparison/clipping or zip truncation.
49
+ if task not in ('binary','multiclass','regression') or not len(y) or len(y)!=len(p):raise ValueError('Known task and nonempty aligned loss inputs required')
50
+ if task=='regression':
51
+ if any(not math.isfinite(a) or not math.isfinite(b) or not math.isfinite((a-b)**2) for a,b in zip(y,p)):raise ValueError('Finite regression loss inputs and squared errors required')
52
+ elif task=='binary':
53
+ if any(a not in (0,1) or not math.isfinite(b) or not 0<=b<=1 for a,b in zip(y,p)):raise ValueError('Binary labels and finite probabilities in [0,1] required')
54
+ else:
55
+ k=len(p[0])
56
+ if k<2 or any(type(a) is not int or not 0<=a<k or len(b)!=k or any(not math.isfinite(v) or not 0<=v<=1 for v in b) or abs(math.fsum(b)-1)>1e-10 for a,b in zip(y,p)):raise ValueError('Aligned multiclass labels and finite probability simplexes required')
57
+ if task=='regression':return array('d',((a-b)**2 for a,b in zip(y,p)))
58
+ if task=='binary':return array('d',(-math.log(max(1e-15,min(1-1e-15,b if a else 1-b))) for a,b in zip(y,p)))
59
+ return array('d',(-math.log(max(1e-15,min(1-1e-15,b[a]))) for a,b in zip(y,p)))
60
+ def risk(task,y,p):return statistics.mean(point_loss(task,y,p))
61
+ def available_memory():
62
+ try:
63
+ import os
64
+ if os.name=='nt':
65
+ import ctypes
66
+ class Status(ctypes.Structure):_fields_=[('length',ctypes.c_ulong),('load',ctypes.c_ulong)]+[(n,ctypes.c_ulonglong) for n in ('total','available','page_total','page_available','virtual_total','virtual_available','extended')]
67
+ s=Status();s.length=ctypes.sizeof(s)
68
+ if not ctypes.WinDLL('kernel32',use_last_error=True).GlobalMemoryStatusEx(ctypes.byref(s)):raise OSError('RAM query')
69
+ return s.available
70
+ return os.sysconf('SC_AVPHYS_PAGES')*os.sysconf('SC_PAGE_SIZE')
71
+ except (OSError,ValueError,AttributeError):return 512*1024**2
72
+
73
+
74
+ class Family:
75
+ def __init__(self,task,genome,params,encoder,models,family):self.task,self.genome,self.params,self.encoder,self.models,self.family=task,genome,params,encoder,models,family
76
+ def probabilities(self,table,resources=None):
77
+ if not self.models or not len(table):raise ValueError('Nonempty family and observations required')
78
+ k=self.models[0].n_classes if self.task=='multiclass' else 1
79
+ if resources is None:resources=managed_resources('auto',inference_seconds(len(table),table.width,sum(len(m.trees) for m in self.models),self.params.max_depth))
80
+ output_bytes=len(table)*(64+40*k if self.task=='multiclass' else 40)+64
81
+ member_row_bytes=64+40*k if self.task=='multiclass' else 40
82
+ row_bytes=table.width*32+len(self.models)*member_row_bytes+256
83
+ resources.reserve(output_bytes+row_bytes*4,'Family prediction output and member workspace')
84
+ batch=max(1,min(16384,(resources.bytes_limit-output_bytes)//max(1,row_bytes*4)));result=[]
85
+ for start in range(0,len(table),batch):
86
+ resources.check();stop=min(len(table),start+batch);block=table if start==0 and stop==len(table) else table.take(range(start,stop));X=self.encoder.transform(block);parts=[]
87
+ for model in self.models:
88
+ resources.check();parts.append(model.predict_raw(X) if self.task=='regression' else model.predict_proba(X))
89
+ # Preserve original member order and math.fsum for each observation.
90
+ if self.task=='multiclass':result.extend([[math.fsum(v[i][j] for v in parts)/len(parts) for j in range(k)] for i in range(len(block))])
91
+ else:result.extend([math.fsum(v[i] for v in parts)/len(parts) for i in range(len(block))])
92
+ del parts,X,block
93
+ resources.check();return result
94
+ def packet(self):return dict(task=self.task,family=self.family,genome=self.genome.to_dict(),params=asdict(self.params),encoder=self.encoder.state(),members=[m.to_dict() for m in self.models])
95
+
96
+
97
+ def managed_resources(memory_budget,seconds,observer=None):
98
+ observation=memory_snapshot(available_memory());observed=observation['effective_available_bytes'];res=Resources(memory_allowance(memory_budget,observed),seconds,observer)
99
+ res.available_bytes_snapshot=observed;res.memory_observation=observation;res.base_commit=res.peak_commit=observation['process_private_bytes'];res.pressure_reader=lambda:memory_snapshot(available_memory());return res
100
+
101
+
102
+ def mutation_order(regions,guidance=True):
103
+ """Research control changes only proposal order; OOF is still computed."""
104
+ if type(guidance) is not bool:raise ValueError('Typed research guidance flag required')
105
+ return ('split_program','growth_policy') if regions and guidance else ('growth_policy','split_program')
106
+
107
+
108
+ def streaming_loss_regions(table,point,folds,resources,primary=None):
109
+ n=len(table)
110
+ if not n or len(point)!=n:raise ValueError('Aligned nonempty OOF point losses')
111
+ resources.reserve(n+table.width*256,'Compact diagnostic masks and output metadata')
112
+ if primary is None:primary=statistics.mean(point)
113
+ groups=[]
114
+ def checked_column(feature):
115
+ for index,value in enumerate(table.column(feature)):
116
+ if index%4096==0:resources.check()
117
+ yield value
118
+ for feature,kind in enumerate(table.kinds):
119
+ resources.check()
120
+ if kind=='categorical':
121
+ resources.reserve(n+table.width*256+min(n,len(table.vocab[feature])+1)*192,'Nominal diagnostic mode dictionary')
122
+ cut=Counter(checked_column(feature)).most_common(1)[0][0]
123
+ mask=bytearray(value==cut for value in checked_column(feature))
124
+ else:
125
+ try:cut=statistics.mean(value for value in checked_column(feature) if math.isfinite(value))
126
+ except statistics.StatisticsError:cut=0
127
+ mask=bytearray(math.isfinite(value) and value>cut for value in checked_column(feature))
128
+ contrasts=[]
129
+ for held in folds:
130
+ resources.check();selected=sum(mask[index] for index in held)
131
+ if min(selected,len(held)-selected)>=8:
132
+ first=statistics.mean(point[index] for index in held if mask[index])
133
+ second=statistics.mean(point[index] for index in held if not mask[index])
134
+ contrasts.append(first-second)
135
+ if len(contrasts)==2 and min(contrasts)>.02*primary+.01:
136
+ groups.append(dict(feature=feature,fold_excess=contrasts))
137
+ resources.check()
138
+ return groups
139
+
140
+
141
+ _PARAMETERS_V1=('random_state','verbosity','time_budget','memory_budget','use_optuna','n_trials','max_mutations','refit_full','capacity','checkpoint_dir')
142
+ _PARAMETERS_V2=_PARAMETERS_V1+('n_jobs','mode','operators')
143
+ _OPERATORS={'split_program:objective_balance':'split_program','growth_policy:depthwise':'growth_policy'}
144
+
145
+
146
+ class _Scalable:
147
+ task='auto'
148
+ def __init__(self,random_state=None,verbosity=0,*,time_budget='auto',memory_budget='auto',use_optuna=True,n_trials='auto',max_mutations=2,refit_full=True,capacity=None,checkpoint_dir='auto',n_jobs=1,mode='adaptive',operators=None):
149
+ if random_state is not None and type(random_state) is not int or type(use_optuna) is not bool or type(refit_full) is not bool:raise ValueError('Integer seed or None and boolean stage controls required')
150
+ if type(max_mutations) is not int or max_mutations<0:raise ValueError('Nonnegative integer mutation budget required')
151
+ if n_trials!='auto' and (type(n_trials) is not int or n_trials<0):raise ValueError('Nonnegative integer trial budget or auto required')
152
+ if n_jobs!='auto' and (type(n_jobs) is not int or n_jobs!=1):raise ValueError('This serial native controller supports n_jobs=1 or auto')
153
+ if mode not in ('adaptive','baseline','research'):raise ValueError('Known baseline/adaptive/research mode required')
154
+ if mode=='baseline' and operators is not None:raise ValueError('Baseline has no structural operators')
155
+ if mode=='research' and operators!=('growth_policy:depthwise',):raise ValueError('Research requires explicit growth_policy:depthwise')
156
+ if operators is not None and (type(operators) is not tuple or not operators or any(type(v) is not str or v not in _OPERATORS for v in operators) or len(set(operators))!=len(operators)):raise ValueError('Distinct supported structural operators required')
157
+ if time_budget!='auto' and (type(time_budget) not in (int,float) or not math.isfinite(time_budget) or time_budget<=0):raise ValueError('Positive finite time allowance or auto')
158
+ if memory_budget!='auto' and (type(memory_budget) is not int or memory_budget<1):raise ValueError('Positive byte allowance or auto')
159
+ if type(verbosity) is not int or not 0<=verbosity<=2 or capacity is not None and type(capacity) is not dict:raise ValueError('Typed verbosity/capacity')
160
+ if capacity:replace(HyperParams(),**capacity)
161
+ if checkpoint_dir is not None and (not isinstance(checkpoint_dir,(str,Path)) or isinstance(checkpoint_dir,str) and not checkpoint_dir):raise ValueError('Local checkpoint parent path, auto or None required')
162
+ self.checkpoint_dir=checkpoint_dir
163
+ self.random_state=random_state;self.verbosity=verbosity;self.time_budget=time_budget;self.memory_budget=memory_budget;self.use_optuna=use_optuna;self.n_trials=n_trials;self.max_mutations=max_mutations;self.refit_full=refit_full;self.capacity=capacity;self.n_jobs=n_jobs;self.mode=mode;self.operators=operators;self._observer=None
164
+ def _resolved_controls(self):
165
+ # Revalidate direct parameter edits before input access or allocation.
166
+ type(self)(**self.get_params(deep=False))
167
+ genes=() if self.mode=='baseline' else tuple(_OPERATORS[v] for v in self.operators) if self.operators is not None else tuple(_OPERATORS.values())
168
+ # Each of the two qualified operators changes a gene to one fixed value.
169
+ # Along the single retained lineage at most m+(m-1)+...+1 children
170
+ # can be attempted; duplicate parent/program pairs are never retrained.
171
+ mutations=min(self.max_mutations,len(genes)*(len(genes)+1)//2)
172
+ trials=(2 if self.n_trials=='auto' else self.n_trials) if self.use_optuna else 0
173
+ return dict(mutations=mutations,trials=trials,genes=genes,families=('boost',) if self.mode=='baseline' else ('boost','bag'),
174
+ automatic_trial_policy='Conservative qualified count 2; numeric capacity and deadlines adapt to resources',
175
+ mutation_scope='Maximum attempts under current finite monotone operator grammar; not requested accepted mutations')
176
+ def __sklearn_tags__(self):
177
+ # Optional integration only; importing/training MorphTree needs no sklearn.
178
+ from sklearn.utils import Tags,TargetTags,InputTags,ClassifierTags,RegressorTags
179
+ classifier=self._estimator_type=='classifier'
180
+ return Tags(estimator_type=self._estimator_type,target_tags=TargetTags(required=True),
181
+ input_tags=InputTags(allow_nan=True,categorical=True,string=True),
182
+ classifier_tags=ClassifierTags() if classifier else None,
183
+ regressor_tags=None if classifier else RegressorTags())
184
+ def __sklearn_is_fitted__(self):return hasattr(self,'model_')
185
+ def as_sklearn(self):
186
+ """Optional array-output view for sklearn scoring/Pipeline; same native estimator."""
187
+ from .sklearn_view import SklearnView
188
+ return SklearnView(self)
189
+ def _resources(self):
190
+ return managed_resources(self.memory_budget,150 if self.time_budget=='auto' else self.time_budget,self._observer)
191
+ def _bin_cache_limit(self,res):return min(32*1024**2,res.bytes_limit//8)
192
+ def fit(self,X,y):
193
+ previous=dict(self.__dict__);start=time.perf_counter();self.last_attempt_=dict(status='IN_PROGRESS',native_fits=0,OOF_fits=0,scalar_trees=0,Optuna_trials=0,cache_hits=0,events=[])
194
+ self._ledger=Ledger();self._attempt_anchor=None;self._active_checkpoint_dir_=None
195
+ self.last_attempt_['requested_stages']=dict(max_mutations=self.max_mutations,use_optuna=self.use_optuna,Optuna_trials=self.n_trials if self.use_optuna else 0,refit_full=self.refit_full,diagnostic_guidance=getattr(self,'_research_use_diagnosis',True))
196
+ try:
197
+ verify_source_identity();self._event('CALL_STARTED',engine_sha256=_ENGINE_SHA256,scope='PRIVATE_QUALITY019_NOT_PUBLISHED',requested_task=self.task)
198
+ type(self)(**self.get_params(deep=False))
199
+ self._requested_random_state=self.random_state
200
+ if self.random_state is None:
201
+ import random
202
+ self.random_state=random.SystemRandom().randrange(2**31)
203
+ self.last_attempt_['effective_seed']=self.random_state
204
+ self.last_attempt_['resolved_n_jobs']=1
205
+ self._fit(X,y);self.__dict__.pop('_retained_variants',None);self.__dict__.pop('_variant_selection',None);self.last_attempt_['status']='COMPLETED';self.last_attempt_['seconds']=time.perf_counter()-start;self.report_=self.last_attempt_
206
+ if self.last_attempt_['native_fits']:
207
+ from .scalable_checkpoint import commit
208
+ commit(self,self.model_.packet(),self.classes_,self._res,final=True)
209
+ self.last_attempt_['seconds']=time.perf_counter()-start;self.__dict__.pop('_res',None)
210
+ self.__dict__.pop('_interrupted_anchor',None);self.__dict__.pop('_interrupted_evidence',None)
211
+ return self
212
+ except BaseException as exc:
213
+ anchor=self._attempt_anchor;evidence=getattr(self,'diagnostic_profiles_',{})
214
+ attempt=self.last_attempt_;ledger=self._ledger
215
+ # Restore fitted state before any fallible error reporting. A single
216
+ # dictionary swap avoids exposing a cleared or partly restored state.
217
+ self.__dict__=previous
218
+ self.last_attempt_=attempt;self._interrupted_anchor=anchor;self._interrupted_evidence=evidence
219
+ try:message=str(exc)
220
+ except BaseException:message='<exception message unavailable>'
221
+ attempt.update(status='STOPPED_NO_RETRY',error=type(exc).__name__,message=message,seconds=time.perf_counter()-start)
222
+ if attempt['events']:
223
+ try:
224
+ attempt['events'].append(ledger.emit('FIT_STOPPED',dict(error_type=type(exc).__name__)))
225
+ attempt['failure_receipt_status']='RECORDED_PARTIAL_CHAIN'
226
+ except BaseException as receipt_error:
227
+ attempt['failure_receipt_status']='TERMINAL_EVENT_UNAVAILABLE'
228
+ attempt['failure_receipt_error']=type(receipt_error).__name__
229
+ else:attempt['failure_receipt_status']='CALL_CHAIN_NOT_STARTED'
230
+ raise
231
+ finally:
232
+ self.random_state=previous['random_state']
233
+ self.__dict__.pop('_requested_random_state',None)
234
+ def _event(self,kind,**kw):self.last_attempt_['events'].append(self._ledger.emit(kind,kw))
235
+ def _ordinary_context_guard(self):
236
+ # An active or orphaned scientific/quota context must never fall through
237
+ # to ordinary memory data. The legacy scientific API owns those contexts.
238
+ from .api import ACTIVE, PUBLIC_SCOPE
239
+ from ._experimental.admission import CURRENT, AdmissionError
240
+ from ._experimental.producer import PIPE_CURRENT
241
+ if any(value.get() is not None for value in (ACTIVE,CURRENT,PIPE_CURRENT,PUBLIC_SCOPE)):
242
+ raise AdmissionError({'status':'REJECTED','reason':'CONTROLLED_EXECUTION_CONTEXT_CANNOT_FALL_THROUGH_TO_ORDINARY_DATA'})
243
+
244
+ def _fit(self,X,y):
245
+ self._ordinary_context_guard()
246
+ from .native_lab_backend import bind_backend
247
+ backend_factory=bind_backend()
248
+ if any(controlled_source(obj) for obj in (X,y)):raise ValueError('Controlled sources cannot use this ordinary experimental input path')
249
+ controls=self._resolved_controls();mutations=controls['mutations'];trials=controls['trials'];self.last_attempt_['resolved_controls']=controls
250
+ res=self._resources();self._res=res;tick=time.perf_counter();source=Input(X)
251
+ hp=replace(HyperParams(n_estimators=2,learning_rate=1.,max_leaves=5,max_depth=3,max_bins=15,min_samples_leaf=4,row_fraction=1.,feature_fraction=1.),**(self.capacity or {}))
252
+ available=res.available_bytes_snapshot
253
+ plan=fit_plan(source.n,source.width,1 if self.task=='regression' else 2,hp,mutations,trials,self.refit_full,available,self.time_budget,res.bytes_limit)
254
+ res.configure_time(plan['seconds']);self.last_attempt_['resource_plan']=plan
255
+ res.reserve(source.n*source.width*10+source.n*(256+80*(1 if self.task=='regression' else 2))+source.width*512,'Minimum configured fit workspace before target/feature allocation')
256
+ task,targets,classes=detect_target(y,self.task,res.check,reserve=res.reserve)
257
+ if len(targets)!=source.n:raise ValueError('Aligned X/y required')
258
+ k=len(classes) if task!='regression' else 1
259
+ plan=fit_plan(source.n,source.width,k,hp,mutations,trials,self.refit_full,available,self.time_budget,res.bytes_limit)
260
+ plan=numeric_capacity_plan(plan,hp,mutations,trials,self.refit_full,'n_estimators' in (self.capacity or {}))
261
+ plan['memory_mode']='auto' if self.memory_budget=='auto' else 'explicit'
262
+ plan['memory_observation']=res.memory_observation
263
+ res.configure_time(plan['seconds']);self.last_attempt_['resource_plan']=plan;self._event('RESOURCE_PLAN',plan=plan)
264
+ res.reserve(plan['estimated_workspace_bytes'],'Conservative fit workspace before feature allocation')
265
+ raw=Table.read(source,res)
266
+ self._event('INPUT_PREPARED',rows=len(raw),features=raw.width,seconds=time.perf_counter()-tick,storage_bytes=raw.storage_bytes,input_identity=data_identity(raw,targets,'USER_INPUT'))
267
+ ti,si=_split(targets,task,self.random_state)
268
+ train,search=raw.take(ti),raw.take(si);ty=array(targets.typecode,(targets[i] for i in ti));sy=array(targets.typecode,(targets[i] for i in si));del ti,si
269
+ cache={};prepared=BinCache(self._bin_cache_limit(res),res.check);task_classes=k
270
+ def acquire(family,table,labels,genome,params,phase,seed):
271
+ res.check();enc=Encoder().fit(table);encoded=enc.transform(table)
272
+ identity=data_identity(table,labels,'TRAIN_FULL' if phase=='REFIT_FULL' else 'TRAIN');preprocessing=encoder_digest(enc)
273
+ key=digest(dict(data=identity,preprocessing_sha256=preprocessing,task=task,family=family,genome=genome.to_dict(),params=asdict(params),seed=seed,engine_sha256=_ENGINE_SHA256))
274
+ if key in cache:
275
+ self.last_attempt_['cache_hits']+=1;self._event('CACHE_HIT',phase=phase,family=family,rows=len(table),cache_key=key);return cache[key]
276
+ models=[];m=1 if family=='boost' else params.n_estimators
277
+ for member in range(m):
278
+ res.check();actual=params if family=='boost' else replace(params,n_estimators=1,learning_rate=1.)
279
+ if family=='bag':
280
+ ids=bootstrap(labels,task,seed+member+1);t=table.take(ids);yy=array(labels.typecode,(labels[i] for i in ids));del ids;xx=enc.transform(t)
281
+ else:xx,yy=encoded,labels
282
+ self.last_attempt_['native_fits']+=1;self.last_attempt_['OOF_fits']+=phase=='OOF';self.last_attempt_['scalar_trees']+=actual.n_estimators*(k if task=='multiclass' else 1)
283
+ actual_table=xx.source;effective_rows=hashlib.sha256(memoryview(actual_table.indices)).hexdigest() if actual_table.indices is not None else digest(dict(range_start=0,range_stop=len(actual_table)))
284
+ effective_labels=hashlib.sha256(memoryview(yy)).hexdigest();component=digest(dict(request=key,member=member,effective_rows=effective_rows,effective_labels=effective_labels,seed=seed+member,params=asdict(actual)))
285
+ self._event('NATIVE_FIT_STARTED',ordinal=self.last_attempt_['native_fits'],phase=phase,rows=len(yy),source_rows=len(table),family=family,cache_key=key,component_key=component,effective_rows_sha256=effective_rows,effective_labels_sha256=effective_labels,seed=seed+member,params_sha256=digest(asdict(actual)),preprocessing_sha256=preprocessing,program_sha256=digest(genome.to_dict()),role=identity['role'],reserved_scalar_trees=actual.n_estimators*(k if task=='multiclass' else 1))
286
+ res.check()
287
+ model=BinaryHistogramBooster(genome,actual,seed+member,backend_factory(encoded._nominal_sizes),memory_enabled=False,objective='softmax' if task=='multiclass' else 'squared_error' if task=='regression' else 'binary_logloss',n_classes=k if task=='multiclass' else None)
288
+ preparation_identity=dict(source_sha256=identity['source_sha256'],schema_sha256=identity['schema_sha256'],effective_rows_sha256=effective_rows,effective_labels_sha256=effective_labels,preprocessing_sha256=preprocessing,engine_sha256=_ENGINE_SHA256)
289
+ token=prepared.bind(xx,preparation_identity,'TRAIN_FULL' if phase=='REFIT_FULL' else 'OOF_TRAIN' if phase=='OOF' else 'TRAIN')
290
+ tick=time.perf_counter();model.fit(xx,yy,callback=lambda *_:res.check() or True,prepared_data=token)
291
+ assert len(model.trees)==actual.n_estimators*(k if task=='multiclass' else 1)
292
+ self._event('NATIVE_FIT_COMPLETED',ordinal=self.last_attempt_['native_fits'],phase=phase,seconds=time.perf_counter()-tick,component_key=component,actual_scalar_trees=len(model.trees));models.append(model)
293
+ fitted=Family(task,genome,params,enc,models,family);cache[key]=fitted;return fitted
294
+ base=AlgorithmGenome();tick=time.perf_counter();screen={f:acquire(f,train,ty,base,hp,'SCREEN',self.random_state) for f in controls['families']}
295
+ losses={f:risk(task,sy,m.probabilities(search,res)) for f,m in screen.items()};family=min(losses,key=lambda f:(losses[f],f));current=screen[family];current_risk=losses[family]
296
+ self._event('SCREEN_COMPLETED',losses=losses,parent=family,seconds=time.perf_counter()-tick)
297
+ self._research_initial_parent=current
298
+ def retain_anchor():
299
+ self._attempt_anchor=dict(model=current.packet(),classes=classes,SEARCH=current_risk,source_role='TRAIN',final_refit=False)
300
+ self.last_attempt_['last_verified_anchor']=dict(model_sha256=digest(current.packet()),SEARCH=current_risk,source_role='TRAIN',final_refit=False)
301
+ from .scalable_checkpoint import commit
302
+ commit(self,self._attempt_anchor['model'],classes,res,final=False)
303
+ retain_anchor()
304
+ seen=set();diagnoses={};self.diagnostic_profiles_=diagnoses
305
+ for generation in range(mutations):
306
+ parent=digest(current.packet())
307
+ if parent not in diagnoses:
308
+ tick=time.perf_counter();folds=oof_folds(ty,task,self.random_state);oof=[None]*len(ty)
309
+ for held in folds:
310
+ ids=compact_oof_train_indices(len(ty),held,res);fold=acquire(family,train.take(ids),array(ty.typecode,(ty[i] for i in ids)),current.genome,hp,'OOF',self.random_state)
311
+ pred=fold.probabilities(train.take(held),res)
312
+ self._event('OOF_FOLD',parent_sha256=parent,program_sha256=digest(current.genome.to_dict()),fit_rows=len(ids),held_rows=len(held),disjoint=True,encoder_sha256=digest(fold.encoder.state()),fit_indices_sha256=hashlib.sha256(array('Q',ids)).hexdigest(),held_indices_sha256=hashlib.sha256(array('Q',held)).hexdigest())
313
+ for i,p in zip(held,pred):oof[i]=p
314
+ point=point_loss(task,ty,oof);primary=statistics.mean(point)
315
+ groups=streaming_loss_regions(train,point,folds,res,primary)
316
+ diagnoses[parent]=dict(parent_sha256=parent,task=task,OOF_primary=primary,fold_rows=list(map(len,folds)),TRAIN_only=True,fold_encoder_local=True,features_examined=train.width,regions=groups,causal_status='HEURISTIC_UNCONFIRMED',seconds=time.perf_counter()-tick)
317
+ profile=diagnoses[parent]
318
+ self._event('DIAGNOSIS',**{key:value for key,value in profile.items() if key!='regions'},region_count=len(groups),profile_sha256=digest(profile));del oof,point,folds
319
+ diag=diagnoses[parent];order=mutation_order(diag['regions'],getattr(self,'_research_use_diagnosis',True));proposal=None
320
+ for gene in order:
321
+ if gene not in controls['genes']:continue
322
+ value=('OBJECTIVE_GAIN','BALANCE','MUL') if gene=='split_program' else 'depthwise'
323
+ if getattr(current.genome,gene)==value:continue
324
+ candidate=current.genome.mutate(**{gene:value});identity=digest(candidate.to_dict())
325
+ if (parent,identity) not in seen:proposal=candidate;seen.add((parent,identity));break
326
+ if proposal is None:break
327
+ tick=time.perf_counter();child=acquire(family,train,ty,proposal,hp,'DESCENDANT',self.random_state);child_risk=risk(task,sy,child.probabilities(search,res));accept=child_risk<current_risk-1e-4*max(1.,abs(current_risk))
328
+ self._event('MUTATION',generation=generation,parent_sha256=parent,child_sha256=digest(child.packet()),child_program=proposal.to_dict(),parent_SEARCH=current_risk,child_SEARCH=child_risk,decision='ACCEPT' if accept else 'ROLLBACK',seconds=time.perf_counter()-tick)
329
+ if accept:current=child;current_risk=child_risk;retain_anchor()
330
+ frozen=current.genome;self._event('PROGRAM_FROZEN',genome_sha256=digest(frozen.to_dict()))
331
+ self._research_structural_before_numeric=current
332
+ if self.use_optuna:
333
+ import optuna
334
+ maximum_estimators=plan['numeric_capacity']['maximum_estimators']
335
+ study=optuna.create_study(direction='minimize',sampler=optuna.samplers.TPESampler(seed=self.random_state,n_startup_trials=2))
336
+ for _ in range(trials):
337
+ res.check();trial=study.ask();self.last_attempt_['Optuna_trials']+=1
338
+ p=replace(hp,l2=trial.suggest_float('l2',.1,10.,log=True),min_samples_leaf=trial.suggest_int('min_samples_leaf',2,8))
339
+ if maximum_estimators>hp.n_estimators:p=replace(p,n_estimators=trial.suggest_int('n_estimators',hp.n_estimators,maximum_estimators))
340
+ self._event('OPTUNA_ASK',number=trial.number,genome_sha256=digest(frozen.to_dict()))
341
+ child=acquire(family,train,ty,frozen,p,'NUMERIC',self.random_state);value=risk(task,sy,child.probabilities(search,res));study.tell(trial,value)
342
+ if value<current_risk:current=child;current_risk=value;retain_anchor()
343
+ self._event('OPTUNA_COMPLETE',number=trial.number,SEARCH=value,parameters=asdict(p))
344
+ self._research_selected_before_refit=current
345
+ if self.refit_full:current=acquire(family,raw,targets,frozen,current.params,'REFIT_FULL',self.random_state)
346
+ self.model_=current;self.classes_=classes;self.task_=task;self.last_attempt_['bin_cache']=prepared.report();self._event('BIN_PREPARATION_CACHE',**prepared.report());prepared.clear()
347
+ self._event('FINAL_MODEL',input_rows=len(raw),selection_TRAIN_rows=len(train),SEARCH_rows=len(search),final_fit_source_rows=len(raw) if self.refit_full else len(train),refit_full=self.refit_full,SEARCH_primary_before_refit=current_risk,selection_metric_is_independent=False,nominal_columns=len(current.encoder.transform(raw)._nominal_sizes),source_storage_bytes=raw.storage_bytes,all_rows_accounted=True)
348
+ self.n_features_in_=raw.width;self.best_family_=family;self.best_params_=asdict(current.params)
349
+ self.last_attempt_['receipt_verification']=verify_receipts(self.last_attempt_['events'])
350
+ self.last_attempt_['resource_usage']=dict(baseline_RSS_bytes=res.base_rss,sampled_peak_RSS_bytes=res.peak_rss,baseline_private_commit_bytes=res.base_commit,sampled_peak_private_commit_bytes=res.peak_commit)
351
+ self.last_attempt_['scope']='PRIVATE_EXPERIMENTAL_NOT_PUBLISHED_2_0_OR_GENERAL_UTILITY_QUALIFICATION';cache.clear();gc.collect()
352
+ def _blocks(self,X):
353
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
354
+ res=self._resources();source=Input(X);k=len(self.classes_) if self.task_!='regression' else 1
355
+ schema=self.model_.encoder
356
+ if schema.names is None:
357
+ if source.names is not None:raise ValueError('Positional TRAIN requires positional prediction')
358
+ ordered=schema.columns
359
+ else:
360
+ if source.names is None or set(source.names)!=set(schema.names):raise ValueError('Feature schema mismatch')
361
+ columns=dict(zip(schema.names,schema.columns));ordered=[columns[name] for name in source.names]
362
+ if len(ordered)!=source.width:raise ValueError('Feature count mismatch')
363
+ # Infer representations from the fitted schema,never refit vocabularies
364
+ # from query dtypes. Preserve typed numeric nominal IDs through Table.
365
+ source.fitted_kinds=tuple(column['kind'] for column in ordered)
366
+ if self.time_budget=='auto':res.configure_time(inference_seconds(source.n,source.width,sum(len(m.trees) for m in self.model_.models),self.model_.params.max_depth))
367
+ member_rows=len(self.model_.models)*(40*k+64 if self.task_=='multiclass' else 40)
368
+ batch=max(1,min(16384,res.bytes_limit//max(1,(source.width*32+member_rows+k*128+256)*4)))
369
+ for start in range(0,source.n,batch):
370
+ raw=Table.read(source,res,start,min(source.n,start+batch));yield self.model_.probabilities(raw,res)
371
+ def predict(self,X):
372
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
373
+ import numpy as np
374
+ from .prediction_output import prediction_array
375
+ n=Input(X).n;res=self._resources()
376
+ res.reserve(n*40+len(self.classes_)*64,'Returned prediction array and class dtype')
377
+ dtype=np.float64 if self.task_=='regression' else prediction_array(self.classes_).dtype
378
+ result=np.empty(n,dtype=dtype);offset=0
379
+ for values in self._blocks(X):
380
+ res.check();stop=offset+len(values)
381
+ if stop>n:raise ValueError('Unexpected prediction row count')
382
+ if self.task_=='regression':result[offset:stop]=values
383
+ elif self.task_=='binary':result[offset:stop]=[self.classes_[int(p>=.5)] for p in values]
384
+ else:result[offset:stop]=[self.classes_[max(range(len(p)),key=p.__getitem__)] for p in values]
385
+ offset=stop
386
+ if offset!=n:raise ValueError('Incomplete prediction rows')
387
+ if self.task_=='regression' and not np.isfinite(result).all():raise ValueError('Finite regression predictions required')
388
+ res.check()
389
+ return result
390
+ def predict_proba(self,X):
391
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
392
+ if self.task_=='regression':raise AttributeError('Regression has no class probabilities')
393
+ import numpy as np
394
+ from .prediction_output import probability_array
395
+ n=Input(X).n;k=len(self.classes_);res=self._resources();res.reserve(n*(k*40+64),'Returned probability array and bounded validation')
396
+ result=np.empty((n,k),dtype=np.float64);offset=0
397
+ for values in self._blocks(X):
398
+ res.check();stop=offset+len(values)
399
+ if stop>n:raise ValueError('Unexpected probability row count')
400
+ result[offset:stop]=[[1-p,p] for p in values] if self.task_=='binary' else values
401
+ offset=stop
402
+ if offset!=n:raise ValueError('Incomplete probability rows')
403
+ return probability_array(result,k,n_samples=n,check=res.check)
404
+ def available_variants(self):
405
+ from .scalable_variants import describe
406
+ return describe(self)
407
+ def _variant_view(self,variant):
408
+ from .scalable_variants import view
409
+ return view(self,variant)
410
+ def save(self,path,*,variant='all'):
411
+ from .scalable_variants import save_bundle
412
+ result=save_bundle(self,path,variant)
413
+ if result is not None:return result
414
+ return self._save_single(path)
415
+ def _save_single(self,path):
416
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
417
+ res=self._serialization_resources('Model snapshot')
418
+ chained=bool(self.report_.get('events')) and all('receipt_schema' in e for e in self.report_['events'])
419
+ if chained:verify_receipts(self.report_['events'])
420
+ packet=dict(schema='SCALABLE_PRIVATE_MODEL_2' if chained else 'SCALABLE_PRIVATE_MODEL_1',model=self.model_.packet(),classes=self.classes_,report=self.report_)
421
+ if chained:packet['receipt_sha256']=digest(self.report_);packet['evidence']=dict(diagnostic_profiles=getattr(self,'diagnostic_profiles_',{}))
422
+ if chained:
423
+ packet['schema']='SCALABLE_PRIVATE_MODEL_3'
424
+ packet['constructor']=self._constructor_state()
425
+ packet['constructor_sha256']=digest(packet['constructor'])
426
+ raw=json.dumps(packet,sort_keys=True,allow_nan=False)
427
+ res.reserve(len(raw.encode())*4,'Serialized model');atomic_text(Path(path),raw+'\n');return hashlib.sha256(Path(path).read_bytes()).hexdigest()
428
+ def _serialization_resources(self,description,*,families=None):
429
+ res=self._resources()
430
+ families=list({id(f):f for f in (families or [self.model_])}.values())
431
+ members=[m for family in families for m in family.models]
432
+ nodes=sum(len(tree.nodes) for model in members for tree in model.trees)
433
+ cuts=sum(len(c) for model in members for c in model.binner.thresholds)
434
+ words=sum(len(v) for family in families for c in family.encoder.columns for v in c.get('vocabulary',()))
435
+ names=sum(len(v) for family in families for v in family.encoder.names or ())
436
+ labels=sum(len(v) for v in self.classes_ if isinstance(v,str))
437
+ estimate=1024+nodes*4096+cuts*64+(words+names+labels)*32+len(self.report_.get('events',()))*4096
438
+ if self.time_budget=='auto':res.configure_time(io_seconds(estimate))
439
+ res.reserve(estimate,description+' estimated serialization workspace');return res
440
+ def export_python(self,path,*,variant='selected',output_format='python'):
441
+ if output_format not in ('python','numpy'):raise ValueError('output_format must be python or numpy')
442
+ if variant!='selected':return self._variant_view(variant).export_python(path,output_format=output_format)
443
+ import inspect
444
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
445
+ res=self._serialization_resources('Standalone snapshot')
446
+ packet=dict(model=self.model_.packet(),classes=self.classes_)
447
+ guard_source=(_PACKAGE/'source_markers.py').read_text('utf8')
448
+ source='# Private frozen Python inference; no training or external libraries.\nMODEL='+repr(packet)+'\n'+guard_source+'\n'+inspect.getsource(memory_snapshot)+'\n'+inspect.getsource(memory_allowance)+'\n'+inspect.getsource(available_memory)+'\n'+inspect.getsource(_standalone)+'\ndef predict(X):return _standalone(X,MODEL,False)\ndef predict_proba(X):return _standalone(X,MODEL,True)\n'
449
+ if output_format=='numpy':
450
+ from .prediction_output import numpy_export_source
451
+ source=numpy_export_source(source,self.classes_,self.task_=='regression')
452
+ res.reserve(len(source.encode())*4,'Standalone source');atomic_text(Path(path),source);return hashlib.sha256(Path(path).read_bytes()).hexdigest()
453
+ @classmethod
454
+ def load(cls,path,*,sha256=None,memory_budget='auto',workdir=None):
455
+ from .api_compat import validate_workdir,wrap_historical
456
+ workdir=validate_workdir(workdir)
457
+ p=Path(path);size=regular_file_size(p);res=managed_resources(memory_budget,io_seconds(size));res.reserve(size*8,'Validated JSON model')
458
+ limit=max(1,res.bytes_limit//8)
459
+ raw=read_regular_bytes(p,limit)
460
+ res.reserve(len(raw)*8,'Immutable JSON snapshot')
461
+ actual_sha256=hashlib.sha256(raw).hexdigest()
462
+ if sha256 is not None and actual_sha256!=sha256:raise ValueError('Changed saved model')
463
+ packet=decode_json_bytes(raw,LoadLimits(max_bytes=limit),reserve=res.reserve)
464
+ if type(packet) is not dict:raise ValueError('Typed model envelope must be an object')
465
+ from .legacy_artifacts import PUBLIC_2_0_SCHEMA,restore_public_2_0
466
+ if packet.get('schema')==PUBLIC_2_0_SCHEMA:
467
+ if set(packet)!={'constructor','origins','schema','selected_variant','variants'} or type(packet['constructor']) is not dict or type(packet['origins']) is not dict or type(packet['selected_variant']) is not str or type(packet['variants']) is not dict or not packet['variants']:raise ValueError('Exact historical model envelope required')
468
+ del packet,raw
469
+ restored=restore_public_2_0(p,actual_sha256,cls.task,workdir=workdir);res.check()
470
+ return wrap_historical(restored,cls,memory_budget)
471
+ if packet.get('schema')=='SCALABLE_PRIVATE_VARIANTS_1':
472
+ from .scalable_variants import restore_bundle
473
+ return restore_bundle(cls,packet,res,limit,actual_sha256,sha256 is not None,memory_budget)
474
+ return cls._restore_packet(packet,res,limit,actual_sha256,sha256 is not None,memory_budget)
475
+ @classmethod
476
+ def _restore_packet(cls,packet,res,limit,actual_sha256,expected_checksum_supplied,memory_budget):
477
+ required={'schema','model','classes','report'}
478
+ if packet.get('schema') in ('SCALABLE_PRIVATE_MODEL_2','SCALABLE_PRIVATE_MODEL_3'):required.update(('receipt_sha256','evidence'))
479
+ if packet.get('schema')=='SCALABLE_PRIVATE_MODEL_3':required.update(('constructor','constructor_sha256'))
480
+ if set(packet)!=required or packet['schema'] not in ('SCALABLE_PRIVATE_MODEL_1','SCALABLE_PRIVATE_MODEL_2','SCALABLE_PRIVATE_MODEL_3') or type(packet['report']) is not dict:raise ValueError('Private model schema')
481
+ if packet['schema'] in ('SCALABLE_PRIVATE_MODEL_2','SCALABLE_PRIVATE_MODEL_3'):
482
+ if packet['receipt_sha256']!=digest(packet['report']):raise ValueError('Changed report identity')
483
+ verification=verify_receipts(packet['report'].get('events'))
484
+ report=packet['report'];events=report['events'];starts=[e for e in events if e['event']=='NATIVE_FIT_STARTED'];ends=[e for e in events if e['event']=='NATIVE_FIT_COMPLETED']
485
+ if any(type(e['ordinal']) is not int for e in starts) or [e['ordinal'] for e in starts]!=list(range(1,len(starts)+1)):raise ValueError('Invalid native component order')
486
+ if report.get('status')=='COMPLETED' and (events[-1]['event']!='FINAL_MODEL' or not starts):raise ValueError('Completed training requires fitted-component and finalization receipts')
487
+ if len(starts)!=report.get('native_fits') or sum(e['phase']=='OOF' for e in starts)!=report.get('OOF_fits') or sum(e['reserved_scalar_trees'] for e in starts)!=report.get('scalar_trees') or sum(e['event']=='OPTUNA_ASK' for e in events)!=report.get('Optuna_trials'):raise ValueError('Receipt/counter mismatch')
488
+ if report.get('status')=='COMPLETED' and (len(starts)!=len(ends) or [e['component_key'] for e in starts]!=[e['component_key'] for e in ends] or sum(e['actual_scalar_trees'] for e in ends)!=report.get('scalar_trees')):raise ValueError('Incomplete fitted-component chain')
489
+ evidence=packet['evidence']
490
+ if type(evidence) is not dict or set(evidence)!={'diagnostic_profiles'} or type(evidence['diagnostic_profiles']) is not dict:raise ValueError('Separate diagnostic evidence schema')
491
+ profiles=evidence['diagnostic_profiles'];receipts=[e for e in events if e['event']=='DIAGNOSIS']
492
+ if set(profiles)!={e['parent_sha256'] for e in receipts} or any(digest(profiles[e['parent_sha256']])!=e['profile_sha256'] for e in receipts):raise ValueError('Changed diagnostic artifact identity')
493
+ else:verification=dict(status='LEGACY_UNCHAINED_EXPERIMENTAL_REPORT',authority='Caller-supplied file custody only; not a verified operation chain')
494
+ v=packet['model']
495
+ if type(v) is not dict or set(v)!={'task','family','genome','params','encoder','members'} or v['task'] not in ('binary','multiclass','regression') or v['family'] not in ('boost','bag'):raise ValueError('Exact native model fields')
496
+ if cls.task=='classification' and v['task']=='regression' or cls.task=='regression' and v['task']!='regression':raise ValueError('Saved task is incompatible with requested facade')
497
+ facade=MorphTreeRegressor if v['task']=='regression' else MorphTreeClassifier
498
+ if packet['schema']=='SCALABLE_PRIVATE_MODEL_3':
499
+ state=packet['constructor']
500
+ if digest(state)!=packet['constructor_sha256']:raise ValueError('Changed refit configuration identity')
501
+ if type(state) is not dict or set(state)!={'schema','parameters','checkpoint_location_reset','scope'} or state['schema'] not in ('SCALABLE_REFIT_CONFIGURATION_1','SCALABLE_REFIT_CONFIGURATION_2') or type(state['parameters']) is not dict or type(state['checkpoint_location_reset']) is not bool or state['scope']!='FUTURE_REFIT_SETTINGS_AT_SNAPSHOT':raise ValueError('Exact typed refit configuration required')
502
+ params=state['parameters']
503
+ keys=_PARAMETERS_V1 if state['schema']=='SCALABLE_REFIT_CONFIGURATION_1' else _PARAMETERS_V2
504
+ if set(params)!=set(keys) or params['checkpoint_dir'] not in ('auto',None):raise ValueError('Safe refit parameters required;artifact output paths cannot be restored')
505
+ params=dict(params)
506
+ if state['schema']=='SCALABLE_REFIT_CONFIGURATION_1':
507
+ # Exact old contract; new constructor fields do not invalidate v1.
508
+ if any(type(params[k]) is not int or not 0<=params[k]<=2 for k in ('n_trials','max_mutations')):raise ValueError('Invalid legacy refit configuration limits')
509
+ params.update(n_jobs=1,mode='adaptive',operators=None)
510
+ elif params['operators'] is not None:
511
+ if type(params['operators']) is not list:raise ValueError('Serialized operator list required')
512
+ params['operators']=tuple(params['operators'])
513
+ if memory_budget!='auto':params['memory_budget']=memory_budget
514
+ obj=facade(**params)
515
+ recovery=dict(available=True,scope=state['scope'],checkpoint_location_reset=state['checkpoint_location_reset'],original_training_settings_inferred=False)
516
+ else:
517
+ obj=facade(memory_budget=memory_budget)
518
+ recovery=dict(available=False,scope='LEGACY_CONFIG_NOT_RECORDED_DEFAULTS_FOR_FUTURE_REFIT',original_training_settings_inferred=False)
519
+ from .native_lab_backend import bind_backend
520
+ enc=Encoder.from_state(v['encoder'],res);hp=HyperParams(**v['params']);genome=AlgorithmGenome(**v['genome']);nominal={i:len(c['vocabulary']) for i,c in enumerate(enc.columns) if c['kind']=='categorical'};backend=bind_backend()(nominal);models=[]
521
+ classes=packet['classes']
522
+ if type(classes) is not list:raise ValueError('Typed class-label list required')
523
+ if v['task']=='regression':
524
+ if classes:raise ValueError('Regression class labels must be empty')
525
+ else:
526
+ normalized=[label_scalar(c) for c in classes]
527
+ if any(c is None for c in normalized) or not (all(type(c) is str for c in normalized) or all(type(c) in (int,float) for c in normalized)):raise ValueError('Consistent finite scalar class labels required')
528
+ if normalized!=sorted(set(normalized)) or len(classes)<2 or v['task']=='binary' and len(classes)!=2:raise ValueError('Distinct canonical task-compatible class labels required')
529
+ if type(v['members']) is not list or len(v['members'])!=(1 if v['family']=='boost' else hp.n_estimators):raise ValueError('Exact bounded family members')
530
+ for raw in v['members']:
531
+ res.check()
532
+ actual=hp if v['family']=='boost' else replace(hp,n_estimators=1,learning_rate=1.)
533
+ limits=LoadLimits(max_features=len(enc.columns),max_trees=actual.n_estimators*(len(classes) if v['task']=='multiclass' else 1),max_nodes=limit,max_thresholds=limit)
534
+ model=decode_multiclass_booster(raw,actual,backend,len(classes),'softmax',limits,genome=genome) if v['task']=='multiclass' else decode_booster(raw,genome,actual,backend,limits,objective='squared_error' if v['task']=='regression' else 'binary_logloss')
535
+ if model.n_features_!=len(enc.columns):raise ValueError('Model/encoder feature mismatch')
536
+ models.append(model)
537
+ res.check()
538
+ obj.model_=Family(v['task'],genome,hp,enc,models,v['family']);obj.classes_=classes;obj.task_=v['task'];obj.report_=copy.deepcopy(packet['report']);obj.report_['receipt_verification']=verification
539
+ obj.report_['constructor_recovery']=recovery
540
+ obj.report_['file_custody']=dict(sha256=actual_sha256,expected_checksum_supplied=expected_checksum_supplied,scope='A local computed hash alone does not authenticate an author or scientific claim')
541
+ obj.diagnostic_profiles_=copy.deepcopy(packet.get('evidence',{}).get('diagnostic_profiles',{}))
542
+ anchor=obj.report_.get('last_verified_anchor')
543
+ if type(anchor) is dict and 'model' in anchor:obj.report_['last_verified_anchor']={k:value for k,value in anchor.items() if k not in ('model','classes')}|dict(model_sha256=digest(anchor['model']),legacy_embedded_model_omitted_from_report_view=True)
544
+ obj.n_features_in_=len(enc.columns);obj.best_family_=v['family'];obj.best_params_=asdict(hp);return obj
545
+ @classmethod
546
+ def load_checkpoint(cls,directory,*,memory_budget='auto',stage='current'):
547
+ """Recover current or selected TRAIN state;never resume training or auto-select from EXTERNAL."""
548
+ from .scalable_checkpoint import read_pointer
549
+ target,record=read_pointer(directory,managed_resources(memory_budget,150),stage=stage)
550
+ obj=cls.load(target,sha256=record['sha256'],memory_budget=memory_budget);events=obj.report_.get('events',[])
551
+ if not events or events[0]['call_id']!=record['call_id']:raise ValueError('Checkpoint call identity mismatch')
552
+ if record['final']:
553
+ final=events[-1]
554
+ if obj.report_.get('status')!='COMPLETED' or final['event']!='FINAL_MODEL' or record['source_role']!=('TRAIN_FULL' if final['refit_full'] else 'TRAIN'):raise ValueError('Checkpoint finalization/role mismatch')
555
+ elif obj.report_.get('status')!='PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT' or digest(obj.model_.packet())!=obj.report_.get('last_verified_anchor',{}).get('model_sha256'):raise ValueError('Checkpoint TRAIN anchor mismatch')
556
+ obj.report_['checkpoint_recovery']=dict(**record,training_resumed=False)
557
+ return obj
558
+ def get_params(self,deep=True):
559
+ params={k:getattr(self,k) for k in _PARAMETERS_V2}
560
+ return copy.deepcopy(params) if deep else params
561
+ def _constructor_state(self):
562
+ params=self.get_params()
563
+ if '_requested_random_state' in self.__dict__:params['random_state']=self._requested_random_state
564
+ reset=params['checkpoint_dir'] not in ('auto',None)
565
+ if reset:params['checkpoint_dir']='auto'
566
+ # Validate executable future settings; never deserialize artifact-chosen
567
+ # output destinations or present inferred defaults as old training facts.
568
+ type(self)(**params)
569
+ return dict(schema='SCALABLE_REFIT_CONFIGURATION_2',parameters=params,
570
+ checkpoint_location_reset=reset,scope='FUTURE_REFIT_SETTINGS_AT_SNAPSHOT')
571
+ def set_params(self,**values):
572
+ if not values:return self
573
+ params=self.get_params(deep=False)
574
+ if set(values)-set(params):raise ValueError('Unknown parameter')
575
+ replacement=type(self)(**(params|values));self.__dict__.clear();self.__dict__.update(replacement.__dict__);return self
576
+ def get_algorithm_program(self,*,variant='selected'):
577
+ if variant!='selected':return self._variant_view(variant).get_algorithm_program()
578
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
579
+ frozen=any(e['event']=='PROGRAM_FROZEN' and e['genome_sha256']==digest(self.model_.genome.to_dict()) for e in self.report_.get('events',[]))
580
+ return dict(schema='SCALABLE_EVOLVED_PROGRAM_1',task=self.task_,family=self.model_.family,genome=self.model_.genome.to_dict(),frozen_before_numeric=frozen,nominal_split='ONE_CATEGORY_VS_REST',training_program_export=True,training_export_scope='FROZEN_FAMILY_NOT_END_TO_END_SEARCH')
581
+ def get_recipe(self,*,variant='selected'):
582
+ if variant!='selected':return self._variant_view(variant).get_recipe()
583
+ return dict(program=self.get_algorithm_program(),parameters=asdict(self.model_.params),preprocessing=copy.deepcopy(self.model_.encoder.state()),classes=list(self.classes_),role='FITTED_MODEL_RECIPE_NOT_AN_EXPORTED_TRAINING_PROCEDURE')
584
+ def get_mutation_history(self):
585
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
586
+ result=[];depth={}
587
+ for e in self.report_.get('events',[]):
588
+ if e['event']!='MUTATION':continue
589
+ parent=e['parent_sha256'];depth.setdefault(parent,0);value=copy.deepcopy(e);value['parent_genealogical_depth']=depth[parent]
590
+ if e['decision']=='ACCEPT':depth[e['child_sha256']]=depth[parent]+1
591
+ value['retained_sha256']=e['child_sha256'] if e['decision']=='ACCEPT' else parent;value['retained_genealogical_depth']=depth[value['retained_sha256']];result.append(value)
592
+ return result
593
+ def report(self):
594
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
595
+ from .scalable_variants import active_metadata
596
+ result=copy.deepcopy(self.report_);result['variant_selection']=active_metadata(self)
597
+ result['trace_scope']='Original fit history and costs; active variant source role is stated separately'
598
+ return result
599
+ def get_search_report(self):return self.report()
600
+ def cost_report(self):
601
+ report=self.report()
602
+ native={}
603
+ for event in report.get('events',()):
604
+ if event['event']=='NATIVE_FIT_COMPLETED':
605
+ phase=event['phase'];native[phase]=native.get(phase,0.)+event['seconds']
606
+ return dict(scope='RECORDED_FIT_COST_NOT_NEW_MEASUREMENT',
607
+ native_fits=report.get('native_fits'),OOF_fits=report.get('OOF_fits'),
608
+ Optuna_trials=report.get('Optuna_trials'),
609
+ scalar_tree_reservations=report.get('scalar_trees'),
610
+ completed_scalar_trees=sum(e['actual_scalar_trees'] for e in report.get('events',()) if e['event']=='NATIVE_FIT_COMPLETED'),
611
+ total_fit_seconds=report.get('seconds'),native_fit_seconds_by_phase=native,
612
+ observed_resources=copy.deepcopy(report.get('resource_usage',{})),
613
+ native_time_scope='Native fit intervals only;OOF included. Excludes preprocessing,diagnosis bookkeeping,Optuna overhead and persistence;not a complete disjoint wall-time partition.',
614
+ new_training_from_this_query=0)
615
+ def save_recipe(self,path,*,variant='selected'):
616
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
617
+ if variant!='selected':return self._variant_view(variant).save_recipe(path)
618
+ res=self._serialization_resources('Recipe snapshot')
619
+ raw=json.dumps(self.get_recipe(),sort_keys=True,allow_nan=False)+'\n'
620
+ res.reserve(len(raw.encode('utf8'))*4,'Serialized recipe')
621
+ atomic_text(Path(path),raw)
622
+ return hashlib.sha256(raw.encode('utf8')).hexdigest()
623
+ def export_standalone(self,path,*,variant='selected',output_format='python'):
624
+ """Export the fitted dependency-free predictor; this does not export training."""
625
+ return self.export_python(path,variant=variant,output_format=output_format)
626
+ def export_algorithm(self,path):
627
+ """Export the frozen family trainer, distinct from fitted inference.
628
+
629
+ train_encoded requires explicit TRAIN identity. This fragment does not
630
+ rerun family screening, OOF, evolution or numerical selection.
631
+ """
632
+ from .training_fragment import export_algorithm
633
+ return export_algorithm(self,path)
634
+ @property
635
+ def stop_reason_(self):
636
+ if not hasattr(self,'model_'):raise AttributeError('stop_reason_ requires a fitted model')
637
+ report=self.report_
638
+ if 'stop_reason' in report:return report['stop_reason']
639
+ if report.get('status')=='COMPLETED':return 'PLANNED_SEARCH_COMPLETE'
640
+ if report.get('status')=='PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT':return 'PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT'
641
+ return 'LEGACY_STOP_REASON_NOT_RECORDED'
642
+ @property
643
+ def feature_names_in_(self):
644
+ if not hasattr(self,'model_'):raise AttributeError('feature_names_in_ requires a fitted model')
645
+ names=self.model_.encoder.names
646
+ return list(names) if names is not None else None
647
+ def _score(self,X,y):
648
+ self._ordinary_context_guard()
649
+ if controlled_source(y):raise ValueError('Controlled sources cannot use ordinary scoring')
650
+ if isinstance(y,(str,bytes,dict)) or getattr(y,'ndim',1)!=1 or not hasattr(y,'__len__'):
651
+ raise ValueError('score requires nonempty aligned scalar targets')
652
+ prediction=self.predict(X);n=len(prediction)
653
+ if not n or len(y)!=n:raise ValueError('score requires nonempty aligned scalar targets')
654
+ res=self._resources()
655
+ if self.time_budget=='auto':res.configure_time(inference_seconds(n,1,0,0))
656
+ regression=self.task=='regression'
657
+ if regression:res.reserve(n*8,'Compact scoring targets');targets=array('d')
658
+ correct=0;count=0
659
+ for raw in y:
660
+ if count%4096==0:res.check()
661
+ if count>=n:raise ValueError('Score target iterator/length mismatch')
662
+ value=scalar(raw) if regression else label_scalar(raw)
663
+ if value is None:raise ValueError('Missing score targets are unsupported')
664
+ if regression:
665
+ if type(value) not in (int,float):raise ValueError('Regression score requires finite numeric targets')
666
+ targets.append(value)
667
+ else:
668
+ predicted=prediction[count]
669
+ correct+=value==(predicted.item() if hasattr(predicted,'item') else predicted)
670
+ count+=1
671
+ if count!=n:raise ValueError('Score target iterator/length mismatch')
672
+ if not regression:return correct/n
673
+ def residuals():
674
+ for index,(target,predicted) in enumerate(zip(targets,prediction)):
675
+ if index%4096==0:res.check()
676
+ value=target-predicted.item()
677
+ if not math.isfinite(value):raise ArithmeticError('Regression residual outside floating-point range')
678
+ yield value
679
+ error_scale=max(map(abs,residuals()),default=0.)
680
+ if error_scale==0:return 1.
681
+ if all(value==targets[0] for value in targets):return 0.
682
+ mean=math.fsum(value/n for value in targets)
683
+ def centered():
684
+ for index,value in enumerate(targets):
685
+ if index%4096==0:res.check()
686
+ difference=value-mean
687
+ if not math.isfinite(difference):raise ArithmeticError('Regression spread outside floating-point range')
688
+ yield difference
689
+ spread_scale=max(map(abs,centered()),default=0.)
690
+ if spread_scale==0:return 0.
691
+ error=error_scale*math.sqrt(math.fsum((value/error_scale)**2 for value in residuals())/n)
692
+ spread=spread_scale*math.sqrt(math.fsum((value/spread_scale)**2 for value in centered())/n)
693
+ ratio=error/spread;result=1-ratio*ratio
694
+ if not math.isfinite(result):raise ArithmeticError('R2 outside floating-point range')
695
+ return result
696
+ def get_diagnostics(self):
697
+ if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
698
+ return copy.deepcopy(getattr(self,'diagnostic_profiles_',{}))
699
+
700
+
701
+ class MorphTreeClassifier(_Scalable):
702
+ task='classification'
703
+ _estimator_type='classifier'
704
+ def score(self,X,y):return self._score(X,y)
705
+ class MorphTreeRegressor(_Scalable):
706
+ task='regression'
707
+ _estimator_type='regressor'
708
+ def score(self,X,y):return self._score(X,y)
709
+ @property
710
+ def predict_proba(self):raise AttributeError('Regression has no class probabilities')
711
+
712
+
713
+ def _standalone(X,packet,probabilities):
714
+ import bisect,json,math,sys
715
+ from numbers import Real
716
+ if controlled_source(X):raise ValueError('Controlled sources cannot use ordinary standalone inference')
717
+ model=packet['model'];schema=model['encoder'];classes=packet['classes'];task=model['task'];columns=schema['columns'];named=hasattr(X,'columns') and hasattr(X,'itertuples')
718
+ names=list(X.columns) if named else None
719
+ if schema['names'] is not None:
720
+ if names is None or len(names)!=len(columns) or set(names)!=set(schema['names']):raise ValueError('Feature schema mismatch')
721
+ positions={name:i for i,name in enumerate(names)};order=[positions[n] for n in schema['names']]
722
+ else:
723
+ if names is not None:raise ValueError('Positional TRAIN schema')
724
+ order=list(range(len(columns)))
725
+ if not len(X):raise ValueError('Nonempty inference input')
726
+ allowance=memory_allowance('auto',memory_snapshot(available_memory())['effective_available_bytes'])
727
+ if len(X)*(len(classes)*40+64 if probabilities else 40)>allowance:raise MemoryError('Standalone returned-result estimate exceeds currently available memory allowance')
728
+ if probabilities and task=='regression':raise AttributeError('Regression has no probabilities')
729
+ rows=X.itertuples(index=False,name=None) if named else iter(X)
730
+ def category_key(v):
731
+ if not isinstance(v,str):
732
+ if type(v) is int:v=v if abs(v)>2**53 else float(v)
733
+ else:
734
+ v=float(v)
735
+ if abs(v)>2**53 and v.is_integer():v=int(v)
736
+ if v==0:v=0.0
737
+ return json.dumps(['s' if isinstance(v,str) else 'n',v],ensure_ascii=False,allow_nan=False,separators=(',',':'))
738
+ lookups={j:{category_key(json.loads(v)[1]):i+1 for i,v in enumerate(c['vocabulary'])} for j,c in enumerate(columns) if c['kind']=='categorical'}
739
+ def sigmoid(v):
740
+ if v>=0:return 1/(1+math.exp(-min(v,745.)))
741
+ z=math.exp(max(v,-745.));return z/(1+z)
742
+ out=[]
743
+ for row in rows:
744
+ if len(row)!=len(columns):raise ValueError('Feature count mismatch')
745
+ values=[]
746
+ for j,c in enumerate(columns):
747
+ v=row[order[j]]
748
+ if isinstance(v,str) and len(v)*32+512>allowance:raise MemoryError('Standalone scalar/key workspace exceeds available allowance')
749
+ if type(v).__module__.split('.')[0]=='numpy' and hasattr(v,'item'):v=v.item()
750
+ missing=v is None or (sys.modules.get('pandas') is not None and v is getattr(sys.modules['pandas'],'NA',None)) or isinstance(v,Real) and math.isnan(float(v))
751
+ if not missing and not(isinstance(v,str) or isinstance(v,Real) and math.isfinite(float(v))):raise ValueError('Invalid scalar')
752
+ if c['kind']=='numeric':
753
+ if isinstance(v,str):raise ValueError('String in numeric feature')
754
+ values.append(math.nan if missing else float(v))
755
+ else:
756
+ k=len(c['vocabulary']);key=None if missing else category_key(v)
757
+ values.append(k+1 if missing else lookups[j].get(key,k+2))
758
+ predictions=[]
759
+ for member in model['members']:
760
+ bins=[int(v) if j in lookups else 0 if math.isnan(v) else bisect.bisect_right(member['binner']['thresholds'][j],v)+1 for j,v in enumerate(values)]
761
+ raw=list(member['base_score']) if task=='multiclass' else member['base_score']
762
+ for index,(tree,step) in enumerate(zip(member['trees'],member['steps'])):
763
+ nodes=tree['nodes'];node=nodes[0]
764
+ for _ in range(len(nodes)+1):
765
+ if node['is_leaf']:break
766
+ j=node['feature'];b=bins[j];left=(b==node['threshold_bin']) if j in lookups else node['missing_left'] if b==0 else b<=node['threshold_bin'];node=nodes[node['left'] if left else node['right']]
767
+ else:raise ValueError('Cyclic model')
768
+ if task=='multiclass':
769
+ raw[index%len(classes)]+=step*node['value']
770
+ if index%len(classes)==len(classes)-1:
771
+ mean=math.fsum(raw)/len(raw);raw=[v-mean for v in raw]
772
+ else:raw+=step*node['value']
773
+ if task=='multiclass':
774
+ weights=[math.exp(v-max(raw)) for v in raw];total=math.fsum(weights);predictions.append([v/total for v in weights])
775
+ else:predictions.append(raw if task=='regression' else sigmoid(raw))
776
+ if task=='multiclass':p=[math.fsum(v[j] for v in predictions)/len(predictions) for j in range(len(classes))]
777
+ else:p=math.fsum(predictions)/len(predictions)
778
+ if probabilities:out.append([1-p,p] if task=='binary' else p)
779
+ elif task=='regression':out.append(p)
780
+ else:out.append(classes[int(p>=.5)] if task=='binary' else classes[max(range(len(p)),key=p.__getitem__)])
781
+ return out