feat: add enums and strategy protocols, and validate learning_rate - #10
Merged
Conversation
Three additions, all backward compatible. No numerical behaviour changes: weights are identical to master across all 81 combinations of training mode, neighborhood, initializer and cyclic setting. Enums for the string-valued options: TrainingMode, Neighborhood, WeightInit, SampleMode. Each member is a `str`, so `TrainingMode.BATCH` and `"batch"` are equal, hash equal, serialise to the same JSON and work as the same dict key. The parameters are typed as the enum or a Literal of the valid strings, so a type checker now rejects `mode="bacth"` while accepting both spellings. The cost is that a variable of plain `str` type no longer satisfies the parameter; the `*Str` aliases are exported for annotating one. `enum.StrEnum` is 3.11+ and this package supports 3.10, so `_StrEnum` reproduces it. A bare `class X(str, Enum)` is not equivalent -- its `str()` returns `'X.MEMBER'`, which would put the wrong text into any f-string, filename or log line built from a member. Behaviour was checked against native StrEnum on 3.10, 3.12 and 3.13. Plain strings are deprecated in writing only. They warn in 0.5.0 and are removed in 1.0.0. Warning now would fire on `mode="batch"`, which is what the documentation showed until this release. This makes 0.5.0 a prerequisite for 1.0.0, since the policy is that a removal in a major release follows at least one minor release that warns. Protocols for the three replaceable strategies, plus KernelFunction, replacing bare Callable aliases. Structural, so nothing inherits from them and every existing callable already satisfies its protocol. Parameters are positional-only: without that a Protocol also requires matching parameter names, and a user's `def my_decay(rate, step, total)` would fail for naming its arguments differently. learning_rate is validated, having been unchecked. Non-positive or non-finite raises ValueError: `0` freezes every model so training completes and changes nothing, and `-1` moves models away from the samples they match, taking the quantization error from 0.0 to 11.7. Above 1 warns rather than raises -- it overshoots and oscillates but does not necessarily diverge, measuring max|w| 3.61 at alpha=5 with decay disabled, and Kohonen gives no upper bound. TRAINING_MODES and INITIALIZATION_MODES are now derived from the enums so the two cannot drift. The public-surface test is split in two: a subset check that nothing 0.3.0 exported has been removed, which is the compatibility promise, and an equality check on the full surface, so an addition shows up as a diff to read rather than a failure to explain. Coverage excludes a line whose entire content is `...`, which is a Protocol method body: a declaration of shape that never executes. Narrow enough to stay honest -- such lines occur only in _core/_protocols.py.
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.
Enums for every string option, Protocols for the three replaceable strategies, and validation for
learning_rate. Fourth of five PRs for 0.4.0. No numerical change: weights identical to masteracross all 81 combinations.
Enums, without breaking a single call
Each member is a
str, so the two are equal, hash equal, serialise to the same JSON, and work asthe same dictionary key.
TrainingMode,Neighborhood,WeightInit,SampleMode.The parameters are typed as the enum or a
Literalof the valid strings, which is what makes thisworth doing — a bare
strannotation would catch nothing:The trade, stated because it will affect someone: a variable of plain
strtype no longersatisfies these parameters. The
TrainingModeStr/NeighborhoodStr/WeightInitStr/SampleModeStraliases are exported for annotating one, andTrainingMode(value)validates atruntime when the value comes from a config file.
Why the base class is hand-rolled
enum.StrEnumis 3.11+ and this package supports 3.10. A bareclass X(str, Enum)is notequivalent:
str(member)enum.StrEnum(3.11+)'batch''batch'class X(str, Enum)'X.BATCH''X.BATCH'_StrEnumhere'batch''batch'The middle row would silently put the wrong text into any filename or log line built from a member.
Checked against native
StrEnumon 3.10, 3.12 and 3.13 — includingformat()andjson.dumps— rather than assumed, and asserted intests/test_typed_seams.py.Strings are deprecated but do not warn
DeprecationWarning.Warning now would fire on
mode="batch", which is exactly what the README and every docs page showeduntil this PR added
docs/options-and-types.md. A minor release that scolds users for following itsown documentation is the wrong trade, and it would also force
pytest.warnswrappers through much ofthe existing suite, since
filterwarnings = ["error"].Roadmap consequence: 0.5.0 is now a prerequisite for 1.0.0. The stated policy is that a removal in
a major release follows at least one minor release that warns, and documented-only does not satisfy
that. Recorded in the plan.
Protocols for the strategy seams
NeighborhoodFunction,DecayFunction,DistanceFunction, plusKernelFunctionfor the batchkernel, replacing bare
Callable[...]aliases that checked little more than argument count.Structural — nothing inherits from them, and every existing callable already satisfies its protocol.
Every parameter is positional-only, which is load-bearing rather than stylistic: without the
/,a Protocol also requires matching parameter names, so a user's
def my_decay(rate, step, total)would fail type checking purely for naming its arguments differently. A test exercises a custom decay
whose parameter names deliberately differ.
learning_rate validation
Unchecked through 0.3.0. Two failure modes, treated differently:
ValueError): non-positive or non-finite.0freezes every model, so training runsto completion and changes nothing.
-1moves models away from the samples they match, taking thequantization error from 0.0 to 11.7 and the largest weight to 30 on a map that started inside the
unit cube.
nanneeds the explicitisfinitecheck, sincenan <= 0isFalseand a barecomparison lets it through.
UserWarning): above 1. It overshoots and oscillates, but does not necessarily diverge— at
alpha = 5with decay disabled the largest weight stayed at 3.61, because the neighborhooddamps the correction away from the winner. Kohonen gives no upper bound, so rejecting it would
invent a limit the sources do not give.
A test asserts the warning's
stacklevelblames the caller's line rather than_som.py.Also
TRAINING_MODESandINITIALIZATION_MODESare now derived from the enums, so the accepted valuesand the enum members cannot drift apart. A test checks the derivation is still wired up.
__all__to exactly what 0.3.0shipped, and this PR adds names, so it fired. Rather than just updating the list, it now asserts two
different things: a subset check that nothing 0.3.0 exported has been removed (the compatibility
promise, which holds until 1.0.0), and an equality check on the full surface (so an addition is a
diff to read rather than a failure to explain).
.... That is a Protocol method body,a declaration of shape that never executes. Written as a rule rather than four identical pragmas,
and narrow enough to stay honest — such lines occur only in
_core/_protocols.py, which is checked.options-and-types.md, in the nav, covering both spellings, the deprecation table,custom strategies and the learning-rate rules.
Verification
ruff format/mypy --strict/mkdocs build --strictclean,
twine checkpasses, all four Python versions.mode="bacth"andneighborhood_function="guassian"produces exactly two mypy errors, while the enum and the correctstrings produce none.
Not in this PR
SOMConfigandfrom_config()were planned here and are deferred to #11. With the 13-argumentconstructor staying, a config dataclass would be a second spelling of the same thing with no caller
of its own — the charge that removed
_progress.pyfrom #7. PR #11 adds provenance andsave_npz/load_npz, which need a serialisable description of a map's configuration and so give ita real second caller.