src.tests.torf_training_tools_test
Classes
- class src.tests.torf_training_tools_test.TORFTrainingToolsTest
- Author:
Alberto M. Esmoris Pena
Test that the TORF NN supports the three standard VL3D training tools:
Transfer learning via the new
TORFTransferHandler, using the same JSON specification as the standardDLTransferHandler. Two configurations are covered: full transfer (no translator) and partial transfer withdefault_to_nullsemantics that resets a specific layer.Freezing layers during training driven by
DLFreezeTrainingExecutor, with weight invariance checks on the frozen layer and a counter-check that an unfrozen layer’s weights did change.Continue-training a previously serialized TORF model: a pickle round-trip preserves the NN weights, and a subsequent
train_nncall reuses the loaded handler instead of re-initializing the network.
Also includes a schedule-preparation parity check that runs the same fixtures as
FreezeTrainingTestagainst the new executor and confirms identical output, guaranteeing the refactor preserved the legacy behavior.- __init__()
Basic configuration for any VL3D test.
- Parameters:
name (str) – Test name
- run()
Run all subtests in sequence. Restores logging levels even on failure (matches the pattern used by
TransfOctoRFTest).
- check_executor_matches_legacy_schedule()
Replay one of the schedules from
FreezeTrainingTestthroughDLFreezeTrainingExecutor.compute_schedule()and confirm the returned(op_epoch, op, lrs)matches an explicit, hand-derived expected value. Also confirms that the wrapperDLTrainingHandler.prepare_freeze_training()is wired so it delegates to the executor (same result).- Returns:
True if the schedule preparation is correct.
- Return type:
bool
- check_executor_trailing_tail()
Regression test for the schedule bug where a
training_intervalwhose end-point is strictly less thantraining_epochswould cause the executor to silently skip the trailing unfrozen segment. DrivesDLFreezeTrainingExecutoron a mock NN and confirms that the recordedfit_fncalls cover ALL training epochs.- Returns:
True if the trailing tail is trained.
- Return type:
bool
- check_transfer_full()
Train a source TORF model (lazy, shared with
check_transfer_default_to_null()), save its NN to a.kerasfile, and train a target TORF model that transfers every layer from the source viann_transfer_weightswithdefault_to_null=falseand an empty translator (so the layer-name match is identity).Confirms a representative dense layer’s weights match the source after a zero-epoch training pass (the transfer happens inside the target’s
_fitflow before any optimizer step).- Returns:
True if the transferred weights match the source.
- Return type:
bool
- check_transfer_default_to_null()
Same as
check_transfer_full()but the translator maps theoutlayer toNoneso that target layer is not transferred. Confirms theoutlayer’s weights differ from the source while an arbitrary other transferred layer matches.Unlike
check_transfer_full(), this subtest bypasses the TORF preprocessing pipeline (RF training, KNN build, HDF5 IO) and invokes the transfer mechanism directly on a freshly built target architecture. The complementary subtestcheck_transfer_full()still exercises the full_fitwiring end-to-end, so the layer-translation behavior here is tested at unit level without losing wiring coverage.- Returns:
True if the partial transfer behaves as expected.
- Return type:
bool
- check_freeze_during_training()
Drive
DLFreezeTrainingExecutorwith a mock NN to confirm that:The requested layer’s
trainableflag is set toFalseat the start of the segment whose op marks it frozen.The flag remains
Falsefor the duration of that segment (i.e., the executor does not flip it back mid-segment).At the end of the schedule the executor unfreezes every layer (
trainable = True).
The contract that
trainable = Falseactually freezes the gradient update is a Keras invariant that we trust and do not need to re-test here. Driving with a mock NN avoids ~3-4 s of Keras training per run and keeps this subtest sub-millisecond.- Returns:
True if the executor mutates trainability as expected.
- Return type:
bool
- check_continue_training()
Pickle round-trip a trained TORF model and resume training: the loaded NN must be reused (not re-initialized) so the weights carry over across the boundary.
Skips the real 1-epoch fit (which alone costs ~500 ms-1 s through the full preprocessing + sequencer pipeline) and replaces it with a deterministic
set_weights()mutation that makes the snapshot distinguishable from the build-time random init. The contracts being tested — weights survive pickle andprepare_nn_handlerreuse path preserves them — do not depend on the snapshot being the product of training.- Returns:
True if the continue-training path reuses the loaded NN.
- Return type:
bool
- check_no_double_transfer_after_reload()
Regression test (minimal): a TORF model configured with
nn_transfer_weightsmust, after a pickle/unpickle round-trip, carry a transfer handler marked as already executed, so a subsequent_fitdoes NOT re-apply the transfer over the restored trained weights. Verified two ways:transfer_handler.transfer_count == 1after reload (the direct contract that guards the parenttransfer()against a second firing).Calling
transfer_handler.transfer()on the reloaded handler does not change the model’s weights.
Skips RF and NN training entirely — the bug lives in
TransfOctoRFClassificationModel.__setstate__(), not in the training loop, so wiring the architecture by hand is enough to exercise the fix in well under a second.- Returns:
True if the contract holds after reload.
- Return type:
bool
- check_overwrite_pretrained_model_same_spec()
Regression test: re-specifying the same
nn_transfer_weightson a continue-training pass must NOT silently reset thetransfer_countand trigger another transfer over the already-trained weights. The orchestrator deep-compares the new spec against the saved one and only rebuilds the handler when the spec actually changed.The contract being tested is purely about
TransfOctoRFClassificationModel.overwrite_pretrained_model()propagating updates toself.nn_handler.transfer_handler. No Keras model is needed — substituting a strict stand-in fornn_handlerkeeps the subtest at a few milliseconds (vs ~800 ms when the actual TORF architecture is built).The stand-in uses a sealed attribute set so that any future change to
overwrite_pretrained_modelthat starts touching a newnn_handlerattribute fails loudly here instead of silently passing — see_StrictHandlerStandIn.- Returns:
True if same-spec preserves and different-spec rebuilds and key omission is a no-op.
- Return type:
bool
- check_executor_rejects_none_spec()
Regression test for the I-3 hardening: the public static
src.model.deeplearn.handle.dl_freeze_training_executor.DLFreezeTrainingExecutor.compute_schedule()must rejectfreeze_training=Nonewith the package’s typedsrc.model.deeplearn.deep_learning_exception..DeepLearningException, not with the opaqueTypeErrorthatfor x in Nonewould surface.The test uses a callable stub
fit_fn(instances of_StubFitFntrack every call), and verifies two properties:Negative:
executor.run(...)withfreeze_training= NoneraisesDeepLearningExceptionand does NOT call the stub (the executor fails fast, no partial state mutations).Positive: the same stub class drives the executor successfully on a valid spec, recording the expected per-segment epoch counts. This confirms the stub pattern itself is sound and that the None-rejection path did not leave any executor-side residue.
- Returns:
True if both negative and positive paths behave as expected.
- Return type:
bool
- check_fit_guards_against_missing_optimizer()
Regression test for the I-1 hardening: TransfOctoRFHandler._fit must self-protect against the post-
__setstate__state where the handler hascompiled = Truebutarch.nn.optimizer is None. Without the guard, freeze-training with an explicitinitial_learning_ratewould crash onKerasUtils.set_learning_rate()(which dereferences the optimizer).Patches
arch.nn.fitto a no-op stub so the test exercises the guard + freeze-executor path (including theset_learning_rateaccess on the re-attached optimizer) without paying for a real gradient step. The actual training is incidental to the contract under test.- Returns:
True if the optimizer is re-attached before the freeze executor runs and the per-segment fit hook is called the expected number of times.
- Return type:
bool
- check_executor_can_rerun_with_same_instance()
Regression test for the I-3 latent invariant: a
DLFreezeTrainingExecutorinstance carries no per-run mutable state, so callingrun()twice with the same spec on different mock NNs must produce the same observable trace AND must not mutate the storedself.freeze_trainingspecification (deep-equality preserved across both calls).The deep-copy snapshot catches a future regression that normalizes the spec in-place on the first run, which would still produce identical observable traces (and thus pass a weaker test) but corrupt the executor’s state for any future caller inspecting
executor.freeze_training.- Returns:
True if both runs produce identical traces, the final trainable flags are restored, and the executor’s spec is unchanged.
- Return type:
bool
- check_optimizer_state_preserved_with_flag()
Regression test for M-1: the opt-in
preserve_optimizer_state=Trueflag must keep the NN optimizer’s state intact across the__getstate__/__setstate__boundary. Two pieces of state are verified end-to-end:optimizer.iterations— the LR-scheduler counter. Without preservation, cosine/exponential decays would restart each reload.One of Adam’s first-moment slot variables (
m) — the actual continue-training payload. Without preservation, Adam restarts with zero moments and convergence stalls.
A hypothetical future regression that preserved
iterationsbut corrupted the moment slots would be caught by the second check.- Returns:
True if both the iteration counter AND the sentinel slot value survive the save/reload cycle.
- Return type:
bool
- check_executor_empty_spec()
Regression test: an empty
freeze_training: []list must degrade gracefully to “no freezing” (a single full-length fit call) instead of raisingIndexErroron the schedule bookkeeping arrays.- Returns:
True if the executor falls back to a single full fit.
- Return type:
bool
- build_small_model(seed=42, nn_transfer_weights=None, nn_freeze_training=None, epochs=2, tiny=False)
Build a small TORF model for the smoke tests. The configuration mirrors a minimal variant of
TransfOctoRFTest.- Parameters:
seed – Random seed.
nn_transfer_weights – Optional transfer-learning spec.
nn_freeze_training – Optional freeze-training spec.
epochs – Training epochs.
tiny – When True, picks the smallest architecture that still builds (
n_h=4, single 4-unit SMLP layer,K=2). Use for subtests that do not actually train (e.g., pickle-roundtrip checks) so the Keras graph build cost is minimal (~200 ms instead of ~900 ms).
- Returns:
The configured but unfit TORF model.
- Return type:
- static make_data(seed)
Build small linearly separable RF + NN datasets.
- Returns:
Tuple
(X_rf, y_rf, X_nn, y_nn)with 4 features per sample and 2 classes.- Return type:
tuple of
np.ndarray