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.
- morphtree/__init__.py +28 -0
- morphtree/__main__.py +3 -0
- morphtree/_experimental/__init__.py +1 -0
- morphtree/_experimental/admission.py +364 -0
- morphtree/_experimental/base.py +215 -0
- morphtree/_experimental/component_cache_v2.py +263 -0
- morphtree/_experimental/evolution_report.py +307 -0
- morphtree/_experimental/execution_v2.py +697 -0
- morphtree/_experimental/facade.py +289 -0
- morphtree/_experimental/integrated.py +748 -0
- morphtree/_experimental/learning.py +418 -0
- morphtree/_experimental/manifest.json +204 -0
- morphtree/_experimental/numeric_fixture.py +21 -0
- morphtree/_experimental/parameter_contract.py +38 -0
- morphtree/_experimental/postfit.py +51 -0
- morphtree/_experimental/preprocessing.py +133 -0
- morphtree/_experimental/processes.py +191 -0
- morphtree/_experimental/producer.py +542 -0
- morphtree/_experimental/product.py +156 -0
- morphtree/_experimental/product_cli.py +43 -0
- morphtree/_experimental/roundtrip_v3.py +311 -0
- morphtree/_experimental/runtime_io.py +65 -0
- morphtree/_experimental/sampling.py +26 -0
- morphtree/_experimental/support.py +281 -0
- morphtree/_experimental/target.py +59 -0
- morphtree/_experimental/training.py +53 -0
- morphtree/_experimental/worker.py +682 -0
- morphtree/_installed.py +17 -0
- morphtree/_native/__init__.py +1 -0
- morphtree/_native/bin/win_amd64/morphtree_kernels.dll +0 -0
- morphtree/_native/buffers.py +82 -0
- morphtree/_native/loader.py +39 -0
- morphtree/_native/manifest.json +29 -0
- morphtree/_native/runtime.py +48 -0
- morphtree/_native/src/histogram.c +61 -0
- morphtree/_native/src/traversal.c +51 -0
- morphtree/api.py +388 -0
- morphtree/api_compat.py +65 -0
- morphtree/backend.py +162 -0
- morphtree/booster.py +252 -0
- morphtree/cli.py +52 -0
- morphtree/config.py +262 -0
- morphtree/control.py +20 -0
- morphtree/data.py +300 -0
- morphtree/diagnostics.py +158 -0
- morphtree/estimators.py +156 -0
- morphtree/examples.py +25 -0
- morphtree/exporting.py +224 -0
- morphtree/legacy_artifacts.py +32 -0
- morphtree/metrics.py +156 -0
- morphtree/model.py +1010 -0
- morphtree/mutations.py +205 -0
- morphtree/native_lab_backend.py +79 -0
- morphtree/numpy_backend.py +180 -0
- morphtree/optuna_tuner.py +154 -0
- morphtree/persistence.py +414 -0
- morphtree/prediction_output.py +35 -0
- morphtree/prepared_bins.py +82 -0
- morphtree/public.py +106 -0
- morphtree/resource_plan.py +170 -0
- morphtree/scalable.py +781 -0
- morphtree/scalable_backend.py +74 -0
- morphtree/scalable_checkpoint.py +129 -0
- morphtree/scalable_data.py +255 -0
- morphtree/scalable_receipts.py +92 -0
- morphtree/scalable_target.py +68 -0
- morphtree/scalable_variants.py +133 -0
- morphtree/sklearn_view.py +79 -0
- morphtree/source_markers.py +25 -0
- morphtree/species.py +47 -0
- morphtree/temporary.py +65 -0
- morphtree/training_fragment.py +130 -0
- morphtree/tree.py +231 -0
- morphtree/tuning.py +150 -0
- morphtree-2.1.0.dist-info/METADATA +212 -0
- morphtree-2.1.0.dist-info/RECORD +79 -0
- morphtree-2.1.0.dist-info/WHEEL +4 -0
- morphtree-2.1.0.dist-info/entry_points.txt +2 -0
- morphtree-2.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Retained genuine model variants. Never train or fabricate absent weights.
|
|
2
|
+
|
|
3
|
+
pure and tuned are verified TRAIN-stage models. selected is the default final
|
|
4
|
+
model, normally refitted on all eligible input. A bundle stores each distinct
|
|
5
|
+
weight packet once and the original fit history once; no dataset is embedded.
|
|
6
|
+
"""
|
|
7
|
+
import copy,hashlib,json
|
|
8
|
+
from dataclasses import asdict,replace
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from .persistence import atomic_text
|
|
11
|
+
|
|
12
|
+
SCHEMA='SCALABLE_PRIVATE_VARIANTS_1'
|
|
13
|
+
NAMES=frozenset(('selected','pure','tuned'))
|
|
14
|
+
BASE_KEYS=frozenset(('schema','model','classes','report','receipt_sha256','evidence','constructor','constructor_sha256'))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _role(report):
|
|
18
|
+
if report.get('status')=='PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT':return 'TRAIN'
|
|
19
|
+
final=next((e for e in reversed(report.get('events',())) if e['event']=='FINAL_MODEL'),None)
|
|
20
|
+
return 'TRAIN_FULL' if final and final['refit_full'] else 'TRAIN' if final else 'UNKNOWN'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def catalog(estimator):
|
|
24
|
+
if not hasattr(estimator,'model_'):raise RuntimeError('Not fitted')
|
|
25
|
+
if hasattr(estimator,'_retained_variants'):return estimator._retained_variants
|
|
26
|
+
result={'selected':dict(model=estimator.model_,origin='SELECTED_FITTED_MODEL',source_role=_role(estimator.report_))}
|
|
27
|
+
pure=getattr(estimator,'_research_structural_before_numeric',None)
|
|
28
|
+
if pure is not None:
|
|
29
|
+
result['pure']=dict(model=pure,origin='PRE_FINAL_NUMERIC_SEARCH',source_role='TRAIN')
|
|
30
|
+
tuned=getattr(estimator,'_research_selected_before_refit',None)
|
|
31
|
+
if tuned is not None and tuned is not pure:
|
|
32
|
+
result['tuned']=dict(model=tuned,origin='POST_FINAL_NUMERIC_SEARCH',source_role='TRAIN')
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve(estimator,name):
|
|
37
|
+
if type(name) is not str or name not in NAMES:raise ValueError('Variant must be selected, pure or tuned')
|
|
38
|
+
if name=='selected':name=getattr(estimator,'_variant_selection','selected')
|
|
39
|
+
variants=catalog(estimator)
|
|
40
|
+
if name not in variants:raise ValueError('Variant has no retained genuine weights; no implicit training is performed')
|
|
41
|
+
return name,variants[name]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def active_metadata(estimator):
|
|
45
|
+
name,item=resolve(estimator,'selected')
|
|
46
|
+
return dict(name=name,origin=item['origin'],source_role=item['source_role'],additional_training=False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def describe(estimator):
|
|
50
|
+
from .scalable import digest
|
|
51
|
+
variants=catalog(estimator);estimator._serialization_resources('Variant inspection',families=[v['model'] for v in variants.values()])
|
|
52
|
+
selected=resolve(estimator,'selected')[0]
|
|
53
|
+
return {name:dict(origin=v['origin'],source_role=v['source_role'],params=asdict(v['model'].params),model_sha256=digest(v['model'].packet()),exportable=True,is_selected=name==selected,additional_training=False) for name,v in variants.items()}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def view(estimator,name):
|
|
57
|
+
name,item=resolve(estimator,name);other=copy.copy(estimator)
|
|
58
|
+
other._retained_variants=catalog(estimator);other._variant_selection=name
|
|
59
|
+
other.model_=item['model'];other.best_family_=other.model_.family;other.best_params_=asdict(other.model_.params)
|
|
60
|
+
return other
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _binding(selected,variants,models):
|
|
64
|
+
return dict(selected_variant=selected,variants=variants,models=models)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def save_bundle(estimator,path,variant):
|
|
68
|
+
from .scalable import digest,verify_receipts
|
|
69
|
+
if type(variant) is not str or variant not in NAMES|{'all'}:raise ValueError('Variant must be all, selected, pure or tuned')
|
|
70
|
+
available=catalog(estimator)
|
|
71
|
+
if variant=='all':names=list(available);selected=resolve(estimator,'selected')[0]
|
|
72
|
+
else:selected=resolve(estimator,variant)[0];names=[selected]
|
|
73
|
+
# Preserve existing single-model formats for old objects without retained
|
|
74
|
+
# phase weights; do not relabel their final model as an unrecorded pure fit.
|
|
75
|
+
if names==['selected'] and set(available)=={'selected'}:return None
|
|
76
|
+
res=estimator._serialization_resources('Variant bundle',families=[available[n]['model'] for n in names])
|
|
77
|
+
verify_receipts(estimator.report_['events'])
|
|
78
|
+
models={};variants={}
|
|
79
|
+
for name in names:
|
|
80
|
+
res.check();item=available[name];packet=item['model'].packet();identity=digest(packet)
|
|
81
|
+
models.setdefault(identity,packet)
|
|
82
|
+
variants[name]=dict(origin=item['origin'],source_role=item['source_role'],model_sha256=identity)
|
|
83
|
+
primary=models.pop(variants[selected]['model_sha256'])
|
|
84
|
+
constructor=estimator._constructor_state()
|
|
85
|
+
packet=dict(schema=SCHEMA,model=primary,classes=estimator.classes_,report=estimator.report_,receipt_sha256=digest(estimator.report_),
|
|
86
|
+
evidence=dict(diagnostic_profiles=getattr(estimator,'diagnostic_profiles_',{})),constructor=constructor,constructor_sha256=digest(constructor),
|
|
87
|
+
selected_variant=selected,variants=variants,models=models,variants_sha256=digest(_binding(selected,variants,models)))
|
|
88
|
+
raw=json.dumps(packet,sort_keys=True,allow_nan=False)+'\n';res.reserve(len(raw.encode('utf8'))*4,'Encoded unique-weight variant bundle')
|
|
89
|
+
atomic_text(Path(path),raw);return hashlib.sha256(raw.encode('utf8')).hexdigest()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def restore_bundle(cls,packet,res,limit,file_sha256,expected_checksum_supplied,memory_budget):
|
|
93
|
+
from .scalable import digest
|
|
94
|
+
required=BASE_KEYS|{'selected_variant','variants','models','variants_sha256'}
|
|
95
|
+
if set(packet)!=required or packet['schema']!=SCHEMA:raise ValueError('Exact variant bundle envelope required')
|
|
96
|
+
variants=packet['variants'];models=packet['models'];selected=packet['selected_variant']
|
|
97
|
+
if type(variants) is not dict or not variants or not set(variants)<=NAMES or type(models) is not dict or len(models)>2 or type(selected) is not str or selected not in variants:
|
|
98
|
+
raise ValueError('Finite named retained variants required')
|
|
99
|
+
if digest(_binding(selected,variants,models))!=packet['variants_sha256']:raise ValueError('Changed variant bundle identity')
|
|
100
|
+
for name,item in variants.items():
|
|
101
|
+
if type(item) is not dict or set(item)!={'origin','source_role','model_sha256'}:raise ValueError('Exact variant identity/role required')
|
|
102
|
+
expected='SELECTED_FITTED_MODEL' if name=='selected' else 'PRE_FINAL_NUMERIC_SEARCH' if name=='pure' else 'POST_FINAL_NUMERIC_SEARCH'
|
|
103
|
+
if item['origin']!=expected or item['source_role'] not in (('TRAIN','TRAIN_FULL','UNKNOWN') if name=='selected' else ('TRAIN',)):
|
|
104
|
+
raise ValueError('Variant origin/source role mismatch')
|
|
105
|
+
h=item['model_sha256']
|
|
106
|
+
if type(h) is not str or len(h)!=64 or any(c not in '0123456789abcdef' for c in h):raise ValueError('Variant model digest required')
|
|
107
|
+
primary=variants[selected]['model_sha256']
|
|
108
|
+
if digest(packet['model'])!=primary:raise ValueError('Changed selected variant weights')
|
|
109
|
+
if set(models)!={v['model_sha256'] for v in variants.values()}-{primary}:raise ValueError('Missing/unreferenced retained weight packet')
|
|
110
|
+
if any(type(v) is not dict or digest(v)!=h for h,v in models.items()):raise ValueError('Changed retained variant weights')
|
|
111
|
+
base={k:packet[k] for k in BASE_KEYS};base['schema']='SCALABLE_PRIVATE_MODEL_3'
|
|
112
|
+
obj=cls._restore_packet(base,res,limit,file_sha256,expected_checksum_supplied,memory_budget)
|
|
113
|
+
if 'selected' in variants and variants['selected']['source_role']!=_role(packet['report']):raise ValueError('Selected role must match original fit finalization')
|
|
114
|
+
families={primary:obj.model_}
|
|
115
|
+
for identity,model in models.items():
|
|
116
|
+
res.check();part=cls._restore_packet(dict(base,model=model),res,limit,file_sha256,expected_checksum_supplied,memory_budget)
|
|
117
|
+
if (part.task_,part.classes_,part.model_.family,part.model_.genome)!=(obj.task_,obj.classes_,obj.model_.family,obj.model_.genome):raise ValueError('Variant task/family/frozen program mismatch')
|
|
118
|
+
if part.model_.encoder.names!=obj.model_.encoder.names or [c['kind'] for c in part.model_.encoder.columns]!=[c['kind'] for c in obj.model_.encoder.columns]:raise ValueError('Variant feature schema mismatch')
|
|
119
|
+
families[identity]=part.model_
|
|
120
|
+
if 'tuned' in variants:
|
|
121
|
+
identity=variants['tuned']['model_sha256'];family=families[identity]
|
|
122
|
+
if identity!=packet['report'].get('last_verified_anchor',{}).get('model_sha256'):raise ValueError('Tuned weights must match retained TRAIN anchor')
|
|
123
|
+
if not any(e['event']=='OPTUNA_COMPLETE' and e['parameters']==asdict(family.params) for e in packet['report']['events']):raise ValueError('Tuned variant needs a completed numerical trial')
|
|
124
|
+
if 'pure' in variants:
|
|
125
|
+
identity=variants['pure']['model_sha256'];family=families[identity]
|
|
126
|
+
capacity=family.params if family.family=='boost' else replace(family.params,n_estimators=1,learning_rate=1.)
|
|
127
|
+
if not any(e['event']=='NATIVE_FIT_STARTED' and e['phase']=='SCREEN' and e['family']==family.family and e['params_sha256']==digest(asdict(capacity)) for e in packet['report']['events']):raise ValueError('Pure variant needs qualified untuned capacity')
|
|
128
|
+
history=[e for e in packet['report']['events'] if e['event']=='MUTATION']
|
|
129
|
+
if history:
|
|
130
|
+
last=history[-1];expected=last['child_sha256'] if last['decision']=='ACCEPT' else last['parent_sha256']
|
|
131
|
+
if identity!=expected:raise ValueError('Pure variant must match retained structural lineage')
|
|
132
|
+
obj._retained_variants={name:dict(model=families[v['model_sha256']],origin=v['origin'],source_role=v['source_role']) for name,v in variants.items()}
|
|
133
|
+
obj._variant_selection=selected;return obj
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Optional sklearn parameter-routing adapter for the native public estimator.
|
|
2
|
+
|
|
3
|
+
The underlying facade already returns NumPy arrays.
|
|
4
|
+
This view delegates fitting to that same facade and adds no training, tuning,
|
|
5
|
+
sampling or preprocessing. sklearn is optional; NumPy is used only for arrays.
|
|
6
|
+
"""
|
|
7
|
+
from .scalable import _Scalable
|
|
8
|
+
from .scalable_data import Input
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _labels(values):
|
|
12
|
+
from .prediction_output import prediction_array
|
|
13
|
+
return prediction_array(values)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _ClassifierMethod:
|
|
17
|
+
"""Keep the public response-method name while hiding it for regression."""
|
|
18
|
+
def __init__(self,function):self.function=function
|
|
19
|
+
def __get__(self,instance,owner):
|
|
20
|
+
if instance is None:return self.function
|
|
21
|
+
if instance.estimator.task!='classification':raise AttributeError('Regression has no probabilities')
|
|
22
|
+
return self.function.__get__(instance,owner)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SklearnView:
|
|
26
|
+
"""Explicit array-output adapter with estimator__parameter configuration.
|
|
27
|
+
|
|
28
|
+
Qualify external tools separately; sample weights, sparse data, metadata
|
|
29
|
+
routing and all estimator-check contracts are not implied by this view.
|
|
30
|
+
Saving via estimator.save() persists the native model; restore it and call
|
|
31
|
+
as_sklearn() again. Cloning always produces an unfitted native estimator.
|
|
32
|
+
"""
|
|
33
|
+
def __init__(self,estimator):
|
|
34
|
+
if not isinstance(estimator,_Scalable) or estimator.task not in ('classification','regression'):
|
|
35
|
+
raise TypeError('A native MorphTree task facade is required')
|
|
36
|
+
self.estimator=estimator
|
|
37
|
+
def get_params(self,deep=True):
|
|
38
|
+
params={'estimator':self.estimator}
|
|
39
|
+
if deep:params.update({'estimator__'+k:v for k,v in self.estimator.get_params(deep=True).items()})
|
|
40
|
+
return params
|
|
41
|
+
def set_params(self,**values):
|
|
42
|
+
if not values:return self
|
|
43
|
+
candidate=values.get('estimator',self.estimator)
|
|
44
|
+
type(self)(candidate)
|
|
45
|
+
unknown=[k for k in values if k!='estimator' and not k.startswith('estimator__')]
|
|
46
|
+
if unknown:raise ValueError('Unknown view parameter: '+', '.join(sorted(unknown)))
|
|
47
|
+
nested={k[len('estimator__'):]:v for k,v in values.items() if k!='estimator'}
|
|
48
|
+
if nested:candidate.set_params(**nested)
|
|
49
|
+
self.estimator=candidate
|
|
50
|
+
return self
|
|
51
|
+
@property
|
|
52
|
+
def _estimator_type(self):return self.estimator._estimator_type
|
|
53
|
+
def __sklearn_tags__(self):return self.estimator.__sklearn_tags__()
|
|
54
|
+
def __sklearn_is_fitted__(self):return self.estimator.__sklearn_is_fitted__()
|
|
55
|
+
@property
|
|
56
|
+
def classes_(self):
|
|
57
|
+
if self.estimator.task!='classification':raise AttributeError('Regression has no classes')
|
|
58
|
+
self.estimator._resources().reserve(len(self.estimator.classes_)*64,'Sklearn class-label array')
|
|
59
|
+
return _labels(self.estimator.classes_)
|
|
60
|
+
@property
|
|
61
|
+
def n_features_in_(self):return self.estimator.n_features_in_
|
|
62
|
+
def fit(self,X,y):
|
|
63
|
+
self.estimator.fit(X,y)
|
|
64
|
+
return self
|
|
65
|
+
def predict(self,X):
|
|
66
|
+
if not self.estimator.__sklearn_is_fitted__():raise RuntimeError('Not fitted')
|
|
67
|
+
resources=self.estimator._resources()
|
|
68
|
+
resources.reserve(Input(X).n*64,'Native prediction list plus sklearn output array')
|
|
69
|
+
values=self.estimator.predict(X);resources.check()
|
|
70
|
+
return values
|
|
71
|
+
@_ClassifierMethod
|
|
72
|
+
def predict_proba(self,X):
|
|
73
|
+
import numpy as np
|
|
74
|
+
if not self.estimator.__sklearn_is_fitted__():raise RuntimeError('Not fitted')
|
|
75
|
+
resources=self.estimator._resources()
|
|
76
|
+
resources.reserve(Input(X).n*(len(self.estimator.classes_)*48+64),'Native probability rows plus sklearn output array')
|
|
77
|
+
values=self.estimator.predict_proba(X);resources.check()
|
|
78
|
+
return values
|
|
79
|
+
def score(self,X,y):return self.estimator.score(X,y)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Explicit/dynamic source metadata is distinct from pandas column/index data.
|
|
2
|
+
|
|
3
|
+
Only genuine inherited pandas attribute handlers qualify for the data-forwarding
|
|
4
|
+
exception. Static descriptors/instance/class markers and custom attribute
|
|
5
|
+
interceptors remain fail-closed. No optional dependency is imported here.
|
|
6
|
+
"""
|
|
7
|
+
import inspect,sys
|
|
8
|
+
MARKERS=('_morphtree_source_role','_morphtree_scientific_source','_morphtree_sealed')
|
|
9
|
+
ABSENT=object()
|
|
10
|
+
|
|
11
|
+
def controlled_source(obj):
|
|
12
|
+
for name in MARKERS:
|
|
13
|
+
if inspect.getattr_static(obj,name,ABSENT) is not ABSENT:return True
|
|
14
|
+
value=getattr(obj,name,ABSENT)
|
|
15
|
+
if value is ABSENT:continue
|
|
16
|
+
pd=sys.modules.get('pandas');base=None
|
|
17
|
+
if pd is not None:
|
|
18
|
+
if isinstance(obj,pd.DataFrame):base=pd.DataFrame;axis='columns'
|
|
19
|
+
elif isinstance(obj,pd.Series):base=pd.Series;axis='index'
|
|
20
|
+
if base is not None:
|
|
21
|
+
handlers=('__getattr__','__getattribute__')
|
|
22
|
+
original=all(inspect.getattr_static(type(obj),method,ABSENT) is inspect.getattr_static(base,method,ABSENT) for method in handlers)
|
|
23
|
+
if original and name in getattr(obj,axis):continue
|
|
24
|
+
return True
|
|
25
|
+
return False
|
morphtree/species.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Reusable algorithm specification, independent of trained tree weights."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import copy
|
|
6
|
+
|
|
7
|
+
from .config import AlgorithmGenome, HyperParams
|
|
8
|
+
from .persistence import decode_species, read_json, species_payload, write_json
|
|
9
|
+
from .public import public_metadata, learning_fingerprint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class AlgorithmSpecies:
|
|
14
|
+
genome: AlgorithmGenome = field(default_factory=AlgorithmGenome)
|
|
15
|
+
parameters: HyperParams = field(default_factory=HyperParams)
|
|
16
|
+
metadata: dict = field(default_factory=dict, compare=False)
|
|
17
|
+
|
|
18
|
+
def __post_init__(self):
|
|
19
|
+
if not isinstance(self.genome, AlgorithmGenome) or not isinstance(self.parameters, HyperParams):
|
|
20
|
+
raise ValueError("Species needs an AlgorithmGenome and HyperParams")
|
|
21
|
+
if not isinstance(self.metadata, dict):
|
|
22
|
+
raise ValueError("Species metadata must be a dict")
|
|
23
|
+
object.__setattr__(self, "metadata", public_metadata(self.metadata))
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def learning_fingerprint(self):
|
|
27
|
+
recipe = self.metadata.get('learning_recipe')
|
|
28
|
+
return learning_fingerprint(self.genome, self.parameters, recipe) if recipe else None
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def structural_fingerprint(self):
|
|
32
|
+
return self.genome.fingerprint()
|
|
33
|
+
|
|
34
|
+
def to_dict(self):
|
|
35
|
+
return species_payload(self.genome, self.parameters, copy.deepcopy(self.metadata))
|
|
36
|
+
|
|
37
|
+
def save(self, path):
|
|
38
|
+
write_json(path, self.to_dict())
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def load(cls, path, *, limits=None):
|
|
42
|
+
genome, params, metadata = decode_species(read_json(path, limits))
|
|
43
|
+
return cls(genome, params, metadata)
|
|
44
|
+
|
|
45
|
+
def export_python(self, path):
|
|
46
|
+
from .exporting import export_algorithm
|
|
47
|
+
export_algorithm(self, path)
|
morphtree/temporary.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""One-attempt private temporary directories and ownership-checked cleanup.
|
|
2
|
+
|
|
3
|
+
No global tempfile configuration or retry loop is changed. Names have the same
|
|
4
|
+
eight-character random suffix length as the previous stdlib directories, using
|
|
5
|
+
48 fresh cryptographic bits per name. Creation is exclusive; a collision or
|
|
6
|
+
permission failure propagates without retry. These are OS resource helpers,
|
|
7
|
+
not learner code or a guarantee against arbitrary filesystem latency.
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import secrets
|
|
12
|
+
import shutil
|
|
13
|
+
import stat
|
|
14
|
+
import tempfile
|
|
15
|
+
import weakref
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def directory(*,prefix='tmp',dir=None):
|
|
19
|
+
if type(prefix) is not str or not prefix or any(c in prefix for c in ('/','\\','\0',':')) or Path(prefix).name!=prefix or prefix in ('.','..'):
|
|
20
|
+
raise ValueError('A single temporary-name prefix is required')
|
|
21
|
+
parent=Path(tempfile.gettempdir() if dir is None else dir).resolve(strict=True)
|
|
22
|
+
if not parent.is_dir():raise NotADirectoryError(parent)
|
|
23
|
+
path=parent/(prefix+secrets.token_urlsafe(6))
|
|
24
|
+
os.mkdir(path,0o700)
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _remove_owned(name,parent,identity):
|
|
29
|
+
path=Path(name);parent=Path(parent)
|
|
30
|
+
try:current=path.lstat()
|
|
31
|
+
except FileNotFoundError:return
|
|
32
|
+
if stat.S_ISLNK(current.st_mode) or getattr(path,'is_junction',lambda:False)():
|
|
33
|
+
raise ValueError('Owned temporary cleanup refuses a link')
|
|
34
|
+
if not stat.S_ISDIR(current.st_mode) or (current.st_dev,current.st_ino)!=identity:
|
|
35
|
+
raise ValueError('Owned temporary directory identity changed')
|
|
36
|
+
if path.parent.resolve()!=parent or not path.resolve().is_relative_to(parent):
|
|
37
|
+
raise ValueError('Owned temporary cleanup escaped its original parent')
|
|
38
|
+
shutil.rmtree(path)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OwnedTemporaryDirectory:
|
|
42
|
+
"""Private name/context/cleanup/finalizer surface needed by existing code."""
|
|
43
|
+
def __init__(self,*,prefix='tmp',dir=None):
|
|
44
|
+
path=directory(prefix=prefix,dir=dir)
|
|
45
|
+
self.name=str(path);self.parent=str(path.parent)
|
|
46
|
+
current=path.lstat();self.identity=(current.st_dev,current.st_ino)
|
|
47
|
+
self._cleanup_attempted=False
|
|
48
|
+
self._finalizer=weakref.finalize(self,_remove_owned,self.name,self.parent,self.identity)
|
|
49
|
+
|
|
50
|
+
def __enter__(self):return self.name
|
|
51
|
+
|
|
52
|
+
def cleanup(self):
|
|
53
|
+
if self._cleanup_attempted:return
|
|
54
|
+
self._cleanup_attempted=True
|
|
55
|
+
self._finalizer.detach()
|
|
56
|
+
_remove_owned(self.name,self.parent,self.identity)
|
|
57
|
+
|
|
58
|
+
def __exit__(self,kind,error,traceback):
|
|
59
|
+
if error is None:
|
|
60
|
+
self.cleanup()
|
|
61
|
+
else:
|
|
62
|
+
try:self.cleanup()
|
|
63
|
+
except BaseException as cleanup_error:
|
|
64
|
+
BaseException.add_note(error,'Owned temporary cleanup also failed: '+type(cleanup_error).__name__)
|
|
65
|
+
return False
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Frozen native family training, never a substitute for evolutionary search.
|
|
2
|
+
|
|
3
|
+
The 2.0 public exporter provided an executor-bound train_encoded fragment.
|
|
4
|
+
This module supplies the scalable equivalent, with explicit TRAIN provenance,
|
|
5
|
+
finite resource guards, the same native booster and unchanged bootstrap/order.
|
|
6
|
+
Exporting never fits; calling the generated trainer is a new training operation.
|
|
7
|
+
"""
|
|
8
|
+
from array import array
|
|
9
|
+
from dataclasses import asdict,replace
|
|
10
|
+
import hashlib,json,time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _validate(spec):
|
|
15
|
+
from .scalable import digest,verify_source_identity,_ENGINE_SHA256
|
|
16
|
+
from .config import AlgorithmGenome,HyperParams,validate_task_genome
|
|
17
|
+
verify_source_identity()
|
|
18
|
+
fields={'schema','task','family','genome','parameters','encoder','classes','seed','runtime_manifest_sha256','scope'}
|
|
19
|
+
if type(spec) is not dict or set(spec)!=fields or spec['schema']!='SCALABLE_FROZEN_TRAINING_1' or spec['scope']!='FROZEN_FAMILY_NOT_END_TO_END_SEARCH':raise ValueError('Exact frozen training fragment required')
|
|
20
|
+
if spec['runtime_manifest_sha256']!=_ENGINE_SHA256:raise ValueError('Training fragment requires its exact source runtime')
|
|
21
|
+
if spec['task'] not in ('binary','multiclass','regression') or spec['family'] not in ('boost','bag') or type(spec['seed']) is not int:raise ValueError('Known native task/family and resolved seed required')
|
|
22
|
+
genome=AlgorithmGenome(**spec['genome']);params=HyperParams(**spec['parameters'])
|
|
23
|
+
validate_task_genome(spec['task'],genome)
|
|
24
|
+
return genome,params
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def export_algorithm(estimator,path):
|
|
28
|
+
from .scalable import digest,verify_source_identity,_ENGINE_SHA256
|
|
29
|
+
from .persistence import atomic_text
|
|
30
|
+
verify_source_identity()
|
|
31
|
+
if not hasattr(estimator,'model_'):raise RuntimeError('Not fitted')
|
|
32
|
+
res=estimator._serialization_resources('Frozen training program')
|
|
33
|
+
seeds=[e['seed'] for e in estimator.report_.get('events',()) if e['event']=='NATIVE_FIT_STARTED']
|
|
34
|
+
seed=estimator.report_.get('effective_seed',seeds[0] if seeds else estimator.random_state)
|
|
35
|
+
spec=dict(schema='SCALABLE_FROZEN_TRAINING_1',task=estimator.task_,family=estimator.model_.family,
|
|
36
|
+
genome=estimator.model_.genome.to_dict(),parameters=asdict(estimator.model_.params),
|
|
37
|
+
encoder=estimator.model_.encoder.state(),classes=list(estimator.classes_),seed=seed,
|
|
38
|
+
runtime_manifest_sha256=_ENGINE_SHA256,scope='FROZEN_FAMILY_NOT_END_TO_END_SEARCH')
|
|
39
|
+
_validate(spec)
|
|
40
|
+
text='''# MorphTree frozen native family training fragment; requires exact installed runtime.
|
|
41
|
+
# No family search, OOF, evolution, Optuna, holdout metric or inference weights.
|
|
42
|
+
from morphtree.training_fragment import train_encoded_fragment
|
|
43
|
+
SPEC = %r
|
|
44
|
+
SPEC_SHA256 = %r
|
|
45
|
+
EXPORT_SCOPE = 'FROZEN_FAMILY_NOT_END_TO_END_SEARCH'
|
|
46
|
+
|
|
47
|
+
def train_encoded(X, y, *, identity, memory_budget='auto', time_budget='auto', check=None):
|
|
48
|
+
return train_encoded_fragment(SPEC, SPEC_SHA256, X, y, identity=identity,
|
|
49
|
+
memory_budget=memory_budget, time_budget=time_budget, check=check)
|
|
50
|
+
''' % (spec,digest(spec))
|
|
51
|
+
res.reserve(len(text.encode('utf8'))*4,'Frozen training source')
|
|
52
|
+
atomic_text(Path(path),text);return hashlib.sha256(text.encode('utf8')).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def train_encoded_fragment(spec,expected_sha256,X,y,*,identity,memory_budget='auto',time_budget='auto',check=None):
|
|
56
|
+
"""Train exactly the frozen family on explicitly encoded TRAIN observations.
|
|
57
|
+
|
|
58
|
+
X must be Encoder.transform(Table) from this runtime. identity is exactly
|
|
59
|
+
{data: data_identity(X.source, compact_labels, 'TRAIN'),
|
|
60
|
+
preprocessing_sha256: encoder_digest(exported_encoder)}.
|
|
61
|
+
Targets are compact internal numeric labels, not raw class names. No rows
|
|
62
|
+
are dropped; bag members use the original declared bootstrap procedure.
|
|
63
|
+
"""
|
|
64
|
+
from .scalable import (digest,Family,MorphTreeClassifier,MorphTreeRegressor,
|
|
65
|
+
managed_resources,_ENGINE_SHA256)
|
|
66
|
+
from .scalable_data import Encoded,Encoder
|
|
67
|
+
from .scalable_receipts import data_identity,encoder_digest,verify
|
|
68
|
+
from .native_lab_backend import bind_backend
|
|
69
|
+
from .booster import BinaryHistogramBooster
|
|
70
|
+
from .prepared_bins import BinCache
|
|
71
|
+
from ._experimental.sampling import bootstrap
|
|
72
|
+
# Admission/context checks precede input traversal or a native component.
|
|
73
|
+
if type(spec) is not dict or check is not None and not callable(check):raise ValueError('Typed fragment and optional resource callback required')
|
|
74
|
+
obj=(MorphTreeRegressor if spec.get('task')=='regression' else MorphTreeClassifier)(
|
|
75
|
+
random_state=spec.get('seed'),memory_budget=memory_budget,time_budget=time_budget,
|
|
76
|
+
use_optuna=False,n_trials=0,max_mutations=0,refit_full=False,checkpoint_dir=None)
|
|
77
|
+
obj._ordinary_context_guard()
|
|
78
|
+
backend_factory=bind_backend()
|
|
79
|
+
if digest(spec)!=expected_sha256:raise ValueError('Changed exported training program')
|
|
80
|
+
genome,params=_validate(spec)
|
|
81
|
+
if not isinstance(X,Encoded):raise ValueError('Explicit compact encoded TRAIN required')
|
|
82
|
+
from .source_markers import controlled_source
|
|
83
|
+
if controlled_source(X) or controlled_source(X.source) or controlled_source(y):raise ValueError('Controlled data require their original context')
|
|
84
|
+
res=managed_resources(memory_budget,150 if time_budget=='auto' else time_budget,check)
|
|
85
|
+
res.reserve(len(X)*(64+X.width*32),'Frozen native TRAIN workspace')
|
|
86
|
+
target_codes=('d',) if spec['task']=='regression' else ('I','Q','q')
|
|
87
|
+
if not isinstance(y,array) or y.typecode not in target_codes or not len(X) or len(X)!=len(y):raise ValueError('Aligned task-compatible native compact TRAIN labels required')
|
|
88
|
+
enc=Encoder.from_state(spec['encoder'],res)
|
|
89
|
+
wanted=dict(data=data_identity(X.source,y,'TRAIN'),preprocessing_sha256=encoder_digest(enc))
|
|
90
|
+
if type(identity) is not dict or identity!=wanted:raise ValueError('Exact TRAIN/preprocessing identity required')
|
|
91
|
+
transformed=enc.transform(X.source)
|
|
92
|
+
if X.order!=transformed.order or X.maps!=transformed.maps or X._nominal_sizes!=transformed._nominal_sizes:raise ValueError('Encoded TRAIN differs from exported preprocessing')
|
|
93
|
+
k=len(spec['classes']) if spec['task']!='regression' else 1
|
|
94
|
+
if spec['task']=='regression':
|
|
95
|
+
import math
|
|
96
|
+
if spec['classes'] or any(not math.isfinite(v) for v in y):raise ValueError('Finite regression TRAIN targets')
|
|
97
|
+
elif k<2 or spec['task']=='binary' and k!=2 or any(v!=int(v) or not 0<=v<k for v in y):raise ValueError('Task-compatible internal class indices required')
|
|
98
|
+
# The same bound as normal training: output model + row workspace + histograms.
|
|
99
|
+
from .resource_plan import fit_plan
|
|
100
|
+
plan=fit_plan(len(X),X.width,k,params,0,0,False,res.available_bytes_snapshot,time_budget,res.bytes_limit)
|
|
101
|
+
res.configure_time(plan['seconds']);res.reserve(plan['estimated_workspace_bytes'],'Frozen native family workspace')
|
|
102
|
+
start=time.perf_counter();obj.last_attempt_=dict(status='IN_PROGRESS',native_fits=0,OOF_fits=0,scalar_trees=0,Optuna_trials=0,cache_hits=0,events=[],effective_seed=spec['seed'],training_scope=spec['scope'])
|
|
103
|
+
from .scalable_receipts import Ledger
|
|
104
|
+
obj._ledger=Ledger();obj._event('CALL_STARTED',engine_sha256=_ENGINE_SHA256,scope=spec['scope'],requested_task=spec['task'])
|
|
105
|
+
obj._event('PROGRAM_FROZEN',genome_sha256=digest(genome.to_dict()))
|
|
106
|
+
prepared=BinCache(min(32*1024**2,res.bytes_limit//8),res.check);models=[]
|
|
107
|
+
try:
|
|
108
|
+
for member in range(1 if spec['family']=='boost' else params.n_estimators):
|
|
109
|
+
res.check();seed=spec['seed'];actual=params if spec['family']=='boost' else replace(params,n_estimators=1,learning_rate=1.)
|
|
110
|
+
if spec['family']=='bag':
|
|
111
|
+
ids=bootstrap(y,spec['task'],seed+member+1);table=X.source.take(ids);yy=array(y.typecode,(y[i] for i in ids));xx=enc.transform(table)
|
|
112
|
+
else:table=X.source;xx=X;yy=y
|
|
113
|
+
data=data_identity(X.source,y,'TRAIN');preprocessing=encoder_digest(enc)
|
|
114
|
+
key=digest(dict(data=data,preprocessing_sha256=preprocessing,task=spec['task'],family=spec['family'],genome=genome.to_dict(),params=asdict(params),seed=seed,engine_sha256=_ENGINE_SHA256))
|
|
115
|
+
rows=hashlib.sha256(memoryview(table.indices)).hexdigest() if table.indices is not None else digest(dict(range_start=0,range_stop=len(table)))
|
|
116
|
+
labels=hashlib.sha256(memoryview(yy)).hexdigest();component=digest(dict(request=key,member=member,effective_rows=rows,effective_labels=labels,seed=seed+member,params=asdict(actual)))
|
|
117
|
+
count=actual.n_estimators*(k if spec['task']=='multiclass' else 1)
|
|
118
|
+
obj.last_attempt_['native_fits']+=1;obj.last_attempt_['scalar_trees']+=count
|
|
119
|
+
obj._event('NATIVE_FIT_STARTED',ordinal=member+1,phase='FROZEN_TRAIN',rows=len(yy),source_rows=len(X),family=spec['family'],cache_key=key,component_key=component,effective_rows_sha256=rows,effective_labels_sha256=labels,seed=seed+member,params_sha256=digest(asdict(actual)),preprocessing_sha256=preprocessing,program_sha256=digest(genome.to_dict()),role='TRAIN',reserved_scalar_trees=count)
|
|
120
|
+
model=BinaryHistogramBooster(genome,actual,seed+member,backend_factory(xx._nominal_sizes),memory_enabled=False,objective='softmax' if spec['task']=='multiclass' else 'squared_error' if spec['task']=='regression' else 'binary_logloss',n_classes=k if spec['task']=='multiclass' else None)
|
|
121
|
+
token=prepared.bind(xx,dict(source_sha256=data['source_sha256'],schema_sha256=data['schema_sha256'],effective_rows_sha256=rows,effective_labels_sha256=labels,preprocessing_sha256=preprocessing,engine_sha256=_ENGINE_SHA256),'TRAIN')
|
|
122
|
+
tick=time.perf_counter();model.fit(xx,yy,callback=lambda *_:res.check() or True,prepared_data=token)
|
|
123
|
+
if len(model.trees)!=count:raise ValueError('Incomplete frozen native component')
|
|
124
|
+
obj._event('NATIVE_FIT_COMPLETED',ordinal=member+1,phase='FROZEN_TRAIN',seconds=time.perf_counter()-tick,component_key=component,actual_scalar_trees=len(model.trees));models.append(model)
|
|
125
|
+
obj.model_=Family(spec['task'],genome,params,enc,models,spec['family']);obj.classes_=list(spec['classes']);obj.task_=spec['task'];obj.n_features_in_=X.width;obj.best_family_=spec['family'];obj.best_params_=asdict(params)
|
|
126
|
+
obj._event('FINAL_MODEL',input_rows=len(X),selection_TRAIN_rows=len(X),SEARCH_rows=0,final_fit_source_rows=len(X),refit_full=False,SEARCH_primary_before_refit=None,selection_metric_is_independent=False,nominal_columns=len(X._nominal_sizes),source_storage_bytes=X.source.storage_bytes,all_rows_accounted=True)
|
|
127
|
+
obj.last_attempt_.update(status='COMPLETED',seconds=time.perf_counter()-start,stop_reason='FROZEN_PROGRAM_TRAINING_COMPLETE')
|
|
128
|
+
obj.last_attempt_['receipt_verification']=verify(obj.last_attempt_['events']);obj.report_=obj.last_attempt_
|
|
129
|
+
return obj
|
|
130
|
+
finally:prepared.clear()
|