ai/neural/perceptron-controller.js

/**
 * @fileoverview Perceptron-Controller  –  Steuerlogik für das Perceptron-Playground.
 *
 * Aufgaben:
 * 1. Dataset auswählen & Datentabelle rendern (mit Vorhersagespalte)
 * 2. Ein einzelnes Neuron (2 Inputs → 1 Output, Step-Aktivierung) steuern
 * 3. Phasenbasiertes Delta-Regel-Training (Schritt / Auto / Speed-Slider)
 * 4. Decision Boundary, Gleichung, Fehler-Highlights live updaten
 * 5. Lernregel-Visualisierung mit Gewichts-Transition
 * 6. Fehler-pro-Epoche Chart
 * 7. Gaussian-Generator Integration
 *
 * Training läuft direkt im Main-Thread (nur 1 Neuron, ~20 Datenpunkte →
 * kein Web-Worker nötig).
 *
 * @module perceptron-controller
 * @author Alexander Wolf
 */

'use strict';

/* global PerceptronVisualizer, PERCEPTRON_DATASETS, GaussianGenerator,
          Neuron, ACTIVATION_FUNCTIONS, NN_CONSTANTS, DebugConfig, DEBUG_DOMAINS */

/**
 * Training Phases for step-through visualization.
 * @enum {number}
 */
const TRAIN_PHASE = {
    IDLE:           0,
    SELECT_POINT:   1,  // Randomly select misclassified point
    UPDATE_W1:      2,  // Show Δw₁ calculation
    UPDATE_W2:      3,  // Show Δw₂ calculation
    APPLY_WEIGHTS:  4,  // Apply new weights, show old→new
    RETRAIN_VIZ:    5,  // Update decision line, table predictions, error chart
};

/**
 * @class PerceptronController
 * @description Orchestriert Perceptron-Training, Visualisierung und UI.
 */
class PerceptronController {

    /**
     * @param {Object} ids  – DOM-Element-IDs
     * @param {string} ids.plotContainer
     * @param {string} ids.archContainer
     * @param {string} ids.equationContainer
     * @param {string} ids.errorChartContainer
     * @param {string} ids.dataTableBody
     * @param {string} ids.dataTableWrapper
     * @param {string} ids.tableTitle
     * @param {string} ids.learningRuleDisplay
     * @param {string} ids.datasetSelector
     * @param {string} ids.learnRateInput
     * @param {string} ids.maxEpochsInput
     * @param {string} ids.speedSlider
     * @param {string} ids.speedLabel
     * @param {string} ids.trainBtn
     * @param {string} ids.stepBtn
     * @param {string} ids.resetBtn
     * @param {string} ids.epochDisplay
     * @param {string} ids.errorDisplay
     * @param {string} ids.accuracyDisplay
     * @param {string} ids.gaussianConfig
     * @param {string} ids.errorChartToggle
     */
    constructor(ids) {
        /** @private */
        this._ids = ids;

        const C = typeof NN_CONSTANTS !== 'undefined' ? NN_CONSTANTS : {};

        // --- Hyperparameter ---
        /** @private */
        this._learningRate = C.PERCEPTRON_DEFAULT_LEARN_RATE || 0.1;
        /** @private */
        this._maxEpochs = C.PERCEPTRON_MAX_EPOCHS || 500;
        /** @private */
        this._trainDelayMin = C.PERCEPTRON_TRAIN_DELAY_MIN_MS || 10;
        /** @private */
        this._trainDelayMax = C.PERCEPTRON_TRAIN_DELAY_MAX_MS || 1200;
        /** @private */
        this._trainDelay = C.PERCEPTRON_TRAIN_DELAY_MS || 200;

        // --- State ---
        /** @private @type {string} */
        this._currentDatasetKey = 'schwimmen';
        /** @private @type {PerceptronDataset|null} */
        this._dataset = null;
        /** @private @type {Neuron|null} */
        this._neuron = null;
        /** @private */
        this._epoch = 0;
        /** @private */
        this._totalError = 0;
        /** @private */
        this._isTraining = false;
        /** @private */
        this._trainTimerId = null;

        // Phase-based step state
        /** @private */
        this._currentPhase = TRAIN_PHASE.IDLE;
        /** @private */
        this._selectedPointIdx = -1;
        /** @private @type {{ w1: number, w2: number, bias: number }|null} */
        this._prevWeights = null;
        /** @private */
        this._currentError = 0;

        // --- Visualizer ---
        /** @private @type {PerceptronVisualizer|null} */
        this._viz = null;

        // --- DOM Refs (cached) ---
        /** @private */
        this._dom = {};

        this._init();
    }

    // ========================================================================
    // INIT
    // ========================================================================

    /** @private */
    _init() {
        this._cacheDom();
        this._bindEvents();

        this._viz = new PerceptronVisualizer({
            plotContainerId:      this._ids.plotContainer,
            archContainerId:      this._ids.archContainer,
            equationContainerId:  this._ids.equationContainer,
            errorChartContainerId: this._ids.errorChartContainer,
        });

        this._loadDataset(this._currentDatasetKey);
        this._resetNeuron();

        this._debugLog('debug', 'Controller initialisiert', {
            dataset: this._currentDatasetKey,
            learningRate: this._learningRate,
        });
    }

    /** @private */
    _cacheDom() {
        const ids = this._ids;
        this._dom = {
            tableBody:       document.getElementById(ids.dataTableBody),
            tableWrapper:    document.getElementById(ids.dataTableWrapper),
            tableTitle:      document.getElementById(ids.tableTitle),
            learnRuleDisp:   document.getElementById(ids.learningRuleDisplay),
            datasetSelect:   document.getElementById(ids.datasetSelector),
            learnRate:       document.getElementById(ids.learnRateInput),
            maxEpochs:       document.getElementById(ids.maxEpochsInput),
            speedSlider:     document.getElementById(ids.speedSlider),
            speedLabel:      document.getElementById(ids.speedLabel),
            trainBtn:        document.getElementById(ids.trainBtn),
            stepBtn:         document.getElementById(ids.stepBtn),
            resetBtn:        document.getElementById(ids.resetBtn),
            epochDisplay:    document.getElementById(ids.epochDisplay),
            errorDisplay:    document.getElementById(ids.errorDisplay),
            accuracyDisplay: document.getElementById(ids.accuracyDisplay),
            gaussianConfig:  document.getElementById(ids.gaussianConfig),
            errorChartToggle: document.getElementById(ids.errorChartToggle),
            errorChartContainer: document.getElementById(ids.errorChartContainer),
        };
    }

    /** @private */
    _bindEvents() {
        const d = this._dom;

        if (d.datasetSelect) {
            d.datasetSelect.addEventListener('change', () => {
                this._currentDatasetKey = d.datasetSelect.value;
                this._loadDataset(this._currentDatasetKey);
                this._resetNeuron();
            });
        }

        if (d.trainBtn)  d.trainBtn.addEventListener('click',  () => this.toggleTrain());
        if (d.stepBtn)   d.stepBtn.addEventListener('click',   () => this.manualStep());
        if (d.resetBtn)  d.resetBtn.addEventListener('click',  () => this._fullReset());

        // Lernrate live
        if (d.learnRate) {
            d.learnRate.addEventListener('change', () => {
                this._learningRate = parseFloat(d.learnRate.value) || 0.1;
            });
        }
        if (d.maxEpochs) {
            d.maxEpochs.addEventListener('change', () => {
                this._maxEpochs = parseInt(d.maxEpochs.value, 10) || 500;
            });
        }

        // Speed slider
        if (d.speedSlider) {
            d.speedSlider.addEventListener('input', () => {
                const val = parseInt(d.speedSlider.value, 10);
                // Invert: slider left = slow (high delay), right = fast (low delay)
                this._trainDelay = this._trainDelayMax - val + this._trainDelayMin;
                this._updateSpeedLabel();
            });
        }

        // Gaussian generator
        this._initGaussianControls();

        // Error chart toggle
        if (this._dom.errorChartToggle) {
            this._dom.errorChartToggle.addEventListener('click', () => {
                this._toggleErrorChart();
            });
        }
    }

    // ========================================================================
    // DATASET
    // ========================================================================

    /**
     * @private
     * @param {string} key
     */
    _loadDataset(key) {
        this._dataset = PERCEPTRON_DATASETS[key];
        if (!this._dataset) return;

        const isGenerator = this._dataset.isGenerated === true;
        this._renderTable();
        this._viz.setDataset(this._dataset);
        this._updateTableTitle();
        this._toggleGaussianConfig(isGenerator);

        this._debugLog('debug', 'Dataset loaded', {
            name: this._dataset.name,
            points: this._dataset.points.length,
        });
    }

    /**
     * Updates the table panel header with dataset name/description.
     * @private
     */
    _updateTableTitle() {
        if (!this._dom.tableTitle || !this._dataset) return;
        const ds = this._dataset;
        this._dom.tableTitle.textContent =
            `📊 ${ds.icon || ''} ${ds.name}`;
    }

    /** @private */
    _renderTable() {
        const tbody = this._dom.tableBody;
        if (!tbody || !this._dataset) return;

        const ds = this._dataset;
        tbody.innerHTML = '';

        // Update header if feature names changed
        const thead = tbody.closest('table')?.querySelector('thead');
        if (thead) {
            const fn = ds.featureNames || ds.axisLabels;
            thead.innerHTML = `<tr>
                <th>#</th>
                <th>${fn.x}</th>
                <th>${fn.y}</th>
                <th>Label</th>
                <th>ŷ</th>
            </tr>`;
        }

        ds.points.forEach((pt, i) => {
            const tr = document.createElement('tr');
            tr.setAttribute('data-idx', i);
            const classLabel = ds.classLabels[pt.label];
            const cssClass = pt.label === 0 ? 'class-0' : 'class-1';

            tr.innerHTML = `
                <td>${i + 1}</td>
                <td>${pt.x.toFixed(1)}</td>
                <td>${pt.y.toFixed(1)}</td>
                <td><span class="label-badge ${cssClass}">${classLabel}</span></td>
                <td class="pred-cell">—</td>
            `;
            tbody.appendChild(tr);
        });
    }

    /**
     * Updates prediction column and row highlights.
     * @private
     */
    _updateTablePredictions() {
        const tbody = this._dom.tableBody;
        if (!tbody || !this._neuron || !this._dataset) return;

        const stepFn = ACTIVATION_FUNCTIONS.step.fn;
        const rows = tbody.querySelectorAll('tr');

        rows.forEach((row) => {
            const idx = parseInt(row.getAttribute('data-idx'), 10);
            if (isNaN(idx)) return;

            const pt = this._dataset.points[idx];
            if (!pt) return;

            const pred = this._neuron.forward(new Float64Array([pt.x, pt.y]), stepFn);
            const predCell = row.querySelector('.pred-cell');
            const isCorrect = pred === pt.label;

            if (predCell) {
                const predLabel = this._dataset.classLabels[pred] || String(pred);
                predCell.innerHTML = `<span class="pred-badge ${isCorrect ? 'correct' : 'wrong'}">${predLabel}</span>`;
            }

            // Row error highlight
            row.classList.toggle('row-error', !isCorrect);
            // Selected row
            row.classList.toggle('row-selected', idx === this._selectedPointIdx);
        });
    }

    // ========================================================================
    // NEURON
    // ========================================================================

    /** @private */
    _resetNeuron() {
        this._stopTraining();
        this._neuron = new Neuron(2, 0, { initMethod: 'random', initScale: NN_CONSTANTS.PERCEPTRON_INIT_SCALE });
        this._epoch = 0;
        this._totalError = 0;
        this._currentPhase = TRAIN_PHASE.IDLE;
        this._selectedPointIdx = -1;
        this._prevWeights = null;

        this._updateViz();
        this._updateStats();
        this._updateTablePredictions();
        this._viz.clearErrors();
        this._viz.clearErrorHistory();
        this._viz.highlightSelectedPoint(-1);
        this._clearLearningRuleDisplay();
    }

    // ========================================================================
    // PHASE-BASED STEP TRAINING
    // ========================================================================

    /**
     * Executes one manual step-through phase.
     * Called by the Step button or by auto-training.
     *
     * @param {boolean} [fastMode=false] - Skip visualization phases
     */
    manualStep(fastMode = false) {
        if (!this._neuron || !this._dataset) return;

        if (fastMode) {
            this._executeFullStep();
            return;
        }

        // Advance through phases
        switch (this._currentPhase) {
            case TRAIN_PHASE.IDLE:
            case TRAIN_PHASE.RETRAIN_VIZ:
                this._phaseSelectPoint();
                break;
            case TRAIN_PHASE.SELECT_POINT:
                this._phaseShowW1Update();
                break;
            case TRAIN_PHASE.UPDATE_W1:
                this._phaseShowW2Update();
                break;
            case TRAIN_PHASE.UPDATE_W2:
                this._phaseApplyWeights();
                break;
            case TRAIN_PHASE.APPLY_WEIGHTS:
                this._phaseRetrainViz();
                break;
            default:
                this._phaseSelectPoint();
        }
    }

    /**
     * Phase 1: Select a random misclassified point.
     * @private
     */
    _phaseSelectPoint() {
        this._currentPhase = TRAIN_PHASE.SELECT_POINT;
        const stepFn = ACTIVATION_FUNCTIONS.step.fn;
        const points = this._dataset.points;

        // Find all misclassified
        const misclassified = [];
        for (let i = 0; i < points.length; i++) {
            const pred = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
            if (pred !== points[i].label) {
                misclassified.push(i);
            }
        }

        if (misclassified.length === 0) {
            // Converged!
            this._epoch++;
            this._totalError = 0;
            this._updateStats();
            this._viz.pushErrorHistory(0);
            this._showLearningRuleText('Alle Punkte korrekt klassifiziert! ✅');
            this._currentPhase = TRAIN_PHASE.IDLE;
            return;
        }

        // Random selection
        this._selectedPointIdx = misclassified[Math.floor(Math.random() * misclassified.length)];
        const pt = points[this._selectedPointIdx];
        const pred = this._neuron.forward(new Float64Array([pt.x, pt.y]), stepFn);
        this._currentError = pt.label - pred;

        // Save old weights
        this._prevWeights = {
            w1: this._neuron.weights[0],
            w2: this._neuron.weights[1],
            bias: this._neuron.bias,
        };

        // Visual updates
        this._viz.highlightSelectedPoint(this._selectedPointIdx);
        this._viz.updateArchInputValues(pt.x, pt.y, null);
        this._updateTablePredictions();

        // Show which point was selected
        this._showLearningRuleText(
            `<strong>Phase 1:</strong> Punkt #${this._selectedPointIdx + 1} ` +
            `(${pt.x.toFixed(1)}, ${pt.y.toFixed(1)}) zufällig gewählt. ` +
            `Label = ${pt.label}, ŷ = ${pred} → ` +
            `δ = ${pt.label} − ${pred} = <span class="rule-part part-delta">${this._currentError}</span>`
        );

        this._updatePhaseIndicator(1);
    }

    /**
     * Phase 2: Show w₁ update formula.
     * @private
     */
    _phaseShowW1Update() {
        this._currentPhase = TRAIN_PHASE.UPDATE_W1;
        const pt = this._dataset.points[this._selectedPointIdx];
        const lr = this._learningRate;
        const delta = this._currentError;
        const oldW1 = this._prevWeights.w1;
        const dw1 = lr * delta * pt.x;
        const newW1 = oldW1 + dw1;

        // Compute sum for architecture display
        const sum = oldW1 * pt.x + this._prevWeights.w2 * pt.y + this._prevWeights.bias;
        this._viz.updateArchInputValues(pt.x, pt.y, sum);

        this._showLearningRuleText(
            `<strong>Phase 2:</strong> w₁<sup>neu</sup> = w₁<sup>alt</sup> + ` +
            `<span class="rule-part part-eta">η=${lr}</span> · ` +
            `<span class="rule-part part-delta">δ=${delta}</span> · ` +
            `<span class="rule-part part-input">${this._featureLabel('x')}=${pt.x.toFixed(1)}</span><br>` +
            `w₁: <span class="weight-old">${oldW1.toFixed(3)}</span> → ` +
            `<span class="weight-new">${newW1.toFixed(3)}</span>`
        );

        this._updatePhaseIndicator(2);
    }

    /**
     * Phase 3: Show w₂ update formula.
     * @private
     */
    _phaseShowW2Update() {
        this._currentPhase = TRAIN_PHASE.UPDATE_W2;
        const pt = this._dataset.points[this._selectedPointIdx];
        const lr = this._learningRate;
        const delta = this._currentError;
        const oldW2 = this._prevWeights.w2;
        const dw2 = lr * delta * pt.y;
        const newW2 = oldW2 + dw2;
        const oldBias = this._prevWeights.bias;
        const db = lr * delta;
        const newBias = oldBias + db;

        this._showLearningRuleText(
            `<strong>Phase 3:</strong> w₂<sup>neu</sup> = w₂<sup>alt</sup> + ` +
            `<span class="rule-part part-eta">η=${lr}</span> · ` +
            `<span class="rule-part part-delta">δ=${delta}</span> · ` +
            `<span class="rule-part part-input">${this._featureLabel('y')}=${pt.y.toFixed(1)}</span><br>` +
            `w₂: <span class="weight-old">${oldW2.toFixed(3)}</span> → ` +
            `<span class="weight-new">${newW2.toFixed(3)}</span><br>` +
            `b<sup>neu</sup> = b<sup>alt</sup> + η · δ → ` +
            `b: <span class="weight-old">${oldBias.toFixed(3)}</span> → ` +
            `<span class="weight-new">${newBias.toFixed(3)}</span>`
        );

        this._updatePhaseIndicator(3);
    }

    /**
     * Phase 4: Apply the weight updates.
     * @private
     */
    _phaseApplyWeights() {
        this._currentPhase = TRAIN_PHASE.APPLY_WEIGHTS;
        const pt = this._dataset.points[this._selectedPointIdx];
        const lr = this._learningRate;
        const delta = this._currentError;

        // Apply
        this._neuron.weights[0] += lr * delta * pt.x;
        this._neuron.weights[1] += lr * delta * pt.y;
        this._neuron.bias       += lr * delta;

        // Show old → new in arch diagram
        this._viz.updateArchWeights(
            this._neuron.weights[0],
            this._neuron.weights[1],
            this._neuron.bias,
            { oldW1: this._prevWeights.w1, oldW2: this._prevWeights.w2, oldBias: this._prevWeights.bias }
        );

        this._showLearningRuleText(
            `<strong>Phase 4:</strong> Gewichte angewendet<br>` +
            `w₁: <span class="weight-old">${this._prevWeights.w1.toFixed(3)}</span> → ` +
            `<span class="weight-new">${this._neuron.weights[0].toFixed(3)}</span> &nbsp; ` +
            `w₂: <span class="weight-old">${this._prevWeights.w2.toFixed(3)}</span> → ` +
            `<span class="weight-new">${this._neuron.weights[1].toFixed(3)}</span> &nbsp; ` +
            `b: <span class="weight-old">${this._prevWeights.bias.toFixed(3)}</span> → ` +
            `<span class="weight-new">${this._neuron.bias.toFixed(3)}</span>`
        );

        this._updatePhaseIndicator(4);
    }

    /**
     * Phase 5: Update vis after weight change.
     * @private
     */
    _phaseRetrainViz() {
        this._currentPhase = TRAIN_PHASE.RETRAIN_VIZ;
        this._epoch++;

        this._updateViz();
        this._updateTablePredictions();

        // Count errors for chart
        const stepFn = ACTIVATION_FUNCTIONS.step.fn;
        const points = this._dataset.points;
        const errorIndices = [];
        for (let i = 0; i < points.length; i++) {
            const pred = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
            if (pred !== points[i].label) errorIndices.push(i);
        }
        this._totalError = errorIndices.length;
        this._viz.highlightErrors(errorIndices);
        this._viz.pushErrorHistory(errorIndices.length);
        this._viz.highlightSelectedPoint(-1);
        this._selectedPointIdx = -1;

        this._updateStats();

        this._showLearningRuleText(
            `<strong>Phase 5:</strong> Visualisierung aktualisiert. ` +
            `Epoche ${this._epoch}: ${errorIndices.length} Fehler.`
        );

        // Reset arch weights to normal display
        this._viz.updateArchWeights(
            this._neuron.weights[0],
            this._neuron.weights[1],
            this._neuron.bias
        );
        this._viz.updateArchInputValues(null, null, null);

        this._updatePhaseIndicator(5);

        this._debugLog('debug', 'Step complete', {
            epoch: this._epoch,
            errors: errorIndices.length,
            w1: this._neuron.weights[0],
            w2: this._neuron.weights[1],
            bias: this._neuron.bias,
        });
    }

    /**
     * Executes a full training step without phase visualization.
     * Used for fast auto-training.
     * @private
     */
    _executeFullStep() {
        if (!this._neuron || !this._dataset) return;

        const stepFn = ACTIVATION_FUNCTIONS.step.fn;
        const points = this._dataset.points;
        const lr = this._learningRate;

        // Find misclassified
        const misclassified = [];
        for (let i = 0; i < points.length; i++) {
            const pred = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
            if (pred !== points[i].label) misclassified.push(i);
        }

        if (misclassified.length === 0) {
            this._epoch++;
            this._totalError = 0;
            this._updateStats();
            this._updateViz();
            this._viz.pushErrorHistory(0);
            return;
        }

        // Random misclassified point
        const idx = misclassified[Math.floor(Math.random() * misclassified.length)];
        const pt = points[idx];
        const pred = this._neuron.forward(new Float64Array([pt.x, pt.y]), stepFn);
        const error = pt.label - pred;

        // Delta rule update
        this._neuron.weights[0] += lr * error * pt.x;
        this._neuron.weights[1] += lr * error * pt.y;
        this._neuron.bias       += lr * error;

        this._epoch++;

        // Recount errors
        const errorIndices = [];
        for (let i = 0; i < points.length; i++) {
            const p = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
            if (p !== points[i].label) errorIndices.push(i);
        }
        this._totalError = errorIndices.length;

        this._updateViz();
        this._updateStats();
        this._updateTablePredictions();
        this._viz.highlightErrors(errorIndices);
        this._viz.pushErrorHistory(errorIndices.length);
    }

    // ========================================================================
    // AUTO-TRAINING
    // ========================================================================

    /** Startet / stoppt das automatische Training. */
    toggleTrain() {
        if (this._isTraining) {
            this._stopTraining();
        } else {
            this._startTraining();
        }
    }

    /** @private */
    _startTraining() {
        if (this._isTraining) return;
        this._isTraining = true;
        if (this._dom.trainBtn)  this._dom.trainBtn.textContent = '⏸ Stopp';
        if (this._dom.stepBtn)   this._dom.stepBtn.disabled = true;
        if (this._dom.datasetSelect) this._dom.datasetSelect.disabled = true;

        // Reset phase for auto
        this._currentPhase = TRAIN_PHASE.IDLE;
        this._trainLoop();
    }

    /** @private */
    _stopTraining() {
        this._isTraining = false;
        if (this._trainTimerId !== null) {
            clearTimeout(this._trainTimerId);
            this._trainTimerId = null;
        }
        if (this._dom.trainBtn)  this._dom.trainBtn.textContent = '▶ Trainieren';
        if (this._dom.stepBtn)   this._dom.stepBtn.disabled = false;
        if (this._dom.datasetSelect) this._dom.datasetSelect.disabled = false;
    }

    /** @private */
    _trainLoop() {
        if (!this._isTraining) return;

        const isFast = this._trainDelay <= this._trainDelayMin * 3;

        if (isFast) {
            // Fast mode: do multiple full steps per tick
            const batchSize = NN_CONSTANTS.PERCEPTRON_FAST_BATCH_SIZE;
            for (let i = 0; i < batchSize; i++) {
                this._executeFullStep();
                if (this._totalError === 0 || this._epoch >= this._maxEpochs) break;
            }
        } else {
            // Visual mode: step through phases
            this.manualStep(false);
        }

        // Stop conditions
        if (this._totalError === 0) {
            this._showLearningRuleText(`Konvergiert nach ${this._epoch} Epochen! ✅`);
            this._showErrorChart();
            this._stopTraining();
            return;
        }
        if (this._epoch >= this._maxEpochs) {
            this._showLearningRuleText(`Max. Epochen (${this._maxEpochs}) erreicht. Keine Konvergenz. ⚠️`);
            this._showErrorChart();
            this._stopTraining();
            return;
        }

        const delay = isFast ? this._trainDelayMin : this._trainDelay;
        this._trainTimerId = setTimeout(() => this._trainLoop(), delay);
    }

    // ========================================================================
    // VIZ UPDATE
    // ========================================================================

    /** @private */
    _updateViz() {
        if (!this._neuron) return;
        const w1 = this._neuron.weights[0];
        const w2 = this._neuron.weights[1];
        const b  = this._neuron.bias;

        this._viz.updateDecisionLine(w1, w2, b);
        this._viz.updateArchWeights(w1, w2, b);
    }

    /** @private */
    _updateStats() {
        const d = this._dom;
        if (d.epochDisplay)    d.epochDisplay.textContent = this._epoch;
        if (d.errorDisplay)    d.errorDisplay.textContent = this._totalError;
        if (d.accuracyDisplay && this._dataset) {
            const total = this._dataset.points.length;
            const correct = total - this._totalError;
            d.accuracyDisplay.textContent = `${(correct / total * 100).toFixed(1)}%`;
        }
    }

    // ========================================================================
    // LEARNING RULE DISPLAY
    // ========================================================================

    /**
     * @private
     * @param {string} html
     */
    _showLearningRuleText(html) {
        if (!this._dom.learnRuleDisp) return;
        this._dom.learnRuleDisp.innerHTML = `<div class="rule-formula">${html}</div>`;
    }

    /** @private */
    _clearLearningRuleDisplay() {
        if (!this._dom.learnRuleDisp) return;
        this._dom.learnRuleDisp.innerHTML =
            '<div class="rule-formula">' +
            'w<sub>i,neu</sub> = w<sub>i,alt</sub> + η · (y − ŷ) · x<sub>i</sub>' +
            '</div>';
    }

    /**
     * @private
     * @param {number} phase  1-5
     */
    _updatePhaseIndicator(phase) {
        if (!this._dom.learnRuleDisp) return;

        let existing = this._dom.learnRuleDisp.querySelector('.phase-indicator');
        if (!existing) {
            existing = document.createElement('div');
            existing.className = 'phase-indicator';
            this._dom.learnRuleDisp.appendChild(existing);
        }

        let html = '';
        for (let i = 1; i <= 5; i++) {
            const cls = i < phase ? 'done' : (i === phase ? 'active' : '');
            html += `<div class="phase-dot ${cls}" title="Phase ${i}"></div>`;
        }
        existing.innerHTML = html;
    }

    /**
     * @private
     * @param {'x'|'y'} axis
     * @returns {string}
     */
    _featureLabel(axis) {
        if (!this._dataset) return axis === 'x' ? 'x₁' : 'x₂';
        if (this._dataset.featureNames) return this._dataset.featureNames[axis];
        return this._dataset.axisLabels ? this._dataset.axisLabels[axis] : (axis === 'x' ? 'x₁' : 'x₂');
    }

    // ========================================================================
    // SPEED SLIDER
    // ========================================================================

    /** @private */
    _updateSpeedLabel() {
        if (!this._dom.speedLabel) return;
        if (this._trainDelay <= this._trainDelayMin * 3) {
            this._dom.speedLabel.textContent = 'Turbo';
        } else if (this._trainDelay > this._trainDelayMax * 0.7) {
            this._dom.speedLabel.textContent = 'Langsam';
        } else {
            this._dom.speedLabel.textContent = 'Mittel';
        }
    }

    // ========================================================================
    // GAUSSIAN GENERATOR
    // ========================================================================

    /** @private */
    _initGaussianControls() {
        const panel = this._dom.gaussianConfig;
        if (!panel) return;

        const btn = panel.querySelector('.btn-generate');
        if (btn) {
            btn.addEventListener('click', () => this._generateGaussianDataset());
        }

        // Bind value displays to sliders
        panel.querySelectorAll('input[type="range"]').forEach((slider) => {
            const valEl = slider.parentElement?.querySelector('.slider-value');
            if (valEl) {
                slider.addEventListener('input', () => {
                    valEl.textContent = slider.value;
                });
            }
        });
    }

    /** @private */
    _generateGaussianDataset() {
        if (typeof GaussianGenerator === 'undefined') return;
        const panel = this._dom.gaussianConfig;
        if (!panel) return;

        const val = (id) => parseFloat(panel.querySelector(`#${id}`)?.value || '0');

        const cluster0 = {
            muX: val('g-mu-x0'), muY: val('g-mu-y0'),
            sigmaX: val('g-sigma-x0'), sigmaY: val('g-sigma-y0'),
            n: parseInt(panel.querySelector('#g-n0')?.value || '10', 10),
            label: 0,
        };
        const cluster1 = {
            muX: val('g-mu-x1'), muY: val('g-mu-y1'),
            sigmaX: val('g-sigma-x1'), sigmaY: val('g-sigma-y1'),
            n: parseInt(panel.querySelector('#g-n1')?.value || '10', 10),
            label: 1,
        };

        const points = GaussianGenerator.generateTwoClusters(cluster0, cluster1);

        // Update dataset
        const ds = PERCEPTRON_DATASETS.generisch;
        ds.points = points;

        // Adjust ranges to fit generated data
        const allX = points.map((p) => p.x);
        const allY = points.map((p) => p.y);
        const margin = 1;
        ds.xRange = [Math.floor(Math.min(...allX) - margin), Math.ceil(Math.max(...allX) + margin)];
        ds.yRange = [Math.floor(Math.min(...allY) - margin), Math.ceil(Math.max(...allY) + margin)];

        this._loadDataset('generisch');
        this._resetNeuron();
    }

    /**
     * Toggles between table view and gaussian generator config.
     * @private
     * @param {boolean} show
     */
    _toggleGaussianConfig(show) {
        const panel = this._dom.gaussianConfig;
        if (!panel) return;
        panel.classList.toggle('visible', show);

        // Hide table wrapper when generator is active, show when not
        if (this._dom.tableWrapper) {
            this._dom.tableWrapper.classList.toggle('hidden', show);
        }
    }

    /**
     * Toggles the error chart visibility inside the learning rule panel.
     * @private
     */
    _toggleErrorChart() {
        const chart = this._dom.errorChartContainer;
        const btn = this._dom.errorChartToggle;
        if (!chart) return;
        const isHidden = chart.classList.toggle('hidden');
        if (btn) btn.classList.toggle('active', !isHidden);
    }

    /**
     * Shows the error chart (used automatically after convergence).
     * @private
     */
    _showErrorChart() {
        const chart = this._dom.errorChartContainer;
        const btn = this._dom.errorChartToggle;
        if (!chart) return;
        chart.classList.remove('hidden');
        if (btn) btn.classList.add('active');
    }

    // ========================================================================
    // FULL RESET
    // ========================================================================

    /** @private */
    _fullReset() {
        this._stopTraining();
        this._loadDataset(this._currentDatasetKey);
        this._resetNeuron();
    }

    // ========================================================================
    // DEBUG LOGGING
    // ========================================================================

    /**
     * @private
     * @param {string} level
     * @param {string} msg
     * @param {Object} [payload]
     */
    _debugLog(level, msg, payload) {
        if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
            DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, level, msg, payload);
        }
    }
}


// ===== Universeller Export =====
(function (root) {
    root.PerceptronController = PerceptronController;
})(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : this);