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,74 @@
1
+ """Pure Python native category-equality splits; never order nominal codes.
2
+
3
+ Numeric operations delegate unchanged to the qualified reference backend.
4
+ Missing and unseen nominal values have separate explicit codes. Each node tests
5
+ one category versus the remainder; arbitrary subsets/target ordering are absent.
6
+ """
7
+ from array import array
8
+ from bisect import bisect_right
9
+ import math
10
+ from .backend import PythonBackend
11
+
12
+
13
+ class Column(array):
14
+ def __new__(cls,kind,values,nominal=False):return super().__new__(cls,kind,values)
15
+ def __init__(self,kind,values,nominal=False):self.nominal=nominal
16
+
17
+
18
+ class Hist(list):
19
+ def __init__(self,values,nominal=False):super().__init__(values);self.nominal=nominal
20
+
21
+
22
+ class ScalableBackend(PythonBackend):
23
+ name='private-python-columnar-nominal'
24
+ def __init__(self,nominal_sizes=None):self.nominal_sizes=nominal_sizes or {}
25
+ def bin(self,binner,X):
26
+ if not hasattr(X,'_compact_columns'):return super().bin(binner,X)
27
+ out=[]
28
+ for j,cuts in enumerate(binner.thresholds):
29
+ if j in self.nominal_sizes:out.append(Column('I',X.column(j),True))
30
+ else:out.append(Column('B' if binner.max_bins<=255 else 'H',(0 if math.isnan(v) else bisect_right(cuts,v)+1 for v in X.column(j))))
31
+ return out
32
+ def node_statistics(self,Xb,ids,g,h,u,features,need_missing,need_uncertainty):
33
+ if not need_missing or not self.nominal_sizes:
34
+ return super().node_statistics(Xb,ids,g,h,u,features,need_missing,need_uncertainty)
35
+ # Numeric missing is bin0; native nominal missing is fitted k+1.
36
+ # Unseen nominal k+2 is separate and must not trigger this local gate.
37
+ columns=tuple((Xb[f],self.nominal_sizes[f]+1 if f in self.nominal_sizes else 0) for f in features)
38
+ missing=sum(any(column[i]==code for column,code in columns) for i in ids)/max(len(ids),1)
39
+ G,H,uncertainty,_=super().node_statistics(Xb,ids,g,h,u,features,False,need_uncertainty)
40
+ return G,H,uncertainty,missing
41
+ def histogram(self,column,*args):return Hist(super().histogram(column,*args),getattr(column,'nominal',False))
42
+ def subtract(self,parent,child):return Hist(super().subtract(parent,child),getattr(parent,'nominal',False))
43
+ def fast_split(self,hist,total,depth,genome,params):
44
+ if not getattr(hist,'nominal',False):return super().fast_split(hist,total,depth,genome,params)
45
+ if genome.split_program not in (('OBJECTIVE_GAIN',),('OBJECTIVE_GAIN','BALANCE','MUL')):raise ValueError('Unsupported private nominal split program')
46
+ n,G,H=total[:3];counts,gs,hs=hist[:3];l1=params.l1
47
+ parent_l2=params.l2*(1+(params.depth_reg_strength*depth if genome.regularization_policy=='depth_scaled' else 0))
48
+ child_l2=params.l2*(1+(params.depth_reg_strength*(depth+1) if genome.regularization_policy=='depth_scaled' else 0))
49
+ def score(g,h,l2):return max(abs(g)-l1,0.)**2/max(h+l2,1e-12)
50
+ parent=score(G,H,parent_l2);best=None
51
+ for code in range(1,len(counts)):
52
+ ln,lg,lh=counts[code],gs[code],hs[code];rn,rg,rh=n-ln,G-lg,H-lh
53
+ if min(ln,rn)<params.min_samples_leaf or min(lh,rh)<params.min_hessian:continue
54
+ gain=.5*(score(lg,lh,child_l2)+score(rg,rh,child_l2)-parent)
55
+ if len(genome.split_program)>1:gain*=2*min(ln,rn)/n
56
+ if math.isfinite(gain) and gain>params.min_gain and (best is None or round(gain,12)>round(best[0],12)):best=(gain,code,True)
57
+ return best
58
+ def partition(self,column,ids,cut,missing_left):
59
+ if not getattr(column,'nominal',False):return super().partition(column,ids,cut,missing_left)
60
+ left=[];right=[]
61
+ for i in ids:(left if column[i]==cut else right).append(i)
62
+ return left,right
63
+ def predict_tree(self,nodes,Xb):
64
+ out=[]
65
+ for i in range(len(Xb[0])):
66
+ k=0
67
+ for _ in range(len(nodes)+1):
68
+ node=nodes[k]
69
+ if node.is_leaf:out.append(node.value);break
70
+ col=Xb[node.feature];b=col[i]
71
+ left=(b==node.threshold_bin) if getattr(col,'nominal',False) else node.missing_left if b==0 else b<=node.threshold_bin
72
+ k=node.left if left else node.right
73
+ else:raise ValueError('Invalid cyclic native tree')
74
+ return out
@@ -0,0 +1,129 @@
1
+ """Local atomic fitted-state checkpoints. Recovery never resumes training."""
2
+ import copy
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import shutil
8
+ import tempfile
9
+
10
+ from .persistence import atomic_text, decode_json_bytes, LoadLimits, read_regular_bytes
11
+ from .scalable_receipts import verify
12
+ from .temporary import directory as bounded_directory
13
+
14
+
15
+ def _safe_directory(path):
16
+ path = Path(path)
17
+ if path.is_symlink() or getattr(path, 'is_junction', lambda: False)():
18
+ raise ValueError('Checkpoint directory cannot be a link')
19
+ if not path.is_dir():
20
+ raise ValueError('Checkpoint parent directory must exist')
21
+ return path.resolve()
22
+
23
+
24
+ def _workspace(value, resources):
25
+ estimate = 8192
26
+ pending = [iter((value,))]
27
+ count = 0
28
+ while pending:
29
+ try:
30
+ node = next(pending[-1])
31
+ except StopIteration:
32
+ pending.pop()
33
+ continue
34
+ count += 1
35
+ estimate += 512
36
+ if isinstance(node, str):
37
+ estimate += len(node) * 32
38
+ elif isinstance(node, dict):
39
+ pending.append(iter(node.items()))
40
+ elif isinstance(node, (list, tuple)):
41
+ pending.append(iter(node))
42
+ if count % 128 == 0:
43
+ resources.reserve(estimate, 'Durable checkpoint workspace')
44
+ resources.reserve(estimate, 'Durable checkpoint workspace')
45
+ return estimate
46
+
47
+
48
+ def commit(estimator, model, classes, resources, *, final):
49
+ from .scalable import digest
50
+ if estimator.checkpoint_dir is None:
51
+ return None
52
+ report = estimator.last_attempt_
53
+ events = report['events']
54
+ verify(events)
55
+ if final:
56
+ if report['status'] != 'COMPLETED' or events[-1]['event'] != 'FINAL_MODEL':
57
+ raise ValueError('Final checkpoint requires completed finalization')
58
+ elif digest(model) != report['last_verified_anchor']['model_sha256']:
59
+ raise ValueError('Checkpoint must match verified TRAIN anchor')
60
+ wanted = {event['parent_sha256'] for event in events if event['event'] == 'DIAGNOSIS'}
61
+ evidence = getattr(estimator, 'diagnostic_profiles_', {})
62
+ resources.reserve(8192, 'Refit configuration header before copying')
63
+ constructor = estimator._constructor_state()
64
+ template = dict(model=model, classes=classes, constructor=constructor, constructor_sha256=digest(constructor), report={k: v for k, v in report.items() if k != 'checkpoint' and (final or k != 'selection_checkpoint')}, evidence=dict(diagnostic_profiles={key: evidence[key] for key in wanted}))
65
+ estimate = _workspace(template, resources)
66
+ packet = copy.deepcopy(template)
67
+ packet['schema'] = 'SCALABLE_PRIVATE_MODEL_3'
68
+ if not final:
69
+ packet['report']['status'] = 'PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT'
70
+ packet['receipt_sha256'] = digest(packet['report'])
71
+ text = json.dumps(packet, sort_keys=True, allow_nan=False) + '\n'
72
+ resources.reserve(estimate + len(text) * 8, 'Encoded durable checkpoint workspace')
73
+ directory = getattr(estimator, '_active_checkpoint_dir_', None)
74
+ if directory is None:
75
+ if estimator.checkpoint_dir == 'auto':
76
+ parent = _safe_directory(tempfile.gettempdir()) / 'morphtree-checkpoints'
77
+ parent.mkdir(exist_ok=True)
78
+ else:
79
+ parent = Path(estimator.checkpoint_dir)
80
+ parent = _safe_directory(parent)
81
+ directory = Path(bounded_directory(prefix='fit-', dir=parent))
82
+ estimator._active_checkpoint_dir_ = directory
83
+ estimator._checkpoint_sequence_ = 0
84
+ directory = _safe_directory(directory)
85
+ report['checkpoint_directory'] = str(directory)
86
+ if shutil.disk_usage(directory).free < len(text) * 4 + 8192:
87
+ raise OSError('Insufficient free disk for atomic checkpoint; previous model preserved')
88
+ name = f"model-{estimator._checkpoint_sequence_:04d}.json"
89
+ target = directory / name
90
+ if target.exists():
91
+ raise FileExistsError('An existing fitted checkpoint cannot be overwritten')
92
+ atomic_text(target, text)
93
+ record = dict(schema='SCALABLE_CHECKPOINT_POINTER_1', path=name, sha256=hashlib.sha256(text.encode('ascii')).hexdigest(), call_id=events[0]['call_id'], source_role='TRAIN_FULL' if final and estimator.refit_full else 'TRAIN', final=final)
94
+ if not final:
95
+ atomic_text(directory / 'SELECTED.json', json.dumps(record, sort_keys=True) + '\n')
96
+ atomic_text(directory / 'CURRENT.json', json.dumps(record, sort_keys=True) + '\n')
97
+ estimator._checkpoint_sequence_ += 1
98
+ receipt = dict(directory=str(directory), **record)
99
+ report['checkpoint'] = receipt
100
+ if not final:
101
+ report['selection_checkpoint'] = receipt
102
+ if final:
103
+ estimator.checkpoint_path_ = str(target)
104
+ return receipt
105
+
106
+
107
+ def read_pointer(directory, resources, *, stage='current'):
108
+ if type(stage) is not str or stage not in ('current', 'selected'):
109
+ raise ValueError('Checkpoint stage must be current or selected')
110
+ directory = _safe_directory(directory)
111
+ pointer = directory / ('CURRENT.json' if stage == 'current' else 'SELECTED.json')
112
+ if pointer.is_symlink():
113
+ raise ValueError('Checkpoint pointer cannot be a link')
114
+ raw = read_regular_bytes(pointer,8192)
115
+ record = decode_json_bytes(raw, LoadLimits(max_bytes=8192), reserve=resources.reserve)
116
+ required = {'schema', 'path', 'sha256', 'call_id', 'source_role', 'final'}
117
+ if set(record) != required or record['schema'] != 'SCALABLE_CHECKPOINT_POINTER_1':
118
+ raise ValueError('Exact checkpoint pointer schema required')
119
+ name = record['path']
120
+ if type(name) is not str or len(name) != 15 or not name.startswith('model-') or not name.endswith('.json') or not name[6:10].isdigit():
121
+ raise ValueError('Checkpoint pointer must name a local numbered model')
122
+ if type(record['sha256']) is not str or len(record['sha256']) != 64 or any(c not in '0123456789abcdef' for c in record['sha256']) or type(record['call_id']) is not str or not record['call_id'] or type(record['final']) is not bool or record['source_role'] not in ('TRAIN', 'TRAIN_FULL') or not record['final'] and record['source_role'] != 'TRAIN':
123
+ raise ValueError('Invalid checkpoint identity or source role')
124
+ if stage == 'selected' and record['final']:
125
+ raise ValueError('Selected checkpoint must be a verified TRAIN anchor')
126
+ target = directory / name
127
+ if target.is_symlink() or target.resolve().parent != directory:
128
+ raise ValueError('Checkpoint model must be a local regular file')
129
+ return target, record
@@ -0,0 +1,255 @@
1
+ """Private scalable representation. No whole DataFrame/ndarray list conversion.
2
+
3
+ Source dictionaries encode input storage only. Learner vocabularies are fitted
4
+ again on each TRAIN/fold; SEARCH-only categories become unknown at validation.
5
+ """
6
+ from array import array
7
+ from bisect import bisect_right
8
+ from collections.abc import Sequence
9
+ import json
10
+ import math
11
+ import os
12
+ from numbers import Real
13
+ import sys
14
+ import time
15
+ from .source_markers import controlled_source
16
+
17
+
18
+ def scalar(v,*,preserve_integral=False):
19
+ kind=type(v)
20
+ if kind is str or v is None:return v
21
+ if kind is int and preserve_integral:return v
22
+ if kind is float or kind is int or kind is bool:
23
+ f=float(v)
24
+ if math.isnan(f):return None
25
+ if math.isfinite(f):return f
26
+ raise ValueError('Finite numeric/string category/None/NaN/nullable scalar required')
27
+ if type(v).__module__.split('.')[0]=='numpy' and hasattr(v,'item'):v=v.item()
28
+ if v is None or (sys.modules.get('pandas') is not None and v is getattr(sys.modules['pandas'],'NA',None)):return None
29
+ if preserve_integral and type(v) is int:return v
30
+ if isinstance(v,Real):
31
+ f=float(v)
32
+ if math.isnan(f):return None
33
+ if math.isfinite(f):return f
34
+ if isinstance(v,str):return v
35
+ raise ValueError('Finite numeric/string category/None/NaN/nullable scalar required')
36
+
37
+
38
+ def category_key(v):
39
+ if not isinstance(v,str):
40
+ if type(v) is int:v=v if abs(v)>2**53 else float(v)
41
+ else:
42
+ v=float(v)
43
+ if abs(v)>2**53 and v.is_integer():v=int(v)
44
+ if v==0:v=0.0
45
+ return json.dumps(['s' if isinstance(v,str) else 'n',v],ensure_ascii=False,allow_nan=False,separators=(',',':'))
46
+
47
+
48
+ def canonical_category_key(key):
49
+ """Exact numeric identity, including compatible legacy float key spelling."""
50
+ entry=json.loads(key)
51
+ if type(entry) is not list or len(entry)!=2:raise ValueError('Malformed typed category')
52
+ tag,value=entry
53
+ if not (tag=='s' and type(value) is str or tag=='n' and type(value) in (int,float)):raise ValueError('Malformed typed category')
54
+ value=scalar(value,preserve_integral=True)
55
+ if value is None:raise ValueError('Missing value is not a vocabulary entry')
56
+ canonical=category_key(value)
57
+ try:legacy=json.dumps([tag,value if tag=='s' else float(value)],ensure_ascii=False,allow_nan=False,separators=(',',':'))
58
+ except OverflowError:legacy=None
59
+ if key not in (canonical,legacy):raise ValueError('Noncanonical typed category')
60
+ return canonical
61
+
62
+
63
+ def resident_bytes():
64
+ """Read-only OS accounting; no learner arithmetic outside Python."""
65
+ try:
66
+ if os.name=='nt':
67
+ import ctypes
68
+ class Counters(ctypes.Structure):_fields_=[('cb',ctypes.c_ulong),('faults',ctypes.c_ulong)]+[(n,ctypes.c_size_t) for n in ('peak','rss','peak_paged','paged','peak_nonpaged','nonpaged','pagefile','peak_pagefile')]
69
+ c=Counters();c.cb=ctypes.sizeof(c)
70
+ if not ctypes.WinDLL('psapi',use_last_error=True).GetProcessMemoryInfo(ctypes.c_void_p(-1),ctypes.byref(c),c.cb):return None
71
+ return c.rss
72
+ with open('/proc/self/statm',encoding='ascii') as handle:
73
+ resident_pages=int(handle.read().split()[1])
74
+ return resident_pages*os.sysconf('SC_PAGE_SIZE')
75
+ except (OSError,ValueError,AttributeError):return None
76
+
77
+
78
+ class Input:
79
+ """Inspect metadata first, then access one column or bounded block."""
80
+ def __init__(self,X):
81
+ if controlled_source(X):raise ValueError('Controlled sources cannot use ordinary input or inference')
82
+ self.X=X;self.frame=hasattr(X,'columns') and hasattr(X,'iloc');self.fitted_kinds=None
83
+ if hasattr(X,'format') and hasattr(X,'tocsr'):raise ValueError('Sparse input requires a separate qualified path')
84
+ if hasattr(X,'shape'):
85
+ if len(X.shape)!=2:raise ValueError('X must have exactly two dimensions')
86
+ self.n,self.width=map(int,X.shape)
87
+ elif isinstance(X,(list,tuple)):
88
+ self.n=len(X);self.width=len(X[0]) if self.n and isinstance(X[0],(list,tuple,array)) else 0
89
+ if any(not isinstance(r,(list,tuple,array)) or len(r)!=self.width for r in X):raise ValueError('Rectangular rows required')
90
+ else:raise ValueError('Supported dense table, ndarray or DataFrame required')
91
+ if self.n<1 or self.width<1:raise ValueError('X must contain rows and columns')
92
+ self.names=tuple(X.columns) if self.frame else None
93
+ if self.names is not None and (len(set(self.names))!=self.width or any(not isinstance(n,str) or not n for n in self.names)):raise ValueError('Unique nonempty string names required')
94
+ def column(self,j,start=0,stop=None):
95
+ stop=self.n if stop is None else stop
96
+ if self.frame:return iter(self.X.iloc[start:stop,j].array)
97
+ if hasattr(self.X,'shape'):return iter(self.X[start:stop,j])
98
+ return (self.X[i][j] for i in range(start,stop))
99
+ def forced_categorical(self,j):return self.fitted_kinds[j]=='categorical' if self.fitted_kinds is not None else self.frame and str(self.X.dtypes.iloc[j])=='category'
100
+
101
+
102
+ class Resources:
103
+ def __init__(self,bytes_limit,seconds,check=None):
104
+ if type(bytes_limit) is not int or bytes_limit<1 or not math.isfinite(seconds) or seconds<=0:raise ValueError('Positive finite resource allowance')
105
+ self.bytes_limit=bytes_limit;self.started=time.monotonic();self.deadline=self.started+seconds;self.external=check;self.base_rss=resident_bytes();self.peak_rss=self.base_rss;self.last_memory_check=0.;self.pressure_reader=None;self.base_commit=None;self.peak_commit=None
106
+ def configure_time(self,seconds):
107
+ if type(seconds) not in (int,float) or not math.isfinite(seconds) or seconds<=0:raise ValueError('Positive finite resource allowance')
108
+ self.deadline=self.started+seconds
109
+ def check(self):
110
+ if time.monotonic()>self.deadline:raise TimeoutError('Finite scalable operation deadline')
111
+ if time.monotonic()-self.last_memory_check>.05:
112
+ self.last_memory_check=time.monotonic();rss=resident_bytes()
113
+ if rss is not None:
114
+ self.peak_rss=max(self.peak_rss or 0,rss)
115
+ if self.base_rss is not None and rss-self.base_rss>self.bytes_limit:raise MemoryError('Observed process RSS increase exceeds operation allowance; no rows were sampled')
116
+ if self.pressure_reader:
117
+ observation=self.pressure_reader();commit=observation.get('process_private_bytes')
118
+ if commit is not None:
119
+ self.peak_commit=max(self.peak_commit or 0,commit)
120
+ if self.base_commit is not None and commit-self.base_commit>self.bytes_limit:raise MemoryError('Observed process committed-memory increase exceeds operation allowance; no rows were sampled')
121
+ if observation['effective_available_bytes']<=0:raise MemoryError('Active RAM/commit/address-space headroom exhausted; no rows were sampled')
122
+ if self.external:self.external()
123
+ def reserve(self,n,description):
124
+ if n>self.bytes_limit:raise MemoryError(f'{description} requires estimated {n} bytes, allowance {self.bytes_limit}; no rows were sampled')
125
+ self.check()
126
+
127
+
128
+ class Rows(Sequence):
129
+ _compact_columns=True
130
+ def __len__(self):return self.n
131
+ def __getitem__(self,i):
132
+ if isinstance(i,slice):return [self[j] for j in range(*i.indices(self.n))]
133
+ if i<0:i+=self.n
134
+ if not 0<=i<self.n:raise IndexError(i)
135
+ return Row(self,i)
136
+ def column(self,j):return (self.value(i,j) for i in range(self.n))
137
+
138
+
139
+ class Row(Sequence):
140
+ def __init__(self,table,i):self.table=table;self.i=i
141
+ def __len__(self):return self.table.width
142
+ def __getitem__(self,j):
143
+ if isinstance(j,slice):return [self[k] for k in range(*j.indices(len(self)))]
144
+ if j<0:j+=len(self)
145
+ if not 0<=j<len(self):raise IndexError(j)
146
+ return self.table.value(self.i,j)
147
+
148
+
149
+ class Table(Rows):
150
+ def __init__(self,columns,kinds,vocab,n,names,indices=None):
151
+ self.columns,self.kinds,self.vocab,self.n,self.names,self.indices=columns,kinds,vocab,n,names,indices;self.width=len(columns)
152
+ @classmethod
153
+ def read(cls,source,resources,start=0,stop=None):
154
+ source=source if isinstance(source,Input) else Input(source);stop=source.n if stop is None else stop;n=stop-start
155
+ if not 0<=start<stop<=source.n:raise ValueError('Nonempty bounded block')
156
+ name_bytes=sum(sys.getsizeof(name)+8*len(name) for name in source.names or ())
157
+ base_bytes=n*source.width*10+source.width*256+name_bytes
158
+ resources.reserve(base_bytes,'Compact source columns and schema names')
159
+ columns=[];kinds=[];vocab=[];vocab_bytes=0
160
+ for j in range(source.width):
161
+ resources.check();first=None
162
+ for i,value in enumerate(source.column(j,start,stop)):
163
+ if i%4096==0:resources.check()
164
+ if isinstance(value,str):resources.reserve(base_bytes+vocab_bytes+len(value)*32+512,'Initial scalar/JSON key workspace')
165
+ first=scalar(value)
166
+ if first is not None:break
167
+ forced=source.forced_categorical(j);cat=forced or isinstance(first,str);a=array('I' if cat else 'd');lookup={};words=[]
168
+ for i,v in enumerate(source.column(j,start,stop)):
169
+ if i%4096==0:resources.check()
170
+ if isinstance(v,str):resources.reserve(base_bytes+vocab_bytes+len(v)*32+512,'Scalar/JSON key workspace before allocation')
171
+ v=scalar(v,preserve_integral=cat)
172
+ if cat:
173
+ if v is None:a.append(0);continue
174
+ if not forced and not isinstance(v,str):raise ValueError('Mixed string/numeric column')
175
+ key=category_key(v)
176
+ if key not in lookup:
177
+ vocab_bytes+=sys.getsizeof(key)+96;resources.reserve(base_bytes+vocab_bytes,'Source columns and category dictionaries')
178
+ lookup[key]=len(words)+1;words.append(key)
179
+ a.append(lookup[key])
180
+ else:
181
+ if isinstance(v,str):raise ValueError('Mixed string/numeric column')
182
+ a.append(math.nan if v is None else v)
183
+ columns.append(a);kinds.append('categorical' if cat else 'numeric');vocab.append(tuple(words))
184
+ return cls(tuple(columns),tuple(kinds),tuple(vocab),n,source.names)
185
+ def take(self,indices):
186
+ ids=array('I' if len(self.columns[0])<2**32 else 'Q',(self.indices[i] if self.indices is not None else i for i in indices))
187
+ return Table(self.columns,self.kinds,self.vocab,len(ids),self.names,ids)
188
+ def value(self,i,j):return self.columns[j][self.indices[i] if self.indices is not None else i]
189
+ def column(self,j):
190
+ c=self.columns[j]
191
+ return iter(c) if self.indices is None else (c[i] for i in self.indices)
192
+ @property
193
+ def storage_bytes(self):return sum(sys.getsizeof(c) for c in self.columns)+sum(sys.getsizeof(s) for v in self.vocab for s in v)
194
+
195
+
196
+ class Encoder:
197
+ """One native nominal feature per category column, TRAIN-only vocabulary."""
198
+ def fit(self,table):
199
+ self.names=table.names;self.columns=[]
200
+ for j,kind in enumerate(table.kinds):
201
+ if kind=='numeric':self.columns.append(dict(kind='numeric'));continue
202
+ observed=set(table.column(j));vocab=sorted({canonical_category_key(table.vocab[j][i-1]) for i in observed if i})
203
+ self.columns.append(dict(kind='categorical',vocabulary=vocab))
204
+ return self
205
+ def state(self):return dict(schema='SCALABLE_TRAIN_ENCODER_1',names=list(self.names) if self.names is not None else None,columns=self.columns)
206
+ @classmethod
207
+ def from_state(cls,state,resources):
208
+ if type(state) is not dict or set(state)!={'schema','names','columns'} or state['schema']!='SCALABLE_TRAIN_ENCODER_1':raise ValueError('Exact private encoder schema')
209
+ cols=state['columns'];names=state['names']
210
+ if type(cols) is not list or not cols:raise ValueError('Nonempty column schema')
211
+ if names is not None and (type(names) is not list or len(names)!=len(cols) or len(set(names))!=len(names) or any(type(n) is not str or not n for n in names)):raise ValueError('Invalid encoder names')
212
+ schema_bytes=len(cols)*256+sum(sys.getsizeof(name)+8*len(name) for name in names or ())
213
+ resources.reserve(schema_bytes,'Decoded encoder metadata before copies')
214
+ for c in cols:
215
+ if c==dict(kind='numeric'):continue
216
+ if type(c) is not dict or set(c)!={'kind','vocabulary'} or c['kind']!='categorical':raise ValueError('Invalid category schema')
217
+ v=c['vocabulary']
218
+ if type(v) is not list or any(type(s) is not str for s in v) or any(v[i]>=v[i+1] for i in range(len(v)-1)):raise ValueError('Canonical typed vocabulary required')
219
+ schema_bytes+=len(v)*128;canonical=set()
220
+ for key in v:
221
+ schema_bytes+=sys.getsizeof(key)*3
222
+ resources.reserve(schema_bytes+len(key)*32+512,'Typed vocabulary validation before JSON allocation')
223
+ normalized=canonical_category_key(key)
224
+ if normalized in canonical:raise ValueError('Duplicate equivalent numeric categories')
225
+ canonical.add(normalized)
226
+ obj=cls();obj.names=tuple(names) if names is not None else None;obj.columns=cols;return obj
227
+ def transform(self,table):
228
+ if self.names is None:
229
+ if table.names is not None:raise ValueError('Positional TRAIN requires positional prediction')
230
+ order=list(range(table.width))
231
+ else:
232
+ if table.names is None or set(table.names)!=set(self.names):raise ValueError('Feature schema mismatch')
233
+ positions={name:i for i,name in enumerate(table.names)};order=[positions[n] for n in self.names]
234
+ if len(order)!=len(self.columns):raise ValueError('Column count mismatch')
235
+ maps={};nominal={}
236
+ for j,(source,c) in enumerate(zip(order,self.columns)):
237
+ if c['kind']!=table.kinds[source]:
238
+ # An all-missing prediction column has no inferred string type.
239
+ if not(c['kind']=='categorical' and all(math.isnan(v) for v in table.column(source))):raise ValueError('Feature type mismatch')
240
+ if c['kind']=='categorical':
241
+ lookup={canonical_category_key(key):i+1 for i,key in enumerate(c['vocabulary'])};k=len(lookup);nominal[j]=k
242
+ maps[j]=[k+1]+[lookup.get(canonical_category_key(key),k+2) for key in table.vocab[source]]
243
+ return Encoded(table,order,maps,nominal)
244
+
245
+
246
+ class Encoded(Rows):
247
+ def __init__(self,table,order,maps,nominal):self.source=table;self.order=order;self.maps=maps;self._nominal_sizes=nominal;self.n=table.n;self.width=len(order)
248
+ def value(self,i,j):
249
+ v=self.source.value(i,self.order[j])
250
+ return (self.maps[j][0] if self.source.kinds[self.order[j]]=='numeric' else self.maps[j][int(v)]) if j in self.maps else v
251
+ def column(self,j):
252
+ values=self.source.column(self.order[j])
253
+ if j not in self.maps:return values
254
+ if self.source.kinds[self.order[j]]=='numeric':return (self.maps[j][0] for _ in values)
255
+ return (self.maps[j][int(v)] for v in values)
@@ -0,0 +1,92 @@
1
+ """Compact operation custody, not an authentication or scientific-usefulness claim.
2
+
3
+ Dataset buffers stay outside JSON. Digests bind typed source columns, schema,
4
+ ordered views and actual labels. Call-local completed-model caches never admit
5
+ foreign persisted results; chain verification does not authorize budget replay.
6
+ """
7
+ import hashlib
8
+ import json
9
+ import sys
10
+ import uuid
11
+
12
+ MAX_OPERATION_BYTES=1024*1024
13
+ ENVELOPE={'receipt_schema','call_id','sequence','previous','sha256'}
14
+ FIELDS={
15
+ 'CALL_STARTED':{'engine_sha256','scope','requested_task'},
16
+ 'RESOURCE_PLAN':{'plan'},
17
+ 'INPUT_PREPARED':{'rows','features','seconds','storage_bytes','input_identity'},
18
+ 'NATIVE_FIT_STARTED':{'ordinal','phase','rows','source_rows','family','cache_key','component_key','effective_rows_sha256','effective_labels_sha256','seed','params_sha256','preprocessing_sha256','program_sha256','role','reserved_scalar_trees'},
19
+ 'NATIVE_FIT_COMPLETED':{'ordinal','phase','seconds','component_key','actual_scalar_trees'},
20
+ 'CACHE_HIT':{'phase','family','rows','cache_key'},
21
+ 'SCREEN_COMPLETED':{'losses','parent','seconds'},
22
+ 'OOF_FOLD':{'parent_sha256','program_sha256','fit_rows','held_rows','disjoint','encoder_sha256','fit_indices_sha256','held_indices_sha256'},
23
+ 'DIAGNOSIS':{'parent_sha256','task','OOF_primary','fold_rows','TRAIN_only','fold_encoder_local','features_examined','region_count','profile_sha256','causal_status','seconds'},
24
+ 'MUTATION':{'generation','parent_sha256','child_sha256','child_program','parent_SEARCH','child_SEARCH','decision','seconds'},
25
+ 'PROGRAM_FROZEN':{'genome_sha256'},
26
+ 'OPTUNA_ASK':{'number','genome_sha256'},
27
+ 'OPTUNA_COMPLETE':{'number','SEARCH','parameters'},
28
+ 'BIN_PREPARATION_CACHE':{'limit_bytes','retained_bytes','entries','hits','misses','bypassed','evictions','peak_retained_bytes','reused_rows','preparation_seconds','integrity_seconds'},
29
+ 'FINAL_MODEL':{'input_rows','selection_TRAIN_rows','SEARCH_rows','final_fit_source_rows','refit_full','SEARCH_primary_before_refit','selection_metric_is_independent','nominal_columns','source_storage_bytes','all_rows_accounted'},
30
+ 'FIT_STOPPED':{'error_type'},
31
+ }
32
+
33
+
34
+ def canonical(v):
35
+ raw=json.dumps(v,sort_keys=True,allow_nan=False,separators=(',',':')).encode('utf8')
36
+ if len(raw)>MAX_OPERATION_BYTES:raise ValueError('Operation metadata exceeds finite allowance; data belong in buffers/artifacts')
37
+ return raw
38
+
39
+
40
+ def sha(v):return hashlib.sha256(canonical(v)).hexdigest()
41
+ def framed(h,text):
42
+ raw=text.encode('utf8');h.update(len(raw).to_bytes(8,'little'));h.update(raw)
43
+
44
+
45
+ def schema_digest(names,kinds,vocabularies):
46
+ h=hashlib.sha256(b'SCALABLE_TYPED_SCHEMA_1')
47
+ framed(h,'POSITIONAL' if names is None else 'NAMED')
48
+ if names is not None:
49
+ for name in names:framed(h,name)
50
+ for kind,words in zip(kinds,vocabularies):
51
+ framed(h,kind);h.update(len(words).to_bytes(8,'little'))
52
+ for word in words:framed(h,word)
53
+ return h.hexdigest()
54
+
55
+
56
+ def encoder_digest(encoder):return schema_digest(encoder.names,[c['kind'] for c in encoder.columns],[c.get('vocabulary',()) for c in encoder.columns])
57
+
58
+
59
+ def data_identity(table,labels,role):
60
+ if role not in ('USER_INPUT','TRAIN','TRAIN_FULL'):raise ValueError('Explicit learning-source role')
61
+ if len(labels)!=len(table):raise ValueError('Aligned source/labels for identity')
62
+ h=hashlib.sha256(b'SCALABLE_ARRAY_SOURCE_1')
63
+ schema=schema_digest(table.names,table.kinds,table.vocab);framed(h,schema);framed(h,sys.byteorder)
64
+ for c in table.columns:
65
+ framed(h,c.typecode);h.update(len(c).to_bytes(8,'little'));h.update(memoryview(c))
66
+ source=h.hexdigest()
67
+ selected=hashlib.sha256(memoryview(table.indices)).hexdigest() if table.indices is not None else sha({'range_start':0,'range_stop':len(table)})
68
+ return dict(schema='SCALABLE_DATA_ID_1',source_sha256=source,schema_sha256=schema,effective_indices_sha256=selected,labels_sha256=hashlib.sha256(memoryview(labels)).hexdigest(),rows=len(table),features=table.width,role=role,byteorder=sys.byteorder,label_typecode=labels.typecode)
69
+
70
+
71
+ class Ledger:
72
+ def __init__(self):self.call_id=uuid.uuid4().hex;self.previous='0'*64;self.sequence=0
73
+ def emit(self,event,fields):
74
+ if event not in FIELDS or set(fields)!=FIELDS[event]:raise ValueError('Exact operation metadata schema: '+event)
75
+ v=dict(event=event,**fields,receipt_schema='SCALABLE_OPERATION_1',call_id=self.call_id,sequence=self.sequence,previous=self.previous);v['sha256']=sha(v)
76
+ self.previous=v['sha256'];self.sequence+=1;return v
77
+
78
+
79
+ def verify(events):
80
+ if type(events) is not list or not events or len(events)>10000:raise ValueError('Bounded nonempty receipt chain')
81
+ previous='0'*64;call_id=None
82
+ for i,event in enumerate(events):
83
+ if type(event) is not dict or event.get('event') not in FIELDS or set(event)!=FIELDS[event['event']]|ENVELOPE|{'event'}:raise ValueError('Invalid receipt fields')
84
+ if event['receipt_schema']!='SCALABLE_OPERATION_1' or type(event['sequence']) is not int or event['sequence']!=i or event['previous']!=previous:raise ValueError('Broken receipt order')
85
+ if type(event['call_id']) is not str or len(event['call_id'])!=32 or any(c not in '0123456789abcdef' for c in event['call_id']):raise ValueError('Malformed call identity')
86
+ if call_id is None:call_id=event['call_id']
87
+ if event['call_id']!=call_id:raise ValueError('Cross-call receipt replay')
88
+ body={k:v for k,v in event.items() if k!='sha256'}
89
+ if event['sha256']!=sha(body):raise ValueError('Changed operation receipt')
90
+ previous=event['sha256']
91
+ if events[0]['event']!='CALL_STARTED':raise ValueError('Missing call identity origin')
92
+ return dict(status='VERIFIED_HASH_CHAIN',events=len(events),call_id=call_id,final_sha256=previous,completion='FINAL_MODEL' if events[-1]['event']=='FINAL_MODEL' else 'PARTIAL_OR_NONTRAINING_CHAIN',authority='Integrity/custody only, not authorization or independent scientific validity')
@@ -0,0 +1,68 @@
1
+ """Compact target normalization without ndarray/Series.tolist or regression sort."""
2
+ from array import array
3
+ import sys
4
+ from .scalable_data import scalar
5
+
6
+
7
+ def label_scalar(value):
8
+ if type(value).__module__.split('.')[0]=='numpy' and hasattr(value,'item'):value=value.item()
9
+ # Class identifiers are not floating-point measurements. Preserve integer
10
+ # identity above2**53 rather than merging distinct labels through float().
11
+ return value if type(value) is int else scalar(value)
12
+
13
+
14
+ def detect_target(y,task,check,*,reserve=None):
15
+ if getattr(y,'ndim',1)!=1 or isinstance(y,(str,bytes,dict)) or not hasattr(y,'__len__') or len(y)<4:raise ValueError('At least four one-dimensional scalar targets required')
16
+ if task not in ('regression','classification','binary','multiclass'):raise ValueError('Explicit classifier/regressor intent required')
17
+ # Width follows the addressable input,not a small arbitrary class ceiling.
18
+ code='d' if task=='regression' else 'Q' if len(y)>2**32 else 'I'
19
+ numeric=None;values=array(code);lookup={};count=0;vocabulary_bytes=0
20
+ if reserve:reserve(len(y)*values.itemsize*(1 if task=='regression' else 2),'Target buffers before iteration')
21
+ for value in y:
22
+ if count%4096==0:check()
23
+ if reserve and isinstance(value,str):reserve(len(y)*values.itemsize*2+vocabulary_bytes+len(value)*32+512,'Target scalar conversion workspace')
24
+ value=scalar(value) if task=='regression' else label_scalar(value)
25
+ if value is None:raise ValueError('Missing target is unsupported')
26
+ is_numeric=type(value) in (int,float)
27
+ if numeric is None:numeric=is_numeric
28
+ if numeric!=is_numeric:raise ValueError('Target must be consistently string or numeric')
29
+ if task=='regression' and not numeric:raise ValueError('Regression requires finite numeric targets')
30
+ if task=='regression':values.append(value)
31
+ else:
32
+ if value not in lookup:
33
+ vocabulary_bytes+=sys.getsizeof(value)+256
34
+ if reserve:reserve(len(y)*values.itemsize*2+vocabulary_bytes,'Target vocabulary/codes/remap workspace')
35
+ lookup[value]=len(lookup)
36
+ values.append(lookup[value])
37
+ count+=1
38
+ if count!=len(y):raise ValueError('Target iterator/length mismatch')
39
+ if task=='regression':return task,values,[]
40
+ classes=sorted(lookup)
41
+ if len(classes)<2 or task=='binary' and len(classes)!=2:raise ValueError('Distinct task-compatible labels required')
42
+ task='binary' if len(classes)==2 else 'multiclass'
43
+ remap=[None]*len(classes)
44
+ for i,value in enumerate(classes):remap[lookup[value]]=i
45
+ return task,array(code,(remap[v] for v in values)),classes
46
+
47
+
48
+ def compact_oof_train_indices(n, held, resources):
49
+ """Validated complement in ascending source order,without Python-int sets.
50
+
51
+ Index storage follows addressable rows; neither fold nor observations are
52
+ sampled. Every returned index is unmarked and every held index is marked,
53
+ which proves the logged disjointness without allocating a second large set.
54
+ """
55
+ if type(n) is not int or n < 2:raise ValueError('Positive OOF partition size')
56
+ code='I' if n < 2**32 else 'Q'
57
+ resources.reserve(n*(1+array(code).itemsize),'OOF membership mask and compact complement')
58
+ marked=bytearray(n)
59
+ for position,index in enumerate(held):
60
+ if position%4096==0:resources.check()
61
+ if type(index) is not int or not 0<=index<n or marked[index]:raise ValueError('Unique bounded held indices required')
62
+ marked[index]=1
63
+ result=array(code)
64
+ for index in range(n):
65
+ if index%4096==0:resources.check()
66
+ if not marked[index]:result.append(index)
67
+ if not result or len(result)==n:raise ValueError('OOF needs nonempty disjoint train and held partitions')
68
+ return result