Skip to content

Commit 189b768

Browse files
committed
Phase 13.9.Fix1: Polynomial function persistence through schema export/import
export_schema_v2 now includes registered_functions. apply_schema calls _reconstruct_registered_functions() which rebuilds polynomials from PolynomialSpec + subframe coefficients. Evaluators skipped by design (GBAI owns persistence). 8 new tests (schema roundtrip, reconstruction, evaluator skip, invariance). 1440 passed, 6 failed (pre-existing).
1 parent a474ca5 commit 189b768

2 files changed

Lines changed: 249 additions & 0 deletions

File tree

UTILS/dfextensions/AliasDataFrame/AliasDataFrame.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8424,6 +8424,11 @@ def export_schema_v2(self, include_precision_stats=False, include_state=True,
84248424
if hasattr(self, '_fit_metadata') and self._fit_metadata:
84258425
result['fit_metadata'] = copy.deepcopy(self._fit_metadata)
84268426

8427+
# 7. Registered functions (Phase 13.9 Fix1)
8428+
reg_funcs = self._schema.get('registered_functions')
8429+
if reg_funcs:
8430+
result['registered_functions'] = copy.deepcopy(reg_funcs)
8431+
84278432
return result
84288433

84298434
def export_definition_schema(self, **kwargs):
@@ -8713,6 +8718,69 @@ def apply_schema(self, schema, validate=True, warn_missing=True):
87138718
UserWarning
87148719
)
87158720
self._fit_metadata = copy.deepcopy(schema['fit_metadata'])
8721+
8722+
# Restore registered functions if present (Phase 13.9 Fix1)
8723+
# Polynomials are reconstructed automatically if subframes are registered.
8724+
# Evaluators require manual re-registration (by design).
8725+
if 'registered_functions' in schema:
8726+
self._schema['registered_functions'] = copy.deepcopy(schema['registered_functions'])
8727+
self._reconstruct_registered_functions()
8728+
8729+
def _reconstruct_registered_functions(self):
8730+
"""
8731+
Reconstruct polynomial functions from schema after load.
8732+
8733+
Requires subframes to be already registered. Evaluator functions
8734+
are NOT reconstructed (by design — GBAI owns evaluator persistence).
8735+
8736+
Phase 13.9 Fix1: Polynomial persistence through export/import.
8737+
"""
8738+
from dfextensions.AliasDataFrame.PolynomialSpec import PolynomialSpec
8739+
8740+
reg_funcs = self._schema.get('registered_functions', {})
8741+
reconstructed = []
8742+
skipped = []
8743+
8744+
for name, spec_dict in reg_funcs.items():
8745+
func_type = spec_dict.get('type')
8746+
8747+
if func_type == 'evaluator':
8748+
# Evaluators must be re-registered manually
8749+
skipped.append(f"{name} (evaluator — re-register manually)")
8750+
continue
8751+
8752+
# Polynomial reconstruction
8753+
coeff_subframe = spec_dict.get('coefficients_subframe')
8754+
coeff_select = spec_dict.get('coeff_select')
8755+
8756+
if not coeff_subframe or not coeff_select:
8757+
skipped.append(f"{name} (missing coefficients_subframe or coeff_select)")
8758+
continue
8759+
8760+
# Check if subframe is registered
8761+
try:
8762+
sf = self.get_subframe(coeff_subframe)
8763+
if sf is None:
8764+
skipped.append(f"{name} (subframe '{coeff_subframe}' not registered)")
8765+
continue
8766+
except (KeyError, AttributeError):
8767+
skipped.append(f"{name} (subframe '{coeff_subframe}' not found)")
8768+
continue
8769+
8770+
# Reconstruct PolynomialSpec from schema
8771+
try:
8772+
poly_spec = PolynomialSpec.from_schema(spec_dict)
8773+
self.register_polynomial_from_subframe(
8774+
name, poly_spec, coeff_subframe, coeff_select, overwrite=True
8775+
)
8776+
reconstructed.append(name)
8777+
except Exception as e:
8778+
skipped.append(f"{name} (reconstruction failed: {e})")
8779+
8780+
if reconstructed:
8781+
print(f"[apply_schema] Reconstructed {len(reconstructed)} polynomial functions: {reconstructed}")
8782+
if skipped:
8783+
print(f"[apply_schema] Skipped {len(skipped)} functions: {skipped}")
87168784

87178785
@classmethod
87188786
def from_schema(cls, schema):
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""
2+
Tests for Phase 13.9.Fix1: Polynomial function persistence
3+
4+
Registered polynomial functions must survive schema export/import cycle.
5+
After loading, polynomial aliases should be functional without manual
6+
re-registration.
7+
"""
8+
9+
import pytest
10+
import numpy as np
11+
import pandas as pd
12+
import json
13+
import sys
14+
import os
15+
16+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17+
from AliasDataFrame import AliasDataFrame
18+
from PolynomialSpec import PolynomialSpec
19+
20+
21+
@pytest.fixture
22+
def adf_with_polynomial():
23+
"""ADF with registered polynomial from subframe."""
24+
df_main = pd.DataFrame({
25+
'group': np.array([0, 0, 1, 1, 0, 1]),
26+
'x': np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]),
27+
'y': np.array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5]),
28+
})
29+
# Coefficients: group 0 → [1.0, 0.5, 0.2, 0.05], group 1 → [2.0, -0.3, 0.1, -0.02]
30+
# Terms: x^0*y^0, x^1*y^0, x^0*y^1, x^1*y^1
31+
df_coeffs = pd.DataFrame({
32+
'group': [0, 1],
33+
'c_x0_y0': [1.0, 2.0],
34+
'c_x1_y0': [0.5, -0.3],
35+
'c_x0_y1': [0.2, 0.1],
36+
'c_x1_y1': [0.05, -0.02],
37+
})
38+
39+
adf = AliasDataFrame(df_main)
40+
adf.register_subframe('Coeffs', AliasDataFrame(df_coeffs), index_columns=['group'])
41+
42+
spec = PolynomialSpec(columns=['x', 'y'], degrees=(1, 1))
43+
coeff_cols = ['c_x0_y0', 'c_x1_y0', 'c_x0_y1', 'c_x1_y1']
44+
adf.register_polynomial_from_subframe('poly', spec, 'Coeffs', coeff_cols)
45+
adf.add_alias('correction', 'poly(x, y)')
46+
47+
return adf, spec, coeff_cols
48+
49+
50+
class TestPolynomialPersistence:
51+
"""Phase 13.9.Fix1: Polynomial function persistence."""
52+
53+
def test_schema_contains_registered_functions(self, adf_with_polynomial):
54+
"""export_schema includes registered_functions."""
55+
adf, _, _ = adf_with_polynomial
56+
schema = adf.export_schema()
57+
assert 'registered_functions' in schema
58+
assert 'poly' in schema['registered_functions']
59+
assert schema['registered_functions']['poly']['coefficients_subframe'] == 'Coeffs'
60+
61+
def test_schema_has_polynomial_spec(self, adf_with_polynomial):
62+
"""Schema stores PolynomialSpec details for reconstruction."""
63+
adf, _, coeff_cols = adf_with_polynomial
64+
schema = adf.export_schema()
65+
poly_schema = schema['registered_functions']['poly']
66+
assert poly_schema['coeff_select'] == coeff_cols
67+
assert 'columns' in poly_schema or 'dimensions' in poly_schema
68+
69+
def test_schema_json_serializable(self, adf_with_polynomial):
70+
"""Schema with registered_functions is JSON serializable."""
71+
adf, _, _ = adf_with_polynomial
72+
schema = adf.export_schema()
73+
json_str = json.dumps(schema)
74+
restored = json.loads(json_str)
75+
assert 'registered_functions' in restored
76+
77+
def test_save_load_schema_roundtrip(self, adf_with_polynomial, tmp_path):
78+
"""Schema saves and loads with registered_functions intact."""
79+
adf, _, _ = adf_with_polynomial
80+
schema_path = str(tmp_path / 'schema.json')
81+
adf.save_schema(schema_path)
82+
83+
loaded_schema = AliasDataFrame.load_schema(schema_path)
84+
assert 'registered_functions' in loaded_schema
85+
assert 'poly' in loaded_schema['registered_functions']
86+
87+
def test_reconstruct_polynomial_from_schema(self, adf_with_polynomial):
88+
"""apply_schema reconstructs polynomial functions."""
89+
adf, spec, coeff_cols = adf_with_polynomial
90+
91+
# Get reference values
92+
adf.materialize_alias('correction')
93+
reference = adf.df['correction'].values.copy()
94+
95+
# Export schema
96+
schema = adf.export_schema()
97+
98+
# Create new ADF with same data + subframe
99+
df_main = adf.df[['group', 'x', 'y']].copy()
100+
sf = adf.get_subframe('Coeffs')
101+
adf2 = AliasDataFrame(df_main)
102+
adf2.register_subframe('Coeffs', AliasDataFrame(sf.df.copy()), index_columns=['group'])
103+
104+
# Apply schema — should reconstruct polynomial
105+
adf2.apply_schema(schema)
106+
107+
# Alias should work now
108+
adf2.materialize_alias('correction')
109+
np.testing.assert_allclose(adf2.df['correction'].values, reference, atol=1e-10)
110+
111+
def test_reconstruct_skips_evaluator(self, adf_with_polynomial):
112+
"""Evaluator functions are skipped during reconstruction (by design)."""
113+
adf, _, _ = adf_with_polynomial
114+
115+
# Add a fake evaluator entry to schema
116+
adf._schema['registered_functions']['my_eval'] = {
117+
'type': 'evaluator',
118+
'coord_columns': ['x'],
119+
'predictor_columns': None,
120+
}
121+
122+
schema = adf.export_schema()
123+
124+
# Create new ADF
125+
df_main = adf.df[['group', 'x', 'y']].copy()
126+
sf = adf.get_subframe('Coeffs')
127+
adf2 = AliasDataFrame(df_main)
128+
adf2.register_subframe('Coeffs', AliasDataFrame(sf.df.copy()), index_columns=['group'])
129+
adf2.apply_schema(schema)
130+
131+
# Polynomial should be reconstructed, evaluator should not
132+
assert 'poly' in adf2._registered_functions
133+
assert 'my_eval' not in adf2._registered_functions
134+
135+
def test_reconstruct_missing_subframe_skips(self):
136+
"""Reconstruction skips if subframe not registered."""
137+
df = pd.DataFrame({'x': [1.0, 2.0], 'y': [0.5, 1.5]})
138+
adf = AliasDataFrame(df)
139+
140+
schema = {
141+
'registered_functions': {
142+
'poly': {
143+
'columns': ['x', 'y'],
144+
'degrees': [1, 1],
145+
'coefficients_subframe': 'MissingSF',
146+
'coeff_select': ['c0', 'c1', 'c2', 'c3'],
147+
}
148+
}
149+
}
150+
151+
# Should not raise — just skip with message
152+
adf.apply_schema(schema)
153+
assert 'poly' not in getattr(adf, '_registered_functions', {})
154+
155+
@pytest.mark.invariance
156+
def test_invariance_before_after_schema_roundtrip(self, adf_with_polynomial, tmp_path):
157+
"""Invariance: polynomial values identical before and after schema roundtrip."""
158+
adf, _, _ = adf_with_polynomial
159+
160+
# Before
161+
adf.materialize_alias('correction')
162+
before = adf.df['correction'].values.copy()
163+
164+
# Save schema
165+
schema_path = str(tmp_path / 'schema.json')
166+
adf.save_schema(schema_path)
167+
168+
# Reconstruct
169+
df_main = adf.df[['group', 'x', 'y']].copy()
170+
sf = adf.get_subframe('Coeffs')
171+
adf2 = AliasDataFrame(df_main)
172+
adf2.register_subframe('Coeffs', AliasDataFrame(sf.df.copy()), index_columns=['group'])
173+
loaded = AliasDataFrame.load_schema(schema_path)
174+
adf2.apply_schema(loaded)
175+
176+
# After
177+
adf2.materialize_alias('correction')
178+
after = adf2.df['correction'].values.copy()
179+
180+
np.testing.assert_allclose(before, after, atol=1e-10,
181+
err_msg="Polynomial values differ after schema roundtrip")

0 commit comments

Comments
 (0)