/**
* @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> ` +
`w₂: <span class="weight-old">${this._prevWeights.w2.toFixed(3)}</span> → ` +
`<span class="weight-new">${this._neuron.weights[1].toFixed(3)}</span> ` +
`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);