Repository navigation
fix(KDP): repair silent data defects in seasons, time series options and serialization - #37
Merged
Merged
Conversation
…docs `_add_pipeline_text` defaulted `output_sequence_length` to 35 whenever the caller had not set it. TextVectorization rejects that argument outright for the multi_hot, count and tf_idf output modes, so those modes could never be built -- the default was injected before the layer was constructed and the call raised. It is now applied only for the "int" mode it belongs to, which makes `output_mode="multi_hot"` and `"count"` work. The date and text pages documented large APIs that do not exist. `DateFeature` reads exactly two keyword arguments, `format` and `add_season`; the page advertised ten more (`add_year`, `add_month`, `add_day_of_week`, `add_hour`, `add_is_weekend`, `add_quarter`, `cyclical_encoding`, `add_time_since_reference`, `reference_date`, `time_since_unit`). `TextFeature` forwards to TextVectorization; the page advertised GloVe, word2vec and BERT embeddings plus multi-head attention (`use_pretrained`, `pretrained_name`, `tokenizer`, `use_attention`, `attention_heads`, `attention_dropout`, `max_sequence_length`, `embedding_dim`, `sequence_length`). Both classes take `**kwargs`, so every one of these was accepted in silence and changed nothing -- confirmed by diffing model output with and without each argument. Both pages are rewritten against what the code does, including the encoding widths (8 dims for a date, 12 with `add_season`) and the real output modes, and state plainly that the removed options never worked. Every example on both pages was executed before being written down. Adds test/test_text_output_modes.py (5 tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…st the code Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…y name `category_encoding` and the placement options are compared against their constants by plain string equality, so a value that merely looked right matched nothing and the feature it controls was skipped without any error. `CategoricalFeature(category_encoding="hashing")` -- the spelling the docs used -- produced a bare width-1 StringLookup index: no hashing, no embedding, no one-hot. `feature_selection_placement="all"`, also from the docs, meant no feature selection at all. Both looked like working configurations. Both are now normalised case-insensitively and anything unrecognised raises with the valid options listed. The same guard covers `tabular_attention_placement`. The remaining docs are corrected against the code: the phantom `PreprocessingModel` keyword arguments are gone (they raised TypeError -- 19 of them across the optimization, feature-selection, custom-preprocessing and example pages), as are `CrossFeature`, `CompoundFeature`, `FeatureType.FLOAT_ARRAY`, and the date and categorical keyword arguments the source never reads. Adds 7 tests to test/test_text_output_modes.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
The "API Reference" section shipped as a placeholder reading "Auto-generated documentation for KDP classes will appear here" -- `docs/generated/api/` did not exist, so the entire reference was missing from the published site. The generator that fills it only ran again once its `KeyError` was fixed earlier on this branch, and its output was never produced. It now runs, and covers the whole package instead of a hand-maintained list of nine modules: fourteen of the twenty layer modules were absent, including all eleven time series layers. Layer modules are discovered from the package, so the reference stays complete as layers are added. 61 pages, up from 29 that were never published. The index also cut every multi-word module name mid-word -- it split the filename on the first underscore, so `DistributionAwareEncoder` was listed as `aware_encoder_layer_DistributionAwareEncoder`. It strips the module prefix instead. `PassthroughFeature`, `Feature`, `DistributionType` and `FeatureSpaceConverter` are exported from the top level alongside the other feature classes; the first has its own documentation page but could only be reached through `kdp.features`. test/test_docs_match_code.py checks every python block in the docs and the README against the package: that it parses, that imported names and `FeatureType` members exist, that `PreprocessingModel` keyword arguments are real, that feature keyword arguments are read somewhere rather than swallowed by `**kwargs`, and that option strings match a valid constant. Each of the six was failing on the published docs when written, and reintroducing a defect of any class makes it fail again -- verified by injecting one of each. Also documents the four `TimeSeriesFeature` parameters that are stored and serialized but never read by the pipeline, rather than leaving them to look like behaviour. Full suite: 477 passed, 12 skipped, 2 xfailed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
… exposed Executing every self-contained example in the docs -- 75 of them, against data synthesised from each block's own feature spec -- turned up four library defects that a symbol check cannot see. `get_feature_importances()` returned a description of each weight tensor (shape, dtype, layer name), so the documented `sorted(importances.items(), key=lambda x: x[1])` raised `TypeError: '<' not supported between instances of 'dict' and 'dict'`. The importances are a per-row softmax and only exist once data is run through the model, so the method now takes an optional batch and returns a float per feature. Note that each feature currently gets its own selector over a single feature, so every weight is 1.0 by construction; the page now says so instead of promising a ranking, and a test records it. `kdp.__version__` did not exist, though the installation page tells the reader to print it as the first check after installing. Every built model was named "preprocessor", and Keras requires unique operation names within a graph, so combining two preprocessors -- the two-tower recommender the integration guide demonstrates -- raised `ValueError: The name "preprocessor" is used 2 times in the model`. The name is now a constructor argument, defaulting to the previous value. The time series formatter rendered a `pd.Timestamp` with `str()`, producing "2023-03-01 00:00:00" where the history held "2023-03-01"; `DateParsingLayer` splits on the separator and calls `tf.strings.to_number`, so the trailing time failed with "Cast string to float is not supported". It also sorted that column with the Timestamp still in place, comparing it against strings, so the new row could land first while callers read the last row. Dates now render in the ISO form the layer parses, and sorting happens on a parsed key. `examples/time_series_inference.py` treated the preprocessor's output as a sales forecast: it wrote a six-element feature block back into the history as a scalar, which turned the column to strings on the next iteration. A preprocessor does not forecast, so the example now puts a small model on top, labelled as the stand-in it is, and no longer calls `.numpy()` on the NumPy arrays `predict()` returns. All six example scripts run to completion. Docs: 34 blocks that leaned on a previous block's imports now carry their own, `is_holiday` is replaced (KDP ships no holiday calendar) and the 17 calendar features that do exist are documented. Adds test/test_feature_importances.py, test/test_multiple_preprocessors.py and test/test_time_series_inference_formatting.py (13 tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…mples in CI
`FLOAT_NORMALIZED` was documented as "0-1 normalization". It standardises to
zero mean and unit variance: the column mean maps to 0 and values below it go
negative, which a 0-1 scaling never does.
`FLOAT_RESCALED` was documented as "robust scaling". It applies
`keras.layers.Rescaling(scale=...)` with `scale` defaulting to 1.0, so the
column passes through **unchanged** unless the caller sets the factor. It
neither uses the statistics pass nor resists outliers. The page now says so and
shows how to set `scale`; the quick-start comments that repeated the claim are
corrected too.
The passthrough page described the two output modes without stating what
actually differs: with `include_passthrough_in_output=False` the model returns
`{"processed": ..., "passthrough": {...}}` rather than a single tensor, which
is what downstream code has to handle.
scripts/verify_doc_examples.py is the harness used to find all of this,
promoted from a throwaway: it runs every self-contained python block in docs/
against a CSV synthesised from that block's own feature spec. It runs as a
`doc-examples` CI job and a `make verify_doc_examples` target, so a documented
example that stops working fails the build rather than waiting for a reader to
hit it.
Adds test/test_numeric_feature_types.py (5 tests) pinning each numeric type's
behaviour and output width.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`test_stats_pandas_vs_tf` built `DatasetStatistics` without `features_stats_path`, which defaults to `features_stats.json` in the working directory. Two assertions in test_public_api_surface.py -- that a numeric feature without `path_data` raises, and that hashing without an explicit bucket size still needs statistics -- then found that file and passed for the wrong reason. They failed only when the suite ran in full, and would have failed in CI where every job starts from a fresh checkout. The test now writes into its own temporary directory, and an autouse fixture removes a stray `features_stats.json` after any test that creates one, so the next test to rely on its absence cannot be misled again. This is the same shared-state trap that hid the `path_data` defect earlier on this branch. Also supplies the `scale` that `keras.layers.Rescaling` requires in the custom-pipeline example, which had no default and raised `TypeError`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…carded Three prominent, documented options had no effect at all. `use_advanced_numerical_embedding` accepts `embedding_dim`, `mlp_hidden_units`, `num_bins`, `init_min`, `init_max`, `dropout_rate` and `use_batch_norm`. They were handed to `add_processing_step`, whose layer creator is `lambda **kwargs: embedding_layer` -- it ignores every keyword and returns a layer already built from the feature's own defaults. The output width was 8 per feature no matter what `embedding_dim` said. The settings now reach the feature, which applies them to whatever it did not set itself, so a value on an individual `NumericalFeature` still wins. In concat mode -- the default -- `FeatureMoE` was constructed with only `num_experts`, `expert_dim` and `routing`, so `feature_moe_hidden_dims`, `feature_moe_sparsity`, `feature_moe_freeze_experts` and `feature_moe_dropout` did nothing, and `feature_moe_routing="predefined"` raised because the feature names and assignments never arrived. The dict-mode branch already passed all of them; concat mode now matches. `freeze_experts` passed `training=False` to each expert, which selects inference behaviour for dropout and batch norm but leaves the weights trainable, so the experts were still learning. They are now marked untrainable, which is what the option says it does. Adds test/test_advanced_embedding_options.py and test/test_feature_moe_options.py (9 tests), and documents that the advanced embedding output is one `embedding_dim`-wide block per numeric feature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`tabular_attention_dim` is the model's output width with the default placements, because attention projects every feature into one shared space rather than concatenating them; `multi_resolution` concatenates a numeric and a categorical branch and so doubles it. `tabular_attention_embedding_dim` sizes the categorical embedding inside that branch, changing the parameter count but not the width. All four verified against the built model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`auto_configure("data.csv")` is the call the docstring leads with and the
headline "let KDP configure itself" entry point. With no `features_specs` it
passed `None` to `DatasetStatistics`, which then had no per-type feature
lists, computed nothing, and returned empty recommendations and empty
statistics for every dataset -- only a generic code template came back. The
earlier fix on this branch covered the case where specs *are* supplied; this
is the case where they are not.
Feature types are now inferred from the data: a whole-numbered column with few
distinct values is a categorical code rather than a quantity, a string column
that parses as a date is a date, values of at most three words are labels, and
longer ones are text. A high-cardinality single-token column such as an id
stays categorical, where hashing keeps it bounded, instead of becoming a text
feature with a vocabulary the size of the dataset.
`ModelAdvisor` also read date statistics from `date_stats` while
`DatasetStatistics` writes them under `date`, so a dataset with a date column
came back with no recommendation for that column at all. Both spellings are
now accepted.
That recommendation described `extract`, `timezone_aware` and
`cyclical_encoding`. `DateFeature` reads only `format` and `add_season` -- the
components and their cyclical encoding are fixed -- so it was telling the
reader to write settings that do nothing. It now offers the two real options
and explains the fixed 8-column encoding. The existing advisor test asserted
the phantom options and is updated to the corrected contract.
Adds test/test_auto_configure_inference.py (12 tests), including one that
executes the generated code snippet, and test/layers/test_date_layers_thorough.py
(20 tests) covering the parsing, encoding and season layers -- day-of-week is
checked against Python's own calendar on leap days and century boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`_process_features_parallel` and `_parallel_setup_inputs` -- 70 lines that group features by type, set up inputs across a thread pool and dispatch each group -- are called from nowhere. Neither appears anywhere else in the package, the tests, or the docs, and no dynamic dispatch reaches them. `_apply_feature_moe` looked for `self.pipeline_<feature>` attributes to pull individual feature outputs from. Nothing ever sets such an attribute, so that branch was always false and the code always fell through to splitting `concat_all`; the split is now the only path. Five helpers in `DistributionTransformLayer` (`_calculate_skewness`, `_calculate_kurtosis`, `_has_negative_values`, `_has_zeros`, `_is_bounded_01`) and one in `DistributionAwareEncoder` are likewise unreferenced -- the `auto` selection computes those statistics inline instead. Adds test/layers/test_distribution_auto_transform.py (9 tests). `auto` had no coverage at all despite choosing the transform for every distribution-aware column: the tests check it stays finite on strictly positive, zero-containing, mixed-sign and bounded data, that it actually reduces skew rather than merely running, that it preserves shape, survives serialization and traces into a functional model. Documents the twelve transformations `transform_type` accepts, three of which (`arcsinh`, `cube-root`, `logit`) appeared nowhere in the documentation, along with what each one requires of its input. Full suite: 550 passed, 12 skipped, 2 xfailed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…tureLayer Twelve of the layer's seventeen calendar features had no test. Two of them did not work. `week_of_year` was NaN for every row. `DatetimeIndex.isocalendar()` returns a frame indexed by the timestamps themselves, so assigning `.week` into a column of a RangeIndex frame aligned on the index, matched nothing, and filled the column with NaN -- which then propagated into the model. The values are taken positionally now, and a test checks them against pandas rather than against a hand-written expectation. `hour`, `minute` and `second` are listed as valid and carry cycle lengths, but the non-cyclic path had no branch for them, so asking for them by their plain names raised; only the `_sin` and `_cos` spellings worked. They now read the timestamp, and the documentation notes that a date-only column has no time and so reports zero for all three. Adds test/layers/test_calendar_feature_layer_all.py (11 tests) covering every feature name: that the flags fire on the right dates (month, quarter and year boundaries, and a Sunday), that normalised values stay within [0, 1], that a `_sin`/`_cos` pair lies on the unit circle, that an unknown name is rejected, and that the configuration round-trips. Layer suite: 275 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`drop_na` defaults to True, and in that mode the layer declared its output shape as `rows - max_lag` while `create_lag_features` had already allocated `max(1, rows - max_lag)` rows. Whenever the input had fewer rows than the largest selected lag -- every single-series batch, and so the default configuration on any one series -- `set_shape` was handed a negative dimension and raised. The declared shape now uses the same clamp as the allocation. Both of the file's skipped tests were skipped for the wrong reason and are now enabled and passing: `test_drop_na` was skipped as "requires a negative batch dimension which is not supported in TensorFlow". That negative dimension was the bug above, not a property of the test. `test_call_2d` was skipped as needing "exact lag feature values that are difficult to match". Its shape and value assertions were always correct; what was wrong was the second half, which read column `i` as holding lag `i`. The layer selects its lags from the autocorrelation -- on this fixture it picks 3, 10 and 13 -- so the columns never held lags 1, 2 and 3. The test now asserts against the lags the layer actually selected, which is the property worth checking. Also adds test/layers/test_tsfresh_features_all.py (16 tests). The layer advertises 26 statistics and most had none; every scalar one is now compared with its numpy equivalent rather than merely asserted finite. Two of them, `peak_count` and `valley_count`, return a rate rather than a count, and `first_location_of_max`/`_min` return a fraction of the series length; the docstring described all four as absolute values and is corrected. Full suite: 581 passed, 10 skipped (down from 12), 2 xfailed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`test_automatic_detection` ran distribution detection over ten generated shapes and printed the result. It asserted nothing, so a detector returning an out-of-range index or the same answer for every input would have passed. Its fixture was also unseeded, which made detection non-deterministic: the same heavy-tailed shape came back as `heavy_tailed` or as `cauchy` depending on the draw. The fixture is seeded now, and the expected mapping was measured across six seeds rather than taken from one run. Detection identifies `heavy_tailed`, `log_normal`, `discrete`, `periodic`, `sparse` and `beta` correctly and consistently. Four shapes are consistently confused with a neighbour -- `normal` and `uniform` both read as `multimodal`, `multimodal` as `periodic`, and `exponential` as `log_normal`. The test asserts the full mapping as it stands, so a change in either direction is visible, and the documentation states the limitation and points at `preferred_distribution` for columns where the shape matters. `test_add_step_and_chain` had its only assertion commented out under a `# TODO: Fix this test`. It named `mock_step.chain`, but `Pipeline.chain` calls `connect` on each step -- the assertion was wrong, not unfixable. It now checks that `connect` is called once with the input and that its return value is what the pipeline hands back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`auto_configure` presents its code snippet as ready to use, so a keyword the target class silently ignores is worse than an error: the reader believes the setting took effect. It emitted `embedding_dim` on `TextFeature`, which forwards its keyword arguments to `TextVectorization` and has no such argument, and `date_format`, `output_format` and `extract` on `DateFeature`, which reads only `format` and `add_season`. All four were accepted and did nothing. The text recommendation no longer records an embedding dimension at all, and the date branch emits the two real options. test/test_auto_configure_inference.py now checks every keyword the snippet writes against what each class actually honours, across a dataset varied enough to exercise all four feature classes, and executes the result. The existing advisor test asserted the phantom `embedding_dim` was present and now asserts it is absent, with the reason. Also covers the inference edge cases: a directory of CSV shards, an empty directory (which reports clearly rather than failing inside pandas), an all-empty column, columns with gaps, and the rule that a float column of few whole values is read as a code rather than a measurement. Coverage across the package is 85%, up from 77% at the start of this pass; missed lines are down from 743 to 477. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…ranches `TimeSeriesInferenceFormatter.generate_multi_step_forecast` called `self._check_history_requirements(...)`. No such method exists -- the real one is `_check_inference_data_sufficiency` -- so this public, documented entry point raised `AttributeError` on every call, with any input. It had no test, which is why nothing caught it. The documentation showed the call with the right arguments, so the example was correct and the method was not. It also reads as though it returns forecasts. It returns a placeholder frame: one row per future date, sort column filled, every time series feature NaN, for the caller to fill in step by step. The page now says so and points at the example that puts a model on top. The advisor's `uniform` branch recommended `FLOAT_RESCALED` while recording only `min` and `max`, which `NumericalFeature` does not read. `FLOAT_RESCALED` multiplies by `scale`, which defaults to 1.0, so the recommendation left the column untouched while its note claimed rescaling. It now derives `scale` from the observed range, guards a zero span, and the snippet writes it. New tests: - test/test_multi_step_forecast.py (8): the frame's shape and contents, the `steps` limit, both error paths, dict input, and a grouped series. - test/test_model_advisor_branches.py (12): every distribution branch produces a preprocessing choice and an explanation, the categorical cardinality paths, the embedding and bin-count helpers, and -- the point of the file -- that every configuration key the advisor emits is one the target class actually reads, and that its global config names only real `PreprocessingModel` parameters. - test/test_stats_serialization.py (9): each branch of `_custom_serializer`, including the bytes case that used to write `"b'paris'"` into vocabularies, plus a save/reload round trip and the numeric statistics checked against the data. KDP reports the sample variance (ddof=1); the test says so rather than comparing against numpy's population default. Full suite: 609 passed, 10 skipped (all TensorFlow internals), 2 xfailed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…tures
The predefined assignment matrix doubles as the router's weights, so a
feature the caller left out of `feature_moe_assignments` got an all-zero
row and its entire representation was multiplied away. The build
succeeded, the model ran, and the feature was simply gone. An expert
index outside the mixture was the same failure by another route.
`FeatureMoE` now validates the map up front and names the problem:
missing features, names the mixture never sees, out-of-range or
non-integer expert indices. Weighted assignments (`{0: 0.7, 1: 0.3}`),
which the matrix builder already supported, are accepted.
Also documents the five constructor parameters that no page mentioned --
`feature_moe_assignments`, `feature_moe_dropout`,
`feature_moe_freeze_experts`, `feature_moe_use_residual` and
`log_to_file` -- and adds a guard so a parameter cannot reach users
undocumented again.
The `_UNSET` sentinel was a bare `object()`, which rendered in the
generated API reference as `<object object at 0x7f37...>`: unreadable,
and a fresh address on every regeneration. It is a small class with a
`<unset>` repr now, and the generator skips private classes so it does
not publish a name nobody can import.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
TransformerBlock had neither `build` nor `get_config`, and no tests: its test file existed and was empty. Keras 3 warned on every model that used it that the layer "does not have a `build()` method implemented and it looks like it has unbuilt state ... which may cause failures down the line". It builds its sub-layers now, reports its configuration, and says plainly when the input width does not match `dim_model` instead of failing inside `Add`. TabularAttention created a dropout, two layer norms and an output projection that `call` never used. They held no weights, so removing them changes no output; it removes four layers that read as part of the computation and are not. `TextVectorizerOutputOptions` existed twice: `kdp.processor` held the strings Keras accepts, `kdp.features` held `auto()` integers. Whichever one a caller imported decided whether their `output_mode` worked or was silently discarded. There is one class now, exported from `kdp`, and `CrossFeatureOutputOptions` is spelled the same way. Six methods the documentation showed did not exist: `get_timing_metrics`, `get_memory_usage`, `plot_model`, `get_feature_importance`, `plot_feature_importance`, `get_top_features`, plus `update_statistics`, `transform` and `fit`. The first three are implemented -- the timing and memory numbers were already being measured by `_monitor_performance` and thrown away -- and the pages using the others now show the working equivalents. Ten cross-links between doc pages pointed at files that do not exist, including the homepage's own link to Feature Selection; `mkdocs --strict` misses these because the docs navigate with raw `<a href>`. The claim that feature selection "identifies your most important features" is corrected wherever it appeared, since every weight it reports is 1.0. Finally, the marker rules assigned `fast`/`slow` *instead of* `unit`, so 26 tests matched no CI group and never ran -- the entire save/load round-trip suite among them, because every one of its names contains "model". Group markers are exhaustive now, and CI fails if a test ever matches none of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…classes `DistributionType` was defined twice, in `kdp.features` and in the encoder layer, and the copies disagreed: the features one carried a `WEIBULL` the encoder has never known. Asking for it was warned about and silently replaced with "normal". The encoder decides which values are valid, so its class is the one `kdp.features` re-exports, and every member the package exposes is now a distribution the encoder accepts. Two guards go with it: no public class may be defined twice, and every member of the exported enum must be accepted by the encoder. The docstring generator built its index by globbing the output directory, so a page written for a class that was later removed or moved stayed in the published API reference indefinitely -- two such pages were still listed. It prunes each module's pages before regenerating them now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…he advisor `kdp.moe` and `kdp.model_advisor` were absent from the generator's module list, so `FeatureMoE`, `ExpertBlock`, `StackFeaturesLayer`, `UnstackLayer` and `ModelAdvisor` had no API page -- while Feature MoE and `auto_configure` are documented as headline features elsewhere. A guard now fails if any module defining public classes is left off the list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`feature_moe_sparsity` is documented as "use only top k experts" and did
nothing: the top-k mask that enforces it sat behind
`routing_activation != "softmax"`, a knob `PreprocessingModel` never
exposes, so the branch was unreachable and every feature was routed
densely to every expert. Forwarding the option, as the previous commit
did, was not enough -- the code reading it could not run. Sparsity now
applies whenever it is smaller than the number of experts, so the
defaults (4 experts, sparsity 2) give the top-2 routing the docs
describe.
The existing test asserted only the output shape, which is identical
either way; it now checks that each feature reaches exactly `sparsity`
experts and that the weights still sum to one.
`get_expert_assignments()` returned `{}` for learned routing -- the
default -- while a pro tip told users to plot those assignments as a
heatmap. The router decides from the feature representations rather than
from its weights alone, so it takes a batch now and answers for both
routing modes as `{feature: {expert_index: weight}}`. Calling it without
a batch under learned routing says why instead of returning nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…ct-use layers Missing values were found with `inputs == mask_value`, and NaN compares equal to nothing -- not even itself -- so a series carrying the marker pandas and numpy actually use passed through untouched, and those NaNs then poisoned every statistic computed from it. No value of `mask_value` could select them. Setting it to NaN now selects NaN; a sentinel such as `0.0` behaves exactly as before. `FFTFeatureLayer`, `SeasonalDecompositionLayer`, `MissingValueHandlerLayer` and `AutoLagSelectionLayer` work and are tested, but `TimeSeriesFeature` has no config for them and no written page named them, so they existed only in the generated API reference. The time series page now shows how to use them directly, with examples that run as written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
`kdp.layers` and the model advisor spell this option `prefered_distribution`, with one "r", while `NumericalFeature` takes `preferred_distribution`. The feature swallows `**kwargs`, so the misspelling was discarded and the feature stayed on automatic detection while looking configured. Six tests in test_processor.py used that spelling and passed while asserting nothing about distributions, and a documented example taught it. The advisor made it worse: it recommended the misspelled key, and the code snippet it generates left the distribution out entirely, so following auto_configure() end to end gave none of its distribution advice. The feature now accepts the misspelling as a deprecated alias with a warning, the advisor emits the correct key, and the snippet writes it onto the feature. Two failures from the last full run are fixed here as well: - `tf.nn.top_k` rejects a float `k`, and a config round-trip brought `sparsity` back as one, so a saved mixture-of-experts model failed to rebuild. It is pinned to an int. - The doc-example harness ran blocks from the repository root, so a block naming a relative `features_stats_path` wrote `features_stats.json` there. Every later test asserting "without data this must raise" then found those statistics. The harness runs from a temporary directory now, and the session clears a stray file before the tests start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
The sweep behind this: perturb one input column and watch the output. A
column whose change moves nothing is being dropped somewhere, silently,
because the model builds and runs either way. Three of seventeen
configurations failed.
`processed_features_dims` was written as a flat `{name: dim}` mapping and
read back as a nested one keyed by "numeric"/"categorical", so the lookup
always missed -- and the loop filling it compared feature *tensors*
against input *names*, so it was empty regardless. The mixture then fell
back to cutting `concat_all` into equal parts. With features of unequal
width that split lands mid-feature: widths 1, 1 and 10 were cut as
[4, 4, 4], so every "feature" the router saw spanned several real ones
and `feature_moe_assignments` named the wrong slice. Nothing raised.
In dict mode the same mismatch raised on the first batch instead of at
build time, because `StackFeaturesLayer.compute_output_shape` reported
the first feature's width whatever the rest were -- a graph the layer
cannot execute. It now refuses to report a width it cannot produce.
Both take their widths and names from the tensors actually concatenated,
and pad features to a common width before stacking. Padding costs no
parameters and leaves a uniform-width model untouched. `use_feature_moe`
with `use_global_numerical_embedding` raises, since the global embedding
merges every numeric feature into one vector and leaves nothing to route.
Separately, `NumericalEmbedding` embeds each column it receives, so a
one-column feature came back rank 2 and a discretised one rank 3. Mixing
them failed in `Concatenate`, and a model of only discretised features
silently produced a three-dimensional output. The embedding is flattened
now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
Learned routing fed a Dense layer the batch *mean* of the features, so the routing weights changed with whatever rows shared a batch. Measured on a twelve-row batch: changing one row moved all twelve outputs, and a record scored alone differed from the same record scored in the batch. That is train/serve skew with no configuration explaining it, and one row's values leaking into another's output. Feature-level routing is a property of the feature, not of the data passing through it -- which is what the code comment claimed it was computing. The logits now live in a trainable weight with one row per feature, created in `build`. Routing is identical for every row and every batch, and still learned. `ExpertBlock` gained the `build` its sub-layers needed, so Keras no longer warns that the block "looks like it has unbuilt state". Two new suites hold the line: one perturbs a single feature and fails if the output does not move, the other checks that rows come back in the same number and order and that a row scored alone matches the same row scored in a batch. Both run across concat and dict modes, attention, transformer blocks, feature selection, the embeddings and the mixture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…-mode defect Extends the save/load matrix from nine configurations to seventeen, and adds a discretised feature so every one of them now runs on a feature set of mixed widths -- the case the mixture used to slice mid-feature and where its padding has to survive the round trip. Sparse routing, predefined routing, multi-resolution attention and dict-mode MoE are covered for the first time. That found a real defect, which this commit records rather than hides: Feature MoE with `output_mode="dict"` does not survive a save/load round trip. Two features come back holding each other's projection kernels -- `score`'s reloaded kernel is byte-identical to `occupation`'s original. Weights, wiring, routing logits and every intermediate tensor up to and including the mixture output match; only the per-feature projections are crossed. Concat mode round-trips exactly, and the model is correct in memory, so the fault is in how the sibling leaves of a dict-output graph are ordered when written and read back. Ruled out along the way: asymmetric graph depth (padding every feature, widest included, changed nothing), layer naming, creation order, and the residual connection. A minimal reproduction with the same stack, mixture and projections round-trips correctly, so the trigger needs more of the real graph than that. It is a strict xfail with the reproduction in its reason, KDP logs a warning when the combination is built, and both the Feature MoE page and the upgrade guide name it with the concat-mode workaround. A separate probe confirmed no value is lost elsewhere: distinct categories, integers, dates, sentences and large ids all stay distinct, normalisation matches (x-mean)/std, and passthrough returns its input unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
Sweeping the options you can put on a *feature* -- as opposed to the model-level switches the earlier passes covered -- found four of twenty-six paths broken, none of them loudly. `output_mode="tf_idf"` could not build. Keras refuses a vocabulary in that mode without matching `idf_weights`, which KDP's statistics never collected, so an exported and documented output mode raised on every use. The vectorizer is adapted on the column now, computing the vocabulary and the weights together from the same data the statistics came from. `NumericalFeature(use_embedding=True, embedding_dim=6)` was accepted and discarded: the flag was assigned in `features.py` and read nowhere, so the feature came through one column wide. It now enables the embedding for that feature alone. `wavelet_transform_config` crashed. The layer computes `min(window, time_steps // 2)`, which is zero for the one-column input a time series feature hands it, and the empty coefficients failed to broadcast; it also wrote the window sizes back onto itself, so the first batch pinned them permanently. Both are fixed, and a wavelet with a single step to work on raises rather than emitting a constant column of zeros -- combine it with lags or rolling statistics so there is a window to decompose. `calendar_feature_config` failed with "Cast string to float is not supported": the pipeline declared the column float and cast it before the calendar layer saw it. Such a feature declares a string column now and skips the numeric front of the pipeline, and combining it with the numeric configs raises, since one column cannot be both a date and a number. `DatasetStatistics._get_csv_file_pattern` is a staticmethod, since the processor needs the same path resolution to adapt a vectorizer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…ic configs Two tests built a `TimeSeriesFeature` with every config at once, calendar included, and asserted only that the attributes were stored. They never built a model from it, so they never surfaced that the combination cannot work: the calendar layer reads date strings where the numeric configs read numbers, out of the same column. They now cover the numeric configs together and the calendar config on its own, and assert that mixing them raises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
Running an example wrote its PNGs into the repository root, so anyone who tried one came back to a dirty working tree with three untracked images. They go to a gitignored `examples/outputs/` beside the scripts now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…mber Every one of them configured `calendar_feature_config` on the same `TimeSeriesFeature` as `lag_config`, `rolling_stats_config` and the rest. The calendar layer reads date strings where those read numbers out of the same column, so a model built from any of these examples raised "Cast string to float is not supported" the moment it was called. The doc verifier had passed them because it builds each block without calling it. They now declare the calendar features on the date column and the numeric transforms on the numeric one, and a guard fails if an example mixes them again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
The last known defect, and the mechanism turned out to be general enough to be worth naming: `.keras` stores a layer's weights under a name derived from its class and the order it was built -- `dense`, `dense_1`, `dense_2` -- not under the name the layer was given. Keras reorders sibling layers when it rebuilds a functional graph from config, so any group of same-class siblings whose rebuild order differs from their build order comes back with its weights permuted. Dict mode built one `Dense` per feature, four siblings, and two of them crossed: `score` came back holding `occupation`'s kernel and vice versa. What isolated it was that `save_weights`/`load_weights` into a freshly built model is exact while a full `save`/`load_model` is not -- the weight file is right, the rebuilt graph is not. Reordering the layers does not help, because the rebuild order is Keras's to choose. They are one `PerFeatureDense` layer now, holding a kernel and bias per feature and applying them with an einsum. The arithmetic is identical to a Dense per feature, and one layer has no sibling to be confused with. Its round trip is exact, so the xfail, the runtime warning and the documented workaround are all gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…lled `get_feature_importances()` returned 1.0 for every feature, on every input. Selection wrapped each feature in its own `VariableSelection` with `nr_features=1`, and a softmax over one element is 1.0 by definition, so the gating was real but the numbers ranked nothing -- the documentation had to warn readers not to read a ranking into them. A single softmax now scores every selected feature against the others, and the score scales that feature's output. The scores sum to one and are sortable. Output widths do not change: each selected feature is still `feature_selection_units` wide, so the shape of the model is the same. A model with one selected feature has nothing to compare against and keeps its weight of 1.0. The scores are stacked, multiplied and unstacked with the layers already in the package rather than sliced out with a `Lambda`. A Lambda carries a serialized Python closure, and this release has already spent enough time on layers that do not survive a round trip; the round trip is asserted. The doc-example verifier also builds every model and never called one, which is precisely how four published examples that raised on their first batch went unnoticed until this week. It now feeds each model it built two rows of the same synthesised data. Proved against a model that builds and then rejects its own input: the harness reports the failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…hannel Two paths reached the user as silence or as an error from inside Keras. A categorical feature whose column holds no values came back with an empty vocabulary, which was replaced with ["<UNK>"]. For a string feature that encoded every real value to the out-of-vocabulary slot -- a constant column, produced without a word. For an integer feature it failed inside Keras with "invalid literal for int() with base 10", naming neither the feature nor the cause. Both now raise a ValueError that names the feature and points at CategoryEncodingOptions.HASHING, which needs no vocabulary and does build on a column with no rows. The check stays narrow on purpose: a column of empty strings has the vocabulary [""] and a constant column has one of length one. Neither is empty, both are legitimate, and the tests hold them to building. AutoLagSelectionLayer chose its lags from the autocorrelation of channel 0 and applied them to every channel. With noise in channel 0 and a clean period-10 wave in channel 1 it selected lags [11, 15, 26]; swapping the two columns of the same data gave [5, 10, 15]. The autocorrelation is now averaged over the channels as well as the batch -- the mean across the batch was already how a single set of lags was chosen -- so every channel contributes and the result no longer depends on column order. The 2-D path is untouched. Also replaces three "Single feature for now" comments that read as an unfinished limitation. nr_features=1 is correct there: the selector gates one feature against itself, and the features are scored against each other afterwards in _score_features_against_each_other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
WaveletTransformLayer(flatten_output=False) returned np.zeros(...) -- every coefficient it had just computed, thrown away -- and then declared a rank-2 shape for the rank-3 tensor it returned, so set_shape rejected it and the option raised on every input before the zeros could reach anyone. It now returns the same coefficients as flatten_output=True, split out per input channel; a test holds the two to being equal under a reshape. TSFreshFeatureLayer(window_size=...) computed the window count from the raw window_size while the extraction clamped it to the length of the series. A window longer than the series declared a negative count and raised "Dimension -1 must be >= 0". call() now sets the shape compute_output_shape reports, which is the one place that arithmetic lives. CalendarFeatureLayer(cyclic_encoding=...) was accepted, stored, serialized and read nowhere: the output is identical whichever value is passed. The sin/cos components are requested by name, and always were, as month_sin and month_cos. The flag is deprecated -- it warns when a caller passes it, stays out of get_config unless it was passed, and still loads. Six documentation examples promised cyclic encoding through the flag and now ask for the components. TimeSeriesFeature.get_output_dim() counted all three of those transforms as columns added to the series. They replace it, so the declared width matched no configuration at all: a wavelet after two lags reported 5 columns against an actual 2, and tsfresh reported 8 against 5. Each layer is now asked for its own width. Sixteen configurations agree, up from six, and the eight that reach a transform are in the parameterized test that compares the declared width to the tensor the stack produces -- which covered none of them before. Inside a feature, a windowed tsfresh and an unflattened wavelet both return rank-3 tensors; both are flattened back into the feature's columns, so what follows still sees (batch, columns). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
MissingValueHandlerLayer has an `extrapolate` option, documented, defaulting to True, naming exactly the case no strategy can reach: a gap at the very start has nothing before it to carry forward, a gap at the end nothing after it to carry back. It was stored, serialized, and read nowhere. With the default forward_fill strategy and mask_value=NaN, a leading NaN came back exactly as it arrived and went into the model; backward_fill and linear_interpolation did the same at the end. Those gaps now take the nearest value that is not itself a gap, so no strategy leaves a missing value behind, and a series missing everywhere becomes zeros rather than NaN. extrapolate=False keeps the old behaviour and now genuinely means it. Six more arguments were stored, round-tripped through get_config, and never read. Each is now documented as inert rather than describing behaviour that does not happen: WaveletTransformLayer(drop_na) nothing carries NaN to drop MovingAverageLayer(pad_value) leading rows keep their own values CalendarFeatureLayer(onehot_categorical) one numeric column per feature DistributionAwareEncoder(handle_sparsity) sparse is a detected distribution DateParsingLayer(date_format) both spellings parse regardless FeatureMoE(routing_activation) routing is always softmax; use sparsity test_no_option_is_silently_ignored.py walks every class in kdp and fails on a constructor argument stored and never read, unless the docstring says it does nothing. It counts reads through subclasses and through getattr, so a feature attribute the processor reads later is not mistaken for a dropped one, and it carries a test that it still catches a dropped argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…n guard The guard cleared an argument when the attribute name was read anywhere in the package, which is what a feature class needs -- `NumericalFeature` stores `use_embedding` and the processor reads it much later, sometimes through getattr. A layer is the opposite: it reads its own options in __init__, build or call and nothing reaches in for them, so a matching name on an unrelated object was evidence of nothing and would have let a dead layer option hide behind it. Layers now count only their own reads, their subclasses', and arguments consumed while building sublayers. Still green, so nothing was hiding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
The heading counted three options that "could not run", but only two of them raised; the third was accepted and did nothing, as were the seven in the table below it. The note about `month` not being cyclic now sits next to the `cyclic_encoding` paragraph rather than after the guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
DateFeature(add_season=True) added four columns that were the same on every row. The pipeline ran SeasonLayer after DateEncodingLayer, and the layer reads the month from column 1 of its input -- which by then was year_cos, 1.0 for every date. Cast to int that is 1, which the layer calls January, so every date came out winter. January, April, July and October all encoded to [1, 0, 0, 0]. The season is now read before the encoding replaces the month, and DateEncodingLayer carries through any columns a layer before it appended instead of returning exactly eight and dropping the rest. The output is the same width as before -- twelve columns, as the documentation already said -- and the four season columns now carry the season. Nothing caught this. The layer's own tests feed it [year, month, day, day_of_week] directly, which is what its docstring asks for and works correctly; the processor tests only asserted the output was at least four columns wide. The new test builds a date-only model and checks that four months from four seasons give four different encodings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
… back get_config wrote punctuation_pattern and stop_words_pattern, two values the layer derives from stop_words and which __init__ never accepted. Keras rebuilds a layer by handing the stored config to the constructor, so any saved model containing this layer failed to load outright with "Unrecognized keyword arguments passed to TextPreprocessingLayer". The layer is reachable through PreprocessorLayerFactory.text_preprocessing_layer, so a custom pipeline using it could be saved and never reopened. get_config now writes only stop_words, and from_config drops the two derived keys so files written by earlier releases still load. A test walks every class in kdp and fails when get_config writes a key __init__ will not take. It found this one and nothing else, and it carries a round trip of the layer through save and load plus an old-style config. This came from the coverage report: both methods were among the few uncovered lines in the package, and neither had ever run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
Two blocks stood out in the coverage report as never having run. Neither was broken, but neither was reachable by any test either. add_feature_moe_to_model stacked the feature outputs as they came, and stacking needs them the same width. Preprocessing rarely produces that, so the helper worked only by coincidence -- and after the stack layer started rejecting mismatched widths, it failed outright on the ordinary case. It now pads to the widest, which is what PreprocessingModel does for the same reason; the widest feature is padded by nothing. Tests cover three width patterns including [1, 1, 10], the shape that exposed the earlier slice-routing bug, and check that perturbing one input still moves that feature's own output and that the result survives a save and load. DistributionTransformLayer builds its candidate transforms from the data's own shape only when a caller passes transform_type="auto" with an explicitly empty auto_candidates; the default None makes __init__ fill the list in, so that branch had never executed. Now covered for strictly positive data, positive data with zeros, mixed signs and data bounded in (0, 1). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
PreprocessingModel.load_model returns (model, metadata), and quick-start.md shows that. Two other pages assigned it to a single name and called that name loaded_preprocessor, so a reader following them would write loaded_preprocessor.predict(...) next and get "'tuple' object has no attribute 'predict'". One of them also passed "my_preprocessor.keras" to save_model, which takes a directory, not a file. The runnable-example harness cannot catch either: it skips every block calling load_model, since it has no saved directory to hand it. A guard in test_docs_match_code.py now fails when a documented call to a PreprocessingModel method annotated as returning a tuple is assigned to one name. It is scoped to the class, so keras.models.load_model -- a different function that really does return a model -- is left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
|
Every class section is appended with a trailing blank line, so each generated file ended with two newlines and the API index with an extra one. `end-of-file-fixer` strips that, so regenerating the docs always left the tree dirty and the pre-commit CI job failed on all 67 files -- which is exactly what happened on this PR's first run. The generator now writes one trailing newline, so its output is already what the hook wants and a regeneration is a no-op when nothing else changed. `pre-commit run --all-files` passes locally, hook for hook. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
…ng directory test_distribution_aware.py and test_distribution_aware_encoder.py both saved a model to "test_model.keras" -- a relative path, so the same file in the shared working directory -- and both deleted it afterwards. CI runs the suite with `pytest -n auto`, so the two land on different workers and race: whichever finished first removed the file, and the other failed with AssertionError: Model saving/loading failed: File not found: filepath=test_model.keras which is what test-matrix (3.10, layers) hit on this PR. The race is latent in both files and only surfaced because this branch adds tests to the layers group, changing how xdist distributes them; the second test swallowed all exceptions, so only the first ever reported. Each now saves into its own TemporaryDirectory, so there is no shared name left to race over -- and the directory cleans itself up when a test fails, which the manual os.remove in the failure path did by hand. The second test no longer catches and prints its exception either: a save/load failure there was invisible. A scan of every relative path literal in test/ found no other pair sharing a name outside a temporary directory. `pytest -c pytest-fast.ini -m layers` passes: 361 passed, 7 skipped. The whole suite under that config passes too: 800 passed, 10 skipped, 2 xfailed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
test_every_feature_still_reaches_its_own_output moved one input from 1.0 to 9.0 and asserted the feature's own output changed. The experts are randomly initialised and end in a ReLU, so for any single value that ReLU can be dead at both the baseline and the perturbation, and the output legitimately does not move -- the test then failed with "True is not false : c" on nothing but an unlucky initialisation. It is a test I added on this branch, and it failed in the unit group under `pytest -n auto`. Measured over eight seeds and three features: one probe misses about once in twenty-four checks; the four now used -- negative, small, large and very large -- miss none. A feature that genuinely did not reach its output would move for none of them, so the assertion still says what it did. The model is also seeded now, and the check runs over three seeds, so a failure is reproducible instead of a fresh model every run. `pytest -c pytest-fast.ini -m unit` passes: 352 passed, 1 skipped, 2 xfailed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 3, 2026
## 1.13.0 (2026-09-03) * chore: save last release version for recovery [skip ci] ([302132d](302132d)) * fix: repo maintenance update n40nk0 (#38) ([5f79944](5f79944)), closes [#38](#38) * fix(KDP): build the transformer block, and make the documented API real ([3193f3b](3193f3b)) * fix(KDP): compute the statistics from the right rows, at the right precision ([a76c574](a76c574)) * fix(KDP): deliver the embedding and Feature-MoE options that were discarded ([97cc77e](97cc77e)) * fix(KDP): dict-mode Feature MoE survives a save/load round trip ([ea95742](ea95742)) * fix(KDP): end generated API docs with one newline, not two ([7e7aa19](7e7aa19)) * fix(KDP): fill gaps at the ends of a series and name the inert options ([e22b654](e22b654)) * fix(KDP): honour preferred_distribution however it is spelled ([e4a5b7e](e4a5b7e)) * fix(KDP): let MissingValueHandlerLayer see NaN, and showcase the direct-use layers ([64d2abe](64d2abe)) * fix(KDP): make auto_configure work without features_specs, as documented ([30f86d1](30f86d1)), closes [hi#cardinality](https://github.com/hi/issues/cardinality) * fix(KDP): make AutoLagSelectionLayer work with its default settings ([7c37096](7c37096)) * fix(KDP): make Feature MoE routing independent of the batch ([0f3ca9f](0f3ca9f)) * fix(KDP): make feature_moe_sparsity real and report expert assignments ([a84b82f](a84b82f)) * fix(KDP): make min-max scale onto the range it is given ([883ded3](883ded3)) * fix(KDP): make text output modes reachable and correct the date/text docs ([d9917f2](d9917f2)) * fix(KDP): make the documented examples run, and fix what running them exposed ([32f0e30](32f0e30)) * fix(KDP): make the saved model, the calendar feature and tsfresh work ([ac6bd85](ac6bd85)) * fix(KDP): one DistributionType, and stop publishing docs for deleted classes ([14e9bd5](14e9bd5)) * fix(KDP): pad the features add_feature_moe_to_model stacks ([67c84c3](67c84c3)) * fix(KDP): put the output columns in the order the features were declared ([7cb956f](7cb956f)) * fix(KDP): read the season from the month, not the cosine of the year ([d532c2c](d532c2c)) * fix(KDP): reject option values that silently disabled the feature they name ([820be2c](820be2c)) * fix(KDP): repair four feature-level options that could not work ([4f3e4ea](4f3e4ea)) * fix(KDP): repair generate_multi_step_forecast and cover the advisor branches ([3d29c43](3d29c43)) * fix(KDP): repair options that were accepted and never did what they said ([11085c1](11085c1)) * fix(KDP): repair silent data defects in seasons, time series options and serialization (#37) ([3e8616a](3e8616a)), closes [#37](#37) * fix(KDP): repair three time series options and the widths they report ([1fb8a30](1fb8a30)) * fix(KDP): repair week_of_year and the clock components of CalendarFeatureLayer ([3554af0](3554af0)) * fix(KDP): report an empty categorical vocabulary and read every lag channel ([83eb49a](83eb49a)) * fix(KDP): route real features through the mixture, not arbitrary slices ([faf2c15](faf2c15)) * fix(KDP): stop predefined MoE routing silently erasing unassigned features ([73a9646](73a9646)) * fix(KDP): stop tests leaking a stats file into the working directory ([2e10d69](2e10d69)) * fix(KDP): stop TextPreprocessingLayer writing a config it cannot read back ([d702b02](d702b02)) * fix(KDP): stop the generated snippet writing options the feature ignores ([f82d96a](f82d96a)) * fix(KDP): stop two layer tests racing over one file name in the working directory ([585e366](585e366)) * test(KDP): cover value fidelity and serialization, and pin a MoE dict-mode defect ([20a557e](20a557e)) * test(KDP): hold layer options to their own evidence in the dead-option guard ([f381a6f](f381a6f)) * test(KDP): make the two assertion-free tests actually assert ([baf89af](baf89af)) * test(KDP): probe the MoE reachability check with several values, not one ([972b859](972b859)) * test(KDP): split the time series tests that mixed calendar with numeric configs ([e75a84e](e75a84e)) * docs(KDP): correct the numeric and passthrough pages, and run the examples in CI ([2fa3f84](2fa3f84)) * docs(KDP): publish the API reference and guard the docs against the code ([ec335a3](ec335a3)) * docs(KDP): publish the API reference for the mixture of experts and the advisor ([60a8997](60a8997)) * docs(KDP): rewrite the categorical and distribution-aware pages against the code ([99c0a9f](99c0a9f)) * docs(KDP): say what the time series section covers ([c2cf4cc](c2cf4cc)) * docs(KDP): state the output width tabular attention produces ([9ecc4c7](9ecc4c7)) * docs(KDP): stop four examples asking one column to be a date and a number ([02d1a09](02d1a09)) * docs(KDP): unpack what load_model returns, and guard the pattern ([0f1cb93](0f1cb93)) * feat(KDP): feature importances rank features, and doc examples get called ([a935051](a935051)) * style(KDP): format the new PerFeatureDense tests ([b4602e7](b4602e7)) * maint(KDP): keep the example scripts out of the repository root ([cb57b07](cb57b07)) * refactor(KDP): remove unreachable code and cover the auto transform ([2a38f25](2a38f25))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Describe your changes shortly
Follow-up to #36, which shipped as 1.12.0. This branch carries 45 further commits fixing defects that were silent — the model built, ran, and returned plausible numbers while quietly losing or fabricating data.
The most consequential:
DateFeature(add_season=True)year_cos,1.0for every row — as the month. Every date came out winter: four constant columns.MissingValueHandlerLayerextrapolateoption was read nowhere, so the defaultforward_fillreturned a leadingNaNuntouched and it reached the model.TextPreprocessingLayerget_configwrote two keys__init__rejects, so any saved model containing the layer failed to load.["<UNK>"]: an all-out-of-vocabulary constant column for strings, an opaqueinvalid literal for int()for integers.AutoLagSelectionLayerWaveletTransformLayer(flatten_output=False)np.zeros(...), then raised on a rank-2set_shape. Unusable on every input.TSFreshFeatureLayer(window_size=…)TimeSeriesFeature.get_output_dim()cyclic_encoding+ 6 more optionsBehaviour changes
Output values change for
add_season, Feature MoE, imputed series edges and the transforms above.docs/getting-started/upgrading.mddocuments each one and tells users to re-fit anything trained on the old output.Guards added
Three static checks, each verified non-vacuous by reverting a fix and watching it fail:
get_configkey its__init__rejects (found 1);Found by CI on this PR, and fixed here
pre-commit-checks—scripts/generate_docstring_docs.pyappended---\n\nper class, so every generated doc ended with two newlines andend-of-file-fixerstripped them: regenerating the docs always left the tree dirty. Fixed in the generator, so a regeneration is now a no-op.test-matrix (3.10, layers)—test_distribution_aware.pyandtest_distribution_aware_encoder.pyboth saved to the relative path"test_model.keras"and both deleted it. Underpytest -n autothey raced and one failed withFile not found. Each now uses its ownTemporaryDirectory. Latent in both files; surfaced because this branch changes how xdist distributes the group.1.0to9.0and asserted the MoE output changed, but the experts end in a ReLU that can be dead at both points. Measured over 8 seeds × 3 features: one probe misses ~1 in 24, four probes miss none. Now seeded and probed across four values.Checklist
pre-commit run --all-filesclean locally, hook for hooksmoke-test, the 3.10/3.11/3.12test-matrix,keras-latest,doc-examples,coverage. Locally 800 passed / 10 skipped / 2 xfailed / 0 failures, coverage 89%doc-examplesjob builds and calls all 79 runnable documentation blocks on real rows, 0 failures; all 6examples/scripts run clean (locally — CI does not coverexamples/)mkdocs build --strictclean, generated API reference regenerated from docstringspoetry buildsucceeds; the wheel was extracted and smoke-tested in isolation (import, build, forward pass, save/load round trip)Notes for the reviewer
feat(KDP)commit, so preserving individual commits yields 1.13.0. A squash using this PR'sfix(KDP)title would yield 1.12.1 instead. fix(KDP): repair core defects, enable the linter and move to the Keras 3 API #36 preserved individual commits, so the default path gives 1.13.0.type(KDP): subjectform thecommit-msghook and semantic-release'sheaderPatternrequire. Nine of them (and one olderchore(KDP), a type the release config does not recognise) would otherwise have contributed no version bump and appeared in no release notes. The trees are byte-identical to before the rewrite.🤖 Generated with Claude Code
https://claude.ai/code/session_01NQmAjbXCdkBSX1CPAd56H2