Skip to content

feat(options): move model hyperparameters onto their Options classes (#2090) - #2158

Open
ooples wants to merge 73 commits into
masterfrom
feature/options-surface-phase-7-audio
Open

ooples wants to merge 73 commits into
masterfrom
feature/options-surface-phase-7-audio

Conversation

@ooples

@ooples ooples commented Sep 11, 2026 •

Copy link
Copy Markdown
Owner

Progress on #2090: model Options classes 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

Session start Now Floor
Baseline (name-credited constructor params) 350 0 0
ConstructorBaseline (strict constructor params) 461 0 0
UnreadBaseline (declared-but-unread properties) 3 (flattering) 3 0
AgreementBaseline (constructors disagreeing about an option) — 4 4
DivergenceBaseline (a model's two implementations reading different options) — 49 0
UncoveredBaseline (options classes with no Validate()) 14 81 —

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.0
in 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 new SpeakerRecognitionOptions base. Three defect forms closed at once, each with a different correct disposition:

  • FftSize/HopLength/NumMfcc were declared but never reached the MFCC front end, which always built from its own defaults — so setting NumMfcc changed the first layer's width while the front end kept emitting 40 coefficients. The two halves of the model could silently disagree.
  • SpeakerVerifierOptions was referenced by nothing but the clone registry. Its VerificationThreshold = 0.7 was never applied; the model hardcoded 0.6. Both thresholds now default to 0.6 — the value actually in force — and both are read.
  • numHeads was deleted, not moved. x-vector (Snyder et al. 2018) is a TDNN with statistics pooling and has no attention; CreateDefaultSpeakerEmbeddingLayers has 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 by GetModelMetadata.

Forecaster transformers (ba7ad0353a, 9cbb619918) — FEDformer, PatchTST, ITransformer, native and ONNX constructors, plus FinancialModelFactory, 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 bare new AdamOptimizer<T, Tensor<T>, Tensor<T>>(this), so no configured learning rate could reach the model at all; each options class gained a LearningRate defaulting to Adam's own 1e-3 so behaviour is unchanged. EAST's geometryType was a string driving geometryType == "QUAD" ? 8 : 5 — a typo silently built the wrong-shaped geometry head, with no error anywhere. It is now EASTGeometryType.

VisionTransformer (04a5ee316a) — hiddenDim, numLayers, numHeads, mlpDim moved; imageHeight/imageWidth/channels/patchSize/numClasses deliberately 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, dropRate and (for 32 of them) modelSize, against a new SegmentationModelOptions : 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. ModelSize is 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, Document
and 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 UnifiedMultimodalNetwork shape —
not a disagreement between the two detectors.

Five defects found while doing it, each of which would have survived the migration silently:

  • A ?? after a ??= un-does it. _options = options ?? new XOptions() placed after
    options ??= new XOptions() re-tests the parameter, and Roslyn joins both branches of the
    ??, so options becomes possibly-null again. That produced 168 CS8602 across ~20 files
    whose 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 copy Charset, so the defensive copy silently
    discarded a user-set charset while copying all 18 other properties.
  • HyperbolicNeuralNetworkOptions.Validate() could never pass. It 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. 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 called
    a constructor overload that does not exist.
  • Four bare optimizers. 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.
  • Clone() dropped the inherited MaxGradNorm on 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.

EnableGradientClipping and MaxGradientNorm were deleted from five options classes rather
than wired: MaxGradNorm on the shared base already documents zero-or-negative as "off", so the
separate boolean expressed no state the single double could not. TabNet's published bound of 2.0
moved onto MaxGradNorm in a new parameterless constructor rather than being dropped with the
duplicate.

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 this
repo's test project compiles silently.

Concerto self-supervised pretraining (8178f54a6a) — the 10 ConcertoOptions properties
configured 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 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. They were reimplemented in engine operations first,
per this repo's contract that a loss defines its forward math once and GradientTape supplies
the 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) — ReturnTimestamps now lives once on
AudioNeuralNetworkOptions with a resolver on the shared base. 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. Declaring it on the
shared base was the measured choice: wiring only Whisper would have left 103 models with a
?? false that does nothing. OnnxOptions fell back to new OnnxModelOptions() rather than 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 file the caller must supply is an input rather than a tunable.

Defect form (4), 129 fields across 33 files (8178f54a6a) — one model whose two
constructors 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 _options after it is assigned in that
constructor — 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 strict
measure. They reduced to exactly two shapes, both of which read as working configurability:

  • 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.
  • Six models (DGCNN, PointNet, PointNetPlusPlus, GaussianSplatting, MeshCNN, SpiralNet) had
    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 a
required input, not a duplicated value. CRAFT separately inherited ImageSize/
BackboneChannels but left both unset, so its options object reported 0 while the model ran at
768/512.

SGPT was the last model carrying [ModelDimensionRole], and TestScaffoldGenerator emitted
its 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 inherited
the 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 safe
to 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 models
the invariant covers — still 7× 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.

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. CreateDefaultUNet3DLayers chose its output activation from numClasses alone and ignored architecture.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.NormalizeOptions shrinks 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 .Data element-wise throw. TimeMachine in the same folder already guards this with x.IsContiguous ? x : x.Contiguous(); that idiom is applied.

UNet3D had no skip connections at all

CreateDefaultUNet3DLayers documented 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 IEngine has no ConvTranspose3D. 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 for stride == kernelSize a transposed convolution is exactly a 1×1×1 convolution to C_out · stride³ channels followed by depth-to-space — the sub-pixel identity of Shi et al. 2016. Conv3DTransposeLayer is built from that identity, and the skip path is three coordinated pieces: encoder taps concatenated via Engine.TensorConcatenate, ForwardForTraining => Forward, and a ResolveLazyLayerShapes override 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

UnreadOptionsRatchetTests asks whether any method in the 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.

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.

Ignoring call sites declared on a ModelHyperparameterOptions subclass takes the count from 3 to 35 against 748 declared getters. Nothing became unread; 32 properties that were already unread stopped being hidden. Largest: ConcertoOptions 10 (an entire self-supervised pretraining configuration the model never consults), FinchOptions 6 (Beta1, Beta2, WeightDecay, MinLearningRate and the clipping pair, none of which reach an optimizer), WhisperOptions 5 and AudioGenOptions 4 (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-parenting FinancialNeuralNetworkOptions appeared to expose 517 unread properties in one step. It had not: CTGANGenerator reads _options.EmbeddingDimension nine times, MedGANGenerator reads _options.AutoencoderDimensions three, TabPFNNetwork reads _options.EmbeddingDimension five — 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: ConcertoOptions is 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. CTGANOptions now reports exactly one unread member, Epochs, which is precisely the one its model never reads.

Also fixed here, unrelated but blocking

SQLitePCLRaw.lib.e_sqlite3 2.1.12 dropped the win-arm (ARM32) runtime but its own buildTransitive/net461 targets still declare runtimes\win-arm\native\e_sqlite3.dll as Content, 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's buildTransitive targets 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:

  • 6f492735c claims it drops 8 duplicate _options.Validate() calls. It does not — that landed in 75f2ff420.
  • f0c8a7e98 claims no other unread-LearningRate instance existed. 63 did; the search behind that claim used [^>]*, which cannot match a C# generic type argument.
  • d1b942f951 (this branch, today) sets UnreadBaseline = 517 and states that "482 of 488 newly-visible properties are read by NOBODY". Both are wrong, and 78110b8974 corrects 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. CTGANGenerator reads _options.EmbeddingDimension nine times while the ratchet called it unread. Note the recurring shape: this is the same generic-type-argument blind spot as f0c8a7e98 above, in a different tool. It has now defeated four separate scans in this issue.

Remaining work

The live ReportRemainingGaps drives this list, not an earlier hand-written backlog — the two had diverged badly.

  1. 3 declared-but-unread properties, down from 112 measured at the start of this branch.
    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-supervised
      stage 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. The
      rule 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 + pre into GetParameters, but a SpikingLayer stack reports 204
      parameters 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.
  2. Paper fidelity — docs/model-paper-defaults.tsv, a [PaperDefaults] attribute and a
    fidelity test. One discrepancy already logged: DepthAnythingV2 PatchSize is 16 where
    DINOv2's is 14. FidelityBaseline already covers the half of this that needs no external
    source: whether a declared default matches the default its own documentation states.
  3. DivergenceBaseline (49) — every tabular model exists twice over one shared options
    class (XNetwork building layers via LayerHelper, XBase building them inline with
    XClassifier/XRegression derived), and the two read different options. The guard was
    written 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/DeltaMax into MambularBase alone pushed it
    49 → 51, so MambularNetwork's LayerHelper path was wired too and it returned to 49.
  4. UncoveredBaseline (81) is a deliberate consequence rather than a backlog: re-parenting
    onto ModelHyperparameterOptions enrols a class in the Validate()-coverage guard, and that
    guard'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 UnreadBaseline 112 → 3 surfaced four defects that are not unread properties, and are
the more interesting half of the result:

  • MatryoshkaEmbedding was built 768 wide while documented and tested as 1536. Its options
    assigned 1536 to MaxEmbeddingDimension, which nothing read, leaving the inherited
    EmbeddingDimension at its 768 default — and the model handed the raw nullable options to
    its base while separately defaulting it, so with no options supplied the base built a
    TransformerEmbeddingOptions and the derived class a MatryoshkaEmbeddingOptions: two
    objects, and the one that sizes every layer was the base's. Three MatryoshkaEmbeddingTests
    had been failing on exactly this. Fixed; all 32 pass.
  • OCTGAN advertised "WGAN-GP training" with an empty #region Gradient Penalty, keeping
    its critic Lipschitz by weight clipping against a hardcoded GanClip = 0.01. The penalty is
    implemented 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.NumFeatures was public new int, so options.NumFeatures returned 5
    while ((RiskModelOptions<T>)options).NumFeatures returned 10 for the same object.
  • The unread scan had a false positive of its own. Its pass-through — a computed property
    carries consumption to what it derives from — was restricted to property getters, so a
    helper method did not carry it: SpeechEmotionRecognizer reads its labels through
    _options.GetEffectiveEmotionLabels() and EmotionLabels was reported unread while changing
    it demonstrably changes the model's ClassLabels. Extending the edge to methods required
    excluding ToString/Equals/GetHashCode/Clone/Validate in the same change, since those
    read 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 full
multi-target build failed with five errors on net471 (double.IsFinite,
string.Replace(_,_,comparison), string.Contains(char, comparison)) — fixed.

🤖 Generated with Claude Code

franklinic and others added 30 commits September 8, 2026 23:51
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>
franklinic and others added 4 commits September 11, 2026 15:43
…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>
franklinic and others added 5 commits September 11, 2026 22:47
…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>
franklinic and others added 7 commits September 14, 2026 10:53
…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>
franklinic and others added 2 commits September 14, 2026 15:37
…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
ooples marked this pull request as ready for review September 15, 2026 10:48
ooples and others added 3 commits September 16, 2026 12:44
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

2 active (1 outdated) deployments
Preview – aidotnet_website — 766f7859 Deployed Sep 24, 2026 by vercel[bot]
Preview – aidotnet-playground-api — 903d7589 Deployed Sep 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Epic: model Options classes expose no hyperparameters — 285 of 1,670 are empty

2 participants