VBAF.ML.TransferLearning.ps1

# ==============================================================================
# VBAF.ML.TransferLearning.ps1
# Visual Brain AI Framework — Transfer Learning Module
# Version : v1.0.0 | Requires PS 5.1 | Part of VBAF v2.1.0
# ==============================================================================
#
# FEATURES
# --------
# 1. Toy model zoo — hand-crafted pretrained weights (no training needed)
# 2. Layer freezing — freeze/unfreeze individual layers
# 3. Feature extraction — get activations at any layer depth
# 4. Fine-tuning — backprop only through unfrozen layers
# 5. Weight save/load — JSON persistence compatible with VBAF.ML.CNN.ps1
#
# LAYER FORMAT
# ------------
# All layers are hashtables (same format as VBAF.ML.Autoencoder.ps1):
# @{ W; B; InSize; OutSize; Activation; Frozen; LastInput; LastPreact; LastOutput }
# Stored in ArrayLists → reference semantics → weight mutations persist ✅
#
# PS 5.1 RULES (same as Autoencoder module)
# ------------------------------------------
# • Hashtables in ArrayList = always references
# • Direct element assignment: $layer.W[$i] = value ← persists ✅
# • Functions in class methods: & (Get-Command Func) -Param val
# • Index loops only — no foreach/ForEach-Object/switch on arrays
# • Comma operator on return: return ,$array ← prevents unrolling
#
# QUICK START
# -----------
# . .\VBAF.ML.TransferLearning.ps1
# Get-VBAFModelZoo # list available models
# $model = Get-VBAFPretrainedModel -Name "ShapeEncoder"
# $feats = Get-VBAFFeatures -Model $model -X $inputVec -FromLayer 1
# Set-VBAFLayerFrozen -Model $model -LayerIndex 0 -Frozen $true
# $r = Invoke-VBAFFineTune -Model $model -Data $ds.X -Labels $ds.Labels -Epochs 50 -LR 0.05
# Test-VBAFTransferLearning
# ==============================================================================

Write-Host ''
Write-Host ' ╔══════════════════════════════════════════════╗' -ForegroundColor Magenta
Write-Host ' ║ VBAF · Transfer Learning Module v1.0.0 ║' -ForegroundColor Magenta
Write-Host ' ║ ModelZoo · Freeze · Features · FineTune ║' -ForegroundColor Magenta
Write-Host ' ║ PS 5.1 | Hashtable-reference layers ║' -ForegroundColor Magenta
Write-Host ' ╚══════════════════════════════════════════════╝' -ForegroundColor Magenta
Write-Host ''

# ==============================================================================
# LAYER PRIMITIVES
# (self-contained — compatible with VBAF.ML.Autoencoder.ps1 layer format)
# ==============================================================================

function New-TLLayer {
<#
.SYNOPSIS
    Create a fully-connected layer hashtable for Transfer Learning.
    Compatible with VBAF.ML.Autoencoder.ps1 layer format.

.DESCRIPTION
    Layers are hashtables stored in ArrayList → always references in PS 5.1.
    Direct element assignment $layer.W[$i] = val PERSISTS. ✅
    Frozen=$false by default — set to $true to skip weight updates.

.PARAMETER InSize Input dimension
.PARAMETER OutSize Output dimension
.PARAMETER Activation 'relu' | 'sigmoid' | 'linear' | 'softmax'
.PARAMETER Seed RNG seed (default 42)

.OUTPUTS Hashtable layer
#>

    param(
        [int]   $InSize,
        [int]   $OutSize,
        [string]$Activation = 'relu',
        [int]   $Seed       = 42
    )
    $rng   = [System.Random]::new($Seed)
    $scale = [Math]::Sqrt(2.0 / $InSize)   # He init
    $wLen  = $InSize * $OutSize
    $W     = @(0.0) * $wLen
    $B     = @(0.0) * $OutSize
    for ($i = 0; $i -lt $wLen; $i++) {
        $W[$i] = ($rng.NextDouble() * 2.0 - 1.0) * $scale
    }
    return @{
        W          = $W
        B          = $B
        InSize     = $InSize
        OutSize    = $OutSize
        Activation = $Activation
        Frozen     = $false
        LastInput  = $null
        LastPreact = $null
        LastOutput = $null
    }
}

function Invoke-TLActivation {
<#
.SYNOPSIS Apply activation function to preactivation array.
.OUTPUTS [double[]] (comma-protected)
#>

    param([double[]]$Z, [string]$Name)
    $n   = $Z.Length
    $out = [double[]]::new($n)

    if ($Name -eq 'relu') {
        for ($i = 0; $i -lt $n; $i++) {
            $out[$i] = if ($Z[$i] -gt 0.0) { $Z[$i] } else { 0.0 }
        }
    } elseif ($Name -eq 'sigmoid') {
        for ($i = 0; $i -lt $n; $i++) {
            $zc      = [Math]::Max(-500.0, [Math]::Min(500.0, $Z[$i]))
            $out[$i] = 1.0 / (1.0 + [Math]::Exp(-$zc))
        }
    } elseif ($Name -eq 'softmax') {
        # Numerically stable softmax: shift by max
        $maxZ = $Z[0]
        for ($i = 1; $i -lt $n; $i++) { if ($Z[$i] -gt $maxZ) { $maxZ = $Z[$i] } }
        $sumE = 0.0
        for ($i = 0; $i -lt $n; $i++) {
            $out[$i] = [Math]::Exp($Z[$i] - $maxZ)
            $sumE   += $out[$i]
        }
        for ($i = 0; $i -lt $n; $i++) { $out[$i] /= $sumE }
    } else {
        # linear
        for ($i = 0; $i -lt $n; $i++) { $out[$i] = $Z[$i] }
    }
    return ,$out
}

function Invoke-TLActivationGrad {
<#
.SYNOPSIS Elementwise activation derivative × upstream gradient.
.OUTPUTS [double[]] dPreact (comma-protected)
#>

    param([double[]]$DOut, [double[]]$Z, [double[]]$A, [string]$Name)
    $n = $DOut.Length
    $d = [double[]]::new($n)

    if ($Name -eq 'relu') {
        for ($i = 0; $i -lt $n; $i++) {
            $d[$i] = if ($Z[$i] -gt 0.0) { $DOut[$i] } else { 0.0 }
        }
    } elseif ($Name -eq 'sigmoid') {
        for ($i = 0; $i -lt $n; $i++) {
            $d[$i] = $DOut[$i] * $A[$i] * (1.0 - $A[$i])
        }
    } elseif ($Name -eq 'softmax') {
        # Jacobian diagonal approximation (cross-entropy upstream assumed)
        for ($i = 0; $i -lt $n; $i++) {
            $d[$i] = $DOut[$i] * $A[$i] * (1.0 - $A[$i])
        }
    } else {
        for ($i = 0; $i -lt $n; $i++) { $d[$i] = $DOut[$i] }
    }
    return ,$d
}

function Invoke-TLLayerForward {
<#
.SYNOPSIS
    Forward pass for one TL layer. Caches input/preact/output for backprop.
.OUTPUTS [double[]] (comma-protected)
#>

    param([hashtable]$Layer, [double[]]$X)

    $inSz  = [int]$Layer.InSize
    $outSz = [int]$Layer.OutSize
    $W     = $Layer.W
    $B     = $Layer.B

    # Cache input copy
    $inp = [double[]]::new($inSz)
    for ($i = 0; $i -lt $inSz; $i++) { $inp[$i] = [double]$X[$i] }
    $Layer.LastInput = $inp

    # Preactivation z[j] = W[j,:] · x + B[j]
    $Z = [double[]]::new($outSz)
    for ($j = 0; $j -lt $outSz; $j++) {
        $sum  = [double]$B[$j]
        $base = $j * $inSz
        for ($i = 0; $i -lt $inSz; $i++) {
            $sum += [double]$W[$base + $i] * [double]$X[$i]
        }
        $Z[$j] = $sum
    }
    $Layer.LastPreact = $Z

    $A = Invoke-TLActivation -Z $Z -Name $Layer.Activation
    $Layer.LastOutput = $A
    return ,$A
}

function Invoke-TLLayerBackward {
<#
.SYNOPSIS
    Backward pass for one TL layer.
    SKIPS weight update if $Layer.Frozen -eq $true.
    Returns dLoss/dInput for propagation to earlier layers.
.OUTPUTS [double[]] dX (comma-protected)
#>

    param([hashtable]$Layer, [double[]]$DOut, [double]$LR)

    $inSz  = [int]$Layer.InSize
    $outSz = [int]$Layer.OutSize
    $X     = [double[]]$Layer.LastInput
    $Z     = [double[]]$Layer.LastPreact
    $A     = [double[]]$Layer.LastOutput

    # Activation gradient
    $DZ = Invoke-TLActivationGrad -DOut $DOut -Z $Z -A $A -Name $Layer.Activation

    # dX[i] = Σ_j DZ[j] * W[j,i]
    $DX = [double[]]::new($inSz)
    for ($i = 0; $i -lt $inSz; $i++) {
        $sum = 0.0
        for ($j = 0; $j -lt $outSz; $j++) {
            $sum += [double]$DZ[$j] * [double]$Layer.W[$j * $inSz + $i]
        }
        $DX[$i] = $sum
    }

    # Weight update — SKIPPED if frozen ✅
    if (-not $Layer.Frozen) {
        for ($j = 0; $j -lt $outSz; $j++) {
            $dz_j = [double]$DZ[$j]
            $base = $j * $inSz
            for ($i = 0; $i -lt $inSz; $i++) {
                # Direct element assignment on hashtable array persists! ✅
                $Layer.W[$base + $i] = [double]$Layer.W[$base + $i] - $LR * $dz_j * [double]$X[$i]
            }
            $Layer.B[$j] = [double]$Layer.B[$j] - $LR * $dz_j
        }
    }

    return ,$DX
}

# ==============================================================================
# TL MODEL (wrapper class — thin shell, all state in hashtable layers)
# ==============================================================================

class TLModel {
<#
    A sequential model for transfer learning.
    Layers are hashtables in an ArrayList — always references in PS 5.1.
    Forward/backward delegate to standalone functions via & (Get-Command ...).
#>

    [System.Collections.ArrayList] $Layers
    [string] $Name
    [string] $Description

    TLModel([string]$name, [string]$description) {
        $this.Name        = $name
        $this.Description = $description
        $this.Layers      = [System.Collections.ArrayList]::new()
    }

    [void] AddLayer([hashtable]$layer) {
        $this.Layers.Add($layer) | Out-Null
    }

    # Forward pass — returns output of last layer
    [object] Forward([double[]]$x) {
        $current = $x
        for ($li = 0; $li -lt $this.Layers.Count; $li++) {
            $lyr     = [hashtable]$this.Layers[$li]
            $current = [double[]]( & (Get-Command Invoke-TLLayerForward) -Layer $lyr -X ([double[]]$current) )
        }
        return $current
    }

    # Forward up to (and including) $ToLayer — for feature extraction
    [object] ForwardTo([double[]]$x, [int]$toLayer) {
        $current = $x
        $limit   = [Math]::Min($toLayer, $this.Layers.Count - 1)
        for ($li = 0; $li -le $limit; $li++) {
            $lyr     = [hashtable]$this.Layers[$li]
            $current = [double[]]( & (Get-Command Invoke-TLLayerForward) -Layer $lyr -X ([double[]]$current) )
        }
        return $current
    }

    # Backward — updates only UNFROZEN layers
    [void] Backward([double[]]$dLoss, [double]$lr) {
        $grad = [double[]]$dLoss
        for ($li = $this.Layers.Count - 1; $li -ge 0; $li--) {
            $lyr  = [hashtable]$this.Layers[$li]
            $grad = [double[]]( & (Get-Command Invoke-TLLayerBackward) -Layer $lyr -DOut $grad -LR $lr )
        }
    }

    [string] ToString() {
        $s = ('TLModel [{0}] ' -f $this.Name)
        for ($li = 0; $li -lt $this.Layers.Count; $li++) {
            $lyr = [hashtable]$this.Layers[$li]
            $frz = if ($lyr.Frozen) { '❄' } else { '' }
            if ($li -gt 0) { $s += ' → ' }
            $s += ('{0}({1}){2}' -f $lyr.Activation, $lyr.OutSize, $frz)
        }
        return $s
    }
}

# ==============================================================================
# MODEL ZOO (toy pretrained models with hand-crafted weights)
# ==============================================================================

function Get-VBAFModelZoo {
<#
.SYNOPSIS
    List all available pretrained models in the VBAF toy model zoo.

.EXAMPLE
    Get-VBAFModelZoo
#>

    Write-Host ''
    Write-Host ' ── VBAF Toy Model Zoo ──────────────────────────' -ForegroundColor Magenta
    Write-Host ''
    Write-Host ' Name Input Output Description' -ForegroundColor DarkGray
    Write-Host ' ─────────────────────────────────────────────────' -ForegroundColor DarkGray
    Write-Host ' EdgeDetector 16 8 Sobel-inspired edge features on 4×4 grid' -ForegroundColor White
    Write-Host ' ShapeEncoder 16 3 Encodes HBar/VBar/Diag patterns (Shapes2D)' -ForegroundColor White
    Write-Host ' PatternFilter 16 4 Low/high frequency pattern detector' -ForegroundColor White
    Write-Host ''
    Write-Host ' Usage: $m = Get-VBAFPretrainedModel -Name "ShapeEncoder"' -ForegroundColor Yellow
    Write-Host ''
}

function Get-VBAFPretrainedModel {
<#
.SYNOPSIS
    Return a TLModel with hand-crafted "pretrained" weights.

.DESCRIPTION
    These are TOY models — weights are carefully hand-designed to capture
    meaningful structure, not trained by gradient descent. They serve as
    realistic starting points for transfer learning experiments.

    EdgeDetector : 16→8 — detects horizontal/vertical edges in 4×4 grids
    ShapeEncoder : 16→8→3 — approximately classifies HBar/VBar/Diag shapes
    PatternFilter: 16→4 — detects low/high frequency spatial patterns

.PARAMETER Name 'EdgeDetector' | 'ShapeEncoder' | 'PatternFilter'

.OUTPUTS TLModel with Layers pre-populated

.EXAMPLE
    $m = Get-VBAFPretrainedModel -Name "ShapeEncoder"
    $m.ToString()
    [double[]]$m.Forward(@(1,1,1,1, 0,0,0,0, 0,0,0,0, 1,1,1,1))
#>

    param([Parameter(Mandatory)][string]$Name)

    if ($Name -eq 'EdgeDetector') {
        # ── EdgeDetector: 16 → 8 ─────────────────────────────────────────────
        # Detects 8 edge features in a 4×4 binary grid:
        # Units 0-3: horizontal edge detectors (row transitions)
        # Units 4-7: vertical edge detectors (column transitions)
        #
        # Input layout (row-major):
        # 0 1 2 3
        # 4 5 6 7
        # 8 9 10 11
        # 12 13 14 15
        #
        # Horizontal edge unit k detects transition between row k and row k+1:
        # w = +1 for pixels in row k, -1 for pixels in row k+1
        # Vertical edge unit k detects transition between col k and col k+1:
        # w = +1 for pixels in col k, -1 for pixels in col k+1

        $model = [TLModel]::new('EdgeDetector', 'Sobel-inspired edge detector on 4x4 grids (16→8)')

        $W = @(0.0) * (16 * 8)
        $B = @(0.0) * 8

        # Units 0-3: horizontal edge detectors
        for ($unit = 0; $unit -lt 4; $unit++) {
            for ($col = 0; $col -lt 4; $col++) {
                if ($unit -lt 3) {
                    # +1 for current row, -1 for next row
                    $W[$unit * 16 + $unit * 4 + $col]       =  1.0
                    $W[$unit * 16 + ($unit+1) * 4 + $col]   = -1.0
                } else {
                    # Last unit: detects bottom row activation
                    $W[$unit * 16 + 12 + $col] = 1.0
                }
            }
        }

        # Units 4-7: vertical edge detectors
        for ($unit = 0; $unit -lt 4; $unit++) {
            for ($row = 0; $row -lt 4; $row++) {
                if ($unit -lt 3) {
                    # +1 for current col, -1 for next col
                    $W[($unit+4) * 16 + $row * 4 + $unit]     =  1.0
                    $W[($unit+4) * 16 + $row * 4 + $unit + 1] = -1.0
                } else {
                    # Last unit: detects right column activation
                    $W[($unit+4) * 16 + $row * 4 + 3] = 1.0
                }
            }
        }

        $layer = New-TLLayer -InSize 16 -OutSize 8 -Activation 'relu' -Seed 1
        $layer.W = $W
        $layer.B = $B
        $model.AddLayer($layer)
        return $model
    }

    elseif ($Name -eq 'ShapeEncoder') {
        # ── ShapeEncoder: 16 → 8 → 3 ─────────────────────────────────────────
        # Approximately classifies Shapes2D patterns: HBar / VBar / Diag
        #
        # Layer 1 (16→8): extracts shape-relevant features
        # Units 0-1: horizontal bar detectors (top/bottom row sums)
        # Units 2-3: vertical bar detectors (left/right column sums)
        # Units 4-5: diagonal detectors (main/anti diagonal)
        # Units 6-7: density detectors (total pixel count high/low)
        #
        # Layer 2 (8→3): combines features → class scores
        # Output 0 = HBar score, 1 = VBar score, 2 = Diag score

        $model = [TLModel]::new('ShapeEncoder', 'Encodes HBar/VBar/Diag shapes from Shapes2D (16→8→3)')

        # Layer 1 weights (8 × 16)
        $W1 = @(0.0) * (16 * 8)
        $B1 = @(0.0) * 8

        # Unit 0: top row detector (pixels 0-3)
        for ($i = 0; $i -lt 4; $i++)  { $W1[0  * 16 + $i]      = 1.0 }
        # Unit 1: bottom row detector (pixels 12-15)
        for ($i = 0; $i -lt 4; $i++)  { $W1[1  * 16 + 12 + $i] = 1.0 }
        # Unit 2: left column detector (pixels 0,4,8,12)
        for ($r = 0; $r -lt 4; $r++)  { $W1[2  * 16 + $r * 4]  = 1.0 }
        # Unit 3: right column detector (pixels 3,7,11,15)
        for ($r = 0; $r -lt 4; $r++)  { $W1[3  * 16 + $r*4+3]  = 1.0 }
        # Unit 4: main diagonal (pixels 0,5,10,15)
        $diag = @(0,5,10,15)
        for ($i = 0; $i -lt 4; $i++)  { $W1[4  * 16 + $diag[$i]] = 1.0 }
        # Unit 5: anti-diagonal (pixels 3,6,9,12)
        $anti = @(3,6,9,12)
        for ($i = 0; $i -lt 4; $i++)  { $W1[5  * 16 + $anti[$i]] = 1.0 }
        # Unit 6: overall density (all pixels positive)
        for ($i = 0; $i -lt 16; $i++) { $W1[6  * 16 + $i]       = 0.25 }
        # Unit 7: sparsity detector (all pixels negative — fires for sparse)
        for ($i = 0; $i -lt 16; $i++) { $W1[7  * 16 + $i]       = -0.25 }
        $B1[7] = 2.0   # bias so it fires when few pixels active

        $layer1 = New-TLLayer -InSize 16 -OutSize 8 -Activation 'relu' -Seed 1
        $layer1.W = $W1
        $layer1.B = $B1
        $model.AddLayer($layer1)

        # Layer 2 weights (3 × 8)
        # HBar ← top-row (unit 0) + bottom-row (unit 1), suppress col/diag
        # VBar ← left-col (unit 2) + right-col (unit 3), suppress row/diag
        # Diag ← main-diag (unit 4) + anti-diag (unit 5), suppress row/col
        $W2 = @(
            2.0, 2.0,-1.0,-1.0,-1.0,-1.0, 0.0, 0.0,   # HBar
           -1.0,-1.0, 2.0, 2.0,-1.0,-1.0, 0.0, 0.0,   # VBar
           -1.0,-1.0,-1.0,-1.0, 2.0, 2.0, 0.0, 0.0    # Diag
        )
        $B2 = @(0.0, 0.0, 0.0)

        $layer2 = New-TLLayer -InSize 8 -OutSize 3 -Activation 'softmax' -Seed 2
        $layer2.W = $W2
        $layer2.B = $B2
        $model.AddLayer($layer2)

        return $model
    }

    elseif ($Name -eq 'PatternFilter') {
        # ── PatternFilter: 16 → 4 ────────────────────────────────────────────
        # Detects spatial frequency characteristics of 4×4 patterns:
        # Unit 0: low-freq horizontal (broad horizontal bands)
        # Unit 1: low-freq vertical (broad vertical bands)
        # Unit 2: high-freq checker (checkerboard / diagonal texture)
        # Unit 3: overall brightness (total pixel density)

        $model = [TLModel]::new('PatternFilter', 'Spatial frequency detector on 4x4 grids (16→4)')

        $W = @(0.0) * (16 * 4)
        $B = @(0.0) * 4

        # Unit 0: low-freq horizontal — top half positive, bottom half negative
        for ($r = 0; $r -lt 4; $r++) {
            $sign = if ($r -lt 2) { 1.0 } else { -1.0 }
            for ($c = 0; $c -lt 4; $c++) {
                $W[0 * 16 + $r * 4 + $c] = $sign * 0.5
            }
        }

        # Unit 1: low-freq vertical — left half positive, right half negative
        for ($r = 0; $r -lt 4; $r++) {
            for ($c = 0; $c -lt 4; $c++) {
                $sign = if ($c -lt 2) { 1.0 } else { -1.0 }
                $W[1 * 16 + $r * 4 + $c] = $sign * 0.5
            }
        }

        # Unit 2: high-freq checker — alternating +/- in checkerboard pattern
        for ($r = 0; $r -lt 4; $r++) {
            for ($c = 0; $c -lt 4; $c++) {
                $sign = if (($r + $c) % 2 -eq 0) { 1.0 } else { -1.0 }
                $W[2 * 16 + $r * 4 + $c] = $sign * 0.5
            }
        }

        # Unit 3: overall brightness — all positive
        for ($i = 0; $i -lt 16; $i++) { $W[3 * 16 + $i] = 0.25 }

        $layer = New-TLLayer -InSize 16 -OutSize 4 -Activation 'relu' -Seed 3
        $layer.W = $W
        $layer.B = $B
        $model.AddLayer($layer)
        return $model
    }

    else {
        throw "Unknown model '$Name'. Run Get-VBAFModelZoo to see available models."
    }
}

# ==============================================================================
# LAYER FREEZING
# ==============================================================================

function Set-VBAFLayerFrozen {
<#
.SYNOPSIS
    Freeze or unfreeze a specific layer in a TLModel.
    Frozen layers skip weight updates during fine-tuning (but still compute gradients).

.PARAMETER Model TLModel instance
.PARAMETER LayerIndex 0-based layer index
.PARAMETER Frozen $true to freeze, $false to unfreeze

.EXAMPLE
    $m = Get-VBAFPretrainedModel -Name "ShapeEncoder"
    Set-VBAFLayerFrozen -Model $m -LayerIndex 0 -Frozen $true # freeze layer 0
    Set-VBAFLayerFrozen -Model $m -LayerIndex 1 -Frozen $false # unfreeze layer 1
#>

    param(
        [Parameter(Mandatory)][TLModel]$Model,
        [Parameter(Mandatory)][int]   $LayerIndex,
        [Parameter(Mandatory)][bool]  $Frozen
    )
    if ($LayerIndex -lt 0 -or $LayerIndex -ge $Model.Layers.Count) {
        throw "LayerIndex $LayerIndex out of range (model has $($Model.Layers.Count) layers)"
    }
    $lyr = [hashtable]$Model.Layers[$LayerIndex]
    $lyr.Frozen = $Frozen
    $status = if ($Frozen) { '❄ frozen' } else { '▶ trainable' }
    Write-Host (" Layer {0} ({1}→{2}): {3}" -f $LayerIndex, $lyr.InSize, $lyr.OutSize, $status) -ForegroundColor Cyan
}

function Set-VBAFAllLayersFrozen {
<#
.SYNOPSIS Freeze or unfreeze ALL layers in a model at once.

.EXAMPLE
    Set-VBAFAllLayersFrozen -Model $m -Frozen $true # freeze everything
    Set-VBAFAllLayersFrozen -Model $m -Frozen $false # unfreeze everything
#>

    param(
        [Parameter(Mandatory)][TLModel]$Model,
        [Parameter(Mandatory)][bool]  $Frozen
    )
    for ($li = 0; $li -lt $Model.Layers.Count; $li++) {
        $lyr        = [hashtable]$Model.Layers[$li]
        $lyr.Frozen = $Frozen
    }
    $status = if ($Frozen) { '❄ ALL frozen' } else { '▶ ALL trainable' }
    Write-Host (" {0} ({1} layers)" -f $status, $Model.Layers.Count) -ForegroundColor Cyan
}

function Get-VBAFFrozenLayers {
<#
.SYNOPSIS Return indices of all frozen layers.
.OUTPUTS int[] (comma-protected)
#>

    param([Parameter(Mandatory)][TLModel]$Model)
    $result = [System.Collections.ArrayList]::new()
    for ($li = 0; $li -lt $Model.Layers.Count; $li++) {
        $lyr = [hashtable]$Model.Layers[$li]
        if ($lyr.Frozen) { $result.Add($li) | Out-Null }
    }
    return ,$result
}

function Get-VBAFTrainableLayers {
<#
.SYNOPSIS Return indices of all trainable (unfrozen) layers.
.OUTPUTS ArrayList of int (comma-protected)
#>

    param([Parameter(Mandatory)][TLModel]$Model)
    $result = [System.Collections.ArrayList]::new()
    for ($li = 0; $li -lt $Model.Layers.Count; $li++) {
        $lyr = [hashtable]$Model.Layers[$li]
        if (-not $lyr.Frozen) { $result.Add($li) | Out-Null }
    }
    return ,$result
}

function Show-VBAFModelStatus {
<#
.SYNOPSIS Print a layer-by-layer summary showing frozen/trainable status.

.EXAMPLE
    Show-VBAFModelStatus -Model $m
#>

    param([Parameter(Mandatory)][TLModel]$Model)
    Write-Host ''
    Write-Host (" ── {0} ──" -f $Model.Name) -ForegroundColor Magenta
    Write-Host (" {0}" -f $Model.Description) -ForegroundColor DarkGray
    Write-Host ''
    Write-Host ' Idx Shape Activation Status Params' -ForegroundColor DarkGray
    Write-Host ' ─────────────────────────────────────────────────' -ForegroundColor DarkGray
    $totalParams    = 0
    $trainableParams = 0
    for ($li = 0; $li -lt $Model.Layers.Count; $li++) {
        $lyr    = [hashtable]$Model.Layers[$li]
        $params = [int]$lyr.InSize * [int]$lyr.OutSize + [int]$lyr.OutSize
        $totalParams += $params
        $status = if ($lyr.Frozen) { '❄ frozen ' } else { '▶ trainable' }
        if (-not $lyr.Frozen) { $trainableParams += $params }
        $color  = if ($lyr.Frozen) { 'DarkCyan' } else { 'Green' }
        Write-Host (" {0,3} {1,4}→{2,-4} {3,-10} {4} {5,6}" -f `
            $li, $lyr.InSize, $lyr.OutSize, $lyr.Activation, $status, $params) -ForegroundColor $color
    }
    Write-Host ' ─────────────────────────────────────────────────' -ForegroundColor DarkGray
    Write-Host (" Total params: {0} Trainable: {1} Frozen: {2}" -f `
        $totalParams, $trainableParams, ($totalParams - $trainableParams)) -ForegroundColor DarkGray
    Write-Host ''
}

# ==============================================================================
# FEATURE EXTRACTION
# ==============================================================================

function Get-VBAFFeatures {
<#
.SYNOPSIS
    Run a forward pass and return activations at a specified layer depth.
    Use this to treat a frozen pretrained encoder as a feature extractor.

.DESCRIPTION
    This is the core of transfer learning:
        1. Load pretrained model
        2. Freeze all layers
        3. Run Get-VBAFFeatures to get rich representations
        4. Feed those representations to a new trainable head

.PARAMETER Model TLModel instance
.PARAMETER X Input vector [double[]]
.PARAMETER FromLayer Layer index whose OUTPUT to return (0-based)
                      -1 = return final output (default)

.OUTPUTS [double[]] activation vector at the requested depth (comma-protected)

.EXAMPLE
    $m = Get-VBAFPretrainedModel -Name "EdgeDetector"
    $feats = Get-VBAFFeatures -Model $m -X $ds.X[0] -FromLayer 0
    # $feats is the 8-dim edge feature vector
#>

    param(
        [Parameter(Mandatory)][TLModel]$Model,
        [Parameter(Mandatory)][double[]]$X,
        [int]$FromLayer = -1
    )
    if ($FromLayer -lt 0) { $FromLayer = $Model.Layers.Count - 1 }
    $feats = [double[]]$Model.ForwardTo($X, $FromLayer)
    return ,$feats
}

function Get-VBAFBatchFeatures {
<#
.SYNOPSIS
    Extract features for an entire dataset. Returns ArrayList of [double[]].

.PARAMETER Model TLModel instance
.PARAMETER Data ArrayList of [double[]] samples
.PARAMETER FromLayer Layer index to extract from (-1 = last layer)

.OUTPUTS ArrayList of [double[]] (comma-protected)

.EXAMPLE
    $m = Get-VBAFPretrainedModel -Name "ShapeEncoder"
    $feats = Get-VBAFBatchFeatures -Model $m -Data $ds.X -FromLayer 0
    # $feats[0] is the 8-dim feature vector for sample 0
#>

    param(
        [Parameter(Mandatory)][TLModel]$Model,
        [Parameter(Mandatory)]$Data,
        [int]$FromLayer = -1
    )
    $results = [System.Collections.ArrayList]::new()
    for ($si = 0; $si -lt $Data.Count; $si++) {
        $x     = [double[]]$Data[$si]
        $feats = [double[]](Get-VBAFFeatures -Model $Model -X $x -FromLayer $FromLayer)
        $results.Add($feats) | Out-Null
    }
    return ,$results
}

# ==============================================================================
# FINE-TUNING
# ==============================================================================

function Get-TLCrossEntropyLoss {
<#
.SYNOPSIS Scalar cross-entropy loss for classification.
           L = -Σ label[i] * log(pred[i] + ε)
#>

    param([double[]]$Pred, [int]$TrueClass)
    $eps = 1e-10
    $p   = [Math]::Max($eps, [double]$Pred[$TrueClass])
    return (-[Math]::Log($p))
}

function Get-TLCrossEntropyGrad {
<#
.SYNOPSIS Gradient of cross-entropy w.r.t. softmax output.
           dL/dPred[i] = Pred[i] - 1(i == TrueClass)
           (Combined softmax + cross-entropy gradient — numerically clean)
.OUTPUTS [double[]] (comma-protected)
#>

    param([double[]]$Pred, [int]$TrueClass)
    $n  = $Pred.Length
    $dL = [double[]]::new($n)
    for ($i = 0; $i -lt $n; $i++) {
        $dL[$i] = [double]$Pred[$i]
    }
    $dL[$TrueClass] -= 1.0
    return ,$dL
}

function Get-TLMSELoss {
<#
.SYNOPSIS MSE loss for reconstruction tasks during fine-tuning.
#>

    param([double[]]$Pred, [double[]]$Target)
    $sum = 0.0; $n = $Pred.Length
    for ($i = 0; $i -lt $n; $i++) {
        $d = [double]$Pred[$i] - [double]$Target[$i]; $sum += $d * $d
    }
    return ($sum / $n)
}

function Get-TLMSEGrad {
<#
.SYNOPSIS Gradient of MSE w.r.t. predictions.
.OUTPUTS [double[]] (comma-protected)
#>

    param([double[]]$Pred, [double[]]$Target)
    $n = $Pred.Length; $dL = [double[]]::new($n)
    for ($i = 0; $i -lt $n; $i++) {
        $dL[$i] = 2.0 * ([double]$Pred[$i] - [double]$Target[$i]) / $n
    }
    return ,$dL
}

function Invoke-VBAFFineTune {
<#
.SYNOPSIS
    Fine-tune a TLModel using backprop only through UNFROZEN layers.
    Frozen layers still compute gradients (needed to pass gradient backward)
    but their weights are NOT updated.

.DESCRIPTION
    Typical transfer learning workflow:
        1. Get-VBAFPretrainedModel load pretrained weights
        2. Set-VBAFAllLayersFrozen $true freeze everything
        3. Set-VBAFLayerFrozen $m -LayerIndex N -Frozen $false unfreeze head
        4. Invoke-VBAFFineTune train only the head

    Loss function:
        Classification (Labels provided) → cross-entropy
        Reconstruction (Targets provided) → MSE

.PARAMETER Model TLModel to fine-tune
.PARAMETER Data ArrayList of [double[]] input samples
.PARAMETER Labels ArrayList of [int] class labels (classification)
.PARAMETER Targets ArrayList of [double[]] target vectors (reconstruction)
.PARAMETER Epochs Training epochs (default 50)
.PARAMETER LR Learning rate (default 0.01)
.PARAMETER PrintEvery Print every N epochs. 0 = silent. (default 10)

.OUTPUTS Hashtable: FinalLoss, LossHistory, Model, Accuracy

.EXAMPLE
    $m = Get-VBAFPretrainedModel -Name "ShapeEncoder"
    Set-VBAFAllLayersFrozen -Model $m -Frozen $true
    Set-VBAFLayerFrozen -Model $m -LayerIndex 1 -Frozen $false
    $r = Invoke-VBAFFineTune -Model $m -Data $ds.X -Labels $ds.Labels -Epochs 50 -LR 0.05
    $r.FinalLoss
    $r.Accuracy
#>

    param(
        [Parameter(Mandatory)][TLModel]$Model,
        [Parameter(Mandatory)]$Data,
        $Labels     = $null,
        $Targets    = $null,
        [int]   $Epochs     = 50,
        [double]$LR         = 0.01,
        [int]   $PrintEvery = 10
    )

    if ($null -eq $Labels -and $null -eq $Targets) {
        throw "Provide either -Labels (classification) or -Targets (reconstruction)"
    }
    $isClassification = ($null -ne $Labels)
    $nSamples         = $Data.Count
    $lossHistory      = [System.Collections.ArrayList]::new()

    # Count trainable params for display
    $trainable = Get-VBAFTrainableLayers -Model $Model
    Write-Host (" Fine-tuning [{0}] {1} trainable layer(s), {2} frozen" -f `
        $Model.Name, $trainable.Count, ($Model.Layers.Count - $trainable.Count)) -ForegroundColor Magenta
    $taskLabel = if ($isClassification) {'Classification'} else {'Reconstruction'}
    Write-Host (" Task: {0} Samples: {1} Epochs: {2} LR: {3}" -f `
        $taskLabel, $nSamples, $Epochs, $LR) -ForegroundColor DarkGray
    Write-Host ''

    for ($ep = 1; $ep -le $Epochs; $ep++) {
        $epochLoss    = 0.0
        $epochCorrect = 0

        for ($si = 0; $si -lt $nSamples; $si++) {
            $x    = [double[]]$Data[$si]
            $pred = [double[]]$Model.Forward($x)

            if ($isClassification) {
                $lbl       = [int]$Labels[$si]
                $epochLoss += Get-TLCrossEntropyLoss -Pred $pred -TrueClass $lbl
                $dLoss      = [double[]](Get-TLCrossEntropyGrad -Pred $pred -TrueClass $lbl)
                # Accuracy: argmax of prediction
                $maxIdx = 0; $maxVal = [double]$pred[0]
                for ($k = 1; $k -lt $pred.Length; $k++) {
                    if ([double]$pred[$k] -gt $maxVal) { $maxVal = [double]$pred[$k]; $maxIdx = $k }
                }
                if ($maxIdx -eq $lbl) { $epochCorrect++ }
            } else {
                $tgt       = [double[]]$Targets[$si]
                $epochLoss += Get-TLMSELoss -Pred $pred -Target $tgt
                $dLoss      = [double[]](Get-TLMSEGrad -Pred $pred -Target $tgt)
            }

            $Model.Backward($dLoss, $LR)
        }

        $avgLoss = $epochLoss / $nSamples
        $lossHistory.Add($avgLoss) | Out-Null

        if ($PrintEvery -gt 0 -and ($ep -eq 1 -or $ep % $PrintEvery -eq 0)) {
            $acc   = if ($isClassification) { ' Acc: {0:P0}' -f ($epochCorrect / $nSamples) } else { '' }
            $color = if ($avgLoss -lt 0.5) { 'Green' } elseif ($avgLoss -lt 1.0) { 'Yellow' } else { 'Red' }
            Write-Host (" Epoch {0,4}/{1} Loss: {2:F5}{3}" -f $ep, $Epochs, $avgLoss, $acc) -ForegroundColor $color
        }
    }

    $finalLoss = [double]$lossHistory[$lossHistory.Count - 1]

    # Final accuracy pass
    $finalCorrect = 0
    if ($isClassification) {
        for ($si = 0; $si -lt $nSamples; $si++) {
            $x    = [double[]]$Data[$si]
            $pred = [double[]]$Model.Forward($x)
            $lbl  = [int]$Labels[$si]
            $maxIdx = 0; $maxVal = [double]$pred[0]
            for ($k = 1; $k -lt $pred.Length; $k++) {
                if ([double]$pred[$k] -gt $maxVal) { $maxVal = [double]$pred[$k]; $maxIdx = $k }
            }
            if ($maxIdx -eq $lbl) { $finalCorrect++ }
        }
    }
    $finalAcc = if ($isClassification) { $finalCorrect / $nSamples } else { 0.0 }

    Write-Host ''
    Write-Host (" ── Fine-tune complete ── Loss: {0:F5} Acc: {1:P0}" -f $finalLoss, $finalAcc) -ForegroundColor Magenta
    Write-Host ''

    return @{
        FinalLoss   = $finalLoss
        LossHistory = $lossHistory
        Accuracy    = $finalAcc
        Model       = $Model
    }
}

# ==============================================================================
# WEIGHT SAVE / LOAD (JSON, compatible with VBAF.ML.CNN.ps1 pattern)
# ==============================================================================

function Save-VBAFTLWeights {
<#
.SYNOPSIS
    Save TLModel weights to JSON.
    Format compatible with VBAF.ML.CNN.ps1 Save-CNNWeights pattern.

.PARAMETER Model TLModel to save
.PARAMETER Path File path for JSON output

.EXAMPLE
    Save-VBAFTLWeights -Model $m -Path "C:\Temp\ShapeEncoder.json"
#>

    param(
        [Parameter(Mandatory)][TLModel]$Model,
        [Parameter(Mandatory)][string]$Path
    )
    $layerData = [System.Collections.ArrayList]::new()
    for ($li = 0; $li -lt $Model.Layers.Count; $li++) {
        $lyr = [hashtable]$Model.Layers[$li]
        $layerData.Add(@{
            Index      = $li
            InSize     = $lyr.InSize
            OutSize    = $lyr.OutSize
            Activation = $lyr.Activation
            Frozen     = $lyr.Frozen
            W          = $lyr.W
            B          = $lyr.B
        }) | Out-Null
    }
    $payload = @{
        ModelName   = $Model.Name
        Description = $Model.Description
        NumLayers   = $Model.Layers.Count
        SavedAt     = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
        Layers      = $layerData
    }
    $payload | ConvertTo-Json -Depth 10 | Out-File -FilePath $Path -Encoding UTF8
    Write-Host (" ✅ Saved [{0}] → {1}" -f $Model.Name, $Path) -ForegroundColor Green
}

function Load-VBAFTLWeights {
<#
.SYNOPSIS
    Load TLModel weights from JSON. Returns a TLModel.

.PARAMETER Path Path to JSON file saved by Save-VBAFTLWeights

.OUTPUTS TLModel with weights restored

.EXAMPLE
    $m = Load-VBAFTLWeights -Path "C:\Temp\ShapeEncoder.json"
    $m.ToString()
#>

    param([Parameter(Mandatory)][string]$Path)

    if (-not (Test-Path $Path)) { throw "File not found: $Path" }

    $raw     = Get-Content $Path -Raw | ConvertFrom-Json
    $model   = [TLModel]::new($raw.ModelName, $raw.Description)

    for ($li = 0; $li -lt $raw.Layers.Count; $li++) {
        $ld    = $raw.Layers[$li]
        $layer = New-TLLayer -InSize $ld.InSize -OutSize $ld.OutSize -Activation $ld.Activation -Seed 1

        # Restore weights — must use index assignment on hashtable array ✅
        $wArr = $ld.W
        for ($i = 0; $i -lt $wArr.Count; $i++) { $layer.W[$i] = [double]$wArr[$i] }
        $bArr = $ld.B
        for ($i = 0; $i -lt $bArr.Count; $i++) { $layer.B[$i] = [double]$bArr[$i] }
        $layer.Frozen = [bool]$ld.Frozen

        $model.AddLayer($layer)
    }
    Write-Host (" ✅ Loaded [{0}] ← {1}" -f $model.Name, $Path) -ForegroundColor Green
    return $model
}

# ==============================================================================
# EVALUATION HELPER
# ==============================================================================

function Test-VBAFTLClassify {
<#
.SYNOPSIS
    Run classification on a dataset and print per-class accuracy.

.PARAMETER Model TLModel (output layer should be softmax)
.PARAMETER Data ArrayList of [double[]]
.PARAMETER Labels ArrayList of [int]
.PARAMETER ClassNames string[] for display

.OUTPUTS Hashtable: Accuracy, CorrectCount, ConfusionMatrix

.EXAMPLE
    $r = Test-VBAFTLClassify -Model $m -Data $ds.X -Labels $ds.Labels -ClassNames $ds.ClassNames
    $r.Accuracy
#>

    param(
        [Parameter(Mandatory)][TLModel]$Model,
        [Parameter(Mandatory)]$Data,
        [Parameter(Mandatory)]$Labels,
        $ClassNames = $null
    )
    $n          = $Data.Count
    $numClasses = 0
    # Infer number of classes from max label
    for ($i = 0; $i -lt $n; $i++) {
        $l = [int]$Labels[$i]
        if ($l -ge $numClasses) { $numClasses = $l + 1 }
    }
    if ($null -eq $ClassNames) {
        $ClassNames = [string[]]::new($numClasses)
        for ($c = 0; $c -lt $numClasses; $c++) { $ClassNames[$c] = "Class$c" }
    }

    # Confusion matrix as flat array [true * numClasses + pred]
    $cm      = @(0) * ($numClasses * $numClasses)
    $correct = 0

    for ($si = 0; $si -lt $n; $si++) {
        $x    = [double[]]$Data[$si]
        $pred = [double[]]$Model.Forward($x)
        $lbl  = [int]$Labels[$si]

        $maxIdx = 0; $maxVal = [double]$pred[0]
        for ($k = 1; $k -lt $pred.Length; $k++) {
            if ([double]$pred[$k] -gt $maxVal) { $maxVal = [double]$pred[$k]; $maxIdx = $k }
        }
        $cm[$lbl * $numClasses + $maxIdx]++
        if ($maxIdx -eq $lbl) { $correct++ }
    }

    $acc = $correct / $n

    Write-Host ''
    Write-Host (' ── Classification Report [{0}] ──' -f $Model.Name) -ForegroundColor Magenta
    $accColor = if ($acc -ge 0.9) {'Green'} elseif ($acc -ge 0.6) {'Yellow'} else {'Red'}
    Write-Host (' Overall accuracy: {0:P1} ({1}/{2})' -f $acc, $correct, $n) -ForegroundColor $accColor
    Write-Host ''
    Write-Host ' Confusion Matrix (rows=true, cols=pred):' -ForegroundColor DarkGray

    # Header
    $header = ' '
    for ($c = 0; $c -lt $numClasses; $c++) { $header += ('{0,8}' -f $ClassNames[$c]) }
    Write-Host $header -ForegroundColor DarkGray

    for ($r = 0; $r -lt $numClasses; $r++) {
        $row = (' {0,-10}' -f $ClassNames[$r])
        for ($c = 0; $c -lt $numClasses; $c++) {
            $val = $cm[$r * $numClasses + $c]
            $row += ('{0,8}' -f $val)
        }
        $color = if ($cm[$r * $numClasses + $r] -gt 0) { 'White' } else { 'Red' }
        Write-Host $row -ForegroundColor $color
    }
    Write-Host ''

    return @{
        Accuracy        = $acc
        CorrectCount    = $correct
        ConfusionMatrix = $cm
        NumClasses      = $numClasses
    }
}

# ==============================================================================
# SMOKE TEST
# ==============================================================================

function Test-VBAFTransferLearning {
<#
.SYNOPSIS
    End-to-end smoke test covering all Transfer Learning features.
    Uses Shapes2D dataset (requires Get-VBAFAEDataset from Autoencoder module).

.DESCRIPTION
    Tests:
    1. Model zoo listing
    2. All three pretrained models load and produce correct output shapes
    3. EdgeDetector: verify edge features fire on HBar pattern
    4. ShapeEncoder: verify correct class predicted (zero-shot, no training)
    5. Layer freezing / unfreezing
    6. Feature extraction (batch)
    7. Fine-tuning (frozen encoder + trainable head)
    8. Save / Load weights round-trip

.EXAMPLE
    Test-VBAFTransferLearning
#>

    Write-Host ''
    Write-Host ' ══════════════════════════════════════════════════' -ForegroundColor Magenta
    Write-Host ' VBAF Transfer Learning — Smoke Test' -ForegroundColor Magenta
    Write-Host ' ══════════════════════════════════════════════════' -ForegroundColor Magenta

    # ── 1. Dataset ────────────────────────────────────────────────────────────
    Write-Host ' [1/8] Loading Shapes2D dataset...' -ForegroundColor Gray
    $ds = $null
    try {
        $ds = Get-VBAFAEDataset -Name 'Shapes2D'
        Write-Host (" {0} samples × {1} dims ({2} classes)" -f `
            $ds.NumSamples, $ds.InputDim, $ds.NumClasses) -ForegroundColor DarkGray
    } catch {
        Write-Host ' ⚠️ Get-VBAFAEDataset not found.' -ForegroundColor Yellow
        Write-Host ' Load VBAF.ML.Autoencoder.ps1 first, or run manually.' -ForegroundColor Yellow
        Write-Host ' Continuing with synthetic data...' -ForegroundColor Yellow
        # Build minimal synthetic dataset
        $ds = @{
            X          = [System.Collections.ArrayList]::new()
            Labels     = [System.Collections.ArrayList]::new()
            ClassNames = @('HBar','VBar','Diag')
            InputDim   = 16
            NumClasses = 3
            NumSamples = 3
        }
        $ds.X.Add([double[]](1,1,1,1, 0,0,0,0, 0,0,0,0, 1,1,1,1)) | Out-Null  # HBar
        $ds.X.Add([double[]](1,0,0,1, 1,0,0,1, 1,0,0,1, 1,0,0,1)) | Out-Null  # VBar
        $ds.X.Add([double[]](1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1)) | Out-Null  # Diag
        $ds.Labels.Add(0) | Out-Null; $ds.Labels.Add(1) | Out-Null; $ds.Labels.Add(2) | Out-Null
    }

    # ── 2. Model Zoo ─────────────────────────────────────────────────────────
    Write-Host ' [2/8] Model zoo...' -ForegroundColor Gray
    Get-VBAFModelZoo

    # ── 3. Load all pretrained models ─────────────────────────────────────────
    Write-Host ' [3/8] Loading pretrained models...' -ForegroundColor Gray
    $edge   = Get-VBAFPretrainedModel -Name 'EdgeDetector'
    $shape  = Get-VBAFPretrainedModel -Name 'ShapeEncoder'
    $filter = Get-VBAFPretrainedModel -Name 'PatternFilter'
    Write-Host (" {0}" -f $edge.ToString())   -ForegroundColor DarkGray
    Write-Host (" {0}" -f $shape.ToString())  -ForegroundColor DarkGray
    Write-Host (" {0}" -f $filter.ToString()) -ForegroundColor DarkGray

    # ── 4. EdgeDetector: check output shape ───────────────────────────────────
    Write-Host ' [4/8] EdgeDetector feature extraction...' -ForegroundColor Gray
    $hbarInput = [double[]]$ds.X[0]
    $edgeFeats = [double[]](Get-VBAFFeatures -Model $edge -X $hbarInput -FromLayer 0)
    Write-Host (" HBar input → {0}-dim edge features" -f $edgeFeats.Length) -ForegroundColor DarkGray
    $featStr = ''
    for ($i = 0; $i -lt $edgeFeats.Length; $i++) {
        $featStr += ('{0:F2} ' -f $edgeFeats[$i])
    }
    Write-Host (" Features: {0}" -f $featStr.TrimEnd()) -ForegroundColor DarkGray

    # ── 5. ShapeEncoder: zero-shot classification ─────────────────────────────
    Write-Host ' [5/8] ShapeEncoder zero-shot classification...' -ForegroundColor Gray
    $zeroShotResult = Test-VBAFTLClassify -Model $shape -Data $ds.X -Labels $ds.Labels -ClassNames $ds.ClassNames

    # ── 6. Freezing / unfreezing ──────────────────────────────────────────────
    Write-Host ' [6/8] Layer freeze/unfreeze...' -ForegroundColor Gray
    Show-VBAFModelStatus -Model $shape
    Set-VBAFLayerFrozen -Model $shape -LayerIndex 0 -Frozen $true
    $frozen    = Get-VBAFFrozenLayers   -Model $shape
    $trainable = Get-VBAFTrainableLayers -Model $shape
    Write-Host (" Frozen layers: {0} Trainable: {1}" -f $frozen.Count, $trainable.Count) -ForegroundColor DarkGray

    # ── 7. Fine-tuning (frozen encoder, trainable head) ───────────────────────
    Write-Host ' [7/8] Fine-tuning (layer 0 frozen, layer 1 trainable)...' -ForegroundColor Gray
    $ftResult = Invoke-VBAFFineTune -Model $shape -Data $ds.X -Labels $ds.Labels `
                                    -Epochs 100 -LR 0.05 -PrintEvery 20
    $ftColor = if ($ftResult.Accuracy -ge 0.8) {'Green'} else {'Yellow'}
    Write-Host (" Post fine-tune accuracy: {0:P0}" -f $ftResult.Accuracy) -ForegroundColor $ftColor
    $postFT = Test-VBAFTLClassify -Model $shape -Data $ds.X -Labels $ds.Labels -ClassNames $ds.ClassNames

    # ── 8. Save / Load round-trip ─────────────────────────────────────────────
    Write-Host ' [8/8] Save / Load weight round-trip...' -ForegroundColor Gray
    $tmpPath = "$env:TEMP\VBAF_TL_ShapeEncoder_test.json"
    Save-VBAFTLWeights -Model $shape -Path $tmpPath
    $loaded  = Load-VBAFTLWeights -Path $tmpPath
    # Verify weights match
    $origW0   = [double[]]([hashtable]$shape.Layers[0]).W
    $loadedW0 = [double[]]([hashtable]$loaded.Layers[0]).W
    $match    = $true
    for ($i = 0; $i -lt [Math]::Min(5, $origW0.Length); $i++) {
        if ([Math]::Abs($origW0[$i] - $loadedW0[$i]) -gt 1e-10) { $match = $false }
    }
    $matchStr = if ($match) { '✅ weights match' } else { '❌ weights mismatch' }
    $matchColor = if ($match) {'Green'} else {'Red'}
    Write-Host (" Round-trip: {0}" -f $matchStr) -ForegroundColor $matchColor

    # ── Summary ───────────────────────────────────────────────────────────────
    Write-Host ' ── Summary ────────────────────────────────────────' -ForegroundColor DarkGray
    Write-Host (" Model zoo: 3 models ✅") -ForegroundColor Green
    Write-Host (" Feature extract: {0}-dim edge features ✅" -f $edgeFeats.Length) -ForegroundColor Green
    $zsColor  = if ($zeroShotResult.Accuracy -ge 0.5) {'Green'} else {'Yellow'}
    $ftColor2 = if ($postFT.Accuracy -ge 0.8) {'Green'} else {'Yellow'}
    $slColor  = if ($match) {'Green'} else {'Red'}
    Write-Host (" Zero-shot acc: {0:P0}" -f $zeroShotResult.Accuracy) -ForegroundColor $zsColor
    Write-Host (" Post-finetune: {0:P0}" -f $postFT.Accuracy)         -ForegroundColor $ftColor2
    Write-Host (" Save/Load: {0}" -f $matchStr)                   -ForegroundColor $slColor
    Write-Host ''
}

# ==============================================================================
# TEST
# 1. Run VBAF.LoadAll.ps1
#
# --- Quick smoke test ---
# 2. Test-VBAFTransferLearning
# # Requires VBAF.ML.Autoencoder.ps1 loaded for Shapes2D dataset
# # Falls back to synthetic 3-sample data if not available
#
# --- Manual step-by-step ---
# 3. # List available pretrained models
# Get-VBAFModelZoo
#
# 4. # Load a pretrained model
# $m = Get-VBAFPretrainedModel -Name "ShapeEncoder"
# $m.ToString()
#
# 5. # Inspect layer status
# Show-VBAFModelStatus -Model $m
#
# 6. # Zero-shot classify Shapes2D
# $ds = Get-VBAFAEDataset -Name "Shapes2D"
# Test-VBAFTLClassify -Model $m -Data $ds.X -Labels $ds.Labels -ClassNames $ds.ClassNames
#
# 7. # Feature extraction (frozen encoder)
# $feats = Get-VBAFBatchFeatures -Model $m -Data $ds.X -FromLayer 0
# # $feats[0] = 8-dim feature vector for sample 0
#
# 8. # Freeze encoder, fine-tune head only
# Set-VBAFAllLayersFrozen -Model $m -Frozen $true
# Set-VBAFLayerFrozen -Model $m -LayerIndex 1 -Frozen $false
# $r = Invoke-VBAFFineTune -Model $m -Data $ds.X -Labels $ds.Labels -Epochs 100 -LR 0.05 -PrintEvery 10
# $r.Accuracy # should be > 0.8 after fine-tuning
#
# 9. # Save / Load weights
# Save-VBAFTLWeights -Model $m -Path "C:\Temp\MyModel.json"
# $m2 = Load-VBAFTLWeights -Path "C:\Temp\MyModel.json"
#
# --- PS 5.1 gotchas (same as Autoencoder module) ---
# -PrintEvery NOT -Verbose (reserved common parameter)
# Weight updates: $layer.W[$i] = value (direct element assignment, persists ✅)
# Functions in class methods: & (Get-Command Func) -Param val
# ==============================================================================

# ==============================================================================
# MODULE FOOTER
# ==============================================================================

Write-Host ' Functions loaded:' -ForegroundColor DarkGray
Write-Host ' Get-VBAFModelZoo Get-VBAFPretrainedModel' -ForegroundColor DarkGray
Write-Host ' Set-VBAFLayerFrozen Set-VBAFAllLayersFrozen' -ForegroundColor DarkGray
Write-Host ' Get-VBAFFrozenLayers Get-VBAFTrainableLayers' -ForegroundColor DarkGray
Write-Host ' Show-VBAFModelStatus Get-VBAFFeatures' -ForegroundColor DarkGray
Write-Host ' Get-VBAFBatchFeatures Invoke-VBAFFineTune' -ForegroundColor DarkGray
Write-Host ' Save-VBAFTLWeights Load-VBAFTLWeights' -ForegroundColor DarkGray
Write-Host ' Test-VBAFTLClassify Test-VBAFTransferLearning' -ForegroundColor DarkGray
Write-Host ' Classes: [TLModel]' -ForegroundColor DarkGray
Write-Host ''
Write-Host ' Quick start: Test-VBAFTransferLearning' -ForegroundColor Cyan
Write-Host ''