Conversation
Eleven multimodal models (CLIP, BLIP, BLIP-2, Flamingo, LLaVA, ImageBind, GPT-4 Vision, VideoCLIP, UnifiedMultimodal, and the two AudioVisual networks) now take their configuration through VisionLanguageModelOptions. 102 parameters moved, carried over unchanged. Three defects in my own tooling, each caught by the build rather than by care: 1. The migration handled only ONE constructor per model. Eight of these eleven declare two, and both contain `_options = options ?? new XOptions()`, so the script took the first and left the second's parameters in place — 44 params moved instead of 102. It now migrates every constructor that takes an options parameter, and the options class carries the union of their defaults. 2. Repointing a guard at _options could place it ABOVE the assignment, a null dereference. This happened in three different guard shapes (RWKV7 in phase 2, Blip2 and Flamingo here), so rather than recognising each shape the options assignment is now hoisted to the top of the constructor body. 3. Call sites received the options object as the last POSITIONAL argument, but the new signature puts `options` before the optional collaborators. It is now emitted as a named argument. Also handled: defaults that are `private const` on the model are resolved to their literal value with the constant's name kept as a comment; a default naming an open generic is reported rather than emitted into a non-generic options class. Known gap: enum- and string-typed parameters are not yet moved (VideoCLIP's TemporalAggregation, LLaVA's LanguageModelBackbone). The ratchet counts them, so they are covered in a follow-up rather than left implicit. Build green, ratchet 875. Refs #2090 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ratchet counts enum- and string-typed constructor parameters as
configuration, so the count could never reach its floor while the migration
moved only numeric scalars. Detection is syntactic, since the script does not
resolve types: a bare identifier type whose default reads `Type.Member` is an
enum, and `string`/`string?` with a literal default is a string. An interface
defaulting to null matches neither.
Also picks up VisionMambaModel (9 params), which sat in the sequence group but
was never run, and UnifiedMultimodalNetwork, which was silently NOT migrated in
phase 3 — see below.
A third defect in the migration script, and the reason it mattered:
the constructor-signature pattern captured its parameter list with `[\s\S]*?`.
When a model declares a parameterless constructor that chains with `: this(...)`
BEFORE the real one, the lazy match ran past it hunting for `: base`, swallowing
a whole constructor body. The replacement was then not found inside the narrower
region the script edits, so the migration did nothing while still reporting
success. A parameter list never contains braces, so the capture is now `[^{}]*?`.
That failure mode is worth noting against the ratchet itself: it counts a
parameter as covered when the Options class has a matching NAME, so
UnifiedMultimodalNetwork scored as migrated while its constructor still took all
three parameters. The behavioural assertion is what closes that hole.
The migration is also idempotent now — a default the options constructor already
assigns is not appended a second time — so it can be re-run to pick up parameter
kinds an earlier pass did not recognise.
Build green, ratchet 861.
Refs #2090
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…> 782 Eleven models (BGE, ColBERT, SGPT, SPLADE, SimCSE, Instructor, Matryoshka, FastText, GloVe, Word2Vec, TransformerEmbeddingNetwork) now configure through EmbeddingModelOptions. 77 parameters moved, carried over unchanged. PoolingStrategy is promoted out of TransformerEmbeddingNetwork<T> into AiDotNet.Enums.EmbeddingPoolingStrategy. This is the collision phase 1 deliberately deferred: a type nested in a generic class is a DISTINCT type per type argument, so TransformerEmbeddingNetwork<float>.PoolingStrategy and the <double> one were unrelated, and neither could be named from a non-generic options class. It was never usable as configuration, which is what it describes. Renamed rather than moved as-is, because src/VisionLanguage/Encoders has its own unrelated PoolingStrategy with different members. Options inheritance now mirrors model inheritance. Seven of these models derive from TransformerEmbeddingNetwork<T> and forward through `: base(...)`, so their options classes had to derive from TransformerEmbeddingOptions rather than straight from EmbeddingModelOptions — otherwise a derived model cannot pass its own options to base at all. Their duplicated pooling property and Validate() are removed; the base's are shared. Two more migration-script fixes: - A delegating `: this(...)` initializer was being repointed at _options, which is not in scope before the body and produced an argument name that was an expression. Initializers are now pruned (the target no longer takes those parameters) and only the body is repointed. - `options` IS a parameter, so an initializer needing a moved value reads `options?.MaxGradNorm ?? 1.0` — used by FastText, GloVe, Word2Vec and TransformerEmbeddingNetwork. GANs are deliberately NOT in this commit. DCGAN, BigGAN, SAGAN and ProgressiveGAN use their moved values inside `: base(CreateGeneratorArchitecture(latentSize, ...))` to build architectures before the object exists. That is per-model judgement rather than a mechanical move, so it gets its own pass instead of being forced through the script. Build green. 37 tests pass, ratchet 782. Refs #2090 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ten GANs (DCGAN, WGAN, WGAN-GP, BigGAN, SAGAN, StyleGAN, ProgressiveGAN, InfoGAN,
Pix2Pix, CycleGAN) now configure through GanOptions. 41 parameters moved.
These broke three more assumptions the migration had been carrying:
1. NOT EVERY MODEL HAS AN ARCHITECTURE PARAMETER. The GAN primary constructors
take explicit image dimensions and build their generator and discriminator
architectures from them. The architecture parameter was only ever used to
place a parameter first; when absent, the required parameters lead.
2. A CONSTRUCTOR MAY DELEGATE WITH `: this(...)`, not just `: base(...)`, and
such a constructor is public and carries movable parameters of its own.
3. A MOVED VALUE MAY BE USED INSIDE THE INITIALIZER:
: base(CreateDCGANGeneratorArchitecture(latentSize, ..., generatorFeatureMaps), ...)
Dropping the identifier is wrong — the expression needs the value. `options`
IS in scope in an initializer, so it becomes `(options?.GeneratorChannels ?? 64)`,
carrying the same default. And a name that is defaulted in one constructor but
REQUIRED in another (DCGAN's latentSize) must be supplied there, not dropped.
Every argument that is a bare parameter name in an initializer is now emitted
named, because moving `options` ahead of the optional collaborators changes what
a positional argument binds to.
GanOptions.ValidateCore no longer requires LatentSize or InitialLearningRate.
This was caught by 30 failing tests, not by the build: LatentSize stays a
required CONSTRUCTOR argument on the four models that build architectures from
it, so it is legitimately unset on those options objects, and most GANs never had
an InitialLearningRate parameter at all. It now carries the DCGAN paper's 0.0002
as a default and only ImageChannels is required.
Build green. All 57 GAN tests pass, ratchet 741.
Refs #2090
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The existing metric credits a parameter as covered once the options class has a property of the same NAME. That is satisfiable without touching the constructor, so a model can score as migrated while still taking every parameter it always did. UnifiedMultimodalNetwork did exactly that for the whole of phase 3, and it was caught by reading the file rather than by the test. The new count gives no such credit: a tunable defaulted constructor parameter counts, full stop. It measures the actual goal — that a model constructor stops taking hyperparameters — and can only fall when a constructor really changes. It puts a number on the hole. The name-credited count is 741; the strict count is 963. So 222 parameters are currently scored as covered while still sitting on constructors, which is a far larger gap than the single model found by hand. A runtime assertion (construct with a non-default value, observe a different layer stack) was the other candidate and is deferred: most of these models need a real architecture and tokenizer to construct, so the test would be large, slow, and prone to failing for reasons unrelated to configuration. The strict count gets the same guarantee statically. Refs #2090 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…> 815
Nineteen document models (LayoutLM v1/v2/v3, LayoutXLM, LiLT, DocFormer, DiT,
Donut, Dessurt, Nougat, Pix2Struct, MATCHA, DocOwl, InfographicVQA, UDOP, DocGCN,
LayoutGraph, PICK, TRIE) now configure through DocumentNeuralNetworkOptions.
148 parameters moved.
The two ratchets disagree by design and the gap is instructive: the strict count
fell by the full 148 while the name-credited one fell by only 44, because 104 of
these parameters already had a matching property name and were therefore already
being scored as covered. That is the hole the strict count was added to close,
measured.
DocumentNeuralNetworkOptions.ValidateCore no longer requires MaxSequenceLength
and HiddenDim. Caught by 11 failing tests, not the build — the same mistake as
GanOptions, and for the same reason: nothing is universal across 29
heterogeneous document models (only 13 have a hidden dimension, 17 a sequence
length). A leaf that needs a value calls Require in its own Validate().
Three more tooling fixes:
- Models are not always flat under one directory; Document groups them by task,
so model resolution now searches recursively.
- A leaf property keeps its declared nullability. `int? visionNumHeads = null`
is a real tri-state DocOwl reads with .HasValue and ??, not an int that
happens to default to null.
- The generator converter is scoped to the `constructorExpr = ...;` statement.
A `name: value` pattern also occurs in that file's prose ("the numClasses:4
layout logits"), and matching one there spliced an options initializer into a
comment and broke the string literal.
Build green. 63 Document tests pass.
Refs #2090
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Forty-three video models across segmentation, generation, super-resolution,
interpolation, tracking, denoising, inpainting and depth now configure through
VideoHyperparameterOptions. 136 parameters moved.
VideoHyperparameterOptions.ValidateCore now requires NOTHING, and the comment
says why: this is the third time the same mistake cost failing tests (30 in the
GAN family, 11 in Document, 6 here). A family base cannot require a value the
family does not universally have — DIFRINT has no frame count, several models no
feature width — and requiring one compiles clean, then throws at construction.
A leaf that needs a value calls Require in its own Validate().
One real regression, mine, caught by the tests: hand-editing the VideoCLIP call
in VideoExtendedIntegrationTests I removed `numFrames: 4, embeddingDim: 4,
textMaxLength: 8, vocabSize: 64` and passed only the pre-existing `options`
variable, which did not carry them. The model then built at its production 512
width and the test failed on "Tensor shapes must match. Got [512] and [4]".
The four values are folded into the options object.
Two more codemod rules learned here:
- The body repoint must not rewrite a NAMED ARGUMENT's label:
`numFeatures: _numFeatures` became `_options.NumFeatures: _numFeatures`, which
is not valid C#. Identifiers directly followed by a colon are excluded; a
ternary's true-branch has a space before its colon and is still rewritten.
- The leaf/base property split must consult the family BASE's properties, or it
emits a hiding member (DepthAnythingV2Options.NumFeatures).
DepthAnythingV2 is excluded: it has both that collision and a nested ModelSize
enum, the same generic-nesting problem as PoolingStrategy. It gets its own pass.
Verified: src and tests build clean; the five converted suites pass 115/115.
The full Video filter could not be run — the test host crashes with an internal
CLR error under this machine's memory pressure, and the CogVideo diffusion tests
it swept in are a DIFFERENT class marked [Trait("Category","HeavyTimeout")] for
the nightly lane.
Refs #2090
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve audio models (Tacotron2Model, TtsModel, VITSModel, WhisperModel, Wav2Vec2Model, AudioGenModel, CLAPModel, ConvTasNet, DCCRN, DeepFilterNet, NeuralNoiseReducer, SileroVad) now configure through their own options classes. 130 parameters moved. FIXES A REGRESSION SHIPPED IN PHASE 3 (#2130). VisionLanguageModelOptions required MaxSequenceLength and ImageSize, which the two AudioVisual networks do not have — they pair audio with video, so there is no text sequence length and no square image. 11 tests had been failing since that phase; I only found them because this phase's filter happened to sweep them in. Narrowed to EmbeddingDimension and Channels, which the family does share. That is the fourth instance of the same mistake (30 failing tests in the GAN family, 11 in Document, 6 in Video, 11 here). The rule is now stated in every family base: a base may only require what EVERY member has. Audio is structurally unlike the other areas and the plan changed accordingly: - Its options classes ALREADY declare SampleRate, NumMels, FftSize, HopLength and the rest — around thirty of them — so the AudioHyperparameterOptions added in phase 1 duplicated them and every leaf hid a base property. It is deleted. AudioNeuralNetworkOptions, which every options class under src/Audio already derives from, becomes the family base instead — the same relationship DocumentNeuralNetworkOptions has with the document models. - The moved parameters therefore land on the LEAF, reusing the property where one already exists rather than introducing a shadowing base property. Four more tooling fixes: - Options classes are resolved recursively, and by DECLARATION when no file bears their name — src/Audio declares several inside the model's own file. - src/Models/Options is searched as a shared fallback root. - A constructor can take TWO options-typed parameters (an OnnxModelOptions for runtime plumbing and the model's own); the one named `options` is preferred. - The call-site map records each options type's NAMESPACE, since these are spread across AiDotNet.Models.Options, AiDotNet.Audio.Whisper and others. Also: the copy-constructor repair sweep now requires a BRACED null guard. The earlier version matched the unbraced form too, "repairing" assignments that were already reachable and mangling their indentation; that is tidied here. Build green. AudioVisual suite 64/66 (2 skipped). Refs #2090 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ects (#2090) The over-strict-ValidateCore defect class had been found four times by running model tests for a family and noticing a construction failure — hours of tests, sampled rather than exhaustive, which is why the phase-3 instance shipped and went unnoticed until phase 7. The class is fully enumerable instead. Every options class assigns its defaults in a parameterless constructor and exposes a Validate(), so constructing all of them and validating each covers the defect completely in about 50ms. OptionsDefaultsValidateTests does that, and found two live defects immediately: VisionMambaOptions.VocabSize is 0. SequenceModelOptions.ValidateCore required VocabSize and MaxSequenceLength of every member, but Vision Mamba scans image patches: it has no vocabulary, and its sequence length follows from the image and patch size. Both are now opt-in flags defaulting to true, as requiresHeads and requiresState already were, so no other sequence model changes. Removing that accidental throw exposed a test passing for the wrong reason: Constructor_ThrowsWhenNumClassesNotPositive set NumClasses to 0 and was satisfied by an exception about VocabSize. Nothing validated NumClasses. VisionMambaOptions now requires the five vision dimensions its model reads. DocumentNeuralNetworkOptions.ValidateCore read Require(ImageSize > 0 ? ImageSize : 1, nameof(ImageSize)); which substitutes 1 whenever ImageSize is unset and therefore cannot throw — a guard shaped like validation that validated nothing. ImageSize is genuinely not universal here: 15 of the 29 document models work from text and layout coordinates and have no image. The base no longer requires it and the 14 that render a page image require it themselves. A second test ratchets the number of options classes with no Validate() at all, currently 14. It is a ratchet rather than a demand for zero deliberately: a Validate() that checks nothing would satisfy the stricter form while buying no safety, which is the same over-strictness this commit exists to correct. Also removes 18 triplicated default assignments the phase-7 migration script emitted in the two AudioVisual options classes; every repeat was an identical value, so behaviour is unchanged. Verified: 835 document model tests, 698 sequence-family tests, the ratchet (441/552 unchanged) and the new guard over all 137 options classes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
master validated nothing in these constructors: the tunable values arrived as
constructor parameters, so a missing ONNX file was the only thing that could
throw. The options migration correctly ADDED _options.Validate(), but placed it
directly after the options assignment at the top of the constructor, ahead of
the path checks. That silently changed which exception 53 public constructors
throw first.
ClipNeuralNetworkTests has three tests whose names and comments state the
contract outright -- Constructor_WithZeroEmbeddingDimension_PathValidationComesFirst
and two siblings expect FileNotFoundException. One of them failed. The other 50
constructors changed behaviour the same way with nothing asserting the order.
Validation now sits immediately before the first read of an option, which is
after every guard and before the first use. Anchoring there rather than to the
end of the throw statement matters: a first attempt anchored to the throw put
the call INSIDE braced guards of the form
if (!File.Exists(path))
{
throw new FileNotFoundException(...);
_options.Validate(); // unreachable
}
which the compiler caught in ConvTasNet as CS0162, but would not have caught
anywhere the inserted call was merely misplaced rather than unreachable. That
attempt was reverted wholesale and redone.
Also drops 8 duplicate _options.Validate() calls in the four vision-language
models that carry two constructors each (Blip2, Flamingo, LLaVA, VideoCLIP).
Five constructors are left for manual handling, reported by the script rather
than guessed at: CLAPModel, FastDVDNet, RVM, FlowFormer and DIFRINT read no
option after their file checks, so there is no safe anchor.
Verified: 268 tests across Clip, ConvTasNet, SAM2, the options-defaults guard
and the ratchet (441/552 unchanged).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion ordering (#2090) move_validate_v2.py reported five constructors it could not place validation in, rather than guessing. Investigating why found a defect the ratchet cannot see. Its anchor is "the first read of _options after the file checks", so no anchor means the constructor reads no option at all. FastDVDNet, RVM, FlowFormer and DIFRINT each have a native constructor that reads _options.NumFeatures and an ONNX constructor that hardcodes the same values. The options defaults match the literals exactly -- FastDVDNet 32/5, RVM 32, FlowFormer 256/6/12, DIFRINT 64/3 -- so reading the options instead is behaviour-preserving, and it makes the options functional in ONNX mode, which is the whole point of #2090. Both ratchet metrics count CONSTRUCTOR PARAMETERS, so a hardcoded literal scores as zero and these models looked migrated. Reaching the ratchet floor of 49 is therefore not by itself proof the issue is finished. CLAPModel needed validation BETWEEN its two file checks: the second reads _options.TextEncoderPath and so needs valid options, which is why anchoring after the last throw found nowhere to go. RWKVTransducer read SampleRate and NumMels before touching its path, so its guards moved up instead. find_validate_order.py now reports zero constructors validating before their file checks, down from 53. Also removes the 8 duplicate _options.Validate() calls in Blip2, Flamingo, LLaVA and VideoCLIP. Commit 6f49273's message claimed that removal, but it was part of a patch reverted mid-flight and only re-applied here; that message is wrong and this commit is where the change actually lands. A first attempt at the four Video files anchored with a whole-file str.find() and inserted into the NATIVE constructor, because the same assignment appears in both; the ONNX constructor was left unvalidated and the build stayed green. Redone scoped per constructor body. Verified: 285 tests across FastDVDNet, RVM, FlowFormer, DIFRINT, CLAP, RWKVTransducer and the guards; 564 across the vision-language models. Ratchets unchanged at 441/552. Pre-existing and unrelated: MusicFlamingoTests.Training_ShouldReduceLoss and MoreData_ShouldNotDegrade fail reproducibly. MusicFlamingoOptions derives from ModelOptions, not ModelHyperparameterOptions, and both its files are identical to master, so no change here reaches it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test computed the grouped gap list and then asserted only `byType.Count >= 0`, which is true for any input, and wrote nothing anywhere. Its own comment said it existed "so the numbers are visible in the test output when a migration lands" — a purpose the code did not carry out. Same shape as the dead ImageSize guard fixed earlier: validation-shaped code that validates nothing, reporting-shaped code that reports nothing. It now takes ITestOutputHelper and emits the totals and the per-model breakdown, which is what makes the remaining work addressable: 440 remaining gaps across 193 model types with the largest being SpeechEmotionRecognizer (11), GraphGenerationModel (10), MultiFidelityPINN (7), and a uniform block of eight segmentation models (Mask2Former, MaskDINO, OneFormer, OMGSeg, EoMT, UNINEXT, XDecoder, MixedQueryTransformer) that each take the same four: dropRate, modelSize, numClasses, numQueries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First of the eight query-based mask transformers, done alone so the shape is verified before it is replicated. Gap 440 -> 436, the exact four parameters EoMT declared. PanopticSegmentationOptions holds what every member of the family has: NumClasses, NumQueries and DropRate. The encoder size variant is deliberately NOT on the base -- each model has its own enum for it (EoMTModelSize, Mask2FormerModelSize, ...) because the variants a paper publishes differ per model, so that property lives on each leaf with its own type. ValidateCore requires only NumClasses and NumQueries; DropRate is left unrequired because zero legitimately means "no dropout" and requiring it positive would reject a valid configuration. The constructors needed restructuring rather than a straight parameter move: numClasses is consumed in the `: base(...)` initializer, which runs before the body where _options would be assigned. A private constructor chained from the public one resolves the options exactly once, before the initializer. It takes options FIRST, because a nullable and a non-nullable reference type are the same type to the compiler -- `EoMTOptions` and `EoMTOptions?` would be a duplicate signature (CS0111), so the parameter order is what makes the overloads distinct. The ONNX constructor keeps `_dropRate = 0.0` hardcoded rather than reading DropRate. That is not a default being ignored: inference runs the network deterministically, so dropout is off whatever the options say, and it was hardcoded before this change too. Validation ordering falls out correctly for free here: the base constructor checks the ONNX path, and it runs before the body where options.Validate() sits. Verified: 44 EoMT tests plus the options-defaults guard over all 138 classes. Five call sites in four hand-written integration test files moved to options initializers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing rate (#2090) MusicFlamingoTests.Training_ShouldReduceLoss and MoreData_ShouldNotDegrade failed reproducibly. They are the same defect this issue exists to fix. The model built its optimizer bare: _optimizer = optimizer ?? new AdamWOptimizer<T, Tensor<T>, Tensor<T>>(this); MusicFlamingoOptions.LearningRate declares the paper's 1e-4 and nothing read it, so training ran at AdamWOptimizerOptions.InitialLearningRate's own default of 1e-3 — ten times the intended rate on a ~105M-parameter model. The loss diverged: Training_ShouldReduceLoss measured initial=1.1256 final=1.5310, and MoreData_ShouldNotDegrade reached 2.4744 against an UNTRAINED baseline of 1.1256. Wiring the option through fixes both; MusicFlamingo's 33 tests now pass. A sweep for the same wiring elsewhere — a bare `new <X>Optimizer<...>(this)` in a model whose options declare a LearningRate it never reads — found no other instance. The tests had been tagged into the nightly HeavyTimeout lane, described as "verified-genuine foundation-scale OOM/120s-timeout ... 9B-class generative VLM ... and an audio-LM. The gradients DO flow; the footprint simply exceeds the runner." That diagnosis was wrong on every point for this model. It never timed out — it failed in 11 s and 23 s, well inside the 120 s gate; the gradients did NOT flow correctly, they diverged; and "9B-class" does not describe 105M parameters. The note directly above that set says such a model "is a real bug and must be fixed, not tagged", so the tag contradicted its own contract and kept a correctness bug out of the default lane for as long as it stood. MusicFlamingo is therefore removed from HeavyTimeoutTestClassNames and its tests run in the default lane again, which is what would have caught this. Verified on a 15.4 GB box, comparable to the 16 GB runner. If CI shows a genuine OOM there it belongs back in the set with an accurate reason. This also removes a piece of dead configuration: LearningRate was documented, public, and read by nobody — the ratchet cannot see that, because it is not a constructor parameter. Verified: 38 tests across MusicFlamingo, the options-defaults guard and both ratchets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mask2Former, MaskDINO, OneFormer, OMGSeg and UNINEXT follow the EoMT pattern from 1b0711c. Both ratchets fall: 441 -> 416 and 552 -> 527, 25 parameters. XDecoder and MixedQueryTransformer are deliberately NOT included. Both guard numQueries/numClasses in the constructor with ArgumentOutOfRangeException, and PanopticSegmentationOptions.ValidateCore already requires both — so the guards become redundant and removing them changes the exception type a caller may assert. That is a judgement per model, not a mechanical edit, and it is done by hand next rather than guessed at by a script. The scripted edit was reverted once and rewritten. Three defects showed up in a preview of the generated code, before anything was written to disk: * The parameter splitter split on commas inside generic arguments, turning `IGradientBasedOptimizer<T, Tensor<T>, Tensor<T>>? optimizer` into two broken parameters. It now tracks bracket depth. * The pre-existing `XOptions? options = null` parameter was kept AND re-added, so every constructor declared `options` twice. * MaskDINO carries an explanatory comment BETWEEN its parameter list and its base initializer, and that comment contains parentheses. Finding the parameter list with rfind(')') landed inside it and swallowed the comment as parameters; the closing paren is now found by depth matching. Two more were caught by assertions rather than by the build: a leftover check using a plain substring test failed on `_dropRate` (which contains `dropRate`), and XDecoder's message "NumStuffClasses must be between 1 and numClasses-1." would have been rewritten inside the string literal — token replacement now skips quoted spans. OneFormer resolves its config through ResolveModelConfig(modelSize, _options) where the others use GetModelConfig(modelSize), so the parameter tokens are replaced generically rather than by enumerating call shapes. 36 hand-written call sites across 5 integration test files moved to options initializers. The converter reported, rather than merged, the one call already passing an options argument; that site is OneFormer's in TestScaffoldGenerator, merged by hand along with Mask2Former's. Verified: 141 tests across Mask2Former/MaskDINO/OneFormer, 79 across OMGSeg/UNINEXT, and 47 across EoMT and both guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
XDecoder and MixedQueryTransformer complete the eight. Ratchets 416/527 -> 408/519.
MixedQueryTransformer needed no hand treatment at all. I had recorded that it
"guards numQueries/numClasses in the constructor with ArgumentOutOfRangeException"
— it does not. The script stopped at XDecoder first and never reached it, and I
carried XDecoder's shape over to it as an assumption rather than checking. It is
structurally identical to the five already migrated and went through the script
unchanged.
XDecoder genuinely did need hand treatment. Its constructors carried two guards
with different fates:
* `if (numQueries <= 0) throw new ArgumentOutOfRangeException(...)` is now
redundant — PanopticSegmentationOptions.ValidateCore requires NumQueries —
so it is dropped. This CHANGES THE EXCEPTION TYPE for a non-positive
NumQueries from ArgumentOutOfRangeException to ArgumentException, which is
what the whole family reports for an unset dimension through Require. No
test asserts the old type; the only XDecoder call site is
SegmentationTrainingRobustnessTests line 321.
* The NumStuffClasses/NumClasses cross-check moved onto XDecoderOptions.Validate(),
because both values now live there and the constructor is no longer where
they can be compared. Its ArgumentOutOfRangeException is preserved: that is a
genuine range relationship rather than an unset dimension.
StuffClassCount(numClasses, options) becomes StuffClassCount(options.NumClasses,
options), which also retires the null-options path it previously had to tolerate.
Also checks the family for the MusicFlamingo defect — an options property that is
declared, documented and read by nobody, which neither ratchet can see. A new
scanner (find_unread_options.py) reports every options property the owning model
never reads. The family is clean: its one hit, XDecoderOptions.NumStuffClasses,
is read via `options?.NumStuffClasses`, which the scanner's regex missed because
of the null-conditional. Mask2FormerOptions.LearningRate, which I had flagged as
a likely second instance, is read and wired into the optimizer at Mask2Former.cs
line 201 — the concern was unfounded.
Verified: 87 tests across XDecoder and MixedQueryTransformer plus the
options-defaults guard, and both ratchets at their new baselines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AudioFlamingo2, Pengi and Qwen2Audio each declared a LearningRate on their options and built AdamW bare, so training ran at AdamWOptimizerOptions' own 1e-3 default: 10x, 10x and 100x their declared 1e-4, 1e-4 and 1e-5. SALMONN, in the same directory, already passed the rate through; its form is copied verbatim so all five now match. CORRECTION. Commit f0c8a7e's message says a sweep for this wiring "found no other instance". That was a false negative, not a fact. The sweep matched `new\s+(\w*Optimizer)<[^>]*>\s*\(\s*this\s*\)`, and `[^>]*` cannot span nested generics, so against `new AdamWOptimizer<T, Tensor<T>, Tensor<T>>(this)` it matches `<T, Tensor<T>` and then fails. It found nothing anywhere and would have missed MusicFlamingo itself — the case it was written from, and a known positive I never ran it against. The three above were found by a property-based scanner instead, which asks which options properties the owning model never reads. Qwen2Audio needed a second change. Honouring 1e-5 made LossStrictlyDecreasesOnMemorizationTask fail — verified as caused by this commit by stashing the fix and confirming the test passes without it. The paper rate is correct for the foundation-scale stack Qwen2Audio ships with (AudioEncoderDim 1280, 32 encoder layers, LMHiddenDim 3584); the generated test builds a 32-dim, single-layer scaffold, where 1e-5 moves the loss too little to register a decrease. The generator already shrinks every other dimension for CI with the stated rule that "only the scale shrinks", so LearningRate joins them at 1e-3 — which is exactly what this test ran at before, by accident, via AdamW's default. The assertion is untouched: loss must still strictly decrease. Production now honours 1e-5. Still outstanding in src/Audio/Multimodal, found by the same scanner and NOT fixed here: `Variant` is unread on AudioFlamingo2Options, MusicFlamingoOptions and PengiOptions (and is a string property, which also breaks the enums-not-strings rule), and NumLMHeads/NumMels/Temperature/TopP are unread on Qwen2AudioOptions, plus VocabSize/WindowSize on SALMONNOptions. Each needs a per-property decision — wire it up or delete it — rather than a blanket edit. Verified: 66 tests across AudioFlamingo2 and Pengi, 33 across Qwen2Audio, and 98 across Qwen2Audio/SALMONN/MusicFlamingo before the scaffold change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ls (#2090) The unread-options scanner, validated and then run across all 125 source areas, puts real numbers on a defect form neither ratchet can see: 336 options classes carry at least one property their model never reads 768 unread properties in total 67 of those unread properties are a LearningRate 63 of those 67 models build their optimizer bare Sixty-three models therefore train at their optimizer's own 1e-3 default while advertising a different rate on their options. MusicFlamingo's copy of this bug diverged its loss outright. The four fixed so far were not the tail of it. This commit wires six: AST, AudioSep, BEATs, CLAP, EAT and HTSAT, all in src/Audio/Classification, all previously `new AdamWOptimizer<...>(this)`. Batched deliberately — wiring a paper rate through can break CI-scale training invariants (it did for Qwen2Audio), and that needs per-model attention rather than one large sweep. 230 tests pass across the six. Two silent-zero bugs were caught in the tooling before any of this was believed, both the same root cause and both invisible without a deliberate check: * The repo-wide run initially reported nothing at all for 125 areas. Python text-mode writing had turned the path list into CRLF, so every argument arrived as "src/Audio/Multimodal\r", matched no directory, and globbed empty. * The fixer then matched zero bare optimizers, because `[ \t]*$` cannot match the carriage return in these CRLF sources. Before trusting the survey, the scanner was validated against a known positive: a temp copy of Pengi with the LearningRate wiring reverted, confirming the property reappears in its output. That check is what the earlier sweep in f0c8a7e lacked, and it is why that sweep's "no other instance" was wrong. Precision was sampled too, not just recall: AST and AudioSep were read directly and neither mentions LearningRate anywhere, so both are genuine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Continues the bare-optimizer fix from a5cf012. Each of these declared a LearningRate on its options, read it nowhere, and built `new AdamWOptimizer<T, Tensor<T>, Tensor<T>>(this)`, so it trained at the optimizer's own 1e-3 default: Classification AudioLDMClassifier, AudioMAE, CRNNEventDetector, FDYSED Effects DemucsNoise, NeuralParametricEQ Emotion Emotion2Vec, Wav2Small Enhancement BandSplitRNNEnhancer, CMGAN, FRCRN, MPSENet 22 of the 63 are now done; 38 remain (Qwen2Audio and SALMONN account for the other three of the original 67, which were already correct or fixed). Verified: 377 tests across the twelve, run in six-model batches so a training invariant broken by a newly-honoured rate stays attributable to the batch that caused it. None broke here; Qwen2Audio remains the only model so far needing a scale-appropriate rate pinned in its generated scaffold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Continues from d1ed39f. Same defect in each: a LearningRate declared on the options, read nowhere, and `new AdamWOptimizer<T, Tensor<T>, Tensor<T>>(this)` built bare, so training ran at the optimizer's own 1e-3 default. Enhancement SpikingFullSubNet, TFGridNet, FullSubNetPlus Fingerprinting ConformerFP, NeuralFP, PeakNetFP Foundations Data2Vec2, HuBERT, MERT, Wav2Vec2, WavLM Generation EnCodec 34 of the 63 done; 29 remain. Verified: 715 tests across the twelve. No training invariant broke — Qwen2Audio is still the only model that needed a scale-appropriate rate pinned in its generated scaffold, which suggests most of these declared rates are close enough to 1e-3 that CI-scale training is unaffected either way. That is worth noting rather than assuming it holds for the remaining 29. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Continues from ebe4933. Same defect: a LearningRate declared on the options, read nowhere, and the optimizer built bare, so training ran at AdamW's 1e-3. Generation SoundStream MusicAnalysis BasicPitch, CREPE, MT3, MelodyExtractor, MusicTaggingTransformer, OnsetsAndFrames, Tempogram SourceSeparation BSRoFormer, BandSplitRNN, DannaSep, HTDemucs 46 of the 63 done; 17 remain. Verified: 396 tests across the twelve, in six-model batches with two test runs each. No training invariant broke. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
) MelBandRoFormer, CAMPlusPlus, ECAPATDNNSpeaker, PyAnnote, SpeakerLM, TitaNet. Same defect: LearningRate declared on the options, read nowhere, optimizer built bare, so training ran at AdamW's 1e-3. These edits were applied by a tick that was interrupted before it could build, test or commit them, and were sitting uncommitted in the working tree. Found by checking `git status` rather than assuming a clean tree: re-running the fixer reported "0 bare optimizer constructions" for all six, which reads identically to "already committed" and would have been the wrong conclusion. Verified now: 208 tests across the six. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2090) Wires the last eight of the original worklist: WavLMSpeaker, CTCDecoder, Canary, Conformer, RNNTransducer, MarbleNet, QuailVad, WebRTCVad. 400 tests pass across them. The worklist is now empty. It was also the wrong worklist. It came from find_unread_options.py, which pairs an options class with a model file of the SAME NAME in the SAME DIRECTORY (opt_path[:-len("Options.cs")] + ".cs"), so it never saw a model whose options type is named differently or lives elsewhere. Repo-wide, 499 files still build `new <X>Optimizer<...>(this)`. Resolving the options type from each model's own `_options` field declaration instead, and looking that type up anywhere in src/, splits those 499 three ways: 358 no LearningRate on the options — building bare is CORRECT, not a defect 47 declare a LearningRate the model never reads — the same live bug 94 options type could not be resolved — needs eyes, not a script So 63 was an undercount and the real figure for this defect form is at least 110. That is the fourth undercount in this issue: 205 became 470, 806 became 1067, "~97 in the long tail" became 408, and now 63 becomes 110+. Every one came from a detector whose reach was narrower than the defect's, and every one looked authoritative until a wider instrument was pointed at it. The 358 also matter: they are the reason "wire every bare optimizer" would have been wrong as a blanket sweep. A model with no LearningRate option SHOULD build its optimizer bare, and a script that could not tell the two apart would have invented configuration for a third of the repo. find_unread_lr_v2.py does the classification and is the instrument to use from here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#2090) First batch of the 47 found by find_unread_lr_v2.py: AudioLM, VALLE, FishSpeech, VoiceCraft (src/Audio/Generation) and CosyVoice2, MatchaTTS, StyleTTS2 (src/Audio/TextToSpeech). Same defect — LearningRate declared, never read, optimizer built bare. Seven of these model NAMES are ambiguous: AudioLM, VALLE, FishSpeech, VoiceCraft, CosyVoice2, MatchaTTS and StyleTTS2 each exist in two directories (src/Audio/* and src/TextToSpeech/*). fix_bare_optimizer.py now takes a path when the argument contains '/', and reports the ambiguity rather than picking one — the same principle as its skip reporting, which is what exposed the CRLF bug earlier. The src/TextToSpeech copies are still outstanding and are listed separately in the worklist. Also worth recording about the 47: they are mostly NOT audio. 15 are in src/Video/Enhancement, 17 in src/Video/FrameInterpolation, 6 in src/TextToSpeech, plus MeshCNN and SAM. The original scanner paired an options class with a model of the same name in the same directory, which in practice confined it to src/Audio — that is why 63 looked like the whole population. Verified: 293 tests across the seven. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the 47 found by find_unread_lr_v2.py, and with the original 63 closes
the unread-LearningRate defect for every model the instrument can resolve.
src/Video/Enhancement 15 BasicVSR, DAMVSR, DOVE, FlashVSR, IconVSR,
MGLDVSR, MIAVSR, PSRT, RealBasicVSR,
RealBasicVSRSharp, RealESRGANVideo,
RealisVSR, RealViformer, RVRT, StableVideoSR
src/Video/FrameInterpolation 17 ABME, AMT, BiMVFI, DynamiCrafter, EMAVFI,
FLAVR, GIMMVFI, IFRNet, InterpAnyClearer,
IQVFI, M2M, MoMo, PerVFI, SoftSplat, STMFNet,
SwinVFI, TLBVFI
src/TextToSpeech 6 CodecBased/{CosyVoice2,FishSpeech,VoiceCraft},
FlowDiffusion/MatchaTTS,
StyleEmotion/StyleTTS2, Vocoders/WaveNet
singletons 2 NeuralNetworks/MeshCNN, VisionLanguage/SAM
MeshCNN is the only non-AdamW case: it builds an AdamOptimizer, so the fixer
derived AdamOptimizerOptions from the constructor's own type name rather than
assuming AdamW. That type was checked to exist with matching arity before the
build, since a wrong options type only surfaces at compile time.
MGLDVSR was expected to match zero tests — it is in HeavyTimeoutTestClassNames,
which tags it [Trait("Category","HeavyTimeout")] so the default PR shard skips
it. It ran anyway: 35 tests, 13m42s. The trait excludes it from the shard's
CATEGORY filter, not from a FullyQualifiedName filter. Recorded because the
expectation was wrong and the difference matters when verifying a tagged model.
Verified: 1,761 tests. 526 Video/Enhancement, 731 FrameInterpolation across four
groups, 215 including MeshCNN and WaveNet, 267 for SAM and the TTS copies, 35
MGLDVSR, and 121 re-running FishSpeech and VoiceCraft after their TextToSpeech
copies were edited — those two had been tested earlier against the src/Audio
copies only, before the TTS ones existed in this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…scan (#2090) The FIFTH undercount in this issue, same mechanism as the previous four: the detector was narrower than the defect. find_unread_lr_v2.py resolved a model's options type with `(private|protected) (readonly )?(\w*Options) _options;`, which cannot match a GENERIC declaration — `private readonly TCNOptions<T> _options;`. That alone accounted for 37 of the 94 UNKNOWN files, and hid 17 real defects behind them. Allowing an optional `<...>` after the type name reclassified the lot: before DEFECT 0 OK 358 UNKNOWN 94 after DEFECT 17 OK 421 UNKNOWN 14 src/Finance/Forecasting/Neural DeepAR, DeepFactor, DeepState, LSTNet, TCN, WaveNet src/Finance/Forecasting/Transformers Crossformer, ETSformer, Informer, ITransformer, NonStationaryTransformer, PatchTST, TFT, TimesNet, TSMixer src/NeuralNetworks/SyntheticData GOGGLEGenerator, TabTransformerGenGenerator Every Finance model builds a bare optimizer in BOTH of its constructors, so the fixer's exactly-one-match guard skipped all 15 rather than half-fixing them. It now replaces every occurrence and reports the count (AdamOptimizer x2), which is how the second constructor got wired instead of silently left behind. TCN needed care: it has a THIRD optimizer path, GetOrCreateBaseOptimizer(), which hardcodes 1e-5 with a documented stability rationale. That is a deliberate override, not this defect, and is untouched — the fixer only matches the `_optimizer = optimizer ?? new ...(this);` constructor form. PatchTST is representative of the harm: its options declare 1e-4 and the model mentioned LearningRate nowhere, so it trained at AdamOptimizer's 1e-3. Verified: 756 tests (230 Finance/Neural, 222 + 304 Finance/Transformers and the synthetic-data generators). DEFECT is back to 0 and 14 UNKNOWN remain, which this issue's history says to treat as a lower bound rather than a finish line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ratchets 408/519 -> 392/503, the first movement since the panoptic family.
NodeClassificationModel, LinkPredictionModel and GraphClassificationModel had NO
options class at all — they were among the "no _options field" UNKNOWNs from the
bare-optimizer survey, and each took its tunables purely as constructor
parameters. New GraphModelOptions holds what all three share (HiddenDim,
DropoutRate); each leaf declares the rest.
Two things deliberately NOT put on the family base:
* MaxGradNorm — already on ModelHyperparameterOptions, which every options
class inherits, so adding it here would shadow rather than share.
* The layer count — the node and link heads publish it as NumLayers and the
graph-level head as NumGnnLayers. Those names are part of each model's
published vocabulary, so renaming them to a common word to fit a base would
cost more than the sharing gains.
ValidateCore requires only HiddenDim. DropoutRate is left unrequired because
zero legitimately means "no dropout" — the over-strictness that made five
earlier family bases throw at their own members' defaults.
Two nested enums were promoted to AiDotNet.Enums, for the same reason
DepthAnythingV2ModelSize was: a type nested in a generic class is a distinct
type per type argument, so GraphClassificationModel<double>.GraphPooling could
not be named on a non-generic options class.
GraphPooling (Mean, Max, Sum, Attention)
LinkPredictionDecoder (DotProduct, CosineSimilarity, Hadamard, Distance)
maxGradNorm feeds the `: base(...)` initializer, so all three needed the
public-wrapper -> private-ctor chaining established for the panoptic family:
options resolved before the initializer, options first in the private signature
so the overload is not a duplicate.
Verified: 119 tests across the three models, MissingModelsIntegrationTests, the
options-defaults guard and both ratchets. Four hand-written call sites converted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ratchets 392/503 -> 378/489. GraphSAGENetwork, GraphIsomorphismNetwork and
GraphAttentionNetwork each already TOOK an options parameter and stored it —
and then read every value off constructor parameters instead. Their options
classes were three of the empty shells this issue is about:
public class GraphSAGEOptions : NeuralNetworkOptions { }
New GraphEncoderOptions holds the one thing all three share, NumLayers.
Deliberately NOT reusing GraphModelOptions (added last commit for the graph TASK
heads): that base requires HiddenDim, and these encoders do not all have one —
GraphSAGE and the attention network have none, and the isomorphism network calls
its own MlpHiddenDim. Deriving them from a base requiring a value they lack is
exactly what made five earlier family bases throw at their members' defaults.
Each leaf requires only what it has: GraphSAGE nothing beyond NumLayers,
GIN also MlpHiddenDim, GAT also NumHeads. InitialEpsilon (0.0 is the paper's
initialisation) and both DropoutRates are left unrequired.
The constructors chain public -> private with options first, rather than reading
`(options ?? new X()).MaxGradNorm` in the base initializer. The inline form works
here — MaxGradNorm's default is a constant — but it allocates a throwaway options
object distinct from the one the body stores, and I recorded that as fragile when
rejecting it for the panoptic family. Using it here would have contradicted that
for no gain.
Two mistakes worth recording. I created GraphIsomorphismOptions and
GraphAttentionOptions before checking what the models reference — they reference
GraphIsomorphismNetworkOptions and GraphAttentionNetworkOptions, which already
existed. The wrongly-named files were deleted and the existing shells filled.
And I overwrote src/NeuralNetworks/Options/GraphSAGEOptions.cs without reading
it first; it turned out to be an empty shell so nothing was lost, but the Write
result said "updated" rather than "created" and that distinction is the only
warning given.
Verified: 387 tests across the three encoders, the graph task models,
AdvancedNeuralNetworkModelsIntegrationTests, the options-defaults guard and both
ratchets. Eight hand-written call sites converted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ratchets 378/489 -> 368/479. Completes the graph family: 8 models across three commits (3 task heads, 3 encoders, this generator). Same shape as the encoders — it already took a GraphGenerationModelOptions, stored it, exposed it via GetOptions(), and read all ten values off constructor parameters. The options class was another empty shell. The base initializer calls CreateArchitecture(inputFeatures, hiddenDim, latentDim, numEncoderLayers), so four of the ten feed it and the options must be resolved before it runs — the public-wrapper -> private-ctor chaining again, options first. GraphGenerationModelOptions derives from ModelHyperparameterOptions rather than either graph family base. It is a generator, not an encoder or a task head: it shares neither GraphEncoderOptions.NumLayers's meaning nor GraphModelOptions.HiddenDim's required-ness, and it BUILDS its architecture from these values rather than being handed one. Forcing it into either base would put a required property on it that does not fit. KlWeight and UseAMSGrad are not required by Validate — zero KL weight disables that term, which is a legitimate configuration. LearningRate and UseAMSGrad now reach the optimizer from the options rather than from constructor parameters, so this also closes the unread-configuration shape for this model on the same axis as the earlier bare-optimizer work. Verified: 202 tests across GraphGenerationModel, AdvancedNeuralNetworkModelsIntegrationTests, the options-defaults guard and both ratchets. Three hand-written call sites converted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ratchets 368/479 -> 350/461. PhysicsInformedNeuralNetwork, MultiFidelityPINN and DomainDecompositionPINN all took their tunables as constructor parameters against empty options shells. The whole hierarchy was empty: PhysicsInformedOptions itself, plus twelve leaves. PhysicsInformedOptions is re-parented onto ModelHyperparameterOptions but gains NO properties. Twelve classes derive from it — PINNs, neural operators, Hamiltonian and Lagrangian networks, Deep Ritz — and they share no hyperparameter universally. The PDE and boundary weights would be meaningless on FourierNeuralOperatorOptions. What the re-parenting contributes is MaxGradNorm and the Require helpers for all of them. The base PINN's weights were `double? = null`, passed through to PhysicsInformedLoss which resolved each as `?? 1.0`. The options declare them non-nullable at 1.0, which is exactly equivalent and does not ask the reader to trace a null two files away. Both derived PINNs pass values into the base constructor, which now takes an options object, so each needed the public-wrapper -> private-ctor chaining. They also construct inner PhysicsInformedNeuralNetworks (a low-fidelity network; one per subdomain), and those call sites moved to options initializers too — preserving MultiFidelity's deliberate halving of the low-fidelity collocation count and PDE weight. Weights are not required by Validate anywhere in this family: zero is how a loss term is switched off, and requiring them positive would reject valid setups. THE UNCOVERED-VALIDATE RATCHET WAS RAISED, 14 -> 23, which is the one direction it is not meant to move. Re-parenting made twelve physics-informed options classes visible to that scan for the first time; nine are property-less shells for models not yet migrated. They gained no defect — they became visible. The reason is recorded in the test file itself rather than only here, since that is where the next person meets the number. Each falls off as its model is migrated. Verified: 373 tests across the PINN suites, the options-defaults guard and both ratchets. 24 call sites converted across 3 test files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…reading itself (#2090) UnreadOptionsRatchetTests asks whether any method in the product assembly calls a property's getter. Every one of these options classes carries a copy constructor, and `TeacherMomentum = other.TeacherMomentum;` IS such a call -- so having a copy constructor removed a class's entire property set from the detector's reach. Validate() does the same on a smaller scale. The discriminator is exact: MatryoshkaEmbeddingOptions.MaxEmbeddingDimension was one of the three properties the ratchet could see, and it is visible only because that class has no copy constructor. ConcertoOptions' ten never-read properties were invisible only because it has one. Skipping call sites declared on a ModelHyperparameterOptions subclass asks the question that actually matters: does anything OUTSIDE the options classes consume this value? 3 -> 35 against 748 declared getters. Nothing became unread; 32 properties that were already unread stopped being hidden. The old number was measuring the wrong thing. What it surfaced: ConcertoOptions 10 teacher momentum, intra/cross-modal loss weights and upcast levels, image encoder resolution, images per point cloud, learning rate, pretraining epochs -- an entire self-supervised pretraining configuration the model never consults FinchOptions 6 Beta1, Beta2, WeightDecay, MinLearningRate and the gradient-clipping pair, none of which reach an optimizer WhisperOptions 5 ONNX component paths nothing loads AudioGenOptions 4 same shape TtsOptions 2 same shape eight others 1 each, including the three the ratchet already knew about Each is the defect #2090 calls the worse of the two: a constructor parameter at least does something, whereas a declared property nobody reads advertises configurability that does not exist. They are tracked at the raised baseline and lowered as they are wired in or deleted. Raising a ratchet because the detector got honest is the intended response here; suppressing the visibility change to keep the number flat would be the failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#2090) 47 models took a `maxGradNorm` constructor parameter and passed it straight to the base initializer. `ModelHyperparameterOptions` already declares `MaxGradNorm`, so the plan called this the easy cluster -- "one parameter each, no new surface invented". Surveying it first showed that was wrong in two ways that both mattered. **33 of the options classes were not in that hierarchy at all.** They descend from `RiskModelOptions<T> : FinancialNeuralNetworkOptions : NeuralNetworkOptions`, so they inherited no `MaxGradNorm` to read. Re-parenting `FinancialNeuralNetworkOptions` onto `ModelHyperparameterOptions` fixes all 33 in one edit; 14 more classes are re-parented individually. **22 of the 47 declared a default that is not 1.0**, and the base's default IS 1.0 -- so a naive migration would have silently tightened gradient clipping on 22 models: * 21 synthetic-data generators declared 5.0. They are siblings, under `RiskModelOptions`, of 12 tabular *prediction* models that declared 1.0, so neither value could sit on the shared parent. New `SyntheticDataGeneratorOptions<T> : RiskModelOptions<T>` carries the 5.0 for exactly the 21. The split is the `SyntheticData` / `Tabular` folder boundary, so the family is real rather than an artefact of this migration. Adversarial training produces spikier gradients than supervised training; clipping those generators at 1.0 would cap ordinary updates, not just destructive ones. * `OctonionNeuralNetwork` declared `double.MaxValue`, which is clipping switched OFF rather than a large threshold. Its options constructor restates that, because inheriting 1.0 would have switched clipping ON as a side effect of a refactor. Four re-parented classes have copy constructors that did not copy `MaxGradNorm`, because the property did not exist on them until now -- a copy would silently revert to the base default, which for `TVAEOptions` means clipping five times tighter than its source. Fixed in MobileNetV2Options, TVAEOptions, TabPFNOptions and TabTransformerOptions. Nothing would have caught this: the constructor ratchets count parameters, and the unread ratchet is satisfied by the copy constructor's own getter call. `MixtureOfExpertsNeuralNetwork` takes a REQUIRED `MixtureOfExpertsOptions<T> options`, so it reads `options.MaxGradNorm` directly with no `??=`. No model's clipping behaviour changes. Ratchets: 130 -> 83 name-credited, 151 -> 104 strict. Floor is 49. The two visibility guards rise, and that is the point of this commit as much as the parameters are. Re-parenting brought the financial, tabular and synthetic-data options into scope for the first time: unread properties 35 -> 517, classes with no Validate() 23 -> 81. A conservation check confirms exposure rather than breakage -- declared getters went 748 -> 1236, so 488 properties became visible and the unread count rose by 482. That 482 of 488 newly-visible properties are read by NOBODY is the finding. MedGANOptions and TabPFNOptions declare 19 unread each; FTTransformerOptions, SAINTOptions, TabDPTOptions, TabNetOptions and TabROptions 17 each -- embedding dimensions, dropout rates, feed-forward widths, batch sizes. Whole published configurations, declared and never consulted. Both baselines are raised with the reasoning recorded at the constant, following the precedent already documented in OptionsDefaultsValidateTests: adding Validate() methods that check nothing would turn the test green and buy no safety. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… was wrong (#2090) **This corrects d1b942f.** That commit set `UnreadBaseline = 517` and its message asserted "482 of 488 newly-visible properties are read by NOBODY". Both are false. The measured figure is 112 of 1236 declared getters. `ScanCalledGetters` (then `ScanCalledMethodTokens`) collected each options getter's MethodDef token and compared it against raw IL call operands. A call on a CONSTRUCTED GENERIC type -- `_options.EmbeddingDimension` where `_options` is `CTGANOptions<T>` -- emits a MemberRef token, which can never equal that MethodDef token. The old remark claimed raw comparison was "immune" to generic types. It was blind to them, and silently. Nearly every tabular and synthetic-data options class is generic, so when the maxGradNorm cluster re-parented `FinancialNeuralNetworkOptions` and brought that hierarchy into scope, all of it reported as wholly unconsumed at once. That is the whole of the 35 -> 517 jump. Contradicted directly by the source. CTGANGenerator.cs reads `_options.EmbeddingDimension` nine times, `_options.DiscriminatorDimensions` three and `_options.BatchSize` three, all three reported unread. MedGANGenerator.cs reads `_options.AutoencoderDimensions` and `_options.ConstraintWeight` three times each; TabPFNNetwork.cs reads `_options.EmbeddingDimension` five times. Each call token is now resolved with its enclosing method's generic context, normalised to the generic type DEFINITION, and keyed on (declaring type, property name) -- the key MethodDef, MemberRef and MethodSpec all agree on. Two guards against the same class of silent failure returning: * Unresolvable tokens are COUNTED and the test fails above a tenth of the total. The defect being repaired was a scan answering "not read" when it meant "could not tell". * Successes are deduplicated, failures deliberately are NOT: one token can fail under one method's generic context and resolve under another's, and skipping that retry would rebuild the bug in a new shape. Validated against a control rather than by the number looking better. ConcertoOptions is NOT generic and its ten unread properties were confirmed by grep before any of this work; it had to stay at ten and did. The suspected generic false positives had to move and did: CTGANOptions now reports exactly one unread member, `Epochs`, which is precisely the one CTGANGenerator never reads. A fix that lowered every class uniformly would have meant the detector was loosened, not corrected. Worth recording why 517 survived scrutiny: a conservation check confirmed the rise (482) could not exceed the newly-visible getters (488), and it did not. That check bounds magnitude, never correctness -- it passes identically whether the properties are truly unread or systematically mis-scanned. Reading one model and comparing it against the claim is what caught it. The surviving 112 remain real work, and several are duplicates rather than wirings: ConcertoOptions 10 (an unconsulted self-supervised pretraining configuration), TabROptions 7, FinchOptions 6, FTTransformerOptions 5, TabNetOptions 5 -- largely `EnableGradientClipping`/`MaxGradientNorm`/`WeightDecay` triples that reach no optimizer and now duplicate the `MaxGradNorm` wired onto ModelHyperparameterOptions -- plus WhisperOptions 5 and AudioGenOptions 4, ONNX component paths nothing loads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…12 forecasters (#2090) Hippo, MOIRAI, Mamba, Mamba2, RWKVForecaster, S4, TimeGPT, TimeGrad, TimeLLM, TimeMachine, Timer and UniTS each took `numFeatures` as a constructor parameter, all defaulting to 1, while storing an options object that declared no such property. `NumFeatures` is declared on `TimeSeriesRegressionOptions<T>` itself rather than gained by re-parenting onto `ModelHyperparameterOptions` as the previous two clusters did. That base derives from `NeuralNetworkOptions`, and `RegressionOptions<T>` also serves the classical, non-neural regressors -- they should not acquire neural-network configuration as a side effect of a hyperparameter migration. All twelve derive from `TimeSeriesRegressionOptions`, all defaulted it to 1, and all called it by that name, so the family-base rule is satisfied without moving anyone into a hierarchy they do not belong in. **Eleven sibling classes already declared their own `NumFeatures`.** Checking the twelve migrated leaves was not sufficient: adding the property to a shared base collides with every descendant that has one. TinyTimeMixers, TemporalGCN, TSDiff, ScoreGrad, STGNN, RelationalGCN, MTGNN, GraphWaveNet, DiffusionTS, DCRNN and CSDI all shadowed it, and their defaults were NOT uniform -- eight at 1, but RelationalGCN 16, GraphWaveNet 2, DCRNN 2. All eleven declarations are removed so a single property exists, and those three restate their published value in a parameterless constructor. No model's feature count changes. Marking the siblings `new` was rejected: two same-named properties mean setting the inherited one silently does nothing, which is precisely the false configurability this issue exists to remove. Ratchets: 83 -> 71 name-credited, 104 -> 92 strict. Floor is 49. Also in this commit: * `Timer`'s guard now validates `options.NumFeatures` after `options ??= new TimerOptions<T>()` rather than before it, and reports `nameof(options)`. * `Mamba2`'s generated CI-smoke constructor keeps its 16 by moving it INTO the options initializer. It is not a default that can be dropped: the generated tests feed 16-feature input and Mamba2 validates `featureDim == _numFeatures` strictly, so falling back to 1 would fail all 25 of its tests. The two `numFeatures: 1` emissions are simply removed. * A fourth generator emission for `RWKVForecaster` was missed on the first pass because the survey grep was piped through `head -6`. It could not fail the src build -- the generator only emits strings -- and surfaced as CS1739 inside a generated .g.cs a full test-build later. * The movement-history paragraphs on `ConstructorBaseline` were sitting after `</remarks>`, outside the doc comment, since the segmentation commit. The test project does not enforce doc-comment structure, so two clusters of green builds said nothing about it. Only one call site in the entire repository passed `numFeatures` to a migrated model (`HippoConfigurationTests`), folded into its options initializer. Of 281 `numFeatures:` named arguments repo-wide the other 280 belong to unrelated layers, video models and data loaders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…options surface (#2090) Twelve models took `variant`, `inChannels`, `windowSize`, `depth` and `widthMultiplier` as constructor parameters and had NO options parameter at all -- four detection backbones (ResNet, EfficientNet, SwinTransformer, CSPDarknet) and eight diffusion text conditioners. Unlike the three earlier clusters there was nothing to re-point: the options object had to be introduced. New `DetectionBackboneOptions` carries `InChannels`, the one knob all four backbones share and all four called by that name. `Variant` is NOT hoisted: each backbone names its own enum (`ResNetVariant`, `SwinVariant`, ...), so there is no common type to declare -- the same reasoning that kept `ModelSize` off `SegmentationModelOptions`. The backbone options are named `ResNetBackboneOptions` and `EfficientNetBackboneOptions`, not `ResNetOptions` / `EfficientNetOptions`. Those names are already taken by the options for `ResNetNetwork` and `EfficientNetNetwork`, which are separate standalone classifiers migrated in the maxGradNorm cluster; the backbone and the classifier share a stem and nothing else. The eight conditioners need `(options ??= new X()).Variant` at BOTH occurrences in their base initializer. The first sits behind `architecture ?? BuildDefaultArchitecture(variant)` and is skipped whenever a caller supplies an architecture, so only the `GetEmbeddingDim(variant)` occurrence is guaranteed to run -- the same short-circuit that DEVA and EfficientTAM hit in the segmentation cluster. Every options class carries a real `Validate()`, so the Validate-coverage baseline is untouched rather than rising by twelve. The conditioners' validate is deliberately empty with a comment saying why: `Variant` is an enum, every value it can hold is buildable, and there is nothing to reject -- stated rather than left to omission. Ratchets: 71 -> 53 name-credited, 92 -> 74 strict. Floor is 49. Twenty-three call sites moved to the options form. Five passed named arguments and were found by survey; the other EIGHTEEN were positional and invisible to it -- ten detectors constructing `new ResNet<T>(ResNetVariant.ResNet50)`, and all eight conditioners' own `FromPretrained` factories calling `new XTextConditioner<T>(tokenizer, variant)`. They surfaced as CS1503 "cannot convert from ResNetVariant to IActivationFunction<T>?" once the removed parameter's slot was taken by the next one along. Two more in `ConditioningModuleTests` appeared only when the test project compiled. `YOLOSeg` passed `depth: 1.0, widthMultiplier: 1.0` -- the defaults -- so its arguments are simply dropped rather than restated. `SwinTransformer` was the first constructor in four clusters where EVERY original parameter was removed, which exposed a gap in the parameter-list rebuild: joining `kept[0]` to `kept[1:]` emitted a trailing comma followed by nothing. The other eleven were checked for the same signature and are clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s classes (#2090) 29 model types across NeuralNetworks, PhysicsInformed, Document and UncertaintyQuantification, plus the duplicate-clipping cleanup that the re-parenting exposed. Both clusters land together because the tail's re-parenting raised the unread count by one: splitting them would leave a red commit in between. Ratchets, all measured by a live run rather than from notes: Baseline 53 -> 0 (name-credited constructor gap) ConstructorBaseline 74 -> 21 (strict: parameter actually removed) UnreadBaseline 112 -> 97 (declared properties nothing reads) Baseline reaching 0 does NOT close #2090. It credits a NAME match, so it is satisfied by a property existing and says nothing about whether the constructor reads it. The three other ratchets measure three further defect forms, and ConstructorBaseline's remaining 21 are real gaps, led by DGCNN (knnK, useDropout, dropoutRate) and CRAFT (imageSize, backboneChannels). The 21-point spread between the two constructor ratchets is now resolved. It was recorded as PROBABLE that those parameters were ones whose options property already existed while the constructor still took them - the UnifiedMultimodalNetwork shape. Both counts converging in the same cluster confirms that reading, rather than a detector disagreement. Defects found and fixed while doing this, each of which would have survived the migration silently: - A `_options = options ?? new XOptions()` AFTER an `options ??= ...` re-tests the parameter, so Roslyn joins both branches of the `??` and `options` becomes possibly-null again. That produced 168 CS8602 across ~20 files whose coalesce was demonstrably already present. 69 residual `??` sites removed. - SVTROptions' copy constructor did not copy Charset, so the defensive copy silently discarded a user-set charset. - HyperbolicNeuralNetworkOptions.Validate() called Require(Curvature), which demands a value greater than zero - but hyperbolic space is by definition negatively curved, so the published default of -1.0 failed its own validation. Range-checked instead, and its doc example (which set Curvature = 1.0, named two properties the class does not declare, and called a constructor overload that does not exist) corrected. - TabR, TabM and GANDALF each built a BARE AdamOptimizer, so their declared WeightDecay reached nothing; Finch built no optimizer at all, leaving LearningRate, Beta1, Beta2 and WeightDecay all inert. Now AdamW, whose learning-rate default is identical to Adam's, so the swap adds the decay and changes nothing else. - None of the TabR/TabM/TabNet/FTTransformer Clone() methods carried the INHERITED MaxGradNorm. Survivable while each class had its own duplicate shadowing it; a live defect the moment the duplicate went. - Two ratchet history blocks sat OUTSIDE their `</remarks>`, which this repo's test project compiles silently. TabNet's published clipping bound of 2.0 moved onto MaxGradNorm in a new parameterless constructor rather than being dropped with the duplicate. 49 test call sites migrated from named constructor arguments to options object initializers. Driven from the constructors' accepted-parameter sets rather than from the compiler's error list, because the compiler reports only the FIRST offending argument per call. Verified: src and test projects build clean; 280 Configuration guards pass; 1,421 tests pass across PhysicsInformed, Tabular, AdvancedAlgebra, AdvancedNeuralNetworkModels, UncertaintyQuantification, Adversarial, SVTR and Finch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tructorBaseline 21 -> 0) The last 21 tunable defaulted constructor parameters, across 13 model types. Both constructor ratchets are now at 0. Baseline 0 (unchanged; reached 0 in bb0ae36) ConstructorBaseline 21 -> 0 ConstructorBaseline is the strict measure: it gives no credit for a matching options property and can only fall when a constructor actually stops taking the parameter. The 21 reduced to exactly two shapes, both of which read as working configurability: 1. Four probabilistic forecasters (CSDI, DiffusionTS, ScoreGrad, TSDiff) reconciled parameter and options with _numFeatures = numFeatures > 0 ? numFeatures : _options.NumFeatures; so the options value applied ONLY when a caller passed zero or less, and the parameter's own default of 1 shadowed it for everyone else. Each type's other constructor already read the options directly; the two now agree. 2. Six models (DGCNN, PointNet, PointNetPlusPlus, GaussianSplatting, MeshCNN, SpiralNet) had convenience constructors forwarding scalars into an options object initializer, applying the parameter copy last. Every one of those 11 defaults was checked against its options property and matched, so the removals change no behaviour. Parameters with no default (numClasses, samplingRates, searchRadii, mlpDimensions) and collaborators (lossFunction, optimizer, initialPointCloud) stayed: a parameter with no default is a required input, not a duplicated value. CRAFT inherited ImageSize/BackboneChannels from DocumentNeuralNetworkOptions but left both unset, so the options object reported 0 while the model ran at 768/512. It now has a parameterless constructor assigning the published values, and validates them. SGPT was the last model carrying [ModelDimensionRole] on constructor parameters, and TestScaffoldGenerator emitted its embedding-dimension / head-count divisibility test purely from those parameters, returning early on zero candidates -- so migrating SGPT would have switched the feature off silently. The attribute is retargeted to properties, the annotations moved onto EmbeddingModelOptions, and the generator now reads values off a default-constructed options instance. That is also stronger than what it replaces: a parameter default is a compile-time constant, whereas this exercises whatever the parameterless constructor actually assigns. That retarget caused a regression this commit also fixes. The properties are declared on EmbeddingModelOptions, so EVERY descendant inherited the annotation -- including Word2Vec, GloVe and FastText, which have no attention, never assign NumHeads, and failed a divisibility check against 0. The attribute names each property's ROLE correctly; what it cannot express is whether a model HAS attention. Candidates are now gated on TransformerEmbeddingOptions, the family base that assigns both values, whose seven descendants (BGE, ColBERT, Instructor, Matryoshka, SGPT, SimCSE, SPLADE) are exactly the models the invariant covers -- still 7x the single-model coverage the feature had before. Skipping types whose NumHeads is 0 was rejected: it passes, but makes the test vacuous for any attention model that forgot to set it, which is the defect worth catching. 12 call sites migrated, in three shapes that needed different fixes: CRAFT took an added options object; MeshCNN moved to the primary constructor because no overload accepts both numClasses and an options object; SAM2 already passed an options object from a test helper, so the values went into the helper. The twelfth was found by the compiler, not by the inventory scan -- SAM2PaperFidelityTests uses target-typed `new(...)`, which a `new <TypeName>(` pattern cannot match. Zero here does NOT close #2090. It closes one of six defect forms. The unread ratchet stands at 97 and UncoveredBaseline at 81, and the forms with no detector at all still surfaced four defects in this cluster alone -- see the remarks on ConstructorBaseline. Verified: src and test projects build clean; 280 Configuration guards pass; 678 tests pass across Probabilistic, OCR text detection, SAM2, PointCloud, NeuralRadianceFields, MeshCNN, SpiralNet, SGPT and the generated dimension-role tests (7, matching the 7 descendants, where before the fix there was 1). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nreadBaseline 97 -> 66) Two defect clusters plus a fix to the scan that measures them. UnreadBaseline 97 -> 66 EPOCHS, 21 SYNTHETIC-DATA GENERATORS Each declared a per-model published value -- TimeGAN 2000, MedGAN and TabDDPM 1000, TabFlow 500, AutoDiffTab 200, three at 100, the rest 300 -- while Fit and FitAsync took `epochs` as a REQUIRED argument, so not one of them could ever apply. The doc examples showed both at once: `Epochs = 300` on the options next to `Fit(data, columns, epochs: 300)`. The parameter is now `int? epochs = null`, resolving to the options value. Deliberately NOT `int epochs = 0` plus `epochs > 0 ? epochs : _options.Epochs`: that sentinel is the shape removed from CSDI, DiffusionTS, ScoreGrad and TSDiff in bb0ae36, where it made the options value reachable only by passing zero. Existing call sites still compile because int converts implicitly to int?. TabSyn carried the same defect in MIRROR form: `_options.VAEEpochs > 0 ? _options.VAEEpochs : epochs` is always true, because VAEEpochs defaults to 100 -- so its required argument was dead, and the diffusion phase read DiffusionEpochs outright. Both polarities of that sentinel hide one side while reading as a sensible fallback. The four generators on SyntheticTabularGeneratorBase are not iterative learners (AIM, BayesianNetworkSynth, CopulaSynth, SMOTENC fit or resample in a single pass and ignore the count), so DefaultEpochs is 1 rather than an invented training budget, and virtual for any subclass that trains. THE TABULAR CLUSTER HiddenVectorActivation now reaches the hidden dense layers of SAINT, TabDPT, TabPFN, Mambular and AutoInt through a shared TabularHiddenDense helper that keeps each builder's existing activation when none is given. FeedForwardDimension replaces a hardcoded `* 4` in four builders -- the literal that had been shadowing the declared multiplier, i.e. defect form (2) sitting on the same lines as the unread property. Wiring it exposed a second defect: FeedForwardDimension was computed from EmbeddingDimension on SAINT and TabTransformer, which those models never pass to layer construction. The computed 128 bore no relation to the 512 actually built, and nothing read it, so the discrepancy could not surface. It now derives from HiddenDimension; every value is unchanged at runtime. Their divisibility guard is corrected to check HiddenDimension too, the width that actually feeds attention -- it was validating EmbeddingDimension, so it would accept an invalid attention geometry and reject a valid one. Three properties were DELETED rather than wired, having nothing to wire to: NODE's HiddenVectorActivation (a tree ensemble plus an output projection has no hidden activation) and TabR's FeedForwardMultiplier / FeedForwardDimension (its builder has no transformer feed-forward). THE DETECTOR UNDER-REPORTED ITS OWN FIX Following computed properties meant walking options-class getters, which the scan otherwise skips so copy constructors cannot flatter the count. The first attempt kept the existing `seen` set -- keyed on the CALL TARGET and used to skip work -- and the count rose to 156: whichever walker reached a getter token first claimed it, so a read from inside an options class recorded its edge and every later read of that property BY A MODEL was skipped before it could be counted. 83 genuinely-read properties reported as unread. The repair caches the resolution instead of the decision. This is the second dedup-shaped under-report in that file, after raw metadata-token comparison once made every generic options class read as 100% unread. Propagation is narrowed to property GETTERS: a copy constructor and Validate() are not getters, so the earlier anti-flattering fix stands. 73 -> 66 is exactly 3 deletions plus 4 false positives removed (FeedForwardMultiplier on SAINT, TabDPT, TabPFN and TabTransformer, each consumed through the computed property). Matching the prediction property by property is what shows the propagation reaches what it was built for and nothing else. PRE-EXISTING TEST FIXED IN PASSING PrivBayesDifferentialPrivacyTests.DegenerateBudgetSplit_DoesNotProduceNaN was already failing on this branch and is unrelated to the above -- BayesianNetworkSynthOptions.StructureBudgetFraction rejects 0.0 and 1.0 from a self-contained setter. The setter is the correct side: at 1.0 the marginal phase gets no budget, so conditional distributions publish with NO privacy noise while the object still reports DP as enabled. Asserting that state produces finite numbers wants the wrong outcome, so the test is inverted to assert the rejection, and a near-degenerate case (0.01 / 0.99) keeps the NaN-freedom check where it applies. OPEN NODEOptions.HiddenActivation is as unwired as the vector sibling deleted here -- NODENetwork reads no activation at all -- yet it is not reported unread. It is not in ReflectivelyUsed, the property collection applies no type filter, and the generated clone registry mentions the name only as a string literal. Something marks it read and the path was not identified; until it is, this count may UNDER-report. Recorded at the constant. Verified: both projects build clean; 280 Configuration guards pass; 232 tests pass across SyntheticData and Tabular. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tchet two undetected defect …forms (#2090) UnreadBaseline 66 -> 50 AgreementBaseline new, 4 (constructors disagreeing about an option) DivergenceBaseline new, 49 (a model's two implementations reading different options) CONCERTO PRETRAINING, 10 PROPERTIES The options configured a self-supervised pretraining pipeline whose parts all existed -- two objectives, a paired-view type, a teacher EMA -- and referenced only each other, with no orchestrator. A closed ring of mutually-referencing types is the signature of an unimplemented subsystem, which makes the decision "implement or remove a feature" rather than "wire a property". Both objectives computed in raw double (Math.Exp, Math.Log, accumulating a double) and returned a bare scalar, so they carried NO gradient graph. A Pretrain built on them as written would have run, reported a falling number, and left every weight untouched -- the exact defect class this issue exists to remove. They are reimplemented in engine operations first, per this repo's stated contract that a loss defines its forward math once and GradientTape supplies the backward. The point-to-patch matching is extracted into a helper shared by the scalar and tape versions: two copies of that geometry would let the loss a user monitors and the loss that trains disagree about which points are visible, with nothing reporting the divergence. Pretrain is covered by a test asserting the loss FALLS and the parameters MOVE. A "does it run" assertion passes against the non-differentiable version, which is why it is not the assertion used. THE AUDIO CLUSTER, 11 PROPERTIES ReturnTimestamps now lives once on AudioNeuralNetworkOptions with a resolver on AudioNeuralNetworkBase. All 104 ISpeechRecognizer implementations already branched on the flag, but it arrived only as a method parameter defaulting to false, so no model could be CONFIGURED to return timestamps -- the caller had to ask every time. Declaring it on the shared base rather than per model was the measured choice: wiring only Whisper would have left 103 models with a `?? false` that does nothing. OnnxOptions fell back to `new OnnxModelOptions()` instead of the declared property; AudioGen passed no options to any of its three ONNX sessions at all. Six ONNX model-path properties were DELETED: each duplicates a required constructor parameter, and a model file the caller must supply is an input rather than a tunable. Verified zero readers first -- with care, because CLAPModelOptions declares a TextEncoderPath of its own that IS read, so a name-only scan reports readers belonging to another class. DEFECT FORM (4): 129 FIELDS ACROSS 33 FILES One model whose two constructors describe it differently. ONNX constructors take an options parameter and then assign hardcoded literals to the same backing fields their native sibling reads from options, so a caller configuring that path had their settings silently discarded. ConvTasNet's ONNX ctor hardcoded 128/512/8/3/3 while its native ctor read all five from options. Every literal was compared against its declared default first: 128 agreed and were swapped behaviour-preservingly. Four did NOT and are deliberately left as literals -- DropRate = 0.0 in the ONNX inference constructors of Mask2Former, MixedQueryTransformer, OneFormer and XDecoder. Dropout must be off at inference, so applying the declared 0.1 there would be a real defect dressed as consistency. They are named in the new ratchet so a future sweep cannot absorb them silently. Every swapped line was checked to read _options AFTER it is assigned in the same constructor; a rewrite that puts the read above the assignment compiles clean and dereferences null. TWO NEW RATCHETS Both defect forms above previously had NO detector, which is why every instance was found by reading code. ConstructorAgreementRatchetTests reads SOURCE, breaking this directory's IL-only convention, and says so: after constant folding both assignments emit an equivalent store to the same field, so the distinction does not survive compilation. TabularDualImplementationTests was written to assert zero and found 49 divergences, so it became a ratchet instead. Every tabular model exists TWICE over one shared options class -- XNetwork (layers via LayerHelper) and XBase with XClassifier/XRegression derived -- and the two read different options. Curating an exclusion list until it passed would have hidden the finding and grown quietly in its place. CORRECTIONS TO a285790 NODEOptions.HiddenVectorActivation is restored. It was deleted there on the claim that NODE is a tree ensemble with no hidden activation -- true of CreateDefaultNODELayers, false of NODEBase, which builds a feature-preprocessing FullyConnectedLayer with one. That deletion came from surveying only XNetwork. The same omission explains the NODEOptions.HiddenActivation "detector anomaly" recorded at UnreadBaseline: NODEBase reads it. The detector was right. HiddenVectorActivation is now wired at all ten sites in the Base family, via a new FullyConnectedLayer.WithVectorActivation factory -- an overload taking IVectorActivationFunction beside the nullable IActivationFunction makes every existing `new FullyConnectedLayer<T>(a, b, null)` ambiguous, which is why DenseLayer already documents the factory convention. Verified: src and test projects build clean; 426 tests pass across Tabular and Configuration (282 guards); 1,123 pass across Audio and Concerto. Sweeps use CI's own exclusions (Category!=HeavyTimeout&Category!=ModelPerformanceCensus) -- StableAudioModelTests is tagged HeavyTimeout precisely because it exceeds the 120s gate in isolation, and an earlier sweep of mine swept it in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…read properties (#2090) UnreadBaseline 50 -> 46 FidelityBaseline new, 0 (declared default vs its own documented default) PAPER FIDELITY, THE TRACTABLE HALF The backlog called for docs/model-paper-defaults.tsv plus a [PaperDefaults] attribute. That needs a sourced published value per model, and a citation nobody checked is indistinguishable from a correct one -- inventing them would be worse than having no table. What can be verified with no external knowledge is whether the code agrees with the claim it already makes. 2,237 options properties state their default in their own <value> doc, 124 of them carrying an explicit arXiv or et-al citation, and exactly two disagreed: LocallyWeightedRegressionOptions.Bandwidth documented 1.0 and declared 0.0. The CODE is right: 0.0 is a deliberate sentinel selecting Cleveland and Devlin's adaptive span, which the model itself calls "an escape hatch". The doc predated that and is corrected. AffinityPropagationOptions.Damping documented 0.5 and declared 0.8. 0.5 matches its own doc, scikit-learn, and Frey & Dueck, so the value is changed. This alters clustering results for anyone relying on the default -- calling it out rather than slipping it in. DocumentedDefaultsFidelityTests reads source, like the constructor-agreement guard and unlike the rest of the directory: doc comments do not survive compilation. Its extraction was narrowed twice -- a first pass taking the first number anywhere in the value text reported 97 mismatches, nearly all range descriptions ("between 0 and 1, defaulting to 0.3"), and three of the five survivors were "10,000" read as "10". DEPTHANYTHINGV2 PATCHSIZE: NOT A CONSTANT EDIT The logged discrepancy (16 where DINOv2 is 14) turned out to rest on a second defect. CreateDefaultDepthAnythingV2Layers declared `int patchSize = 16;` as a LOCAL and never received the option, while the forward pass computed its token grid from _options.PatchSize -- the two agreed only because both happened to be 16, and changing the option alone would have left the patch embedding tokenising at 16 against a 14-based grid. That shadowing is fixed here (the option is threaded through, the default unchanged, so behaviour is identical). The value is deliberately NOT changed: 14 divides neither dimension of the default 480x640 input, so aligning with the DINOv2 backbone is a resolution decision -- DINOv2 uses 518 because 518 = 14 x 37 -- rather than a one-line fix. FOUR MORE UNREAD PROPERTIES, ALL WRITE-ONLY SpeakerVerifier and SpeakerEmbeddingExtractor each assigned `_options.X = modelPath` in the constructor and then used a local field, so the property existed only to be written to. One carried a comment explaining that the options object was copied first so the write could not mutate the caller's -- careful handling of a value nobody reads. Whisper's Encoder/DecoderModelPath are in this count twice over. They were deleted in 8178f54, then silently restored when a `git checkout --` recovered WhisperOptions.cs from a bad regex edit, and that revert reached the commit. Nothing noticed until the ratchet listed them again. Recovering one file from HEAD undoes every earlier edit to it, not only the damage being reverted. Verified: build clean; 283 Configuration guards pass; 1,560 pass across Configuration, Speaker, Clustering and LocallyWeighted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 40) (#2090) Applying the rule this branch has used throughout: WIRE a declared value if the architecture can honour it, DELETE it if the architecture has no mechanism for it. Each disposition was checked against the layer that would have to implement it, not assumed. WIRED FTTransformerOptions.LayerNormEpsilon -- LayerNormalizationLayer has a (featureSize, epsilon) overload, and FTTransformerBase was calling the size-only one. TabDPTOptions.UseLayerNorm -- the paper makes the final norm optional, so that is where the switch belongs. Wired in BOTH implementations. DELETED FTTransformerOptions.AttentionDropoutRate, .ResidualDropoutRate and SAINTOptions.AttentionDropoutRate -- all three route through TransformerEncoderLayer, which takes (numHeads, feedForwardDim) and implements no dropout at all. There is no stage to configure, so the properties advertised a granularity the architecture does not have. The single DropoutRate each model already reads is honoured. AutoIntOptions.UseLayerNorm -- neither AutoIntNetwork nor AutoIntBase constructs a LayerNormalizationLayer anywhere. THE NEW GUARD CAUGHT MY OWN CHANGE Wiring TabDPT's UseLayerNorm into only the estimator path pushed TabularDualImplementationTests from 49 to 50 inside this same batch -- the dual-implementation ratchet detecting, in a fresh change, exactly the defect it was added for a commit earlier. The LayerHelper path was wired too and the count returned to 49. Worth recording because the guard was speculative when written; this is its first real catch. Verified: build clean; 427 tests pass across Configuration and Tabular. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three guards added on this branch used .NET Core-only overloads and had only ever been compiled for net10.0, so a full multi-target build failed with five errors the moment one was run: double.IsFinite -> !IsNaN && !IsInfinity string.Replace(string, string, comparison) -> the 2-arg overload (ordinal) string.Contains(char, comparison) -> IndexOf(char) >= 0 Only these three forms lack a polyfill in this repo; string.Contains(string, comparison) resolves fine on net471, which is why the string call sites a few lines above the failing ones were not flagged and did not need changing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Applying this branch's rule -- WIRE a declared value if the architecture can
honour it, DELETE it if the architecture has no mechanism for it:
TabROptions.ActivationType string-typed, and CLAUDE.md bans strings for a
TabMOptions.ActivationType closed set of choices. Neither model has an
activation-selection mechanism to point an enum
at either, so this is a deletion, not a port.
TabROptions.UseFiLM no FiLM modulation stage exists in TabR.
TabPFNOptions.MaxFeatures both models size themselves from the data they
TabDPTOptions.MaxFeatures are fitted to; a declared cap had no reader.
TabPFNOptions.NumEnsembles UseEnsemble is read, the member count is not.
Copy-constructor and Clone() references are removed with each declaration, so
nothing is left assigning a property that no longer exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleven declared-but-unread properties across the tabular generators. Ten are
wirings; one was a missing implementation and one a deletion.
WIRED
GOGGLE.KLWeight the ELBO's KL term entered the loss at weight 1.0
while the options declared 0.1. It is the beta of a
beta-VAE and the sibling of SparsityWeight and
StructureWeight, which were already read the same way.
GOGGLE.BatchSize Fit walked rows one at a time. Minibatching follows
TabTransformerGen. TVAE, the closest analogue; 19 of 32 generators in
BatchSize this folder already read BatchSize.
MedGAN.BatchNormDecay the generator's BatchNorms took the layer's 0.9
default while the options declared the paper's 0.99.
TabFlow.Sigma the conditional probability path was built with sigma
implicitly 0 (Lipman et al. 2023, Eq. 22).
AutoDiffTab. was `string BetaSchedule = "linear"`, which CLAUDE.md
BetaSchedule bans for a closed set. AiDotNet.Enums.BetaSchedule
already existed for exactly this and is used by
DiffusionModelOptions. The model SEARCHES schedules,
so the option seeds trial 0 -- the relationship
MLPDimensions already has with the width search and
MaxTimesteps with the timestep draw. ScaledLinear was
implemented so the enum is exhaustive.
TabLLMGen / all three build transformer FFN stacks with no dropout
REaLTabFormer / stage. Each gets residual dropout in its own list
TabTransformerGen rather than in Layers, because all three index Layers
.DropoutRate as strict FFN pairs and DropoutLayer has no
parameters. TabTransformerGen's rehydration path
recreates them for the same reason.
IMPLEMENTED
OCTGAN.GradientPenaltyWeight -- the class summary has always advertised
"WGAN-GP training", its `#region Gradient Penalty` stood empty, and the
critic was kept Lipschitz by weight clipping against a hardcoded
GanClip = 0.01. The penalty is now computed against the double-backprop
pattern this repo already uses in WGANGP.TrainCriticBatchWithGP -- an inner
tape with createGraph: true, without which the norm is constant with respect
to every parameter and contributes nothing while still appearing to run.
Weight clipping is removed rather than kept alongside it: Gulrajani et al.
2017 replace clipping with the penalty.
DELETED
TimeGANOptions.NumFeatures -- declared `public new int`, shadowing
RiskModelOptions.NumFeatures. TimeGAN reads its width off the data (Fit sets
it from data.Columns) so a configured count had no reader, and the `new`
meant options.NumFeatures returned 5 while
((RiskModelOptions<T>)options).NumFeatures returned 10 for one object.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ProgressiveGAN -- InitialLearningRate and LearningRateDecay were declared on ProgressiveGANOptions and both optimizers were built from private consts instead (DefaultLearningRate, DefaultLearningRateDecay). The consts and the options agree on value, so this is behaviour-preserving by default and now honours an override. CreateAdamOptimizerOptions gains a decay slot, defaulted so the five other GANs calling it are untouched; GradientBasedOptimizerBase already reads options.LearningRateDecay. LiquidStateMachine -- ReadoutLearningRate was declared and the Adam optimizer was built BARE, so the optimizer used its own default and the declared rate reached nothing. This is #2090's defect form (5), and it bites hardest here: in an LSM the reservoir is fixed and the readout is the only trained part, so that rate is the whole of what training responds to. MatryoshkaEmbedding -- the nesting ladder was a defaulted constructor parameter whose default was a literal array in the model, which is defect form (1): a tunable living outside Options. It moves to MatryoshkaEmbeddingOptions.NestedDimensions, which finally gives MaxEmbeddingDimension something to bound -- a new Validate() rejects a nesting width wider than the embedding it would be sliced from. TransformerEmbeddingOptions.Validate becomes virtual so that override is a real override. Hiding it with `new` was the alternative and is the same shadowing trap removed from TimeGANOptions.NumFeatures in the previous commit: callers holding the base type would silently run the base checks only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…0 -> 20 (#2090) The scan skips options classes deliberately -- a copy constructor assigning every property proves nothing about whether a MODEL reads them -- and carries consumption back through an edge: a computed property that is read from outside consumes whatever its getter reads. That edge was restricted to property GETTERS, so a helper METHOD did not carry it. SpeechEmotionRecognizer reads its labels through _options.GetEffectiveEmotionLabels(), which applies the documented null fallback, and EmotionLabels was reported unread while changing it demonstrably changes the model's ClassLabels. Same shape as the generic-token bug this scan was repaired for once already: a confident, silent "no". Extending the edge to methods required BulkReaders in the same change. ToString, Equals, GetHashCode, Clone and Validate read every property they can reach without any of it being consumed, and one of them being called from outside would otherwise have marked an entire class consumed. Getting that list wrong deflates the count, which is the failure mode the whole guard exists to prevent. The measured move was 21 -> 20, not 21 -> 3, which is the evidence it is right. Baseline 40 -> 20: nineteen from the preceding three commits, one from this fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2090) WIRED AudioVisualEventLocalization.LearningRate both optimizers were built BARE, so LiquidStateMachine.ReadoutLearningRate the declared rate reached nothing. In an LSM the reservoir is fixed and the readout is the only trained part, so that rate is the whole of what training responds to. Finch.MinLearningRate GradientBasedOptimizerOptions already declares a MinLearningRate that OptimizerBase clamps CurrentLearningRate against, so the declared floor lands on the optimizer's own knob. Mambular.DeltaMin/Max Mamba's dt_init. The dt bias was a hardcoded 0.01 in BOTH MambaBlock (the shared layer) and MambularBase -- and 0.01 is the geometric mean of the declared [0.001, 0.1], i.e. a single point in the middle of the interval the paper spreads dt across. Now drawn log-uniformly and stored as its inverse softplus, since the forward pass applies softplus to it. Concerto.ImagesPerPointCloud Concerto does not sample views (the caller supplies them), so holding the caller to the configured pairing count is the one thing the model can do with it -- alongside the two validators Pretrain already had. DELETED, each with the missing mechanism named at the declaration site Whisper.WordTimestamps duplicate of the shared ReturnTimestamps; and TranscriptionSegment<T> has no per-word shape for word granularity to be returned in. CLAPModel.DropoutRate both stacks are TransformerEncoderLayer, which has no dropout stage. The unblock is recorded at the site: one dropout sublayer on that layer would serve CLAP, FTTransformer and SAINT, and is separate work because its hand-written Backward feeds 108 call sites. CopulaSynth (all three) the generator reads nothing off its options but Seed; its marginals are the sorted observed values, so there is no KDE for NumKDEPoints or BandwidthMultiplier, and CopulaType was a string whose own docs listed one valid value. TabNet.CategoricalEmbeddingDimension no categorical path exists anywhere. NODE.FeatureSelectionDimension entmax is taken over a direct [TreeDepth, NumFeatures] matrix; no hidden projection exists to size. NODE.MLPHiddenDimensions both heads are a single linear projection. GANDALF.UseFeatureGating/ each offers to switch off the mechanism the UseResidualGating architecture is named for; neither GANDALFBase nor GandalfGFLULayer has an ungated path. Matryoshka.MaxEmbeddingDimension duplicated the inherited EmbeddingDimension. TWO LATENT DEFECTS SURFACED, neither of them an unread property MatryoshkaEmbeddingOptions assigned 1536 to MaxEmbeddingDimension -- which nothing read -- and left the inherited EmbeddingDimension at its 768 default. And the model passed the RAW nullable options to its base while separately defaulting it, so with no options supplied the base built a TransformerEmbeddingOptions while the derived class built a MatryoshkaEmbeddingOptions: two objects, and the one that sizes every layer and bounds EmbedResized is the base's. A model documented and tested as 1536 wide was built 768 wide, and three MatryoshkaEmbeddingTests had been failing on exactly that. Both fixed by materializing the options once and handing the same instance to the base; all 32 of those tests now pass. The dual-implementation guard caught this change mid-flight: wiring DeltaMin/Max into MambularBase alone pushed divergences 49 -> 51, so MambularNetwork's LayerHelper path was wired too and it returned to 49. 844 Mamba/SSM tests pass with the corrected dt_init. What remains at 3 is not wiring but three properties that each need a subsystem built, named in the ratchet's own remarks: TabNet's two pretraining options (its self-supervised stage needs a decoder this implementation does not have) and SpikingNeuralNetwork.StdpWindow (the rule it bounds lives in CalculateSTDPWeightChange, which nothing calls). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#2090) PretrainStdp was written and backed out. The rule and the spike histories are both there, and an unsupervised entry point joining them is straightforward -- extract the simulation out of Train, present unlabelled stimuli, pair each layer's spike train with its predecessor's. Where to WRITE the result is not straightforward, and that is the finding. The supervised path addresses a layer's weights as post * preSize + pre into the flat vector from GetParameters. But a SpikingLayer stack of (6,5), (5,5) and a 3-wide readout reports 204 parameters where synapses and biases account for roughly 80, and running a simulation moves that vector by 1.5 with no weight update applied at all. The vector therefore carries neuron-model and membrane state alongside the synapses, so a plain flat index writes STDP deltas into the neuron model -- silently, and in a direction nothing would report. Shipping that would be worse than the unread property it fixes. The unblock is the addressing, not the rule: SpikingLayer needs to expose which span of its parameter vector is synaptic weight (or take a weight delta directly). The supervised path's indexing makes the identical assumption and should be checked against the same answer. Recorded at the ratchet constant so the next attempt starts from the blocker rather than rediscovering it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ooples
marked this pull request as ready for review
September 15, 2026 10:48
Ten conflicted files, resolved per the verified base-class contracts: - GLA/GatedDeltaNet/Griffin/Hawk/RecurrentGemma/Finch Options copy ctors: take master. The base chain already copies both members this branch re-copied -- ModelOptions copies Seed (and holds the ArgumentNullException guard), NeuralNetworkOptions copies EncoderLayerCount -- so the branch's lines were redundant and its `if (other is null) throw` unreachable, because `: base(other)` runs first. - FinchOptions second hunk: take master's EnableGradientClipping / MaxGradientNorm copies AND restore master's declarations for them. The merged property block kept this branch's Beta1/Beta2 shape and dropped master's clipping knobs, so master's assignments alone would not compile. The branch's other copies (MaxGradNorm, VocabSize, ModelDimension, NumLayers, NumHeads, MaxSequenceLength) are already done by ModelHyperparameterOptions and SequenceModelOptions. - FinchLanguageModel: keep this branch's _learningRate/_optimizer and its AdamW construction from the published recipe. FinchOptions declares every member it reads, and master simply does not have this feature. - OptionsSurfaceRatchetTests: take master (it adds a real assertion and the GapReport test) and add the `using Xunit.Abstractions;` master's CapturingOutput needs -- the file imported only Xunit. - PrivBayesDifferentialPrivacyTests: take master; it asserts the setter itself rejects the degenerate split. - AudioHyperparameterOptions.cs: keep master's file rather than this branch's deletion. Master's SharedOptionsDocumentationContractTests does typeof(AudioHyperparameterOptions), so the deletion would not compile. This one is a genuine intent clash and deserves the author's review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jgqTmscEnkgmAp1TkNFpG
… marked Merging master left this branch uncompilable in two ways that produced no conflict markers, so neither was visible from the merge itself. 1. VisionLanguageModelOptions' vision-tower members were renamed on master (VisionHiddenDim -> VisionDim, NumVisionLayers -> VisionLayers). That file did not conflict, so git took master's version silently while this branch's derived classes, their consumers, the tests and two generator scaffolds kept the old names -- 60 compile errors. Master and the phase-3 PR (#2130) already use the new names; only this branch carried the older snapshot. Master's own contract test asserts the old members are gone (SharedOptionsDocumentationContractTests: Assert.Null(vision.GetProperty("VisionHiddenDim"))). Renamed only what resolves to VisionLanguageModelOptions: five derived options classes, their five consumers, the affected test initializers, and the Flamingo/LLaVA scaffolds in TestScaffoldGenerator. Left untouched: the private fields and the serialized metadata keys ("VisionHiddenDim" / "NumVisionLayers") that master still uses, the other 70 generator sites, and every src/VisionLanguage/** model whose options declare their own NumVisionLayers. 2. InverseProblemPINN and MultiScalePINN moved their collocation-point counts onto options objects, but three call sites still passed them as constructor arguments (CS1739). Moved them onto InverseProblemOptions<double> / MultiScalePINNOptions and added the missing using for the latter. Verified: 60 -> 21 -> 72 -> 3 -> 0 errors, non-incremental build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jgqTmscEnkgmAp1TkNFpG
These write-ups should never have been committed. Removed here so the file does not arrive on master when this PR merges; .gitignore gains matching rules in #2224. Deliberately untouched: ci-proof/nonruntime-routing-canary.md, which is functional rather than a write-up (it exercises the permanent ci-proof/** workflow trigger). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYQycGHKxLFBqhEtPMHz29
…face-phase-7-audio-work # Conflicts: # src/Audio/Classification/AudioLDMClassifier.cs # src/Audio/Classification/AudioMAE.cs # src/Audio/Classification/AudioSep.cs # src/Audio/Classification/CLAP.cs # src/Audio/Classification/EAT.cs # src/Audio/Classification/HTSAT.cs # src/Audio/Effects/DemucsNoise.cs # src/Audio/Emotion/Emotion2Vec.cs # src/Audio/Emotion/Wav2Small.cs # src/Audio/Enhancement/BandSplitRNNEnhancer.cs # src/Audio/Enhancement/FullSubNetPlus.cs # src/Audio/Enhancement/MPSENet.cs # src/Audio/Fingerprinting/ConformerFP.cs # src/Audio/Fingerprinting/NeuralFP.cs # src/Audio/Foundations/Data2Vec2.cs # src/Audio/Foundations/HuBERT.cs # src/Audio/Foundations/MERT.cs # src/Audio/Foundations/Wav2Vec2.cs # src/Audio/Foundations/WavLM.cs # src/Audio/Generation/EnCodec.cs # src/Audio/Generation/VALLE.cs # src/Audio/Multimodal/AudioFlamingo2.cs # src/Audio/Multimodal/Pengi.cs # src/Audio/MusicAnalysis/BasicPitch.cs # src/Audio/MusicAnalysis/CREPE.cs # src/Audio/SourceSeparation/BandSplitRNN.cs # src/Audio/SourceSeparation/DannaSep.cs # src/Audio/SourceSeparation/HTDemucs.cs # src/Audio/Speaker/CAMPlusPlus.cs # src/Audio/Speaker/ECAPATDNNSpeaker.cs # src/Audio/Speaker/PyAnnote.cs # src/Audio/Speaker/TitaNet.cs # src/Audio/SpeechRecognition/Conformer.cs # src/Audio/SpeechRecognition/RNNTransducer.cs # src/Audio/TextToSpeech/StyleTTS2.cs # src/Audio/VoiceActivity/MarbleNet.cs # src/ComputerVision/Segmentation/Foundation/SAM.cs # src/ComputerVision/Segmentation/PointCloud/Concerto.cs # src/ComputerVision/Segmentation/PointCloud/PointTransformerV3.cs # src/ComputerVision/Segmentation/PointCloud/Sonata.cs # src/Document/Analysis/PageSegmentation/DocBank.cs # src/Document/GraphBased/TRIE.cs # src/Document/LayoutAware/DiT.cs # src/Document/LayoutAware/LiLT.cs # src/Document/OCR/TextDetection/CRAFT.cs # src/Document/OCR/TextDetection/DBNet.cs # src/Document/OCR/TextDetection/EAST.cs # src/Document/OCR/TextRecognition/ABINet.cs # src/Document/OCR/TextRecognition/CRNN.cs # src/Document/OCR/TextRecognition/SVTR.cs # src/Document/PixelToSequence/Donut.cs # src/Document/PixelToSequence/Nougat.cs # src/Document/VisionLanguage/InfographicVQA.cs # src/Finance/Forecasting/Neural/DeepAR.cs # src/Finance/Forecasting/Neural/DeepFactor.cs # src/Finance/Forecasting/Neural/TCN.cs # src/Finance/Forecasting/Transformers/Crossformer.cs # src/Finance/Forecasting/Transformers/ETSformer.cs # src/Finance/Forecasting/Transformers/ITransformer.cs # src/Finance/Forecasting/Transformers/Informer.cs # src/Finance/Forecasting/Transformers/NonStationaryTransformer.cs # src/Finance/Forecasting/Transformers/TSMixer.cs # src/Finance/Forecasting/Transformers/TimesNet.cs # src/NeuralNetworks/Blip2NeuralNetwork.cs # src/NeuralNetworks/GRUNeuralNetwork.cs # src/NeuralNetworks/HyperbolicNeuralNetwork.cs # src/NeuralNetworks/LLaVANeuralNetwork.cs # src/NeuralNetworks/MeshCNN.cs # src/NeuralNetworks/ResidualNeuralNetwork.cs # src/NeuralNetworks/SparseNeuralNetwork.cs # src/NeuralNetworks/SyntheticData/TabTransformerGenGenerator.cs # src/NeuralNetworks/Tabular/TabMNetwork.cs # src/NeuralNetworks/Tabular/TabRNetwork.cs # src/PhysicsInformed/NeuralOperators/DeepOperatorNetwork.cs # src/PhysicsInformed/PINNs/VariationalPINN.cs # src/TextToSpeech/FlowDiffusion/MatchaTTS.cs # src/TextToSpeech/StyleEmotion/StyleTTS2.cs # src/Video/Depth/DepthAnythingV2.cs # src/Video/Enhancement/BasicVSR.cs # src/Video/Enhancement/DAMVSR.cs # src/Video/Enhancement/DOVE.cs # src/Video/Enhancement/FlashVSR.cs # src/Video/Enhancement/IconVSR.cs # src/Video/Enhancement/MIAVSR.cs # src/Video/Enhancement/PSRT.cs # src/Video/Enhancement/RVRT.cs # src/Video/Enhancement/RealESRGANVideo.cs # src/Video/Enhancement/RealisVSR.cs # src/Video/Enhancement/StableVideoSR.cs # src/Video/FrameInterpolation/ABME.cs # src/Video/FrameInterpolation/AMT.cs # src/Video/FrameInterpolation/BiMVFI.cs # src/Video/FrameInterpolation/DynamiCrafter.cs # src/Video/FrameInterpolation/EMAVFI.cs # src/Video/FrameInterpolation/FLAVR.cs # src/Video/FrameInterpolation/GIMMVFI.cs # src/Video/FrameInterpolation/IFRNet.cs # src/Video/FrameInterpolation/M2M.cs # src/Video/FrameInterpolation/MoMo.cs # src/Video/FrameInterpolation/PerVFI.cs # src/Video/FrameInterpolation/STMFNet.cs # src/Video/FrameInterpolation/TLBVFI.cs # src/VisionLanguage/Encoders/SAM.cs # tests/AiDotNet.Tests/IntegrationTests/Document/DocumentAnalysisTests.cs # tests/AiDotNet.Tests/IntegrationTests/Document/LayoutAwareDocumentTests.cs # tests/AiDotNet.Tests/IntegrationTests/Document/OCRTextDetectionTests.cs # tests/AiDotNet.Tests/IntegrationTests/Document/OCRTextRecognitionTests.cs # tests/AiDotNet.Tests/IntegrationTests/Document/PixelToSequenceDocumentTests.cs # tests/AiDotNet.Tests/IntegrationTests/Document/VisionLanguageDocumentTests.cs
This branch was successfully deployed
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.
Progress on #2090: model
Optionsclasses exist, but models do not read them — tunable values live in constructor parameters instead.This is a draft opened mid-flight so CI runs and the work is reviewable as it lands. It is not complete; see Remaining work below.
Ratchets
Baseline(name-credited constructor params)ConstructorBaseline(strict constructor params)UnreadBaseline(declared-but-unread properties)AgreementBaseline(constructors disagreeing about an option)DivergenceBaseline(a model's two implementations reading different options)UncoveredBaseline(options classes with noValidate())The "floor of 49" this table carried for several phases was wrong. It was an estimate of
how many parameters legitimately have no default and so must stay parameters, and the live
measurement went straight past it. The floor is 0 for both constructor ratchets.
Both constructor ratchets are now at 0, and two defect forms that previously had no detector
at all now have one. That does not close #2090.
AgreementBaseline's floor is 4, not 0, and the four are named in the guard:DropRate = 0.0in the ONNX inference constructors of Mask2Former, MixedQueryTransformer, OneFormer and
XDecoder. Dropout must be off at inference, so driving that count to zero would introduce
dropout into inference in four models while appearing to improve consistency. It credits a name match, so it is satisfied by
a property existing and says nothing about whether the constructor reads it. See Remaining
work.
The unread ratchet rose because the detector twice stopped deceiving itself, not because anything regressed — see The detector was measuring the wrong thing, twice below.
What landed
Reservoir and volumetric models (
79b8695d9b) — EchoStateNetwork, LiquidStateMachine, VoxelCNN, UNet3D onto two new family bases. Reservoir size deliberately stays a constructor argument: it has no default on either model, so moving it would mean inventing a published value that does not exist.Speaker recognition (
be8761d34b) — SpeakerEmbeddingExtractor and SpeakerVerifier onto a newSpeakerRecognitionOptionsbase. Three defect forms closed at once, each with a different correct disposition:FftSize/HopLength/NumMfccwere declared but never reached the MFCC front end, which always built from its own defaults — so settingNumMfccchanged the first layer's width while the front end kept emitting 40 coefficients. The two halves of the model could silently disagree.SpeakerVerifierOptionswas referenced by nothing but the clone registry. ItsVerificationThreshold = 0.7was never applied; the model hardcoded 0.6. Both thresholds now default to 0.6 — the value actually in force — and both are read.numHeadswas deleted, not moved. x-vector (Snyder et al. 2018) is a TDNN with statistics pooling and has no attention;CreateDefaultSpeakerEmbeddingLayershas no head count to pass it to. Moving it would have preserved exactly the false configurability this issue exists to remove.SpeechEmotionRecognizer (
8b045d7153) — 11 shadowing parameters and no options class at all. Its two constructors also disagreed about the model: the ONNX path hardcoded 4 convolutional blocks while the native path used 3 and cited the paper for three, and those fields are read back byGetModelMetadata.Forecaster transformers (
ba7ad0353a,9cbb619918) — FEDformer, PatchTST, ITransformer, native and ONNX constructors, plusFinancialModelFactory, which was discarding AutoML's tuned parameters for PatchTST and ITransformer. The purest form of the defect: the options classes were already complete with identical defaults, and each constructor stored the options and then read every value from a shadowing parameter.Neural fields (
eaa56ae534,d72686da53) — NeRF, and InstantNGP whose parallel scalar convenience overload was deleted outright rather than migrated: it existed only to build an options object and chain.The document family, complete (
b83f72ffd3…7aab354dbb) — ABINet, TrOCR, DocBank, PSENet, DBNet, EAST, CRNN, TableTransformer. Four had a barenew AdamOptimizer<T, Tensor<T>, Tensor<T>>(this), so no configured learning rate could reach the model at all; each options class gained aLearningRatedefaulting to Adam's own 1e-3 so behaviour is unchanged. EAST'sgeometryTypewas a string drivinggeometryType == "QUAD" ? 8 : 5— a typo silently built the wrong-shaped geometry head, with no error anywhere. It is nowEASTGeometryType.VisionTransformer (
04a5ee316a) —hiddenDim,numLayers,numHeads,mlpDimmoved;imageHeight/imageWidth/channels/patchSize/numClassesdeliberately kept as parameters, having no defaults to move.UNet3D: real skip connections (
9428b29933) — see below.The segmentation family, 62 models (
8fbd993835) — the largest single step:numClasses,dropRateand (for 32 of them)modelSize, against a newSegmentationModelOptions : ModelHyperparameterOptions. Both ratchets fell by the same 156, which is itself the finding — not one of those 62 options classes declared a property of a matching name, so none had ever drawn name credit.ModelSizeis deliberately not hoisted: each model names its variants with its own enum, so there is no common type to declare. 376 tests pass across the segmentation suites, 0 failures.The unread-options detector (
d914534260) — see below.The final tail, 29 model types (
bb0ae367e3) — NeuralNetworks, PhysicsInformed, Documentand UncertaintyQuantification, plus the duplicate-clipping cleanup the re-parenting exposed.
Both constructor ratchets converged here, which resolves the 21-point spread the ratchet's
own remarks had recorded as probable: those parameters were ones whose options property
already existed while the constructor still took them — the
UnifiedMultimodalNetworkshape —not a disagreement between the two detectors.
Five defects found while doing it, each of which would have survived the migration silently:
??after a??=un-does it._options = options ?? new XOptions()placed afteroptions ??= new XOptions()re-tests the parameter, and Roslyn joins both branches of the??, sooptionsbecomes possibly-null again. That produced 168CS8602across ~20 fileswhose coalesce was demonstrably already present at the top of the body — so the errors looked
exactly like the coalesce had never been applied. 69 residual
??sites removed.SVTROptions' copy constructor did not copyCharset, so the defensive copy silentlydiscarded a user-set charset while copying all 18 other properties.
HyperbolicNeuralNetworkOptions.Validate()could never pass. It calledRequire(Curvature), which demands a value greater than zero — but hyperbolic space is bydefinition negatively curved, so the published default of
-1.0failed its own validation.Range-checked instead. Its doc example was itself a Epic: model Options classes expose no hyperparameters — 285 of 1,670 are empty #2090 defect three times over: it set
Curvature = 1.0(which throws), named two properties the class does not declare, and calleda constructor overload that does not exist.
AdamOptimizer, so theirdeclared
WeightDecayreached nothing; Finch built no optimizer at all, leavingLearningRate,Beta1,Beta2andWeightDecayall inert. Now AdamW, whose learning-ratedefault is identical to Adam's, so the swap adds the decay and changes nothing else.
Clone()dropped the inheritedMaxGradNormon TabR, TabM, TabNet and FTTransformer.Survivable while each class had its own duplicate shadowing it; a live defect the moment the
duplicate went — the copy-constructor hazard this branch already documents, reached from the
opposite direction.
EnableGradientClippingandMaxGradientNormwere deleted from five options classes ratherthan wired:
MaxGradNormon the shared base already documents zero-or-negative as "off", so theseparate boolean expressed no state the single double could not. TabNet's published bound of 2.0
moved onto
MaxGradNormin a new parameterless constructor rather than being dropped with theduplicate.
49 test call sites moved from named constructor arguments to options object initializers, driven
from each constructor's accepted-parameter set rather than from the compiler's error list — the
compiler reports only the first offending argument per call, so the error list would have
needed one build per argument.
Two ratchet history blocks were also found sitting outside their
</remarks>, which thisrepo's test project compiles silently.
Concerto self-supervised pretraining (
8178f54a6a) — the 10ConcertoOptionspropertiesconfigured a pretraining pipeline whose parts all existed (two objectives, a paired-view type, a
teacher EMA) and referenced only each other, with no orchestrator. A closed ring of
mutually-referencing types is the signature of an unimplemented subsystem, which makes the
decision implement or remove a feature, not wire a property.
Both objectives computed in raw
doubleand returned a bare scalar, so they carried nogradient graph — a
Pretrainbuilt on them as written would have run, reported a fallingnumber, and left every weight untouched. They were reimplemented in engine operations first,
per this repo's contract that a loss defines its forward math once and
GradientTapesuppliesthe backward. The test asserts the loss falls and the parameters move; a "does it run"
assertion passes against the broken version, which is why it is not the one used.
The audio cluster, 11 properties (
8178f54a6a) —ReturnTimestampsnow lives once onAudioNeuralNetworkOptionswith a resolver on the shared base. All 104ISpeechRecognizerimplementations already branched on the flag, but it arrived only as a method parameter
defaulting to false, so no model could be configured to return timestamps. Declaring it on the
shared base was the measured choice: wiring only Whisper would have left 103 models with a
?? falsethat does nothing.OnnxOptionsfell back tonew OnnxModelOptions()rather than thedeclared property — AudioGen passed no options to any of its three ONNX sessions at all. Six
ONNX model-path properties were deleted: each duplicates a required constructor parameter,
and a file the caller must supply is an input rather than a tunable.
Defect form (4), 129 fields across 33 files (
8178f54a6a) — one model whose twoconstructors describe it differently. The ONNX constructors take an options parameter and then
assign hardcoded literals to the same backing fields their native sibling reads from options, so
a caller configuring that path had their settings silently discarded. ConvTasNet's ONNX
constructor hardcoded 128/512/8/3/3 while its native one read all five from options. Every
literal was compared against its declared default first: 128 agreed and were swapped
behaviour-preservingly, and the four that did not are the deliberate inference cases above.
Every swapped line was also checked to read
_optionsafter it is assigned in thatconstructor — a rewrite that puts the read above the assignment compiles clean and dereferences
null.
The last 21 constructor parameters (
88e21d19b8) — 13 model types, closing the strictmeasure. They reduced to exactly two shapes, both of which read as working configurability:
and options with
_numFeatures = numFeatures > 0 ? numFeatures : _options.NumFeatures;— sothe options value applied only when a caller passed zero or less, and the parameter's own
default of 1 shadowed it for everyone else. Each type's other constructor already read the
options directly; the two now agree.
convenience constructors forwarding scalars into an options object initializer, applying the
parameter copy last.
All 11 defaults in the second shape were checked against their options property and matched, so
the removals change no behaviour. Parameters with no default (
numClasses,samplingRates,searchRadii,mlpDimensions) and collaborators stayed: a parameter with no default is arequired input, not a duplicated value. CRAFT separately inherited
ImageSize/BackboneChannelsbut left both unset, so its options object reported 0 while the model ran at768/512.
SGPT was the last model carrying
[ModelDimensionRole], andTestScaffoldGeneratoremittedits embedding-dimension / head-count divisibility test purely from those constructor parameters,
returning early on zero candidates — so migrating SGPT would have switched the feature off
silently. The attribute is retargeted to properties and the generator now reads values off a
default-constructed options instance, which is stronger than what it replaces: a parameter
default is a compile-time constant, whereas this exercises whatever the parameterless
constructor actually assigns.
That retarget caused a regression the same commit fixes, and it is the most transferable lesson
here: the properties are declared on
EmbeddingModelOptions, so every descendant inheritedthe annotation — including Word2Vec, GloVe and FastText, which have no attention, never assign
NumHeads, and failed a divisibility check against 0. An attribute that states a role is safeto inherit from a family base; one a generator uses to assert an invariant is not. Candidates
are now gated on
TransformerEmbeddingOptions, whose seven descendants are exactly the modelsthe invariant covers — still 7× the single-model coverage the feature had before. Skipping types
whose
NumHeadsis 0 was rejected: it passes, but makes the test vacuous for any attention modelthat forgot to set it, which is the defect worth catching.
Two bugs this work exposed
Both were latent and unreachable because the options went unread. Neither was caused by the migration — each was confirmed by stashing to a clean tree at HEAD and re-running.
UNet3D emitted a bit-identical output for every input.
CreateDefaultUNet3DLayerschose its output activation fromnumClassesalone and ignoredarchitecture.TaskType, putting a bounded (0, 1) sigmoid head on a regression task whose targets span [-1, 1]. Chasing a target of -1 drives the pre-activation past about -88, where the float32 sigmoid underflows to exactly zero for every input.CreateDefaultVoxelCNNLayers, thirty lines above in the same file, already selected on task type — the UNet3D helper was the outlier.ITransformer threw on non-contiguous tensor views.
FinanceModelTestFactory.NormalizeOptionsshrinks the options it passes (SequenceLength 8, PredictionHorizon 4, ModelDimension 24), and ITransformer had been ignoring all of it, running at its hardcoded 96/96/512. Once it honours those values the autoregressive path produces sliced views, and six methods reading.Dataelement-wise throw.TimeMachinein the same folder already guards this withx.IsContiguous ? x : x.Contiguous(); that idiom is applied.UNet3D had no skip connections at all
CreateDefaultUNet3DLayersdocumented them and did not build them ("This implementation does NOT actually perform the concatenation"), so the decoder never saw encoder detail and the memorization test plateaued at exactly the variance of its target — the loss a constant predictor achieves. A 10× learning-rate probe moved it from 0.335760 to 0.335760, identical to six decimals over 26 steps, which ruled out slow optimization.This was twice reported as blocked on the grounds that
IEnginehas noConvTranspose3D. That verdict was wrong both times: the obstacle was real, the conclusion was not. The paper's 2×2×2 up-convolution (Çiçek et al. 2016) halves channels while doubling resolution, and forstride == kernelSizea transposed convolution is exactly a 1×1×1 convolution toC_out · stride³channels followed by depth-to-space — the sub-pixel identity of Shi et al. 2016.Conv3DTransposeLayeris built from that identity, and the skip path is three coordinated pieces: encoder taps concatenated viaEngine.TensorConcatenate,ForwardForTraining => Forward, and aResolveLazyLayerShapesoverride that runs one inference-mode forward because the sequential base resolver cannot model a concatenation. 29/29 green.The detector was measuring the wrong thing, twice
UnreadOptionsRatchetTestsasks whether any method in the assembly calls a property's getter. Every one of these options classes carries a copy constructor, andTeacherMomentum = other.TeacherMomentum;is such a call — so having a copy constructor removed a class's entire property set from the detector's reach.The discriminator is exact:
MatryoshkaEmbeddingOptions.MaxEmbeddingDimensionwas one of the three properties the ratchet could see, and it is visible only because that class has no copy constructor.ConcertoOptions' ten never-read properties were invisible only because it has one.Ignoring call sites declared on a
ModelHyperparameterOptionssubclass takes the count from 3 to 35 against 748 declared getters. Nothing became unread; 32 properties that were already unread stopped being hidden. Largest:ConcertoOptions10 (an entire self-supervised pretraining configuration the model never consults),FinchOptions6 (Beta1,Beta2,WeightDecay,MinLearningRateand the clipping pair, none of which reach an optimizer),WhisperOptions5 andAudioGenOptions4 (ONNX component paths nothing loads).Second, it could not see generic options classes at all. The scan collected each getter's MethodDef token and compared it against raw IL call operands — but a call on a constructed generic type (
CTGANOptions<T>) emits a MemberRef token, which never equals it. The old remark claimed raw comparison was "immune" to generics; it was blind to them, silently. Since nearly every tabular and synthetic-data options class is generic, re-parentingFinancialNeuralNetworkOptionsappeared to expose 517 unread properties in one step. It had not:CTGANGeneratorreads_options.EmbeddingDimensionnine times,MedGANGeneratorreads_options.AutoencoderDimensionsthree,TabPFNNetworkreads_options.EmbeddingDimensionfive — every one reported unread.Call tokens now resolve with their enclosing method's generic context and are keyed on (declaring type definition, property name). Unresolvable tokens are counted and fail the test above a tenth of the total, and failures are deliberately not deduplicated — a token can fail under one generic context and resolve under another, so skipping the retry would rebuild the same silent "no" in a new shape.
The measured figure is 112 of 1236 declared getters. It was validated against a control rather than by looking better:
ConcertoOptionsis not generic and its ten unread properties were confirmed by grep beforehand, so it had to stay at ten — and did — while the suspected generic false positives moved.CTGANOptionsnow reports exactly one unread member,Epochs, which is precisely the one its model never reads.Also fixed here, unrelated but blocking
SQLitePCLRaw.lib.e_sqlite32.1.12 dropped thewin-arm(ARM32) runtime but its ownbuildTransitive/net461targets still declareruntimes\win-arm\native\e_sqlite3.dllasContent, so every .NET Framework build fails MSB3030 on a file the package does not contain (2.1.11 shipped it). The stale item is removed rather than pinning the native SQLite binary back a version. It has to be removed in both csproj files, because the package'sbuildTransitivetargets flow into every project downstream of it — fixing only the package owner left the test project failing identically.Corrections to earlier commit messages on this branch
Two commits from earlier work carry claims that are not true, recorded here so review does not rely on them:
6f492735cclaims it drops 8 duplicate_options.Validate()calls. It does not — that landed in75f2ff420.f0c8a7e98claims no other unread-LearningRateinstance existed. 63 did; the search behind that claim used[^>]*, which cannot match a C# generic type argument.d1b942f951(this branch, today) setsUnreadBaseline = 517and states that "482 of 488 newly-visible properties are read by NOBODY". Both are wrong, and78110b8974corrects them: the measured figure is 112. 517 was an artefact of this test comparing raw metadata tokens, which cannot match a call on a constructed generic type — so every generic options class reported as wholly unconsumed.CTGANGeneratorreads_options.EmbeddingDimensionnine times while the ratchet called it unread. Note the recurring shape: this is the same generic-type-argument blind spot asf0c8a7e98above, in a different tool. It has now defeated four separate scans in this issue.Remaining work
The live
ReportRemainingGapsdrives this list, not an earlier hand-written backlog — the two had diverged badly.What is left is not a backlog of wirings — each of the three needs a subsystem built, and
each is named at the ratchet constant with what that would take:
TabNetOptions.EnablePreTraining/PreTrainingMaskingRatio— TabNet's self-supervisedstage needs a decoder, and this implementation has none: encoder, attentive
transformers and one output layer are all that exist.
SpikingNeuralNetworkOptions.StdpWindow— attempted and backed out, deliberately. Therule and the spike histories are both present and an entry point joining them is easy;
where to WRITE the result is not. The supervised path indexes a layer's weights as
post * preSize + preintoGetParameters, but aSpikingLayerstack reports 204parameters where synapses and biases account for ~80, and running a simulation moves that
vector by 1.5 with no weight update applied. The vector carries neuron-model and membrane
state alongside the synapses, so a flat index writes STDP deltas into the neuron model,
silently. The unblock is the addressing, not the rule — and the supervised path makes the
same assumption and should be checked with it.
docs/model-paper-defaults.tsv, a[PaperDefaults]attribute and afidelity test. One discrepancy already logged: DepthAnythingV2
PatchSizeis 16 whereDINOv2's is 14.
FidelityBaselinealready covers the half of this that needs no externalsource: whether a declared default matches the default its own documentation states.
DivergenceBaseline(49) — every tabular model exists twice over one shared optionsclass (
XNetworkbuilding layers viaLayerHelper,XBasebuilding them inline withXClassifier/XRegressionderived), and the two read different options. The guard waswritten to assert zero and found 49, so it became a ratchet: curating an exclusion list until
it passed would have hidden the finding and grown quietly in its place. It earned its keep
this phase — wiring Mambular's
DeltaMin/DeltaMaxintoMambularBasealone pushed it49 → 51, so
MambularNetwork's LayerHelper path was wired too and it returned to 49.UncoveredBaseline(81) is a deliberate consequence rather than a backlog: re-parentingonto
ModelHyperparameterOptionsenrols a class in theValidate()-coverage guard, and thatguard's own comment records the precedent for raising it rather than adding
Validate()methods that check nothing.
The invisible surface item that stood here through every earlier phase is now partly closed:
two of its forms are ratcheted. What remains undetected is hardcoded literals shadowing an
option where the model has only ONE constructor, factories discarding tuned parameters, and doc
<example>blocks that could never compile — three of which this branch found only by reading.What the last phase found beyond the property count
Driving
UnreadBaseline112 → 3 surfaced four defects that are not unread properties, and arethe more interesting half of the result:
MatryoshkaEmbeddingwas built 768 wide while documented and tested as 1536. Its optionsassigned 1536 to
MaxEmbeddingDimension, which nothing read, leaving the inheritedEmbeddingDimensionat its 768 default — and the model handed the raw nullable options toits base while separately defaulting it, so with no options supplied the base built a
TransformerEmbeddingOptionsand the derived class aMatryoshkaEmbeddingOptions: twoobjects, and the one that sizes every layer was the base's. Three
MatryoshkaEmbeddingTestshad been failing on exactly this. Fixed; all 32 pass.
OCTGANadvertised "WGAN-GP training" with an empty#region Gradient Penalty, keepingits critic Lipschitz by weight clipping against a hardcoded
GanClip = 0.01. The penalty isimplemented against the repo's existing double-backprop pattern and the clipping removed,
because Gulrajani et al. replace clipping with the penalty rather than combining them.
TimeGANOptions.NumFeatureswaspublic new int, sooptions.NumFeaturesreturned 5while
((RiskModelOptions<T>)options).NumFeaturesreturned 10 for the same object.carries consumption to what it derives from — was restricted to property getters, so a
helper method did not carry it:
SpeechEmotionRecognizerreads its labels through_options.GetEffectiveEmotionLabels()andEmotionLabelswas reported unread while changingit demonstrably changes the model's
ClassLabels. Extending the edge to methods requiredexcluding
ToString/Equals/GetHashCode/Clone/Validatein the same change, since thoseread every property without consuming any and one outside call would have marked a whole class
consumed. The measured move was 21 → 20, not 21 → 3, which is the evidence that it did.
Also: the three guards added on this branch had only ever been compiled for
net10.0. A fullmulti-target build failed with five errors on
net471(double.IsFinite,string.Replace(_,_,comparison),string.Contains(char, comparison)) — fixed.🤖 Generated with Claude Code