morphtree 2.1.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. morphtree/__init__.py +28 -0
  2. morphtree/__main__.py +3 -0
  3. morphtree/_experimental/__init__.py +1 -0
  4. morphtree/_experimental/admission.py +364 -0
  5. morphtree/_experimental/base.py +215 -0
  6. morphtree/_experimental/component_cache_v2.py +263 -0
  7. morphtree/_experimental/evolution_report.py +307 -0
  8. morphtree/_experimental/execution_v2.py +697 -0
  9. morphtree/_experimental/facade.py +289 -0
  10. morphtree/_experimental/integrated.py +748 -0
  11. morphtree/_experimental/learning.py +418 -0
  12. morphtree/_experimental/manifest.json +204 -0
  13. morphtree/_experimental/numeric_fixture.py +21 -0
  14. morphtree/_experimental/parameter_contract.py +38 -0
  15. morphtree/_experimental/postfit.py +51 -0
  16. morphtree/_experimental/preprocessing.py +133 -0
  17. morphtree/_experimental/processes.py +191 -0
  18. morphtree/_experimental/producer.py +542 -0
  19. morphtree/_experimental/product.py +156 -0
  20. morphtree/_experimental/product_cli.py +43 -0
  21. morphtree/_experimental/roundtrip_v3.py +311 -0
  22. morphtree/_experimental/runtime_io.py +65 -0
  23. morphtree/_experimental/sampling.py +26 -0
  24. morphtree/_experimental/support.py +281 -0
  25. morphtree/_experimental/target.py +59 -0
  26. morphtree/_experimental/training.py +53 -0
  27. morphtree/_experimental/worker.py +682 -0
  28. morphtree/_installed.py +17 -0
  29. morphtree/_native/__init__.py +1 -0
  30. morphtree/_native/bin/win_amd64/morphtree_kernels.dll +0 -0
  31. morphtree/_native/buffers.py +82 -0
  32. morphtree/_native/loader.py +39 -0
  33. morphtree/_native/manifest.json +29 -0
  34. morphtree/_native/runtime.py +48 -0
  35. morphtree/_native/src/histogram.c +61 -0
  36. morphtree/_native/src/traversal.c +51 -0
  37. morphtree/api.py +388 -0
  38. morphtree/api_compat.py +65 -0
  39. morphtree/backend.py +162 -0
  40. morphtree/booster.py +252 -0
  41. morphtree/cli.py +52 -0
  42. morphtree/config.py +262 -0
  43. morphtree/control.py +20 -0
  44. morphtree/data.py +300 -0
  45. morphtree/diagnostics.py +158 -0
  46. morphtree/estimators.py +156 -0
  47. morphtree/examples.py +25 -0
  48. morphtree/exporting.py +224 -0
  49. morphtree/legacy_artifacts.py +32 -0
  50. morphtree/metrics.py +156 -0
  51. morphtree/model.py +1010 -0
  52. morphtree/mutations.py +205 -0
  53. morphtree/native_lab_backend.py +79 -0
  54. morphtree/numpy_backend.py +180 -0
  55. morphtree/optuna_tuner.py +154 -0
  56. morphtree/persistence.py +414 -0
  57. morphtree/prediction_output.py +35 -0
  58. morphtree/prepared_bins.py +82 -0
  59. morphtree/public.py +106 -0
  60. morphtree/resource_plan.py +170 -0
  61. morphtree/scalable.py +781 -0
  62. morphtree/scalable_backend.py +74 -0
  63. morphtree/scalable_checkpoint.py +129 -0
  64. morphtree/scalable_data.py +255 -0
  65. morphtree/scalable_receipts.py +92 -0
  66. morphtree/scalable_target.py +68 -0
  67. morphtree/scalable_variants.py +133 -0
  68. morphtree/sklearn_view.py +79 -0
  69. morphtree/source_markers.py +25 -0
  70. morphtree/species.py +47 -0
  71. morphtree/temporary.py +65 -0
  72. morphtree/training_fragment.py +130 -0
  73. morphtree/tree.py +231 -0
  74. morphtree/tuning.py +150 -0
  75. morphtree-2.1.0.dist-info/METADATA +212 -0
  76. morphtree-2.1.0.dist-info/RECORD +79 -0
  77. morphtree-2.1.0.dist-info/WHEEL +4 -0
  78. morphtree-2.1.0.dist-info/entry_points.txt +2 -0
  79. morphtree-2.1.0.dist-info/licenses/LICENSE +21 -0
morphtree/booster.py ADDED
@@ -0,0 +1,252 @@
1
+ """One native scalar boosting loop for binary logloss and squared error."""
2
+ from __future__ import annotations
3
+
4
+ from array import array
5
+ import math
6
+ import random
7
+
8
+ from .backend import PythonBackend
9
+ from .data import Binner
10
+ from .config import AlgorithmGenome, validate_task_genome
11
+ from .metrics import logloss, sigmoid, rmse, multiclass_metrics
12
+ from .tree import HistogramTree
13
+
14
+
15
+ class TrainingCache:
16
+ """One fit-call cache. Never shared with other datasets or a full-data refit."""
17
+ def __init__(self, X_train, X_search, backend):
18
+ self.X_train, self.X_search, self.backend = X_train, X_search, backend
19
+ self.entries = {}
20
+
21
+ def get(self, genome, params):
22
+ key = (genome.binning_policy, params.max_bins)
23
+ if key not in self.entries:
24
+ binner = Binner(params.max_bins, genome.binning_policy).fit(self.X_train)
25
+ self.entries[key] = (binner, self.backend.bin(binner, self.X_train), self.backend.bin(binner, self.X_search))
26
+ return self.entries[key]
27
+
28
+
29
+ class BinaryHistogramBooster:
30
+ """Historical private name retained; regression uses this same tree/fit loop."""
31
+ def __init__(self, genome, params, seed=42, backend=None, *, memory_enabled=True, collect_diagnostics=False,
32
+ objective='binary_logloss', n_classes=None):
33
+ if objective not in ('binary_logloss', 'squared_error', 'softmax', 'ovr'):
34
+ raise ValueError('Unsupported boosting objective')
35
+ validate_task_genome('binary' if objective=='binary_logloss' else 'regression' if objective=='squared_error' else 'multiclass',genome)
36
+ if objective != 'binary_logloss' and (memory_enabled or collect_diagnostics):
37
+ raise ValueError('New tasks support fixed Base without memory or discovery')
38
+ if objective in ('softmax', 'ovr') and (type(n_classes) is not int or n_classes < 2):
39
+ raise ValueError('Multiclass requires an integer class dimension of at least two')
40
+ self.n_classes = n_classes
41
+ self.objective = objective
42
+ self.memory_enabled = memory_enabled
43
+ self.collect_diagnostics = collect_diagnostics
44
+ self.genome, self.params, self.seed = genome, params, seed
45
+ self.backend = backend or PythonBackend()
46
+ self.binner = None
47
+ self.trees, self.steps = [], []
48
+ self.base_score = 0.0
49
+ self.n_features_ = 0
50
+ self.memory_ = None
51
+ self.search_curve_ = []
52
+ self.stopped_early_ = False
53
+
54
+ def fit(self, X, y, *, eval_set=None, callback=None, cache=None, early_stopping_rounds=0, prepared_data=None):
55
+ if prepared_data is not None and (cache is not None or eval_set is not None):
56
+ raise ValueError('Prepared training data cannot be mixed with another cache/evaluation scope')
57
+ if self.n_classes is not None:
58
+ return self._fit_multiclass(X, y, eval_set, callback, early_stopping_rounds, prepared_data)
59
+ self.n_features_ = len(X[0])
60
+ if self.objective == 'squared_error':
61
+ self.base_score = y[0] if all(v == y[0] for v in y) else math.fsum(v / len(y) for v in y)
62
+ else:
63
+ prior = min(1 - 1e-12, max(1e-12, sum(y) / len(y)))
64
+ self.base_score = math.log(prior / (1 - prior))
65
+ if prepared_data is not None:
66
+ self.binner, Xb = prepared_data.get(X, self.genome, self.params, self.backend)
67
+ eval_bins = None
68
+ elif cache is not None:
69
+ if cache.X_train is not X or eval_set is None or cache.X_search is not eval_set[0]:
70
+ raise ValueError('Training cache belongs to different input objects')
71
+ self.binner, Xb, eval_bins = cache.get(self.genome, self.params)
72
+ else:
73
+ self.binner = Binner(self.params.max_bins, self.genome.binning_policy).fit(X)
74
+ Xb = self.backend.bin(self.binner, X)
75
+ eval_bins = self.backend.bin(self.binner, eval_set[0]) if eval_set else None
76
+ sizes = [len(c) + 2 for c in self.binner.thresholds]
77
+ rng = random.Random(self.seed)
78
+ raw = [self.base_score] * len(y)
79
+ eval_raw = [self.base_score] * len(eval_set[1]) if eval_set else None
80
+ needs_memory = (self.memory_enabled or self.collect_diagnostics or self.genome.error_policy != "none"
81
+ or self.genome.sample_policy != "uniform" or self.genome.leaf_policy == "uncertainty"
82
+ or "UNCERTAINTY_GAIN" in self.genome.split_program)
83
+ if needs_memory:
84
+ from .diagnostics import ErrorMemory
85
+ self.memory_ = ErrorMemory(len(y), self.params.error_memory_beta)
86
+ else:
87
+ self.memory_ = None
88
+ no_uncertainty = array("d", [0.0]) * len(y)
89
+ self.trees, self.steps, self.search_curve_ = [], [], []
90
+ self.stopped_early_ = False
91
+ sample_count = self.params.row_sample_count(len(y))
92
+ best_loss, best_count, stale = math.inf, 0, 0
93
+ for iteration in range(self.params.n_estimators):
94
+ p, gradient, hessian = (self.backend.regression_gradients(raw, y) if self.objective == 'squared_error'
95
+ else self.backend.gradients(raw, y))
96
+ if needs_memory:
97
+ if not self.memory_enabled:
98
+ self.memory_ = ErrorMemory(len(y), self.params.error_memory_beta)
99
+ self.memory_.update(y, p)
100
+ residual = array("d", (label - probability for label, probability in zip(y, p)))
101
+ uncertainty = self.memory_.uncertainty if needs_memory else no_uncertainty
102
+ if self.genome.error_policy == "persistent_weighting":
103
+ for i, hardness in enumerate(self.memory_.hardness):
104
+ weight = 1 + self.params.error_weight * hardness
105
+ gradient[i] *= weight
106
+ hessian[i] *= weight
107
+ ids = None
108
+ if sample_count < len(y):
109
+ count = sample_count
110
+ if self.genome.sample_policy == "uniform":
111
+ ids = sorted(rng.sample(range(len(y)), count))
112
+ else:
113
+ weights = self.memory_.hardness if self.genome.sample_policy == "hardness" else self.memory_.uncertainty
114
+ keys = [(-math.log(max(rng.random(), 1e-15)) / max(w, 1e-6), i) for i, w in enumerate(weights)]
115
+ ids = sorted(i for _, i in sorted(keys)[:count])
116
+ tree = HistogramTree(self.genome, self.params, self.backend).fit(Xb, gradient, hessian, residual, uncertainty,
117
+ rng, indices=ids, sizes=sizes)
118
+ step = self.params.learning_rate
119
+ if self.genome.update_policy == "decay":
120
+ step /= 1 + self.params.update_decay * iteration / max(self.params.n_estimators - 1, 1)
121
+ raw = self.backend.add_update(raw, step, tree.predict_binned(Xb))
122
+ self.trees.append(tree)
123
+ self.steps.append(step)
124
+ loss = None
125
+ if eval_set:
126
+ eval_raw = self.backend.add_update(eval_raw, step, tree.predict_binned(eval_bins))
127
+ loss = (rmse(eval_set[1], eval_raw) if self.objective == 'squared_error'
128
+ else logloss(eval_set[1], self.backend.probabilities(eval_raw)))
129
+ self.search_curve_.append(loss)
130
+ if loss < best_loss - (0.0 if self.objective == 'squared_error' else 1e-12):
131
+ best_loss, best_count, stale = loss, len(self.trees), 0
132
+ else:
133
+ stale += 1
134
+ if callback is not None and callback(iteration + 1, loss) is False:
135
+ self.stopped_early_ = True
136
+ break
137
+ if early_stopping_rounds and eval_set and stale >= early_stopping_rounds:
138
+ self.stopped_early_ = True
139
+ break
140
+ if early_stopping_rounds and best_count and best_count < len(self.trees):
141
+ self.trees = self.trees[:best_count]
142
+ self.steps = self.steps[:best_count]
143
+ # Rebuild memory so it describes the retained ensemble, not discarded trees.
144
+ raw = [self.base_score] * len(y)
145
+ if needs_memory:
146
+ self.memory_ = ErrorMemory(len(y), self.params.error_memory_beta)
147
+ for tree, step in zip(self.trees, self.steps):
148
+ if needs_memory:
149
+ self.memory_.update(y, self.backend.probabilities(raw))
150
+ raw = self.backend.add_update(raw, step, tree.predict_binned(Xb))
151
+ if needs_memory:
152
+ self.memory_.update(y, self.backend.probabilities(raw))
153
+ if any(not math.isfinite(float(v)) for v in raw):
154
+ raise ArithmeticError("Training produced nonfinite predictions")
155
+ return self
156
+
157
+ def _fit_multiclass(self, X, y, eval_set, callback, early_stopping_rounds, prepared_data=None):
158
+ """K scalar trees per round; one binner/tree builder and O(n*K) scores.
159
+
160
+ Derivatives of every class use the same frozen round probabilities.
161
+ OvR optimizes K binary losses; only its output is simplex-normalized.
162
+ """
163
+ k = self.n_classes
164
+ self.n_features_ = len(X[0])
165
+ counts = [0]*k
166
+ for label in y:
167
+ counts[label] += 1
168
+ priors = [min(1-1e-12, max(1e-12, count/len(y))) for count in counts]
169
+ self.base_score = [math.log(p) if self.objective == 'softmax' else math.log(p/(1-p)) for p in priors]
170
+ if self.objective == 'softmax':
171
+ self.backend.center_multiclass([self.base_score])
172
+ if prepared_data is None:
173
+ self.binner = Binner(self.params.max_bins, self.genome.binning_policy).fit(X)
174
+ bins = self.backend.bin(self.binner, X)
175
+ else:
176
+ self.binner, bins = prepared_data.get(X, self.genome, self.params, self.backend)
177
+ eval_bins = self.backend.bin(self.binner, eval_set[0]) if eval_set else None
178
+ sizes = [len(c)+2 for c in self.binner.thresholds]
179
+ raw = self.backend.multiclass_raw(self.base_score, len(y))
180
+ eval_raw = self.backend.multiclass_raw(self.base_score, len(eval_set[1])) if eval_set else None
181
+ rng = random.Random(self.seed)
182
+ zeros = array('d', [0.])*len(y)
183
+ self.trees, self.steps, self.search_curve_ = [], [], []
184
+ self.stopped_early_ = False
185
+ best, best_count, stale = math.inf, 0, 0
186
+ count = self.params.row_sample_count(len(y))
187
+ for iteration in range(self.params.n_estimators):
188
+ probabilities = self.backend.multiclass_probabilities(raw, self.objective, normalize=False)
189
+ for column in range(k):
190
+ if callback is not None and callback(iteration, None) is False:
191
+ # A partial round must never become a fitted/serialized model.
192
+ raise RuntimeError('Multiclass fit interrupted during a round')
193
+ gradient, hessian = self.backend.multiclass_gradient_column(probabilities, y, column, self.objective)
194
+ residual = array('d', (-v for v in gradient))
195
+ ids = sorted(rng.sample(range(len(y)), count)) if count < len(y) else None
196
+ tree = HistogramTree(self.genome, self.params, self.backend).fit(
197
+ bins, gradient, hessian, residual, zeros, rng, indices=ids, sizes=sizes)
198
+ step = self.params.learning_rate
199
+ self.backend.multiclass_update(raw, column, step, tree.predict_binned(bins))
200
+ if eval_set:
201
+ self.backend.multiclass_update(eval_raw, column, step, tree.predict_binned(eval_bins))
202
+ self.trees.append(tree)
203
+ self.steps.append(step)
204
+ if self.objective == 'softmax':
205
+ self.backend.center_multiclass(raw)
206
+ if eval_set:
207
+ self.backend.center_multiclass(eval_raw)
208
+ loss = None
209
+ if eval_set:
210
+ loss = multiclass_metrics(eval_set[1], self.backend.multiclass_probabilities(eval_raw, self.objective))['logloss']
211
+ self.search_curve_.append(loss)
212
+ if loss < best:
213
+ best, best_count, stale = loss, len(self.trees), 0
214
+ else:
215
+ stale += 1
216
+ if callback is not None and callback(iteration+1, loss) is False:
217
+ self.stopped_early_ = True
218
+ break
219
+ if early_stopping_rounds and eval_set and stale >= early_stopping_rounds:
220
+ self.stopped_early_ = True
221
+ break
222
+ if early_stopping_rounds and best_count:
223
+ self.trees, self.steps = self.trees[:best_count], self.steps[:best_count]
224
+ if any(not math.isfinite(float(v)) for row in raw for v in row):
225
+ raise ArithmeticError('Multiclass training produced nonfinite scores')
226
+ return self
227
+
228
+ def predict_raw(self, X):
229
+ if self.binner is None:
230
+ raise RuntimeError("Booster is not fitted")
231
+ Xb = self.backend.bin(self.binner, X)
232
+ if self.n_classes is not None:
233
+ raw = self.backend.multiclass_raw(self.base_score, len(X))
234
+ for index, (tree, step) in enumerate(zip(self.trees, self.steps)):
235
+ column = index % self.n_classes
236
+ self.backend.multiclass_update(raw, column, step, tree.predict_binned(Xb))
237
+ if column == self.n_classes-1 and self.objective == 'softmax':
238
+ self.backend.center_multiclass(raw)
239
+ return raw
240
+ raw = [self.base_score] * len(X)
241
+ for tree, step in zip(self.trees, self.steps):
242
+ raw = self.backend.add_update(raw, step, tree.predict_binned(Xb))
243
+ return raw
244
+
245
+ def predict_proba(self, X):
246
+ if self.n_classes is not None:
247
+ return self.backend.multiclass_probabilities(self.predict_raw(X), self.objective)
248
+ return self.backend.probabilities(self.predict_raw(X))
249
+
250
+ def to_dict(self):
251
+ return {"base_score": self.base_score, "n_features": self.n_features_, "binner": self.binner.to_dict(),
252
+ "steps": self.steps, "trees": [tree.to_dict() for tree in self.trees]}
morphtree/cli.py ADDED
@@ -0,0 +1,52 @@
1
+ """Inspect, predict and export saved public MorphTree models (no training)."""
2
+ import argparse,json
3
+ from pathlib import Path
4
+ from . import load_model
5
+ from .persistence import regular_file_size,read_regular_bytes,decode_json_bytes,LoadLimits
6
+
7
+
8
+ class _NamedRows:
9
+ """JSON rows with column names for both supported model generations."""
10
+ def __init__(self,rows,names):
11
+ self.rows=rows;self.columns=tuple(names);self.shape=(len(rows),len(names))
12
+ def __len__(self):return len(self.rows)
13
+ def to_numpy(self):return self.rows
14
+ def itertuples(self,index=False,name=None):return iter(self.rows)
15
+ @property
16
+ def iloc(self):return self
17
+ def __getitem__(self,key):
18
+ class Column:
19
+ def __init__(self,values):self.array=values
20
+ return Column([row[key[1]] for row in self.rows[key[0]]])
21
+
22
+ def main(argv=None):
23
+ p=argparse.ArgumentParser(description=__doc__)
24
+ p.add_argument('command',choices=('inspect','predict','export'))
25
+ p.add_argument('--model',required=True);p.add_argument('--sha256')
26
+ p.add_argument('--input');p.add_argument('--output')
27
+ a=p.parse_args(argv)
28
+ try:
29
+ m=load_model(a.model,sha256=a.sha256)
30
+ if a.command=='inspect':v={'program':m.get_algorithm_program(),'recipe':m.get_recipe(),'history':m.get_mutation_history()}
31
+ elif a.command=='export':
32
+ if not a.output:raise ValueError('--output required')
33
+ m.export_standalone(a.output);v={'standalone':a.output,'new_fits':0}
34
+ else:
35
+ if not a.input:raise ValueError('--input required')
36
+ f=Path(a.input)
37
+ from .scalable import managed_resources
38
+ from .resource_plan import io_seconds
39
+ size=regular_file_size(f);res=managed_resources('auto',io_seconds(size))
40
+ if size<1:raise ValueError('Nonempty regular JSON input required')
41
+ res.reserve(size*12+4096,'CLI JSON input workspace')
42
+ raw=read_regular_bytes(f,max(1,res.bytes_limit//12))
43
+ positional=raw.lstrip().startswith(b'[')
44
+ if positional:raw=b'{"rows":'+raw+b'}'
45
+ v=decode_json_bytes(raw,LoadLimits(max_bytes=max(1,res.bytes_limit//12)+9),reserve=res.reserve)
46
+ if positional:v=v['rows']
47
+ x=_NamedRows(v['rows'],v['columns']) if type(v) is dict and set(v)=={'columns','rows'} else v
48
+ v={'prediction':m.predict(x).tolist()}
49
+ if m.task_!='regression':v['probabilities']=m.predict_proba(x).tolist()
50
+ print(json.dumps(v,ensure_ascii=False,allow_nan=False));return 0
51
+ except (ValueError,TypeError,OSError,RuntimeError,ImportError,MemoryError) as e:p.exit(2,type(e).__name__+': '+str(e)+'\n')
52
+ if __name__=='__main__':raise SystemExit(main())
morphtree/config.py ADDED
@@ -0,0 +1,262 @@
1
+ """Immutable structural genes, numerical parameters and protocol settings."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import asdict, dataclass, replace
5
+ import hashlib
6
+ import json
7
+ import math
8
+ from numbers import Real
9
+
10
+ VERSION = "0.6.0.dev0"
11
+ SPEC_VERSION = 1
12
+ POLICIES = {
13
+ "growth_policy": ("leafwise", "depthwise", "lookahead"),
14
+ "binning_policy": ("quantile", "uniform"),
15
+ "missing_policy": ("learned", "left", "right"),
16
+ "leaf_policy": ("newton", "smoothed", "clipped", "uncertainty"),
17
+ "regularization_policy": ("global", "depth_scaled"),
18
+ "sample_policy": ("uniform", "hardness", "uncertainty"),
19
+ "error_policy": ("none", "persistent_weighting"),
20
+ "local_policy": ("none", "low_support", "missing_heavy"),
21
+ "update_policy": ("constant", "decay"),
22
+ }
23
+ SIGNALS = ("OBJECTIVE_GAIN", "RESIDUAL_VARIANCE_GAIN", "BALANCE", "HESSIAN_BALANCE", "UNCERTAINTY_GAIN")
24
+
25
+
26
+ def checked_number(name, value, low=0.0, high=None, *, strict=False):
27
+ """Validate the actual float used by the runtime, not a wider external scalar."""
28
+ if isinstance(value, bool) or not isinstance(value, Real):
29
+ raise ValueError(f"{name} must be a finite real number")
30
+ try:
31
+ value = float(value)
32
+ except (OverflowError, TypeError, ValueError) as exc:
33
+ raise ValueError(f"{name} must be representable as a finite Python float") from exc
34
+ if not math.isfinite(value):
35
+ raise ValueError(f"{name} must be a finite number")
36
+ if value < low or (strict and value == low) or (high is not None and value > high):
37
+ raise ValueError(f"{name} is outside its permitted range")
38
+ return value
39
+
40
+
41
+ def _set_number(instance, name, low=0.0, high=None, *, strict=False):
42
+ value = checked_number(name, getattr(instance, name), low, high, strict=strict)
43
+ object.__setattr__(instance, name, value)
44
+
45
+
46
+ def checked_int(name, value, low, high):
47
+ if type(value) is not int or not low <= value <= high:
48
+ raise ValueError(f"{name} must be an integer in [{low}, {high}]")
49
+
50
+
51
+ def canonical_hash(data):
52
+ text = json.dumps(data, sort_keys=True, separators=(",", ":"), allow_nan=False)
53
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
54
+
55
+
56
+ def validate_program(program):
57
+ if not isinstance(program, (list, tuple)) or not 1 <= len(program) <= 15:
58
+ raise ValueError("A split program needs 1..15 instructions")
59
+ depth = 0
60
+ for token in program:
61
+ if not isinstance(token, str):
62
+ raise ValueError("Program instructions must be strings")
63
+ if token in SIGNALS:
64
+ depth += 1
65
+ elif token in ("MUL_SLOT:0", "MUL_SLOT:1"):
66
+ if depth < 1:
67
+ raise ValueError("Split program stack underflow")
68
+ elif token in ("ADD", "MUL", "MIN", "MAX"):
69
+ if depth < 2:
70
+ raise ValueError("Split program stack underflow")
71
+ depth -= 1
72
+ else:
73
+ raise ValueError(f"Unknown split instruction: {token}")
74
+ if depth != 1 or "OBJECTIVE_GAIN" not in program:
75
+ raise ValueError("Split program must return one score and include OBJECTIVE_GAIN")
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class AlgorithmGenome:
80
+ growth_policy: str = "leafwise"
81
+ binning_policy: str = "quantile"
82
+ missing_policy: str = "learned"
83
+ leaf_policy: str = "newton"
84
+ regularization_policy: str = "global"
85
+ sample_policy: str = "uniform"
86
+ error_policy: str = "none"
87
+ local_policy: str = "none"
88
+ update_policy: str = "constant"
89
+ split_program: tuple[str, ...] = ("OBJECTIVE_GAIN",)
90
+
91
+ def __post_init__(self):
92
+ for name, choices in POLICIES.items():
93
+ if getattr(self, name) not in choices:
94
+ raise ValueError(f"Invalid {name}")
95
+ validate_program(self.split_program)
96
+ object.__setattr__(self, "split_program", tuple(self.split_program))
97
+
98
+ def mutate(self, **changes):
99
+ return replace(self, **changes)
100
+
101
+ def to_dict(self):
102
+ data = asdict(self)
103
+ data["split_program"] = list(self.split_program)
104
+ return data
105
+
106
+ def fingerprint(self):
107
+ return canonical_hash({"spec_version": SPEC_VERSION, "genome": self.to_dict()})
108
+
109
+ def active_parameters(self):
110
+ names = ["learning_rate", "n_estimators", "max_bins", "max_leaves", "max_depth", "min_samples_leaf",
111
+ "l1", "l2", "min_hessian", "min_gain", "feature_fraction", "row_fraction"]
112
+ conditional = {
113
+ "path_smooth": self.leaf_policy == "smoothed",
114
+ "max_delta_step": self.leaf_policy == "clipped",
115
+ "depth_reg_strength": self.regularization_policy == "depth_scaled",
116
+ "error_memory_beta": self.error_policy != "none" or self.sample_policy != "uniform",
117
+ "error_weight": self.error_policy != "none",
118
+ "update_decay": self.update_policy == "decay",
119
+ "gate_strength": self.local_policy != "none" or self.leaf_policy == "uncertainty",
120
+ }
121
+ names.extend(name for name, active in conditional.items() if active)
122
+ names.extend(f"signal_slot_{i}" for i in range(2) if f"MUL_SLOT:{i}" in self.split_program)
123
+ return tuple(names)
124
+
125
+ def complexity(self):
126
+ base = AlgorithmGenome()
127
+ return sum(getattr(self, k) != getattr(base, k) for k in POLICIES) + len(self.split_program) - 1
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class HyperParams:
132
+ learning_rate: float = 0.1
133
+ n_estimators: int = 32
134
+ max_bins: int = 31
135
+ max_leaves: int = 7
136
+ max_depth: int = 5
137
+ min_samples_leaf: int = 6
138
+ l1: float = 0.0
139
+ l2: float = 2.0
140
+ min_hessian: float = 1e-3
141
+ min_gain: float = 1e-5
142
+ feature_fraction: float = 1.0
143
+ row_fraction: float = 0.8
144
+ path_smooth: float = 8.0
145
+ max_delta_step: float = 2.0
146
+ depth_reg_strength: float = 0.2
147
+ error_memory_beta: float = 0.9
148
+ error_weight: float = 1.0
149
+ update_decay: float = 0.75
150
+ gate_strength: float = 1.0
151
+ signal_slot_0: float = 0.25
152
+ signal_slot_1: float = 0.25
153
+
154
+ def __post_init__(self):
155
+ for name, low, high in (("n_estimators", 1, 4096), ("max_bins", 2, 4096), ("max_leaves", 2, 4096),
156
+ ("max_depth", 1, 128), ("min_samples_leaf", 1, 10**9)):
157
+ checked_int(name, getattr(self, name), low, high)
158
+ _set_number(self, "learning_rate", 0, 1, strict=True)
159
+ _set_number(self, "min_hessian", 0, 1e6, strict=True)
160
+ for name in ("l1", "l2", "min_gain", "path_smooth", "depth_reg_strength", "error_weight", "update_decay", "gate_strength"):
161
+ _set_number(self, name, 0, 1e6)
162
+ _set_number(self, "max_delta_step", 0, 1e6, strict=True)
163
+ for name in ("feature_fraction", "row_fraction"):
164
+ _set_number(self, name, 0, 1, strict=True)
165
+ _set_number(self, "error_memory_beta", 0, 1)
166
+ if self.error_memory_beta >= 1:
167
+ raise ValueError("error_memory_beta must be less than 1")
168
+ for name in ("signal_slot_0", "signal_slot_1"):
169
+ _set_number(self, name, 0, 10)
170
+
171
+ def row_sample_count(self, n_rows):
172
+ """Fixed-size training subsample; leaf support can require the full dataset."""
173
+ if type(n_rows) is not int or n_rows < 1:
174
+ raise ValueError("n_rows must be a positive Python integer")
175
+ return min(n_rows, max(2 * self.min_samples_leaf, round(n_rows * self.row_fraction)))
176
+
177
+
178
+ @dataclass(frozen=True)
179
+ class MorphTreeConfig:
180
+ seed: int = 42
181
+ search_fraction: float = 0.15
182
+ guard_fraction: float = 0.15
183
+ enable_morphing: bool = False
184
+ enable_tuning: bool = True
185
+ structural_rounds: int = 16
186
+ structural_mutants: int = 3
187
+ structural_patience: int = 2
188
+ structural_evaluations: int | None = None
189
+ search_timeout: float | None = None
190
+ freeze_on_budget: bool = False
191
+ diagnostic_guidance: bool = False
192
+ memory_enabled: bool = False
193
+ min_morph_improvement: float = 0.0002
194
+ guard_tolerance: float = 0.001
195
+ global_guard_tolerance: float = 0.002
196
+ complexity_penalty: float = 0.00001
197
+ confidence_z: float = 0.0
198
+ probe_fraction: float = 0.35
199
+ probe_margin: float = 0.04
200
+ tuner: str = "none"
201
+ tune_trials: int = 16
202
+ optuna_startup_trials: int = 5
203
+ tune_patience: int = 6
204
+ tune_timeout: float | None = None
205
+ early_stopping_rounds: int = 6
206
+ optuna_storage: str | None = None
207
+ optuna_study_name: str | None = None
208
+ refit_full: bool = True
209
+
210
+ def __post_init__(self):
211
+ for name in ("enable_morphing", "enable_tuning", "refit_full", "freeze_on_budget", "diagnostic_guidance", "memory_enabled"):
212
+ if type(getattr(self, name)) is not bool:
213
+ raise ValueError(f"{name} must be a bool")
214
+ for name, low, high in (("seed", 0, 2**32 - 1), ("structural_rounds", 0, 10000),
215
+ ("structural_mutants", 1, 128), ("structural_patience", 1, 100),
216
+ ("tune_trials", 1, 100000), ("tune_patience", 1, 10000),
217
+ ("optuna_startup_trials", 1, 100000),
218
+ ("early_stopping_rounds", 0, 4096)):
219
+ checked_int(name, getattr(self, name), low, high)
220
+ for name in ("search_fraction", "guard_fraction"):
221
+ _set_number(self, name, 0, 0.5, strict=True)
222
+ if self.search_fraction + self.guard_fraction >= 0.6:
223
+ raise ValueError("SEARCH and GUARD must leave more than 40% for TRAIN")
224
+ for name in ("min_morph_improvement", "guard_tolerance", "global_guard_tolerance", "complexity_penalty", "probe_margin"):
225
+ _set_number(self, name, 0, 10)
226
+ _set_number(self, "confidence_z", 0, 5)
227
+ _set_number(self, "probe_fraction", 0, 1)
228
+ if self.tune_timeout is not None:
229
+ _set_number(self, "tune_timeout", 0, 1e9, strict=True)
230
+ if self.search_timeout is not None:
231
+ _set_number(self, "search_timeout", 0, 1e9, strict=True)
232
+ if self.structural_evaluations is not None:
233
+ checked_int("structural_evaluations", self.structural_evaluations, 0, 1000000)
234
+ if self.tuner not in ("none", "native", "optuna"):
235
+ raise ValueError("tuner must be none, native or optuna")
236
+ for name in ("optuna_storage", "optuna_study_name"):
237
+ value = getattr(self, name)
238
+ if value is not None and (not isinstance(value, str) or not value.strip()):
239
+ raise ValueError(f"{name} must be a nonempty string or None")
240
+
241
+
242
+ # Versioned experimental capability; consolidated config.py is unchanged.
243
+ TASK_GROWTH_FORMATS = {'regression':'MorphTreeRegressionGrowthV2',
244
+ 'multiclass':'MorphTreeMulticlassGrowthV2'}
245
+
246
+ def validate_task_genome(task, genome):
247
+ if task not in ('binary','multiclass','regression') or not isinstance(genome,AlgorithmGenome):
248
+ raise ValueError('Known task and native AlgorithmGenome required')
249
+ if task!='binary' and (genome.split_program not in (("OBJECTIVE_GAIN",),("OBJECTIVE_GAIN","BALANCE","MUL")) or genome!=AlgorithmGenome(growth_policy=genome.growth_policy,split_program=genome.split_program)):
250
+ raise ValueError('ADV001 laboratory admits growth and objective-times-balance only; not a public format qualification')
251
+ return genome
252
+
253
+ def decode_task_genome(task, payload):
254
+ if payload.get('format')!=TASK_GROWTH_FORMATS[task]:
255
+ if 'genome' in payload:raise ValueError('Legacy native format cannot acquire a genome field')
256
+ return AlgorithmGenome()
257
+ if type(payload.get('genome')) is not dict:
258
+ raise ValueError('V2 native growth requires an explicit genome')
259
+ try:genome=validate_task_genome(task,AlgorithmGenome(**payload['genome']))
260
+ except (TypeError,ValueError):raise ValueError('Invalid task-compatible V2 genome') from None
261
+ if genome==AlgorithmGenome():raise ValueError('Base retains its legacy native format')
262
+ return genome
morphtree/control.py ADDED
@@ -0,0 +1,20 @@
1
+ """Cooperative cancellation at candidate and boosting-iteration boundaries."""
2
+ import time
3
+
4
+ class SearchInterrupted(Exception):
5
+ def __init__(self, reason):
6
+ self.reason = reason
7
+ super().__init__(reason)
8
+
9
+ class RunControl:
10
+ def __init__(self, cancel=None, timeout=None):
11
+ if cancel is not None and not callable(cancel):
12
+ raise ValueError('cancel must be callable or None')
13
+ self.cancel = cancel
14
+ self.deadline = None if timeout is None else time.monotonic() + timeout
15
+
16
+ def check(self):
17
+ if self.cancel is not None and self.cancel():
18
+ raise SearchInterrupted('cancelled')
19
+ if self.deadline is not None and time.monotonic() >= self.deadline:
20
+ raise SearchInterrupted('timeout')