| undefined,
+ ): Unsubscriber {
+ return this.store.subscribe(run, invalidate);
+ }
+}
diff --git a/src/exampleDataset.json b/src/exampleDataset.json
index df490469b..dadd8e717 100644
--- a/src/exampleDataset.json
+++ b/src/exampleDataset.json
@@ -2,6 +2,7 @@
{
"ID": 1705437793875,
"name": "Shake",
+ "color": "#FCA311",
"recordings": [
{
"ID": 1705437992143,
@@ -366,6 +367,7 @@
{
"ID": 1705437833024,
"name": "Still",
+ "color": "#00fff4",
"recordings": [
{
"ID": 1705437969952,
@@ -616,6 +618,7 @@
{
"ID": 1705437876740,
"name": "Circle",
+ "color": "#b1e400",
"recordings": [
{
"ID": 1705438034019,
diff --git a/src/main.ts b/src/main.ts
index cd197a048..e5ad61524 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -7,7 +7,6 @@
import App from './App.svelte';
import './appInsights';
import 'virtual:windi.css';
-
const app = new App({
target: document.body,
});
diff --git a/src/menus/DataMenu.svelte b/src/menus/DataMenu.svelte
index 1f2852958..3ef02616c 100644
--- a/src/menus/DataMenu.svelte
+++ b/src/menus/DataMenu.svelte
@@ -5,9 +5,10 @@
-->
diff --git a/src/messages/ui.da.json b/src/messages/ui.da.json
index 68be5e698..05072efba 100644
--- a/src/messages/ui.da.json
+++ b/src/messages/ui.da.json
@@ -56,6 +56,7 @@
"content.trainer.failure.todo": "Gå tilbage til datasiden og ændr i din data.",
"content.trainer.controlbar.filters": "Filtre",
"content.trianer.lossGraph.title": "Loss over tid",
+ "content.trainer.knn.neighbours": "Naboer",
"content.filters.NoDataHeader": "Der er ikke noget data",
"content.filters.NoDataBody": "Gå til Data-siden for at indsamle data.",
"content.filters.max.title": "Maksværdier",
@@ -123,6 +124,7 @@
"menu.trainer.TrainingFinished.body": "Gå til Model-siden for at undersøge hvor godt din model virker",
"menu.trainer.noFilters": "Du har ikke valgt nogle filtre. Der skal mindst være valgt 1 filter for at kunne træne en model",
"menu.trainer.isTrainingModelButton": "Træner model...",
+ "menu.trainer.knn.onlyTwoFilters": "For at kunne se visualisering skal du vælge præcist 2 filtre",
"menu.model.helpHeading": "Model",
"menu.model.helpBody": "Modellen kan bruges i et interaktivt system. Her bruger vi den trænede model til at forudsige bevægelser. Du kan tilslutte endnu en micro:bit og få den til at reagere på de bevægelser du laver.",
"menu.model.noModel": "Ingen model",
diff --git a/src/messages/ui.en.json b/src/messages/ui.en.json
index 8cbe68866..f3b6502b6 100644
--- a/src/messages/ui.en.json
+++ b/src/messages/ui.en.json
@@ -56,6 +56,7 @@
"content.trainer.failure.todo": "Return to the data page and change your data.",
"content.trainer.controlbar.filters": "Filters",
"content.trianer.lossGraph.title": "Loss over time",
+ "content.trainer.knn.neighbours": "Neighbours",
"content.filters.NoDataHeader": "No available data",
"content.filters.NoDataBody": "Go to the Data page to collect data samples.",
"content.filters.max.title": "Max values",
@@ -123,6 +124,7 @@
"menu.trainer.TrainingFinished.body": "Go to the Model-page to examine how well your model works",
"menu.trainer.noFilters": "No filters have been selected. You must enable at least 1 filter in order to train a model",
"menu.trainer.isTrainingModelButton": "Training model",
+ "menu.trainer.knn.onlyTwoFilters": "In order to visualize the data, you need to select exactly two filters",
"menu.model.helpHeading": "Model",
"menu.model.helpBody": "The model can be used in an interactive system. Here we use the trained model to predict gestures. You can connect another micro:bit and make it respond to the predicted gestures.",
"menu.model.noModel": "No model",
diff --git a/src/pages/DataPage.svelte b/src/pages/DataPage.svelte
index 51765e3ec..19a57e929 100644
--- a/src/pages/DataPage.svelte
+++ b/src/pages/DataPage.svelte
@@ -14,17 +14,18 @@
import NewGestureButton from '../components/NewGestureButton.svelte';
import StandardButton from '../components/buttons/StandardButton.svelte';
import { startConnectionProcess } from '../script/stores/connectDialogStore';
- import PleaseConnectFirst from '../components/PleaseConnectFirst.svelte';
import DataPageControlBar from '../components/datacollection/DataPageControlBar.svelte';
import Information from '../components/information/Information.svelte';
import { onMount } from 'svelte';
- import { gestures } from '../script/stores/Stores';
import FileUtility from '../script/repository/FileUtility';
import { get } from 'svelte/store';
import exampleDataset from '../exampleDataset.json';
import { GestureData } from '../script/domain/stores/gesture/Gesture';
+ import { stores } from '../script/stores/Stores';
+ import PleaseConnect from '../components/PleaseConnect.svelte';
let isConnectionDialogOpen = false;
+ const gestures = stores.getGestures();
$: hasSomeData = (): boolean => {
if ($gestures.length === 0) {
@@ -88,7 +89,7 @@
{#if !hasSomeData() && !$state.isInputConnected}
{:else}
@@ -148,12 +149,4 @@
{/if}
- {#if !hasSomeData()}
-
-
-
- {$t('content.data.noData.templateDataButton')}
-
-
- {/if}
diff --git a/src/pages/PlaygroundPage.svelte b/src/pages/PlaygroundPage.svelte
index e52909ca9..6e8080cce 100644
--- a/src/pages/PlaygroundPage.svelte
+++ b/src/pages/PlaygroundPage.svelte
@@ -6,7 +6,7 @@
+
+
+
+
+ {$t('menu.trainer.notEnoughDataHeader1')}
+
+
+ {$t('menu.trainer.notEnoughDataInfoBody')}
+
+
+
diff --git a/src/pages/training/KnnModelTrainingPageView.svelte b/src/pages/training/KnnModelTrainingPageView.svelte
new file mode 100644
index 000000000..c6d6419f8
--- /dev/null
+++ b/src/pages/training/KnnModelTrainingPageView.svelte
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
changeK(-1)}
+ class="bg-secondary font-bold text-secondarytext cursor-pointer select-none hover:bg-opacity-60 border-primary border-r-1 content-center px-2 rounded-l-xl">
+ -
+
+
changeK(1)}
+ class="bg-secondary border-primary text-secondarytext cursor-pointer hover:bg-opacity-60 select-none content-center px-2 rounded-r-xl">
+ +
+
+
+
+ {$knnConfig.k}
+ {$t('content.trainer.knn.neighbours')}
+
+
+
+
+ {#each $gestures as gesture, index}
+
+
+ {#if $state.isInputReady}
+
+ {(($confidences.get(gesture.ID) ?? 0) * 100).toFixed(2)}%
+
+ {/if}
+
+ {/each}
+
+
+ {#if $filters.length == 2}
+
+ {:else}
+
+
+ {$t('menu.trainer.knn.onlyTwoFilters')}
+
+
+ {/if}
+
diff --git a/src/pages/training/NeuralNetworkTrainingPageView.svelte b/src/pages/training/NeuralNetworkTrainingPageView.svelte
new file mode 100644
index 000000000..d1206e74e
--- /dev/null
+++ b/src/pages/training/NeuralNetworkTrainingPageView.svelte
@@ -0,0 +1,58 @@
+
+
+
+
+ {#if $model.isTraining}
+
+
+
+ {#if !hasFeature(Feature.LOSS_GRAPH)}
+
{$t('menu.trainer.isTrainingModelButton')}
+ {/if}
+ {:else}
+ {#if $model.isTrained && !hasFeature(Feature.LOSS_GRAPH)}
+
{$t('menu.trainer.TrainingFinished')}
+
{$t('menu.trainer.TrainingFinished.body')}
+ {/if}
+
+ {$t(trainButtonSimpleLabel)}
+
+ {/if}
+ {#if $loss.length > 0 && hasFeature(Feature.LOSS_GRAPH)}
+
+ {/if}
+
diff --git a/src/pages/training/TrainModelButton.svelte b/src/pages/training/TrainModelButton.svelte
index 75d5db58b..4d384dc6a 100644
--- a/src/pages/training/TrainModelButton.svelte
+++ b/src/pages/training/TrainModelButton.svelte
@@ -6,68 +6,25 @@
{#if hasFeature(Feature.KNN_MODEL)}
diff --git a/src/pages/training/TrainModelButton.ts b/src/pages/training/TrainModelButton.ts
new file mode 100644
index 000000000..41ca3cb76
--- /dev/null
+++ b/src/pages/training/TrainModelButton.ts
@@ -0,0 +1,72 @@
+/**
+ * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+import { Writable, get } from 'svelte/store';
+import { DropdownOption } from '../../components/buttons/Buttons';
+import { highlightedAxis } from '../../script/stores/uiStore';
+import ModelTrainer from '../../script/domain/ModelTrainer';
+import MLModel from '../../script/domain/MLModel';
+import Axes from '../../script/domain/Axes';
+import { stores } from '../../script/stores/Stores';
+import StaticConfiguration from '../../StaticConfiguration';
+import KNNNonNormalizedModelTrainer from '../../script/mlmodels/KNNNonNormalizedModelTrainer';
+import { extractAxisFromTrainingData } from '../../script/utils/graphUtils';
+import LayersModelTrainer, {
+ LossTrainingIteration,
+} from '../../script/mlmodels/LayersModelTrainer';
+import { FilterType } from '../../script/domain/FilterTypes';
+import Filters from '../../script/domain/Filters';
+import ModelRegistry, { ModelInfo } from '../../script/domain/ModelRegistry';
+import { knnConfig } from '../../script/stores/knnConfig';
+
+const classifier = stores.getClassifier();
+
+export const options: DropdownOption[] = ModelRegistry.getModels().map(model => {
+ return {
+ id: model.id,
+ label: model.title,
+ };
+});
+
+export const getModelTrainer = (
+ model: ModelInfo,
+ onTrainingIteration: (iteration: LossTrainingIteration) => void,
+): ModelTrainer => {
+ const currentAxis = get(highlightedAxis);
+ if (model.id === ModelRegistry.KNN.id) {
+ const offset = currentAxis === Axes.X ? 0 : currentAxis === Axes.Y ? 1 : 2;
+ return new KNNNonNormalizedModelTrainer(get(knnConfig).k, data =>
+ extractAxisFromTrainingData(data, offset, 3),
+ );
+ }
+ highlightedAxis.set(undefined);
+
+ return new LayersModelTrainer(StaticConfiguration.layersModelTrainingSettings, h => {
+ onTrainingIteration(h);
+ });
+};
+
+export const trainModel = (
+ selectedOption: Writable,
+ onTrainingIteration: (iteration: LossTrainingIteration) => void,
+) => {
+ const selectedModel = ModelRegistry.getModels().find(
+ model => model.id === get(selectedOption).id,
+ );
+ const model = classifier.getModel();
+
+ if (selectedModel?.id === 'KNN') {
+ const knnFilters = [FilterType.MAX, FilterType.MEAN];
+ const filters: Filters = classifier.getFilters();
+ filters.clear();
+ for (const filter of knnFilters) {
+ filters.add(filter);
+ }
+ }
+
+ if (selectedModel) {
+ model.train(getModelTrainer(selectedModel, onTrainingIteration));
+ }
+};
diff --git a/src/pages/training/TrainingFailedDialog.svelte b/src/pages/training/TrainingFailedDialog.svelte
index ffd7f1489..5d2ab229a 100644
--- a/src/pages/training/TrainingFailedDialog.svelte
+++ b/src/pages/training/TrainingFailedDialog.svelte
@@ -9,12 +9,12 @@
import StandardDialog from '../../components/dialogs/StandardDialog.svelte';
import { slide } from 'svelte/transition';
- import { classifier } from '../../script/stores/Stores';
import { TrainingStatus } from '../../script/domain/stores/Model';
+ import { stores } from '../../script/stores/Stores';
let isFailedTrainingDialogOpen = false;
- const model = classifier.getModel();
+ const model = stores.getClassifier().getModel();
$: {
if ($model.trainingStatus === TrainingStatus.Failure) {
diff --git a/src/pages/training/TrainingPage.svelte b/src/pages/training/TrainingPage.svelte
index 8e785f76e..de29f8e79 100644
--- a/src/pages/training/TrainingPage.svelte
+++ b/src/pages/training/TrainingPage.svelte
@@ -5,160 +5,29 @@
-->
-
-
- {
- navigate(Paths.FILTERS);
- }}>
- {$t('content.trainer.controlbar.filters')}
-
-
-
- {#if isUsingKNNModel}
-
- {/if}
- {#if !sufficientData}
-
-
- {$t('menu.trainer.notEnoughDataHeader1')}
-
-
- {$t('menu.trainer.notEnoughDataInfoBody')}
-
-
- {:else}
-
- {#if !$model.isTraining}
- {#if $filters.length == 0}
-
- {$t('menu.trainer.noFilters')}
-
- {:else}
-
- {
- resetLoss();
- trackModelEvent();
- }}
- onTrainingIteration={trainingIterationHandler} />
-
- {/if}
- {/if}
-
- {#if $model.isTrained}
-
- {$t('menu.trainer.TrainingFinished')}
-
-
- {$t('menu.trainer.TrainingFinished.body')}
-
- {/if}
- {#if $filters.length == 0}
-
- {$t('menu.trainer.noFilters')}
-
- {/if}
-
- {/if}
- {#if !$state.isInputConnected && !isUsingKNNModel}
-
- {#if $loss.length > 0 || $model.isTraining}
- {#if !CookieManager.hasFeatureFlag('loss-graph')}
- {#if $model.isTraining}
-
-
-
-
-
-
- {$t('menu.trainer.isTrainingModelButton')}
-
-
-
- {/if}
- {:else}
-
- {/if}
- {/if}
-
- {/if}
- {#if !$state.isInputConnected}
-
- {/if}
-
+
+ {#if !sufficientData}
+
+ {:else}
+
+ {/if}
+ {#if !$state.isInputConnected}
+
+ {/if}
diff --git a/src/pages/training/TrainingPage.ts b/src/pages/training/TrainingPage.ts
new file mode 100644
index 000000000..5fef88df3
--- /dev/null
+++ b/src/pages/training/TrainingPage.ts
@@ -0,0 +1,78 @@
+/**
+ * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+import { get, writable } from 'svelte/store';
+import { highlightedAxis, selectedModel } from '../../script/stores/uiStore';
+import Axes from '../../script/domain/Axes';
+import KNNNonNormalizedModelTrainer from '../../script/mlmodels/KNNNonNormalizedModelTrainer';
+import StaticConfiguration from '../../StaticConfiguration';
+import { extractAxisFromTrainingData } from '../../script/utils/graphUtils';
+import { stores } from '../../script/stores/Stores';
+import CookieManager from '../../script/CookieManager';
+import { appInsights } from '../../appInsights';
+import ModelRegistry, { ModelInfo } from '../../script/domain/ModelRegistry';
+import LayersModelTrainer, {
+ LossTrainingIteration,
+} from '../../script/mlmodels/LayersModelTrainer';
+import { knnConfig } from '../../script/stores/knnConfig';
+import Logger from '../../script/utils/Logger';
+
+export const loss = writable([]);
+
+const trainingIterationHandler = (h: LossTrainingIteration) => {
+ loss.update(newLoss => {
+ newLoss.push(h);
+ return newLoss;
+ });
+};
+
+const trainNNModel = async () => {
+ highlightedAxis.set(undefined);
+ loss.set([]);
+ const modelTrainer = new LayersModelTrainer(
+ StaticConfiguration.layersModelTrainingSettings,
+ trainingIterationHandler,
+ );
+ await stores.getClassifier().getModel().train(modelTrainer);
+};
+
+const trainKNNModel = async () => {
+ if (get(highlightedAxis) === undefined) {
+ highlightedAxis.set(Axes.X);
+ }
+ const currentAxis = get(highlightedAxis);
+ const offset = currentAxis === Axes.X ? 0 : currentAxis === Axes.Y ? 1 : 2;
+ const modelTrainer = new KNNNonNormalizedModelTrainer(
+ get(knnConfig).k,
+ data => {
+ const extractedData = extractAxisFromTrainingData(data, offset, 3);
+ Logger.log('TrainingPage', 'Extracted data: \n' + JSON.stringify(extractedData));
+ return extractedData;
+ }, // 3 assumes 3 axis
+ );
+ await stores.getClassifier().getModel().train(modelTrainer);
+};
+
+export const trainModel = async (model: ModelInfo) => {
+ Logger.log('TrainingPage', 'Training new model: ' + model.title);
+ // highlightedAxis.set(undefined);
+ if (ModelRegistry.KNN.id === model.id) {
+ await trainKNNModel();
+ } else if (ModelRegistry.NeuralNetwork.id === model.id) {
+ await trainNNModel();
+ }
+ trackModelEvent();
+};
+
+const trackModelEvent = () => {
+ if (CookieManager.getComplianceChoices().analytics) {
+ appInsights.trackEvent({
+ name: 'ModelTrained',
+ properties: {
+ modelType: get(selectedModel).id,
+ },
+ });
+ }
+};
diff --git a/src/pages/training/TrainingPageModelView.svelte b/src/pages/training/TrainingPageModelView.svelte
new file mode 100644
index 000000000..bdb8ba2a8
--- /dev/null
+++ b/src/pages/training/TrainingPageModelView.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ {#if showFilterList}
+
+ {/if}
+ {#if $selectedModel.id === ModelRegistry.KNN.id}
+
+ {:else if $selectedModel.id === ModelRegistry.NeuralNetwork.id}
+
+ {/if}
+
+
diff --git a/src/pages/training/TrainingPageTabs.svelte b/src/pages/training/TrainingPageTabs.svelte
new file mode 100644
index 000000000..5c21040b0
--- /dev/null
+++ b/src/pages/training/TrainingPageTabs.svelte
@@ -0,0 +1,58 @@
+
+
+
+{#if showTabBar}
+
+
+
+{:else}
+
+
+
+{/if}
diff --git a/src/script/ControlledStorage.ts b/src/script/ControlledStorage.ts
index d16d7c82b..73b242cad 100644
--- a/src/script/ControlledStorage.ts
+++ b/src/script/ControlledStorage.ts
@@ -1,3 +1,5 @@
+import StaticConfiguration from '../StaticConfiguration';
+
/**
* (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
*
@@ -9,10 +11,21 @@ type StoredValue = {
};
class ControlledStorage {
+ public static readonly localStorageVersion = 3;
+
public static get(key: string): T {
const storedValue = this.getStoredItem(key);
- const parsedValue = this.parseItem(storedValue);
- return parsedValue.value;
+ try {
+ const parsedValue = this.parseItem(storedValue);
+ return parsedValue.value;
+ } catch (error) {
+ console.log(
+ `An error occurred while parsing the stored value with key ${key}. The stored value will be deleted`,
+ error,
+ );
+ localStorage.removeItem(key);
+ }
+ throw new Error(`Could not parse value '${storedValue}'`);
}
public static set(key: string, value: T): void {
@@ -21,7 +34,17 @@ class ControlledStorage {
localStorage.setItem(key, stringified);
}
- public static has(key: string): boolean {
+ public static hasValid(key: string): boolean {
+ try {
+ this.parseItem(this.getStoredItem(key));
+ } catch (error) {
+ console.log(
+ `An error occurred while parsing the stored value with key ${key}. The stored value will be deleted`,
+ error,
+ );
+ localStorage.removeItem(key);
+ return false;
+ }
return !!localStorage.getItem(key);
}
@@ -46,12 +69,17 @@ class ControlledStorage {
if (!('value' in parsed)) {
throw new Error(`Could not parse value '${storedValue}'. It did not contain value`);
}
+ if (parsed.version !== ControlledStorage.localStorageVersion) {
+ throw new Error(
+ `Could not parse value '${storedValue}'. Version mismatch. Expected version ${ControlledStorage.localStorageVersion}, found version ${parsed.version}`,
+ );
+ }
return parsed;
}
private static encapsulateItem(value: T): StoredValue {
return {
- version: 1, // todo move this magic constant
+ version: ControlledStorage.localStorageVersion,
value,
};
}
diff --git a/src/script/FeatureToggles.ts b/src/script/FeatureToggles.ts
index bff7aa5f2..7caa22155 100644
--- a/src/script/FeatureToggles.ts
+++ b/src/script/FeatureToggles.ts
@@ -10,6 +10,8 @@ import Logger from './utils/Logger';
export enum Feature {
KNN_MODEL = 'knnModel',
TITLE = 'title',
+ LOSS_GRAPH = 'lossGraph',
+ MAKECODE = 'makecode',
}
export const hasFeature = (feature: Feature): boolean => {
diff --git a/src/script/domain/ClassifierFactory.ts b/src/script/domain/ClassifierFactory.ts
index 9194af007..53bf4cd8f 100644
--- a/src/script/domain/ClassifierFactory.ts
+++ b/src/script/domain/ClassifierFactory.ts
@@ -68,11 +68,7 @@ class ClassifierFactory {
return recordings.map(recording => {
const data = recording.data;
return {
- value: [
- ...filters.compute(data.x),
- ...filters.compute(data.y),
- ...filters.compute(data.z),
- ],
+ value: [...filters.compute(data.z)],
};
});
}
diff --git a/src/script/domain/ClassifierInput.ts b/src/script/domain/ClassifierInput.ts
index 30449a1b8..7ff74e507 100644
--- a/src/script/domain/ClassifierInput.ts
+++ b/src/script/domain/ClassifierInput.ts
@@ -7,6 +7,8 @@ import Filters from './Filters';
interface ClassifierInput {
getInput(filters: Filters): number[];
+
+ getNumberOfSamples(): number;
}
export default ClassifierInput;
diff --git a/src/script/domain/ClassifierRepository.ts b/src/script/domain/ClassifierRepository.ts
index edec00ef2..5eb367dfb 100644
--- a/src/script/domain/ClassifierRepository.ts
+++ b/src/script/domain/ClassifierRepository.ts
@@ -4,12 +4,15 @@
* SPDX-License-Identifier: MIT
*/
import Classifier from './stores/Classifier';
+import Confidences from './stores/Confidences';
import GestureConfidence from './stores/gesture/GestureConfidence';
interface ClassifierRepository {
getClassifier(): Classifier;
getGestureConfidence(gestureId: number): GestureConfidence;
+
+ getConfidences(): Confidences;
}
export default ClassifierRepository;
diff --git a/src/script/domain/Filter.ts b/src/script/domain/Filter.ts
index e65eb34ad..46d90653d 100644
--- a/src/script/domain/Filter.ts
+++ b/src/script/domain/Filter.ts
@@ -13,6 +13,8 @@ interface Filter {
getName(): string;
getDescription(): string;
+
+ getMinNumberOfSamples(): number;
}
export default Filter;
diff --git a/src/script/domain/LiveDataBuffer.ts b/src/script/domain/LiveDataBuffer.ts
index 8a2483838..ccbaa12ce 100644
--- a/src/script/domain/LiveDataBuffer.ts
+++ b/src/script/domain/LiveDataBuffer.ts
@@ -1,13 +1,15 @@
+import { LiveDataVector } from './stores/LiveDataVector';
+
/**
* (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
*
* SPDX-License-Identifier: MIT
*/
-export type TimestampedData = {
+export type TimestampedData = {
value: T;
timestamp: number;
};
-class LiveDataBuffer {
+class LiveDataBuffer {
private buffer: (TimestampedData | null)[];
private bufferPtr = 0; // The buffer pointer keeps increasing from 0 to infinity each time a new item is added
private bufferUtilization = 0;
diff --git a/src/script/domain/ModelRegistry.ts b/src/script/domain/ModelRegistry.ts
new file mode 100644
index 000000000..57f28012d
--- /dev/null
+++ b/src/script/domain/ModelRegistry.ts
@@ -0,0 +1,30 @@
+/**
+ * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+export type ModelInfo = {
+ id: string;
+ title: string;
+ label: string;
+};
+
+class ModelRegistry {
+ public static NeuralNetwork: ModelInfo = {
+ id: 'NN',
+ title: 'Neural network',
+ label: 'neural network',
+ };
+
+ public static KNN: ModelInfo = {
+ id: 'KNN',
+ title: 'KNN',
+ label: 'KNN',
+ };
+
+ public static getModels(): ModelInfo[] {
+ return [this.NeuralNetwork, this.KNN];
+ }
+}
+
+export default ModelRegistry;
diff --git a/src/script/domain/Repositories.ts b/src/script/domain/Repositories.ts
index b1340367b..e49450864 100644
--- a/src/script/domain/Repositories.ts
+++ b/src/script/domain/Repositories.ts
@@ -5,6 +5,7 @@
*/
import ClassifierRepository from './ClassifierRepository';
import GestureRepository from './GestureRepository';
+import Confidences from './stores/Confidences';
interface Repositories {
getGestureRepository(): GestureRepository;
diff --git a/src/script/domain/stores/Classifier.ts b/src/script/domain/stores/Classifier.ts
index 8cc2a33a2..e4c21c3fd 100644
--- a/src/script/domain/stores/Classifier.ts
+++ b/src/script/domain/stores/Classifier.ts
@@ -40,6 +40,11 @@ class Classifier implements Readable {
const filteredInput = input.getInput(this.filters);
const predictions = await this.getModel().predict(filteredInput);
predictions.forEach((confidence, index) => {
+ if (isNaN(confidence)) {
+ throw new Error(
+ `Classifier returned NaN confidence for gesture at index ${index}`,
+ );
+ }
const gesture = get(this.gestures)[index];
this.confidenceSetter(gesture.getId(), confidence);
});
diff --git a/src/script/domain/stores/Confidences.ts b/src/script/domain/stores/Confidences.ts
new file mode 100644
index 000000000..2efbb380b
--- /dev/null
+++ b/src/script/domain/stores/Confidences.ts
@@ -0,0 +1,48 @@
+/**
+ * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+import {
+ Readable,
+ Subscriber,
+ Unsubscriber,
+ Writable,
+ get,
+ writable,
+} from 'svelte/store';
+import { GestureID } from './gesture/Gesture';
+
+type GestureConfidenceMap = Map;
+
+class Confidences implements Readable {
+ private confidenceStore: Writable;
+
+ constructor() {
+ this.confidenceStore = writable(new Map());
+ }
+
+ public subscribe(
+ run: Subscriber,
+ invalidate?: ((value?: GestureConfidenceMap | undefined) => void) | undefined,
+ ): Unsubscriber {
+ return this.confidenceStore.subscribe(run, invalidate);
+ }
+
+ public setConfidence(gestureId: GestureID, confidence: number): void {
+ this.confidenceStore.update((map: GestureConfidenceMap) => {
+ map.set(gestureId, confidence);
+ return map;
+ });
+ }
+
+ public getConfidence(gestureId: GestureID): number {
+ const confidence = get(this.confidenceStore).get(gestureId);
+ if (confidence === undefined) {
+ throw new Error(`No confidence value found for gesture with ID ${gestureId}`);
+ }
+ return confidence;
+ }
+}
+
+export default Confidences;
diff --git a/src/script/domain/stores/LiveData.ts b/src/script/domain/stores/LiveData.ts
index f7e92f886..1c1b019aa 100644
--- a/src/script/domain/stores/LiveData.ts
+++ b/src/script/domain/stores/LiveData.ts
@@ -5,11 +5,12 @@
*/
import { Readable } from 'svelte/store';
import LiveDataBuffer from '../LiveDataBuffer';
+import { LiveDataVector } from './LiveDataVector';
/**
* A container for real-time data. Uses a LiveDataBuffer to store data points.
*/
-interface LiveData extends Readable {
+interface LiveData extends Readable {
/**
* Inserts a new data point to the LiveData object
*/
@@ -30,11 +31,6 @@ interface LiveData extends Readable {
* Returns labels accociated with each data point (Such as for the LiveGraph)
*/
getLabels(): string[];
-
- /**
- * Returns the property names of generic type T. Useful for iterating over individual data points for a single sample point.
- */
- getPropertyNames(): string[];
}
export default LiveData;
diff --git a/src/script/domain/stores/LiveDataVector.ts b/src/script/domain/stores/LiveDataVector.ts
new file mode 100644
index 000000000..60c3822d7
--- /dev/null
+++ b/src/script/domain/stores/LiveDataVector.ts
@@ -0,0 +1,13 @@
+/**
+ * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+export interface LiveDataVector {
+ getVector(): number[];
+
+ getSize(): number;
+
+ getLabels(): string[];
+}
diff --git a/src/script/domain/stores/gesture/Gesture.ts b/src/script/domain/stores/gesture/Gesture.ts
index 5aa8a6499..feacd5a1d 100644
--- a/src/script/domain/stores/gesture/Gesture.ts
+++ b/src/script/domain/stores/gesture/Gesture.ts
@@ -12,14 +12,14 @@ import BindableValue from '../BindableValue';
export type GestureID = number;
-export type GestureData = PersistantGestureData & {
- confidence: {
- currentConfidence: number;
- requiredConfidence: number;
- isConfident: boolean;
- };
+export type Confidence = {
+ currentConfidence: number;
+ requiredConfidence: number;
+ isConfident: boolean;
};
+export type GestureData = PersistantGestureData & { confidence: Confidence };
+
export type GestureOutput = {
matrix?: boolean[];
sound?: SoundData;
@@ -73,6 +73,23 @@ class Gesture implements Readable {
return get(this.store).name;
}
+ /**
+ * Get the HEX color assigned to this gesture.
+ */
+ public getColor(): string {
+ return get(this.store).color;
+ }
+
+ /**
+ * Set the HEX color assigned to this gesture.
+ */
+ public setColor(color: string): void {
+ this.persistedData.update(val => {
+ val.color = color;
+ return val;
+ });
+ }
+
public addRecording(recording: RecordingData) {
this.onRecordingsChanged();
this.persistedData.update(newVal => {
@@ -131,13 +148,14 @@ class Gesture implements Readable {
private deriveStore(): Readable {
return derived([this.persistedData, this.gestureConfidence], stores => {
- const peristantData = stores[0];
+ const persistantData = stores[0];
const confidenceData = stores[1];
const derivedData: GestureData = {
- ID: peristantData.ID,
- name: peristantData.name,
- recordings: peristantData.recordings,
- output: peristantData.output,
+ ID: persistantData.ID,
+ name: persistantData.name,
+ recordings: persistantData.recordings,
+ output: persistantData.output,
+ color: persistantData.color,
confidence: {
currentConfidence: confidenceData.confidence,
requiredConfidence: confidenceData.requiredConfidence,
diff --git a/src/script/domain/stores/gesture/Gestures.ts b/src/script/domain/stores/gesture/Gestures.ts
index 3bde2148b..30b80c5fe 100644
--- a/src/script/domain/stores/gesture/Gestures.ts
+++ b/src/script/domain/stores/gesture/Gestures.ts
@@ -12,22 +12,22 @@ import {
get,
writable,
} from 'svelte/store';
-import Gesture, { GestureData, GestureID, GestureOutput } from './Gesture';
+import Gesture, { Confidence, GestureData, GestureID, GestureOutput } from './Gesture';
import StaticConfiguration from '../../../../StaticConfiguration';
import GestureRepository from '../../GestureRepository';
+import ClassifierRepository from '../../ClassifierRepository';
export type PersistantGestureData = {
name: string;
ID: GestureID;
recordings: RecordingData[];
output: GestureOutput;
+ color: string;
};
export type RecordingData = {
ID: number;
data: {
- x: number[];
- y: number[];
z: number[];
};
};
@@ -73,17 +73,23 @@ class Gestures implements Readable {
return this.repository.getGesture(gestureID);
}
+ // TODO: Change to getCurrent() or something else maybe
public getGestures(): Gesture[] {
return get(Gestures.subscribableGestures);
}
public createGesture(name = ''): Gesture {
const newId = Date.now();
+ const color =
+ StaticConfiguration.gestureColors[
+ this.getNumberOfGestures() % StaticConfiguration.gestureColors.length
+ ];
return this.addGestureFromPersistedData({
ID: newId,
recordings: [],
output: {}, //TODO: ADD DEFAULT VALUES HERE
name: name,
+ color,
});
}
@@ -133,6 +139,7 @@ class Gestures implements Readable {
name: gesture.getName(),
recordings: gesture.getRecordings(),
output: gesture.getOutput(),
+ color: gesture.getColor(),
confidence: {
currentConfidence: gesture.getConfidence().getCurrentConfidence(),
requiredConfidence: gesture.getConfidence().getRequiredConfidence(),
diff --git a/src/script/engine/PollingPredictorEngine.ts b/src/script/engine/PollingPredictorEngine.ts
index 0b6267043..eb6f96fba 100644
--- a/src/script/engine/PollingPredictorEngine.ts
+++ b/src/script/engine/PollingPredictorEngine.ts
@@ -4,13 +4,17 @@
* SPDX-License-Identifier: MIT
*/
import { Subscriber, Unsubscriber, Writable, derived, get, writable } from 'svelte/store';
-import AccelerometerClassifierInput from '../mlmodels/AccelerometerClassifierInput';
-import { MicrobitAccelerometerData } from '../livedata/MicrobitAccelerometerData';
+import AccelerometerClassifierInput, {
+ SingleAxisClassifierInput,
+} from '../mlmodels/AccelerometerClassifierInput';
import StaticConfiguration from '../../StaticConfiguration';
import { TimestampedData } from '../domain/LiveDataBuffer';
import Engine, { EngineData } from '../domain/stores/Engine';
import Classifier from '../domain/stores/Classifier';
import LiveData from '../domain/stores/LiveData';
+import { LiveDataVector } from '../domain/stores/LiveDataVector';
+import Logger from '../utils/Logger';
+import ClassifierInput from '../domain/ClassifierInput';
/**
* The PollingPredictorEngine will predict on the current input with consistent intervals.
@@ -21,7 +25,7 @@ class PollingPredictorEngine implements Engine {
constructor(
private classifier: Classifier,
- private liveData: LiveData,
+ private liveData: LiveData,
) {
this.isRunning = writable(true);
this.startPolling();
@@ -58,35 +62,49 @@ class PollingPredictorEngine implements Engine {
}
private predict() {
- if (this.classifier.getModel().isTrained() && get(this.isRunning)) {
- void this.classifier.classify(this.bufferToInput());
+ if (!this.classifier.getModel().isTrained()) {
+ return;
}
+ if (!get(this.isRunning)) {
+ return;
+ }
+ const input = this.bufferToInput();
+ const numberOfSamples = input.getNumberOfSamples();
+ const requiredNumberOfSamples = Math.max(
+ ...get(this.classifier.getFilters()).map(filter => filter.getMinNumberOfSamples()),
+ );
+ if (numberOfSamples < requiredNumberOfSamples) {
+ return;
+ }
+ void this.classifier.classify(input);
}
- private bufferToInput(): AccelerometerClassifierInput {
+ private bufferToInput(): ClassifierInput {
const bufferedData = this.getRawDataFromBuffer(
StaticConfiguration.pollingPredictionSampleSize,
);
- const xs = bufferedData.map(data => data.value.x);
- const ys = bufferedData.map(data => data.value.y);
- const zs = bufferedData.map(data => data.value.z);
- return new AccelerometerClassifierInput(xs, ys, zs);
+ const zs = bufferedData.map(data => data.value.getVector()[0]);
+ // TODO: Generalize
+ return new SingleAxisClassifierInput(zs);
}
/**
* Searches for an applicable amount of data, by iterately trying fewer data points if buffer fetch fails
*/
- private getRawDataFromBuffer(
- sampleSize: number,
- ): TimestampedData[] {
+ private getRawDataFromBuffer(sampleSize: number): TimestampedData[] {
try {
return this.liveData
.getBuffer()
.getSeries(StaticConfiguration.pollingPredictionSampleDuration, sampleSize);
} catch (_e) {
if (sampleSize < 8) {
+ Logger.log(
+ 'PollingPredictorEngine',
+ 'Too few samples available, returning empty array',
+ );
return []; // The minimum number of points is 8, otherwise the filters will throw an exception
} else {
+ // If too few samples are available, try again with fewer samples
return this.getRawDataFromBuffer(
sampleSize - StaticConfiguration.pollingPredictionSampleSizeSearchStepSize,
);
diff --git a/src/script/filters/FilterWithMaths.ts b/src/script/filters/FilterWithMaths.ts
index 2baa29d92..c70c93947 100644
--- a/src/script/filters/FilterWithMaths.ts
+++ b/src/script/filters/FilterWithMaths.ts
@@ -8,6 +8,7 @@ import Filter from '../domain/Filter';
import { FilterType } from '../domain/FilterTypes';
abstract class FilterWithMaths implements Filter {
+ abstract getMinNumberOfSamples(): number;
abstract filter(inValues: number[]): number;
abstract getType(): FilterType;
diff --git a/src/script/filters/MaxFilter.ts b/src/script/filters/MaxFilter.ts
index ea5deb011..1499621c9 100644
--- a/src/script/filters/MaxFilter.ts
+++ b/src/script/filters/MaxFilter.ts
@@ -21,5 +21,8 @@ class MaxFilter implements Filter {
public filter(inValues: number[]): number {
return Math.max(...inValues);
}
+ public getMinNumberOfSamples(): number {
+ return 1;
+ }
}
export default MaxFilter;
diff --git a/src/script/filters/MeanFilter.ts b/src/script/filters/MeanFilter.ts
index 0eb72c011..c23f2bc72 100644
--- a/src/script/filters/MeanFilter.ts
+++ b/src/script/filters/MeanFilter.ts
@@ -23,6 +23,10 @@ class MeanFilter extends FilterWithMaths {
public getName(): string {
return get(t)('content.filters.mean.title');
}
+
+ public getMinNumberOfSamples(): number {
+ return 1;
+ }
}
export default MeanFilter;
diff --git a/src/script/filters/MinFilter.ts b/src/script/filters/MinFilter.ts
index ecf7648be..449321728 100644
--- a/src/script/filters/MinFilter.ts
+++ b/src/script/filters/MinFilter.ts
@@ -22,6 +22,9 @@ class MinFilter implements Filter {
public filter(inValues: number[]): number {
return Math.min(...inValues);
}
+ public getMinNumberOfSamples(): number {
+ return 1;
+ }
}
export default MinFilter;
diff --git a/src/script/filters/PeaksFilter.ts b/src/script/filters/PeaksFilter.ts
index 5d7265062..0b6f6a1ec 100644
--- a/src/script/filters/PeaksFilter.ts
+++ b/src/script/filters/PeaksFilter.ts
@@ -9,6 +9,9 @@ import FilterWithMaths from './FilterWithMaths';
import { t } from 'svelte-i18n';
class PeaksFilter extends FilterWithMaths {
+ private lag = 5;
+ private threshold = 3.5;
+ private influence = 0.5;
public getName(): string {
return get(t)('content.filters.peaks.title');
}
@@ -20,30 +23,26 @@ class PeaksFilter extends FilterWithMaths {
}
public filter(inValues: number[]): number {
- const lag = 5;
- const threshold = 3.5;
- const influence = 0.5;
-
let peaksCounter = 0;
- if (inValues.length < lag + 2) {
+ if (inValues.length < this.lag + 2) {
throw new Error('data sample is too short');
}
// init variables
const signals = Array(inValues.length).fill(0) as number[];
const filteredY = inValues.slice(0);
- const lead_in = inValues.slice(0, lag);
+ const lead_in = inValues.slice(0, this.lag);
const avgFilter: number[] = [];
- avgFilter[lag - 1] = this.mean(lead_in);
+ avgFilter[this.lag - 1] = this.mean(lead_in);
const stdFilter: number[] = [];
- stdFilter[lag - 1] = this.stddev(lead_in);
+ stdFilter[this.lag - 1] = this.stddev(lead_in);
- for (let i = lag; i < inValues.length; i++) {
+ for (let i = this.lag; i < inValues.length; i++) {
if (
Math.abs(inValues[i] - avgFilter[i - 1]) > 0.1 &&
- Math.abs(inValues[i] - avgFilter[i - 1]) > threshold * stdFilter[i - 1]
+ Math.abs(inValues[i] - avgFilter[i - 1]) > this.threshold * stdFilter[i - 1]
) {
if (inValues[i] > avgFilter[i - 1]) {
signals[i] = +1; // positive signal
@@ -54,19 +53,24 @@ class PeaksFilter extends FilterWithMaths {
signals[i] = -1; // negative signal
}
// make influence lower
- filteredY[i] = influence * inValues[i] + (1 - influence) * filteredY[i - 1];
+ filteredY[i] =
+ this.influence * inValues[i] + (1 - this.influence) * filteredY[i - 1];
} else {
signals[i] = 0; // no signal
filteredY[i] = inValues[i];
}
// adjust the filters
- const y_lag = filteredY.slice(i - lag, i);
+ const y_lag = filteredY.slice(i - this.lag, i);
avgFilter[i] = this.mean(y_lag);
stdFilter[i] = this.stddev(y_lag);
}
return peaksCounter;
}
+
+ public getMinNumberOfSamples(): number {
+ return this.lag + 2;
+ }
}
export default PeaksFilter;
diff --git a/src/script/filters/RootMeanSquareFilter.ts b/src/script/filters/RootMeanSquareFilter.ts
index 3f1e68e8d..6520a1ef5 100644
--- a/src/script/filters/RootMeanSquareFilter.ts
+++ b/src/script/filters/RootMeanSquareFilter.ts
@@ -22,6 +22,10 @@ class RootMeanSquareFilter implements Filter {
public filter(inValues: number[]): number {
return Math.sqrt(inValues.reduce((a, b) => a + Math.pow(b, 2), 0) / inValues.length);
}
+
+ public getMinNumberOfSamples(): number {
+ return 1;
+ }
}
export default RootMeanSquareFilter;
diff --git a/src/script/filters/StandardDeviationFilter.ts b/src/script/filters/StandardDeviationFilter.ts
index ed8859f4e..925abcc94 100644
--- a/src/script/filters/StandardDeviationFilter.ts
+++ b/src/script/filters/StandardDeviationFilter.ts
@@ -26,6 +26,10 @@ class StandardDeviationFilter extends FilterWithMaths {
public getDescription(): string {
return get(t)('content.filters.std.description');
}
+
+ public getMinNumberOfSamples(): number {
+ return 2;
+ }
}
export default StandardDeviationFilter;
diff --git a/src/script/filters/TotalAccFilter.ts b/src/script/filters/TotalAccFilter.ts
index 21bb87e5b..6d9a6bc05 100644
--- a/src/script/filters/TotalAccFilter.ts
+++ b/src/script/filters/TotalAccFilter.ts
@@ -21,6 +21,9 @@ class TotalAccFilter implements Filter {
public filter(inValues: number[]): number {
return inValues.reduce((a, b) => a + Math.abs(b));
}
+ public getMinNumberOfSamples(): number {
+ return 2;
+ }
}
export default TotalAccFilter;
diff --git a/src/script/filters/ZeroCrossingRateFilter.ts b/src/script/filters/ZeroCrossingRateFilter.ts
index 338df0265..44d46e530 100644
--- a/src/script/filters/ZeroCrossingRateFilter.ts
+++ b/src/script/filters/ZeroCrossingRateFilter.ts
@@ -31,6 +31,10 @@ class ZeroCrossingRateFilter implements Filter {
}
return count / (inValues.length - 1);
}
+
+ public getMinNumberOfSamples(): number {
+ return 2;
+ }
}
export default ZeroCrossingRateFilter;
diff --git a/src/script/livedata/BaseVector.ts b/src/script/livedata/BaseVector.ts
new file mode 100644
index 000000000..0ff56cec5
--- /dev/null
+++ b/src/script/livedata/BaseVector.ts
@@ -0,0 +1,28 @@
+/**
+ * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+import { LiveDataVector } from '../domain/stores/LiveDataVector';
+
+class BaseVector implements LiveDataVector {
+ public constructor(
+ private numbers: number[],
+ private labels: string[],
+ ) {}
+
+ public getLabels(): string[] {
+ return this.labels;
+ }
+
+ public getSize(): number {
+ return this.numbers.length;
+ }
+
+ public getVector(): number[] {
+ return this.numbers;
+ }
+}
+
+export default BaseVector;
diff --git a/src/script/livedata/MicrobitAccelerometerData.ts b/src/script/livedata/MicrobitAccelerometerData.ts
index 56896039b..df0d88049 100644
--- a/src/script/livedata/MicrobitAccelerometerData.ts
+++ b/src/script/livedata/MicrobitAccelerometerData.ts
@@ -6,6 +6,7 @@
import { Subscriber, Unsubscriber, Writable, get, writable } from 'svelte/store';
import LiveDataBuffer from '../domain/LiveDataBuffer';
import LiveData from '../domain/stores/LiveData';
+import { LiveDataVector } from '../domain/stores/LiveDataVector';
export type MicrobitAccelerometerData = {
x: number;
@@ -13,40 +14,129 @@ export type MicrobitAccelerometerData = {
z: number;
};
-class MicrobitAccelerometerLiveData implements LiveData {
- private store: Writable;
- constructor(private dataBuffer: LiveDataBuffer) {
- this.store = writable({
- x: 0,
- y: 0,
- z: 0,
- });
+export const asAccelerometerData = (input: LiveDataVector) => {
+ if (input.getSize() != 3) {
+ throw new Error('Cannot cast input as accelerometer data, size is not 3');
}
+ const data = new MicrobitAccelerometerDataVector({
+ x: input.getVector()[0],
+ y: input.getVector()[1],
+ z: input.getVector()[2],
+ });
- public getBuffer(): LiveDataBuffer {
+ input.getLabels().forEach((label, index) => {
+ if (data.getLabels()[index] !== label) {
+ throw new Error('Cannot cast input as accelerometer data, labels do not match');
+ }
+ });
+ return data;
+};
+
+export class MicrobitAccelerometerDataVector implements LiveDataVector {
+ public constructor(private data: MicrobitAccelerometerData) {}
+
+ public getLabels(): string[] {
+ return ['X', 'Y', 'Z'];
+ }
+ public getSize(): number {
+ return this.getVector().length;
+ }
+
+ public getVector(): number[] {
+ return [this.data.x, this.data.y, this.data.z];
+ }
+
+ public getAccelerometerData(): MicrobitAccelerometerData {
+ return this.data;
+ }
+}
+
+export class MicrobitAccelerometerZDataVector implements LiveDataVector {
+ public constructor(private data: number) {}
+ getVector(): number[] {
+ return [this.data];
+ }
+ getSize(): number {
+ return this.getVector().length;
+ }
+ getLabels(): string[] {
+ return ['Z'];
+ }
+}
+
+export class MicrobitAccelerometerZLiveData
+ implements LiveData
+{
+ private store: Writable;
+ constructor(private dataBuffer: LiveDataBuffer) {
+ this.store = writable(new MicrobitAccelerometerZDataVector(0));
+ }
+
+ public getBuffer(): LiveDataBuffer {
return this.dataBuffer;
}
- public put(data: MicrobitAccelerometerData): void {
+ public put(data: MicrobitAccelerometerZDataVector): void {
this.store.set(data);
this.dataBuffer.addValue(data);
}
public getSeriesSize(): number {
- return 3;
+ // TODO: Could be replaced with the version in the store, as it is initialized in constructor
+ return new MicrobitAccelerometerZDataVector(0).getSize();
}
public getLabels(): string[] {
- return ['X', 'Y', 'Z'];
+ // TODO: Could be replaced with the version in the store, as it is initialized in constructor
+ return new MicrobitAccelerometerZDataVector(0).getLabels();
+ }
+
+ public subscribe(
+ run: Subscriber,
+ invalidate?:
+ | ((value?: MicrobitAccelerometerZDataVector | undefined) => void)
+ | undefined,
+ ): Unsubscriber {
+ return this.store.subscribe(run, invalidate);
+ }
+}
+
+class MicrobitAccelerometerLiveData implements LiveData {
+ private store: Writable;
+ constructor(private dataBuffer: LiveDataBuffer) {
+ this.store = writable(
+ new MicrobitAccelerometerDataVector({
+ x: 0,
+ y: 0,
+ z: 0,
+ }),
+ );
+ }
+
+ public getBuffer(): LiveDataBuffer {
+ return this.dataBuffer;
+ }
+
+ public put(data: MicrobitAccelerometerDataVector): void {
+ this.store.set(data);
+ this.dataBuffer.addValue(data);
}
- public getPropertyNames(): string[] {
- return Object.getOwnPropertyNames(get(this.store));
+ public getSeriesSize(): number {
+ // TODO: Could be replaced with the version in the store, as it is initialized in constructor
+ return new MicrobitAccelerometerDataVector({ x: 0, y: 0, z: 0 }).getSize();
+ }
+
+ public getLabels(): string[] {
+ // TODO: Could be replaced with the version in the store, as it is initialized in constructor
+ return new MicrobitAccelerometerDataVector({ x: 0, y: 0, z: 0 }).getLabels();
}
public subscribe(
- run: Subscriber,
- invalidate?: ((value?: MicrobitAccelerometerData | undefined) => void) | undefined,
+ run: Subscriber,
+ invalidate?:
+ | ((value?: MicrobitAccelerometerDataVector | undefined) => void)
+ | undefined,
): Unsubscriber {
return this.store.subscribe(run, invalidate);
}
diff --git a/src/script/livedata/SmoothedLiveData.ts b/src/script/livedata/SmoothedLiveData.ts
index 404039e3d..42fd0ed11 100644
--- a/src/script/livedata/SmoothedLiveData.ts
+++ b/src/script/livedata/SmoothedLiveData.ts
@@ -7,14 +7,16 @@ import { Readable, Subscriber, Unsubscriber, derived } from 'svelte/store';
import LiveDataBuffer from '../domain/LiveDataBuffer';
import { smoothNewValue } from '../utils/graphUtils';
import LiveData from '../domain/stores/LiveData';
+import { LiveDataVector } from '../domain/stores/LiveDataVector';
+import BaseVector from './BaseVector';
/**
* Uses interpolation to produce a 'smoothed' representation of a live data object.
*
* Each entry in the SmoothedLiveData will be interpolated with previous values seen. I.e `y_i = 0.75x_(i-1) + 0.25x_i`
*/
-class SmoothedLiveData implements LiveData {
- private smoothedStore: Readable;
+class SmoothedLiveData implements LiveData {
+ private smoothedStore: Readable;
/**
* Creates a new SmoothedLiveData store, using the provided LiveData store as data reference.
@@ -51,8 +53,8 @@ class SmoothedLiveData implements LiveData {
}
public subscribe(
- run: Subscriber,
- invalidate?: ((value?: T | undefined) => void) | undefined,
+ run: Subscriber,
+ invalidate?: ((value?: LiveDataVector | undefined) => void) | undefined,
): Unsubscriber {
return this.smoothedStore.subscribe(run, invalidate);
}
@@ -64,14 +66,10 @@ class SmoothedLiveData implements LiveData {
return this.referenceStore.getLabels();
}
- public getPropertyNames(): string[] {
- return this.referenceStore.getPropertyNames();
- }
-
/**
* Uses the buffer of the original store to derive a store with smoothed values when subscribing
*/
- private deriveStore() {
+ private deriveStore(): Readable {
return derived([this.referenceStore], stores => {
const referenceData = stores[0];
@@ -81,12 +79,16 @@ class SmoothedLiveData implements LiveData {
return referenceData;
}
- const newObject: T = { ...referenceData };
- for (const property in newObject) {
- const values = oldValues.map(val => val![property] as number);
- newObject[property] = smoothNewValue(...values) as never;
+ const newVector: LiveDataVector = new BaseVector(
+ [...referenceData.getVector()],
+ referenceData.getLabels(),
+ );
+
+ for (let i = 0; i < newVector.getVector().length; i++) {
+ const values = oldValues.map(val => val!.getVector()[i]);
+ newVector.getVector()[i] = smoothNewValue(...values);
}
- return newObject;
+ return newVector;
});
}
}
diff --git a/src/script/microbit-interfacing/MicrobitBluetooth.ts b/src/script/microbit-interfacing/MicrobitBluetooth.ts
index d76f976f3..b1e0c04e2 100644
--- a/src/script/microbit-interfacing/MicrobitBluetooth.ts
+++ b/src/script/microbit-interfacing/MicrobitBluetooth.ts
@@ -6,6 +6,7 @@
import Environment from '../Environment';
import TypingUtils from '../TypingUtils';
+import Logger from '../utils/Logger';
import MBSpecs from './MBSpecs';
/**
@@ -314,7 +315,7 @@ export class MicrobitBluetooth {
this.onReconnect?.(this);
})
.catch(e => {
- Environment.isInDevelopment && console.error(e);
+ Logger.log('MicrobitBluetooth', e);
void this.onReconnectFailed();
});
} else {
diff --git a/src/script/microbit-interfacing/Microbits.ts b/src/script/microbit-interfacing/Microbits.ts
index 91b871f9c..d947d0e2d 100644
--- a/src/script/microbit-interfacing/Microbits.ts
+++ b/src/script/microbit-interfacing/Microbits.ts
@@ -31,7 +31,7 @@ export enum HexOrigin {
type UARTMessageType = 'g' | 's';
/**
- * Entry point for microbit interfaces / Facade pattern
+ * Entry point for microbit interfaces
*/
class Microbits {
public static hexFiles: { 1: string; 2: string; universal: string } = {
@@ -39,13 +39,13 @@ class Microbits {
2: 'firmware/MICROBIT.hex',
universal: 'firmware/universal-hex.hex',
};
- private static assignedInputMicrobit: MicrobitBluetooth | undefined = undefined;
- private static assignedOutputMicrobit: MicrobitBluetooth | undefined = undefined;
- private static inputName: string | undefined = undefined;
- private static outputName: string | undefined = undefined;
+ private static assignedInputMicrobit: MicrobitBluetooth | undefined;
+ private static assignedOutputMicrobit: MicrobitBluetooth | undefined;
+ private static inputName: string | undefined;
+ private static outputName: string | undefined;
private static inputVersion: MBSpecs.MBVersion | undefined;
private static outputVersion: MBSpecs.MBVersion | undefined;
- private static linkedMicrobit: MicrobitUSB | undefined = undefined;
+ private static linkedMicrobit: MicrobitUSB | undefined;
private static outputIO: BluetoothRemoteGATTCharacteristic | undefined;
private static outputMatrix: BluetoothRemoteGATTCharacteristic | undefined;
@@ -57,13 +57,11 @@ class Microbits {
private static outputOrigin = HexOrigin.UNKNOWN;
private static inputOrigin = HexOrigin.UNKNOWN;
- private static inputBuildVersion: number | undefined = undefined;
- private static outputBuildVersion: number | undefined = undefined;
+ private static inputBuildVersion: number | undefined;
+ private static outputBuildVersion: number | undefined;
- private static inputVersionIdentificationTimeout: NodeJS.Timeout | undefined =
- undefined;
- private static outputVersionIdentificationTimeout: NodeJS.Timeout | undefined =
- undefined;
+ private static inputVersionIdentificationTimeout: NodeJS.Timeout | undefined;
+ private static outputVersionIdentificationTimeout: NodeJS.Timeout | undefined;
/**
* Maps pin to the number of times, it has been asked to turn on.
@@ -889,11 +887,6 @@ class Microbits {
'No input microbit has be defined! Please check that it is connected before using it',
);
}
- if (!this.inputName) {
- throw new Error(
- 'Something went wrong. Input microbit was specified, but without name!',
- );
- }
this.assignedOutputMicrobit = this.getInput();
this.outputName = this.inputName;
this.outputVersion = this.inputVersion;
diff --git a/src/script/microbit-interfacing/connection-behaviours/InputBehaviour.ts b/src/script/microbit-interfacing/connection-behaviours/InputBehaviour.ts
index 3bccb28a4..b7cc2829e 100644
--- a/src/script/microbit-interfacing/connection-behaviours/InputBehaviour.ts
+++ b/src/script/microbit-interfacing/connection-behaviours/InputBehaviour.ts
@@ -18,7 +18,13 @@ import LoggingDecorator from './LoggingDecorator';
import TypingUtils from '../../TypingUtils';
import { DeviceRequestStates } from '../../stores/connectDialogStore';
import StaticConfiguration from '../../../StaticConfiguration';
-import { liveAccelerometerData } from '../../stores/Stores';
+import MicrobitAccelerometerLiveData, {
+ MicrobitAccelerometerDataVector,
+ MicrobitAccelerometerZDataVector,
+ MicrobitAccelerometerZLiveData,
+} from '../../livedata/MicrobitAccelerometerData';
+import { stores } from '../../stores/Stores';
+import LiveDataBuffer from '../../domain/LiveDataBuffer';
let text = get(t);
t.subscribe(t => (text = t));
@@ -40,10 +46,13 @@ class InputBehaviour extends LoggingDecorator {
onIdentifiedAsOutdated(): void {
super.onIdentifiedAsOutdated();
+ /*
+ TODO: Disabled for now as the results are unpredictable
state.update(s => {
s.isInputOutdated = true;
return s;
});
+ */
}
onVersionIdentified(versionNumber: number): void {
@@ -72,6 +81,7 @@ class InputBehaviour extends LoggingDecorator {
onReady() {
super.onReady();
+
clearTimeout(this.reconnectTimeout);
state.update(s => {
s.isInputReady = true;
@@ -123,6 +133,10 @@ class InputBehaviour extends LoggingDecorator {
onConnected(name?: string): void {
super.onConnected(name);
+ const buffer = new LiveDataBuffer(
+ StaticConfiguration.accelerometerLiveDataBufferSize,
+ );
+ stores.setLiveData(new MicrobitAccelerometerZLiveData(buffer));
state.update(s => {
s.isInputConnected = true;
@@ -143,15 +157,11 @@ class InputBehaviour extends LoggingDecorator {
accelerometerChange(x: number, y: number, z: number): void {
super.accelerometerChange(x, y, z);
- const accelX = x / 1000.0;
- const accelY = y / 1000.0;
+ //const accelX = x / 1000.0;
+ //const accelY = y / 1000.0;
const accelZ = z / 1000.0;
- liveAccelerometerData.put({
- x: accelX,
- y: accelY,
- z: accelZ,
- });
+ get(stores).liveData.put(new MicrobitAccelerometerZDataVector(accelZ));
}
buttonChange(buttonState: MBSpecs.ButtonState, button: MBSpecs.Button): void {
diff --git a/src/script/microbit-interfacing/connection-behaviours/OutputBehaviour.ts b/src/script/microbit-interfacing/connection-behaviours/OutputBehaviour.ts
index 286cc8fd1..71a042981 100644
--- a/src/script/microbit-interfacing/connection-behaviours/OutputBehaviour.ts
+++ b/src/script/microbit-interfacing/connection-behaviours/OutputBehaviour.ts
@@ -33,10 +33,13 @@ class OutputBehaviour extends LoggingDecorator {
onIdentifiedAsOutdated(): void {
super.onIdentifiedAsOutdated();
+ /*
+ TODO: Disabled for now as the results are unpredictable
state.update(s => {
s.isOutputOutdated = true;
return s;
});
+ */
}
onVersionIdentified(versionNumber: number): void {
diff --git a/src/script/mlmodels/AccelerometerClassifierInput.ts b/src/script/mlmodels/AccelerometerClassifierInput.ts
index 8e425ea35..e5ffe5ff6 100644
--- a/src/script/mlmodels/AccelerometerClassifierInput.ts
+++ b/src/script/mlmodels/AccelerometerClassifierInput.ts
@@ -37,6 +37,22 @@ class AccelerometerClassifierInput implements ClassifierInput {
...filters.compute(this.zs),
];
}
+
+ public getNumberOfSamples(): number {
+ return this.xs.length; // Assuming all axes have the same length
+ }
+}
+
+export class SingleAxisClassifierInput implements ClassifierInput {
+ constructor(private axis: number[]) {}
+
+ public getInput(filters: Filters): number[] {
+ return [...filters.compute(this.axis)];
+ }
+
+ public getNumberOfSamples(): number {
+ return this.axis.length;
+ }
}
export default AccelerometerClassifierInput;
diff --git a/src/script/mlmodels/KNNNonNormalizedModelTrainer.ts b/src/script/mlmodels/KNNNonNormalizedModelTrainer.ts
index 16d9605a8..912dbe969 100644
--- a/src/script/mlmodels/KNNNonNormalizedModelTrainer.ts
+++ b/src/script/mlmodels/KNNNonNormalizedModelTrainer.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: MIT
*/
import ModelTrainer, { TrainingData } from '../domain/ModelTrainer';
+import Logger from '../utils/Logger';
import KNNNonNormalizedMLModel, { LabelledPoint } from './KNNNonNormalizedMLModel';
/**
@@ -17,8 +18,12 @@ class KNNNonNormalizedModelTrainer implements ModelTrainer {
+ Logger.log('KNNNonNormalizedModelTrainer', 'Training model');
if (this.dataFilterer) {
+ Logger.log('KNNNonNormalizedModelTrainer', 'Filtering training data');
trainingData = this.dataFilterer(trainingData);
+ } else {
+ Logger.log('KNNNonNormalizedModelTrainer', 'No data filtering');
}
const points: LabelledPoint[] = [];
diff --git a/src/script/mlmodels/LayersModelTrainer.ts b/src/script/mlmodels/LayersModelTrainer.ts
index 9cbfb0fa3..5ac7b20e2 100644
--- a/src/script/mlmodels/LayersModelTrainer.ts
+++ b/src/script/mlmodels/LayersModelTrainer.ts
@@ -3,7 +3,6 @@
*
* SPDX-License-Identifier: MIT
*/
-import { LossTrainingIteration } from '../../components/graphs/LossGraphUtil';
import ModelTrainer, { TrainingData } from '../domain/ModelTrainer';
import LayersMLModel from './LayersMLModel';
import * as tf from '@tensorflow/tfjs';
@@ -15,6 +14,11 @@ export type LayersModelTrainingSettings = {
batchSize: number;
};
+export type LossTrainingIteration = {
+ loss: number;
+ epoch: number;
+};
+
class LayersModelTrainer implements ModelTrainer {
constructor(
private settings: LayersModelTrainingSettings,
diff --git a/src/script/repository/FileUtility.ts b/src/script/repository/FileUtility.ts
index 71c62d96a..af278fe1f 100644
--- a/src/script/repository/FileUtility.ts
+++ b/src/script/repository/FileUtility.ts
@@ -5,7 +5,7 @@
*/
import { GestureData } from '../domain/stores/gesture/Gesture';
import { PersistantGestureData } from '../domain/stores/gesture/Gestures';
-import { gestures } from '../stores/Stores';
+import { stores } from '../stores/Stores';
class FileUtility {
public static loadDatasetFromFile(file: File) {
@@ -16,7 +16,7 @@ class FileUtility {
}
const contents = e.target.result;
if (typeof contents === 'string') {
- gestures.importFrom(JSON.parse(contents) as PersistantGestureData[]);
+ stores.getGestures().importFrom(JSON.parse(contents) as PersistantGestureData[]);
}
};
reader.readAsText(file as Blob);
diff --git a/src/script/repository/LocalStorageClassifierRepository.ts b/src/script/repository/LocalStorageClassifierRepository.ts
index e2cf659bc..0e9523cb6 100644
--- a/src/script/repository/LocalStorageClassifierRepository.ts
+++ b/src/script/repository/LocalStorageClassifierRepository.ts
@@ -17,6 +17,7 @@ import ClassifierRepository from '../domain/ClassifierRepository';
import Gesture, { GestureID } from '../domain/stores/gesture/Gesture';
import Classifier from '../domain/stores/Classifier';
import GestureConfidence from '../domain/stores/gesture/GestureConfidence';
+import Confidences from '../domain/stores/Confidences';
export type TrainerConsumer = (
trainer: ModelTrainer,
@@ -24,18 +25,15 @@ export type TrainerConsumer = (
class LocalStorageClassifierRepository implements ClassifierRepository {
private static readonly PERSISTANT_FILTERS_KEY = 'filters';
- private static confidences: Writable