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
@@ -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()