diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9de949bdf..fccebd173 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v3 - name: Run Tests run: | - npm install + npm ci npm test formatting_and_linting: name: Prettier / svelte Check @@ -44,29 +44,11 @@ jobs: - name: Run prettier shell: bash run: | - npm install - npm run prettier - # if we find the string modified after running git status, it indicates that prettier has changed files! - if git status | grep -c "modified" -eq 0; then - exit 1 - else - exit 0 - fi - - name: Missing format report - shell: bash - if: ${{ failure() }} - run: | - echo "Missing prettier formatting. Please format files using 'npm run prettier' and resubmit!" - exit 1 + npm ci + npm run checkFormat - name: Svelte check shell: bash run: npm run check - - name: Svelte check report - shell: bash - if: ${{ failure() }} - run: | - echo "Running npm run check found warnings, please resolve them before merging!" - exit 1 build_and_deploy: if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 2269771ae..9eb66a708 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v3 - name: Run Tests run: | - npm install + npm ci npm test build_and_deploy: if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') diff --git a/.prettierignore b/.prettierignore index 4958c8934..71b433bc3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,3 @@ -src/translations.ts \ No newline at end of file +src/translations.ts +src/appInsights.ts +src/messages/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a1c3381a3..0999a1039 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,7 +22,16 @@ "a11y-structure": "ignore", "a11y-click-events-have-key-events": "ignore", "a11y-missing-content": "ignore", - "a11y-no-noninteractive-element-interactions":"ignore", + "a11y-no-noninteractive-element-interactions": "ignore", "a11y-no-static-element-interactions": "ignore" - } -} + }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + }, + "hide-files.files": [] +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..c6414b2c5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,5 @@ +services: + ml: + build: . + ports: + - "5174:8080" diff --git a/dockerfile b/dockerfile new file mode 100644 index 000000000..1ef2ca06b --- /dev/null +++ b/dockerfile @@ -0,0 +1,25 @@ +# A Dockerfile must begin with a FROM instruction. +# New base image will be built based on the Node.js version 20 +# Image that is based on Debian Linux. +FROM node:20 + +# Create app directory +WORKDIR /usr/src/app + +# A wildcard is used to ensure both package.json AND package-lock.json are copied +COPY package*.json ./ + +# Install app dependencies +RUN npm install +RUN npm install express + +# Bundle app source +COPY . . + +# Creates a "dist" folder with the production build +RUN npm run build + +EXPOSE 8080 + +# Start the server using the production build +CMD [ "node", "dist/main.cjs" ] \ No newline at end of file diff --git a/features.json b/features.json index b770c3d48..1c4b872b5 100644 --- a/features.json +++ b/features.json @@ -1,4 +1,6 @@ { - "title": "Learning tool", - "knnModel": true + "title": "ML-Machine", + "knnModel": false, + "lossGraph": false, + "makecode": false } diff --git a/prepEnv.js b/prepEnv.js index 2bffca672..15b1f0fa4 100644 --- a/prepEnv.js +++ b/prepEnv.js @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ // This script is used to setup different build configurations // run by ` node prepEnv.js branded ` for ml-machine-branded config -import { copyFile } from 'node:fs/promises'; +import { copyFile } from 'fs'; // Validate input const args = process.argv; @@ -31,6 +31,7 @@ const fileMoveTargets = { ['./src/__viteBuildVariants__/ml-machine-simple/features.json', './features.json'] ] } + const availableTargets = Object.getOwnPropertyNames(fileMoveTargets); const buildVariantTarget = args[2]; if (!availableTargets.includes(buildVariantTarget)) { @@ -41,14 +42,14 @@ if (!availableTargets.includes(buildVariantTarget)) { // The actual work const copyFiles = fileMoveTargets[buildVariantTarget]; - copyFiles.forEach(element => { const source = element[0]; const destination = element[1]; - copyFile(source, destination).then(() => { + copyFile(source, destination, (err) => { console.log("Copied ", element[0], " -> ", element[1]) - }).catch((err) => { - console.error("Failed to move ", source, " to ", destination) - throw new Error(err) + if (err) { + console.error("Failed to move ", source, " to ", destination) + throw new Error(err) + } }) }); \ No newline at end of file diff --git a/public/imgs/dotted_graph_line.svg b/public/imgs/dotted_graph_line.svg new file mode 100644 index 000000000..96cfb156d --- /dev/null +++ b/public/imgs/dotted_graph_line.svg @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/public/imgs/graph_left.svg b/public/imgs/graph_left.svg new file mode 100644 index 000000000..9dc611b5f --- /dev/null +++ b/public/imgs/graph_left.svg @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/public/imgs/graph_line_rounded.svg b/public/imgs/graph_line_rounded.svg new file mode 100644 index 000000000..0483197e2 --- /dev/null +++ b/public/imgs/graph_line_rounded.svg @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/public/imgs/parallel.svg b/public/imgs/parallel.svg new file mode 100644 index 000000000..245fe8836 --- /dev/null +++ b/public/imgs/parallel.svg @@ -0,0 +1,15 @@ + + + + + + + chart--parallel + + + \ No newline at end of file diff --git a/public/main.cjs b/public/main.cjs new file mode 100644 index 000000000..17e480977 --- /dev/null +++ b/public/main.cjs @@ -0,0 +1,13 @@ +const express = require('express'); +const path = require('path'); +const app = express(); + +app.use(express.static(path.join(__dirname, '.'))); + +app.get('/', async (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + +app.listen(8080, () => { + console.log("Server successfully running on port 8080"); +}); \ No newline at end of file diff --git a/src/App.svelte b/src/App.svelte index ec6cfd4f7..6c5e4f6a6 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -39,6 +39,8 @@ import { DeviceRequestStates } from './script/stores/connectDialogStore'; import Router from './router/Router.svelte'; import { Feature, getFeature } from './script/FeatureToggles'; + import { welcomeLog } from './script/utils/Logger'; + welcomeLog(); ConnectionBehaviours.setInputBehaviour(new InputBehaviour()); ConnectionBehaviours.setOutputBehaviour(new OutputBehaviour()); diff --git a/src/StaticConfiguration.ts b/src/StaticConfiguration.ts index c93289bdc..a80daf9ca 100644 --- a/src/StaticConfiguration.ts +++ b/src/StaticConfiguration.ts @@ -45,7 +45,7 @@ class StaticConfiguration { // Link to the MakeCode firmware template public static readonly makecodeFirmwareUrl = - 'https://makecode.microbit.org/#pub:54705-16835-80762-83855'; + 'https://makecode.microbit.org/#pub:52042-28239-00563-08630'; public static readonly isMicrobitOutdated = (origin: HexOrigin, version: number) => { // Current versions, remember to update these, whenever changes to firmware are made! @@ -58,13 +58,22 @@ class StaticConfiguration { }; // Line colors are picked in the order of this array. - public static readonly liveGraphColors = ['#f9808e', '#80f98e', '#808ef9']; + public static readonly liveGraphColors = [ + '#ff606e', + '#30f09e', + '#3030ff', + '#58355E', + '#E0FF4F', + '#FF2ECC', + '#F28F3B', + '#C8553D', + ]; // Colors to assign to gestures, useful for identifying gestures on graphs. public static readonly gestureColors = [ '#FCA311', - '#65334D', - '#CB04A5', + '#00ff81', + '#b1e400', '#ADFCF9', '#89A894', '#4B644A', @@ -114,7 +123,9 @@ class StaticConfiguration { */ public static readonly minNoOfGestures = 2; - // The settings given to the LayersModelTrainer + /** + * The neural network training settings + */ public static readonly layersModelTrainingSettings: LayersModelTrainingSettings = { noOfEpochs: 80, batchSize: 16, @@ -123,6 +134,9 @@ class StaticConfiguration { noOfUnits: 16, // size of hidden layer }; - public static readonly knnNeighbourCount = 3; + /** + * How many samples should the KNN model use for prediction? i.e the k-value. + */ + public static readonly defaultKnnNeighbourCount = 3; } export default StaticConfiguration; diff --git a/src/__tests__/classifier.test.ts b/src/__tests__/classifier.test.ts index c63888687..d341ac81b 100644 --- a/src/__tests__/classifier.test.ts +++ b/src/__tests__/classifier.test.ts @@ -7,37 +7,37 @@ * SPDX-License-Identifier: MIT */ -import { classifier, gestures } from '../script/stores/Stores'; +import { stores } from '../script/stores/Stores'; import TestMLModelTrainer from './mocks/mlmodel/TestMLModelTrainer'; describe('Classifier tests', () => { test('Changing matrix does not mark model as untrained', async () => { - const gesture = gestures.createGesture('some gesture'); - gestures.createGesture('some gesture2'); - await classifier.getModel().train(new TestMLModelTrainer(2)); + const gesture = stores.getGestures().createGesture('some gesture'); + stores.getGestures().createGesture('some gesture2'); + await stores.getClassifier().getModel().train(new TestMLModelTrainer(2)); gesture.setLEDOutput(new Array(25).fill(false) as boolean[]); - expect(classifier.getModel().isTrained()).toBe(true); + expect(stores.getClassifier().getModel().isTrained()).toBe(true); }); test('Adding gesture marks model as untrained', async () => { - gestures.createGesture('some gesture'); - gestures.createGesture('some gesture2'); - await classifier.getModel().train(new TestMLModelTrainer(2)); + stores.getGestures().createGesture('some gesture'); + stores.getGestures().createGesture('some gesture2'); + await stores.getClassifier().getModel().train(new TestMLModelTrainer(2)); - gestures.createGesture('Added gesture'); + stores.getGestures().createGesture('Added gesture'); - expect(classifier.getModel().isTrained()).toBe(false); + expect(stores.getClassifier().getModel().isTrained()).toBe(false); }); test('Removing gesture marks model as untrained', async () => { - gestures.createGesture('some gesture'); - gestures.createGesture('some gesture2'); - const gesture3 = gestures.createGesture('some gesture2'); - await classifier.getModel().train(new TestMLModelTrainer(2)); + stores.getGestures().createGesture('some gesture'); + stores.getGestures().createGesture('some gesture2'); + const gesture3 = stores.getGestures().createGesture('some gesture2'); + await stores.getClassifier().getModel().train(new TestMLModelTrainer(2)); - gestures.removeGesture(gesture3.getId()); + stores.getGestures().removeGesture(gesture3.getId()); - expect(classifier.getModel().isTrained()).toBe(false); + expect(stores.getClassifier().getModel().isTrained()).toBe(false); }); }); diff --git a/src/__tests__/data-representation.test.ts b/src/__tests__/data-representation.test.ts new file mode 100644 index 000000000..f0cc4b4c1 --- /dev/null +++ b/src/__tests__/data-representation.test.ts @@ -0,0 +1,81 @@ +/** + * @vitest-environment jsdom + */ +/** + * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ + +import LiveDataBuffer from '../script/domain/LiveDataBuffer'; +import LiveData from '../script/domain/stores/LiveData'; +import MicrobitAccelerometerLiveData, { + MicrobitAccelerometerDataVector, +} from '../script/livedata/MicrobitAccelerometerData'; +import { repeat } from './testUtils'; +import { get } from 'svelte/store'; +import { LiveDataVector } from '../script/domain/stores/LiveDataVector'; +import SmoothedLiveData from '../script/livedata/SmoothedLiveData'; +import { smoothNewValue } from '../script/utils/graphUtils'; + +describe('Data representation tests', () => { + test('Creating accelerometer live data does not throw', () => { + expect(() => { + new MicrobitAccelerometerLiveData(new LiveDataBuffer(10)); + }).not.toThrow(); + }); + + test('Number of elements in buffer does not exceed set amount', () => { + const elemsInBuffer = 10; + const liveData = new MicrobitAccelerometerLiveData(new LiveDataBuffer(elemsInBuffer)); + + repeat( + () => liveData.put(new MicrobitAccelerometerDataVector({ x: 0, y: 0, z: 0 })), + 20, + ); + + expect(() => liveData.getBuffer().getSeries(100, elemsInBuffer)).not.toThrow(); + expect(liveData.getBuffer().getSeries(100, 10).length).toEqual(10); + + expect(() => liveData.getBuffer().getSeries(100, elemsInBuffer + 1)).toThrow(); + }); + + test('Can extract vectors from live data', () => { + const liveData: LiveData = new MicrobitAccelerometerLiveData( + new LiveDataBuffer(10), + ); + + repeat( + () => liveData.put(new MicrobitAccelerometerDataVector({ x: 1, y: 2, z: 3 })), + 20, + ); + + expect(() => get(liveData).getVector()).not.toThrow(); + expect(get(liveData).getVector()).toEqual([1, 2, 3]); + }); + + test('Test smoothed values', () => { + const liveData: LiveData = + new MicrobitAccelerometerLiveData(new LiveDataBuffer(20)); + const smoothLiveData = new SmoothedLiveData(liveData, 2); + + const point1 = new MicrobitAccelerometerDataVector({ x: 3, y: 2, z: 1 }); + const point2 = new MicrobitAccelerometerDataVector({ x: 1, y: 2, z: 3 }); + + liveData.put(point1); + liveData.put(point2); + + expect(get(smoothLiveData).getVector()[0]).toBeCloseTo( + smoothNewValue(point2.getVector()[0], point1.getVector()[0]), + 10, + ); + expect(get(smoothLiveData).getVector()[1]).toBeCloseTo( + smoothNewValue(point2.getVector()[1], point1.getVector()[1]), + 10, + ); + expect(get(smoothLiveData).getVector()[2]).toBeCloseTo( + smoothNewValue(point2.getVector()[2], point1.getVector()[2]), + 10, + ); + }); +}); diff --git a/src/__tests__/engine.test.ts b/src/__tests__/engine.test.ts new file mode 100644 index 000000000..6259c49a5 --- /dev/null +++ b/src/__tests__/engine.test.ts @@ -0,0 +1,26 @@ +/** + * @vitest-environment jsdom + */ +/** + * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ + +import { get } from 'svelte/store'; +import LiveDataBuffer from '../script/domain/LiveDataBuffer'; +import MicrobitAccelerometerLiveData from '../script/livedata/MicrobitAccelerometerData'; +import { stores } from '../script/stores/Stores'; + +describe('Engine behaviour test', () => { + test('Engine should stop predicting when the LiveData store is set', () => { + const liveData = new MicrobitAccelerometerLiveData(new LiveDataBuffer(10)); + stores.setLiveData(liveData); + const engine = stores.getEngine(); + expect(get(engine).isRunning).toBe(true); + const newLiveData = new MicrobitAccelerometerLiveData(new LiveDataBuffer(10)); + stores.setLiveData(newLiveData); + expect(get(engine).isRunning).toBe(false); + expect(get(stores.getEngine()).isRunning).toBe(true); + }); +}); diff --git a/src/__tests__/gestures.test.ts b/src/__tests__/gestures.test.ts index 0a517354d..85ff6a529 100644 --- a/src/__tests__/gestures.test.ts +++ b/src/__tests__/gestures.test.ts @@ -7,38 +7,38 @@ * SPDX-License-Identifier: MIT */ -import { gestures } from '../script/stores/Stores'; +import { stores } from '../script/stores/Stores'; describe('Tests of Gestures', () => { beforeEach(() => { - gestures.clearGestures(); + stores.getGestures().clearGestures(); }); test('Creating gesture does not throw', () => { expect(() => { - gestures.createGesture('test'); + stores.getGestures().createGesture('test'); }).not.toThrow(); }); test('Creating 2 gestures makes total number of gesture 2', () => { - gestures.createGesture('Gesture1'); - gestures.createGesture('Gesture2'); + stores.getGestures().createGesture('Gesture1'); + stores.getGestures().createGesture('Gesture2'); - expect(gestures.getGestures().length).toBe(2); + expect(stores.getGestures().getGestures().length).toBe(2); }); test('Can get gesture after creation', () => { const gestureName = 'test1234'; - const gesture = gestures.createGesture(gestureName); - const fetchedGesture = gestures.getGesture(gesture.getId()); + const gesture = stores.getGestures().createGesture(gestureName); + const fetchedGesture = stores.getGestures().getGesture(gesture.getId()); expect(fetchedGesture.getName()).toBe(gestureName); }); test('Clearing gestures clears gestures', () => { - gestures.createGesture('gestureName'); - gestures.createGesture('gestureName'); + stores.getGestures().createGesture('gestureName'); + stores.getGestures().createGesture('gestureName'); - gestures.clearGestures(); + stores.getGestures().clearGestures(); - expect(gestures.getGestures().length).toBe(0); + expect(stores.getGestures().getGestures().length).toBe(0); }); }); diff --git a/src/__tests__/testUtils.ts b/src/__tests__/testUtils.ts new file mode 100644 index 000000000..602db82f7 --- /dev/null +++ b/src/__tests__/testUtils.ts @@ -0,0 +1,10 @@ +/** + * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ +export const repeat = (func: (a?: any) => any, n: number) => { + for (let i = 0; i < n; i++) { + func(); + } +}; diff --git a/src/__viteBuildVariants__/ml-machine-simple/features.json b/src/__viteBuildVariants__/ml-machine-simple/features.json index e08c11f19..1c4b872b5 100644 --- a/src/__viteBuildVariants__/ml-machine-simple/features.json +++ b/src/__viteBuildVariants__/ml-machine-simple/features.json @@ -1,4 +1,6 @@ { "title": "ML-Machine", - "knnModel": false + "knnModel": false, + "lossGraph": false, + "makecode": false } diff --git a/src/__viteBuildVariants__/ml-machine/features.json b/src/__viteBuildVariants__/ml-machine/features.json index a45488f93..15cb272de 100644 --- a/src/__viteBuildVariants__/ml-machine/features.json +++ b/src/__viteBuildVariants__/ml-machine/features.json @@ -1,4 +1,6 @@ { "title": "ML-Machine", - "knnModel": true + "knnModel": true, + "lossGraph": true, + "makecode": true } diff --git a/src/__viteBuildVariants__/unbranded/features.json b/src/__viteBuildVariants__/unbranded/features.json index b770c3d48..97c1de396 100644 --- a/src/__viteBuildVariants__/unbranded/features.json +++ b/src/__viteBuildVariants__/unbranded/features.json @@ -1,4 +1,6 @@ { "title": "Learning tool", - "knnModel": true + "knnModel": true, + "lossGraph": true, + "makecode": true } diff --git a/src/components/3d-inspector/View3DLive.svelte b/src/components/3d-inspector/View3DLive.svelte index 205f7ff37..628769fe0 100644 --- a/src/components/3d-inspector/View3DLive.svelte +++ b/src/components/3d-inspector/View3DLive.svelte @@ -6,7 +6,7 @@ diff --git a/src/components/Gesture.svelte b/src/components/Gesture.svelte index 437b2e3c8..ea8ff72dd 100644 --- a/src/components/Gesture.svelte +++ b/src/components/Gesture.svelte @@ -21,13 +21,15 @@ import ImageSkeleton from './skeletonloading/ImageSkeleton.svelte'; import GestureTilePart from './GestureTilePart.svelte'; import StaticConfiguration from '../StaticConfiguration'; - import { gestures, liveAccelerometerData } from '../script/stores/Stores'; import Gesture from '../script/domain/stores/gesture/Gesture'; import { RecordingData } from '../script/domain/stores/gesture/Gestures'; + import { stores } from '../script/stores/Stores'; // Variables for component export let onNoMicrobitSelect: () => void; export let gesture: Gesture; + const gestures = stores.getGestures(); + $: liveData = $stores.liveData; const defaultNewName = $t('content.data.classPlaceholderNewClass'); const recordingDuration = StaticConfiguration.recordingDuration; @@ -70,13 +72,11 @@ isThisRecording = true; // New array for data - let newData: { x: number[]; y: number[]; z: number[] } = { x: [], y: [], z: [] }; + let newData: { z: number[] } = { z: [] }; // Set timeout to allow recording in 1s - const unsubscribe = liveAccelerometerData.subscribe(data => { - newData.x.push(data.x); - newData.y.push(data.y); - newData.z.push(data.z); + const unsubscribe = liveData.subscribe(data => { + newData.z.push(data.getVector()[0]); }); // Once duration is over (1000ms default), stop recording @@ -84,7 +84,7 @@ $state.isRecording = false; isThisRecording = false; unsubscribe(); - if (StaticConfiguration.pollingPredictionSampleSize <= newData.x.length) { + if (StaticConfiguration.pollingPredictionSampleSize <= newData.z.length) { const recording = { ID: Date.now(), data: newData } as RecordingData; gesture.addRecording(recording); } else { @@ -185,6 +185,10 @@
+
+
import { areActionsAllowed } from '../script/stores/uiStore'; import { t } from '../i18n'; - import { gestures } from '../script/stores/Stores'; + import { stores } from '../script/stores/Stores'; + + const gestures = stores.getGestures(); const defaultNewName = $t('content.data.classPlaceholderNewClass'); diff --git a/src/components/PleaseConnectFirst.svelte b/src/components/PleaseConnect.svelte similarity index 84% rename from src/components/PleaseConnectFirst.svelte rename to src/components/PleaseConnect.svelte index 014d8e99c..764d018bc 100644 --- a/src/components/PleaseConnectFirst.svelte +++ b/src/components/PleaseConnect.svelte @@ -16,10 +16,10 @@
-

+

{$t('menu.trainer.notConnected1')}

-

+

{$t('menu.trainer.notConnected2')}

@@ -27,6 +27,6 @@ class="m-auto arrow-filter-color" src="/imgs/down_arrow.svg" alt="down arrow icon" - width="50px" /> + width="35px" />
diff --git a/src/components/bottom/BottomPanel.svelte b/src/components/bottom/BottomPanel.svelte index da9089c6b..5a33d7340 100644 --- a/src/components/bottom/BottomPanel.svelte +++ b/src/components/bottom/BottomPanel.svelte @@ -44,9 +44,10 @@ {#if !$state.isInputAssigned} -
- {$t('footer.connectButtonNotConnected')} +
+ + {$t('footer.connectButtonNotConnected')} +
{:else} diff --git a/src/components/bottom/ConnectedLiveGraphButtons.svelte b/src/components/bottom/ConnectedLiveGraphButtons.svelte index e3e8db31a..4def58b1f 100644 --- a/src/components/bottom/ConnectedLiveGraphButtons.svelte +++ b/src/components/bottom/ConnectedLiveGraphButtons.svelte @@ -7,7 +7,7 @@ diff --git a/src/components/buttons/StandardButton.svelte b/src/components/buttons/StandardButton.svelte index 3ba59abbd..4f660c1d3 100644 --- a/src/components/buttons/StandardButton.svelte +++ b/src/components/buttons/StandardButton.svelte @@ -35,8 +35,16 @@ -
+
diff --git a/src/components/connection-prompt/ConnectDialogContainer.svelte b/src/components/connection-prompt/ConnectDialogContainer.svelte index 31c765bd2..9458850b5 100644 --- a/src/components/connection-prompt/ConnectDialogContainer.svelte +++ b/src/components/connection-prompt/ConnectDialogContainer.svelte @@ -22,6 +22,7 @@ import { btPatternInput, btPatternOutput } from '../../script/stores/connectionStore'; import MBSpecs from '../../script/microbit-interfacing/MBSpecs'; import BrokenFirmwareDetected from './usb/BrokenFirmwareDetected.svelte'; + import { onMount } from 'svelte'; let flashProgress = 0; @@ -56,12 +57,15 @@ }) .catch((e: Error) => { // Couldn't find name. Set to manual transfer progress instead + + $connectionDialogState.connectionState = ConnectDialogStates.MANUAL_TUTORIAL; + /* TODO: Disabled the broken firmware warning for now if (e.message.includes('No valid interfaces found')) { // Edge case, caused by a bad micro:bit firmware $connectionDialogState.connectionState = ConnectDialogStates.BAD_FIRMWARE; } else { - $connectionDialogState.connectionState = ConnectDialogStates.MANUAL_TUTORIAL; } +*/ }); } diff --git a/src/components/connection-prompt/bluetooth/BluetoothConnectDialog.svelte b/src/components/connection-prompt/bluetooth/BluetoothConnectDialog.svelte index 94ee6a805..a38381306 100644 --- a/src/components/connection-prompt/bluetooth/BluetoothConnectDialog.svelte +++ b/src/components/connection-prompt/bluetooth/BluetoothConnectDialog.svelte @@ -162,8 +162,11 @@
- {$t('popup.connectMB.bluetooth.connect')} +
+ + {$t('popup.connectMB.bluetooth.connect')} + +
{/if} diff --git a/src/components/connection-prompt/usb/FindUsbDialog.svelte b/src/components/connection-prompt/usb/FindUsbDialog.svelte index 8e652b52c..4f625dc81 100644 --- a/src/components/connection-prompt/usb/FindUsbDialog.svelte +++ b/src/components/connection-prompt/usb/FindUsbDialog.svelte @@ -43,13 +43,15 @@

{/if}
- { - step = 2; - }}> - {$t(step === 1 ? 'connectMB.usb.button1' : 'connectMB.usb.button2')} - +
+ { + step = 2; + }}> + {$t(step === 1 ? 'connectMB.usb.button1' : 'connectMB.usb.button2')} + +
diff --git a/src/components/connection-prompt/usb/manual/ManualInstallTutorial.svelte b/src/components/connection-prompt/usb/manual/ManualInstallTutorial.svelte index ae678ef9d..df8b73fe0 100644 --- a/src/components/connection-prompt/usb/manual/ManualInstallTutorial.svelte +++ b/src/components/connection-prompt/usb/manual/ManualInstallTutorial.svelte @@ -68,7 +68,8 @@
- {$t('connectMB.usb.manual.done')} + + {$t('connectMB.usb.manual.done')} +
diff --git a/src/components/control-bar/ControlBar.svelte b/src/components/control-bar/ControlBar.svelte index 32def58ab..78f3c8e63 100644 --- a/src/components/control-bar/ControlBar.svelte +++ b/src/components/control-bar/ControlBar.svelte @@ -5,8 +5,13 @@ --> -
+
diff --git a/src/components/filters/FilterList.ts b/src/components/filters/FilterList.ts new file mode 100644 index 000000000..35501f6b5 --- /dev/null +++ b/src/components/filters/FilterList.ts @@ -0,0 +1,21 @@ +/** + * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ +import { writable } from 'svelte/store'; +import { FilterType } from '../../script/domain/FilterTypes'; +import { stores } from '../../script/stores/Stores'; + +export const toggleFilterCheckmarkClickHandler = + (filterType: FilterType) => (e: MouseEvent) => { + e.preventDefault(); + const selectedFilters = stores.getClassifier().getFilters(); + selectedFilters.has(filterType) + ? selectedFilters.remove(filterType) + : selectedFilters.add(filterType); + }; + +export const highlightedFilter = writable(FilterType.MAX); +export const showHighlighted = writable(false); +export const anchorElement = writable(null); diff --git a/src/components/filters/FilterListFilterPreview.svelte b/src/components/filters/FilterListFilterPreview.svelte new file mode 100644 index 000000000..64573e806 --- /dev/null +++ b/src/components/filters/FilterListFilterPreview.svelte @@ -0,0 +1,27 @@ + + + +{#if $showHighlighted} +
+
+

{filter.getName()}

+

{filter.getDescription()}

+
+ +
+{/if} diff --git a/src/components/filters/FilterListRow.svelte b/src/components/filters/FilterListRow.svelte new file mode 100644 index 000000000..f584c4939 --- /dev/null +++ b/src/components/filters/FilterListRow.svelte @@ -0,0 +1,60 @@ + + + +{#key `filter-${filterType}-${checked}`} +
+
+
+ + +
+
+ { + highlightedFilter.set(filterType); + showHighlighted.set(true); + }} + on:mouseleave={() => { + showHighlighted.set(false); + }} + on:click={() => { + navigate(Paths.FILTERS); + showHighlighted.set(false); + highlightedFilter.set(filterType); + }} + src="imgs/parallel.svg" + alt="data representation icon" + class="w-6 hover:opacity-60 mr-0.5 cursor-pointer" /> +
+
+
+{/key} diff --git a/src/components/filters/FiltersList.svelte b/src/components/filters/FiltersList.svelte new file mode 100644 index 000000000..dcd61a31d --- /dev/null +++ b/src/components/filters/FiltersList.svelte @@ -0,0 +1,23 @@ + + + +
+ {#each availableFilters as filterType} + + {/each} +
diff --git a/src/components/graphs/DimensionLabels.svelte b/src/components/graphs/DimensionLabels.svelte index 4dfe89c56..17b507def 100644 --- a/src/components/graphs/DimensionLabels.svelte +++ b/src/components/graphs/DimensionLabels.svelte @@ -14,9 +14,9 @@ - -
- -
diff --git a/src/components/graphs/LossGraph.svelte b/src/components/graphs/LossGraph.svelte index ad6da1abe..af50b5b64 100644 --- a/src/components/graphs/LossGraph.svelte +++ b/src/components/graphs/LossGraph.svelte @@ -21,7 +21,7 @@ import { onMount } from 'svelte'; import { Readable } from 'svelte/store'; - import { LossTrainingIteration } from './LossGraphUtil'; + import { LossTrainingIteration } from '../../script/mlmodels/LayersModelTrainer'; export let loss: Readable; export let maxX: number | undefined = undefined; @@ -71,7 +71,7 @@ }, }, y: { - type: 'logarithmic', + type: 'linear', min: 0, max: 1, grid: { diff --git a/src/components/graphs/LossGraphUtil.ts b/src/components/graphs/LossGraphUtil.ts deleted file mode 100644 index 38b1b5516..000000000 --- a/src/components/graphs/LossGraphUtil.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors - * - * SPDX-License-Identifier: MIT - */ - -// TODO: Where to place this? It is used in the LayersModelTrainer, and maybe in other trainers in the future. -// It is primarily used by graph at the moment (28. feb 24) -// - Move to graphUtils.ts 10 march 24 -export type LossTrainingIteration = { - loss: number; - epoch: number; -}; diff --git a/src/components/graphs/MicrobitLiveGraph.svelte b/src/components/graphs/MicrobitLiveGraph.svelte index b057f744f..5a413d86f 100644 --- a/src/components/graphs/MicrobitLiveGraph.svelte +++ b/src/components/graphs/MicrobitLiveGraph.svelte @@ -8,45 +8,27 @@ import StaticConfiguration from '../../StaticConfiguration'; import { Feature, hasFeature } from '../../script/FeatureToggles'; import Axes from '../../script/domain/Axes'; - import { liveAccelerometerData } from '../../script/stores/Stores'; + import { stores } from '../../script/stores/Stores'; import { highlightedAxis } from '../../script/stores/uiStore'; import LiveGraph from './LiveGraph.svelte'; - import LiveGraphHighlighted from './LiveGraphHighlighted.svelte'; + //axis={Axes.X} export let width: number; - $: showhighlit = hasFeature(Feature.KNN_MODEL) && $highlightedAxis !== undefined; + console.log(hasFeature(Feature.KNN_MODEL) && $highlightedAxis !== undefined); + $: highlightedVectorIndex = + $highlightedAxis === Axes.X ? 0 : $highlightedAxis === Axes.Y ? 1 : 2; {#if showhighlit} - {#if $highlightedAxis === Axes.X} - - {/if} - {#if $highlightedAxis === Axes.Y} - - {/if} - {#if $highlightedAxis === Axes.Z} - - {/if} + {/key} {:else} - + {/if} diff --git a/src/components/graphs/RecordingGraph.svelte b/src/components/graphs/RecordingGraph.svelte index 324ec0654..b882826d4 100644 --- a/src/components/graphs/RecordingGraph.svelte +++ b/src/components/graphs/RecordingGraph.svelte @@ -18,7 +18,7 @@ } from 'chart.js'; import RecordingInspector from '../3d-inspector/RecordingInspector.svelte'; - export let data: { x: number[]; y: number[]; z: number[] }; + export let data: { z: number[] }; let verticalLineX = NaN; let hoverIndex = NaN; @@ -29,11 +29,9 @@ const getDataByIndex = (index: number) => { if (isNaN(index)) { - return { x: 0, y: 0, z: 0 }; + return { z: 0 }; } return { - x: data.x[index], - y: data.y[index], z: data.z[index], }; }; @@ -81,37 +79,17 @@ { x: number; y: number }[], string > { - const x: { x: number; y: number }[] = []; - const y: { x: number; y: number }[] = []; const z: { x: number; y: number }[] = []; - for (let i = 1; i < data.x.length; i++) { - x.push({ x: i, y: data.x[i - 1] }); - y.push({ x: i, y: data.y[i - 1] }); + for (let i = 1; i < data.z.length; i++) { z.push({ x: i, y: data.z[i - 1] }); } return { type: 'line', data: { datasets: [ - { - label: 'x', - borderColor: 'red', - borderWidth: 1, - pointRadius: 0, - pointHoverRadius: 0, - data: x, - }, - { - label: 'y', - borderColor: 'green', - borderWidth: 1, - pointRadius: 0, - pointHoverRadius: 0, - data: y, - }, { label: 'z', - borderColor: 'blue', + borderColor: 'red', borderWidth: 1, pointRadius: 0, pointHoverRadius: 0, @@ -131,7 +109,7 @@ x: { type: 'linear', min: 0, - max: data.x.length, + max: data.z.length, grid: { color: '#f3f3f3', }, @@ -232,7 +210,7 @@
diff --git a/src/components/graphs/knngraph/AxesFilterVectorView.svelte b/src/components/graphs/knngraph/AxesFilterVectorView.svelte index 6b2c3c3a4..9bbc8a28b 100644 --- a/src/components/graphs/knngraph/AxesFilterVectorView.svelte +++ b/src/components/graphs/knngraph/AxesFilterVectorView.svelte @@ -5,16 +5,21 @@ --> -
-
- -
- {#each $gestures as gesture, index} -
-
-
-
-
-

{gesture.name}

-
- {#if $state.isInputReady} -

{($confidences.get(gesture.ID).currentConfidence * 100).toFixed(2)}%

- {/if} -
- {/each} -
-
-
-
+
+
diff --git a/src/components/graphs/knngraph/KnnModelGraphSvgWithControls.svelte b/src/components/graphs/knngraph/KnnModelGraphSvgWithControls.svelte index 97a5e62d9..d2e08e11c 100644 --- a/src/components/graphs/knngraph/KnnModelGraphSvgWithControls.svelte +++ b/src/components/graphs/knngraph/KnnModelGraphSvgWithControls.svelte @@ -67,9 +67,10 @@
- - - +
+ + +
import StaticConfiguration from '../../../StaticConfiguration'; - import { classifier } from '../../../script/stores/Stores'; + import { stores } from '../../../script/stores/Stores'; import { knnHighlightedPoint } from './KnnPointToolTip'; const offsetX = 7; @@ -20,7 +20,7 @@ values: [ $knnHighlightedPoint?.pointTransformed.x, $knnHighlightedPoint?.pointTransformed.y, - classifier.getFilters().count() === 3 + stores.getClassifier().getFilters().count() === 3 ? $knnHighlightedPoint?.pointTransformed.z : undefined, ], diff --git a/src/components/output/OutputGestureStack.svelte b/src/components/output/OutputGestureStack.svelte index 59e92ea3a..44df6b3a1 100644 --- a/src/components/output/OutputGestureStack.svelte +++ b/src/components/output/OutputGestureStack.svelte @@ -28,9 +28,10 @@ import Information from '../information/Information.svelte'; import { PinTurnOnState } from './PinSelectorUtil'; import MBSpecs from '../../script/microbit-interfacing/MBSpecs'; - import { gestures } from '../../script/stores/Stores'; import Gesture, { SoundData } from '../../script/domain/stores/gesture/Gesture'; + import { stores } from '../../script/stores/Stores'; + const gestures = stores.getGestures(); type TriggerAction = 'turnOn' | 'turnOff' | 'none'; // Variables for component @@ -193,6 +194,10 @@
+
+
import Gesture from '../../script/domain/stores/gesture/Gesture'; - import AccelerometerClassifierInput from '../../script/mlmodels/AccelerometerClassifierInput'; - import { classifier, engine, gestures } from '../../script/stores/Stores'; + import AccelerometerClassifierInput, { + SingleAxisClassifierInput, + } from '../../script/mlmodels/AccelerometerClassifierInput'; + import { stores } from '../../script/stores/Stores'; import playgroundContext from './PlaygroundContext'; import TrainKnnModelButton from './TrainKNNModelButton.svelte'; import TrainLayersModelButton from './TrainLayersModelButton.svelte'; + const classifier = stores.getClassifier(); + const gestures = stores.getGestures(); + const engine = stores.getEngine(); + const getRandomGesture = (): Gesture => { return gestures.getGestures()[ Math.floor(Math.random() * gestures.getNumberOfGestures()) @@ -22,10 +28,8 @@ playgroundContext.addMessage( 'Predicting on random recording of: ' + randGesture.getName(), ); - const xs = randGesture.getRecordings()[0].data.x; - const ys = randGesture.getRecordings()[0].data.y; const zs = randGesture.getRecordings()[0].data.z; - const input = new AccelerometerClassifierInput(xs, ys, zs); + const input = new SingleAxisClassifierInput(zs); classifier.classify(input).then(() => { playgroundContext.addMessage('Finished predicting'); }); diff --git a/src/components/playground/LiveDataBufferUtilizationPercentage.svelte b/src/components/playground/LiveDataBufferUtilizationPercentage.svelte index 6d0a5b107..21cba9f05 100644 --- a/src/components/playground/LiveDataBufferUtilizationPercentage.svelte +++ b/src/components/playground/LiveDataBufferUtilizationPercentage.svelte @@ -5,11 +5,12 @@ --> diff --git a/src/components/playground/StoresDisplay.svelte b/src/components/playground/StoresDisplay.svelte index 7b8123d63..15696716c 100644 --- a/src/components/playground/StoresDisplay.svelte +++ b/src/components/playground/StoresDisplay.svelte @@ -4,14 +4,13 @@ SPDX-License-Identifier: MIT --> @@ -46,9 +45,9 @@

LiveData store

- {JSON.stringify($liveAccelerometerData, null, 2).substring( + {JSON.stringify($liveDataStore, null, 2).substring( 2, - JSON.stringify($liveAccelerometerData, null, 2).length - 1, + JSON.stringify($liveDataStore, null, 2).length - 1, )}

diff --git a/src/components/playground/TrainKNNModelButton.svelte b/src/components/playground/TrainKNNModelButton.svelte index 797e22385..b10e767d0 100644 --- a/src/components/playground/TrainKNNModelButton.svelte +++ b/src/components/playground/TrainKNNModelButton.svelte @@ -6,9 +6,11 @@ -

Synthesis interval ({$accelerometerSynthesizer.intervalSpeed}), lower is faster

+

Synthesis interval ({$liveDataSynthesizer.intervalSpeed}), lower is faster

setIntervalValue(e.detail.value)} /> diff --git a/src/components/playground/inputSynthesizer/MicrobitAccelerometerDataSynthesizer.svelte b/src/components/playground/inputSynthesizer/LiveDataSynthesizer.svelte similarity index 58% rename from src/components/playground/inputSynthesizer/MicrobitAccelerometerDataSynthesizer.svelte rename to src/components/playground/inputSynthesizer/LiveDataSynthesizer.svelte index 46d19555a..c1019a6f1 100644 --- a/src/components/playground/inputSynthesizer/MicrobitAccelerometerDataSynthesizer.svelte +++ b/src/components/playground/inputSynthesizer/LiveDataSynthesizer.svelte @@ -6,12 +6,14 @@
@@ -20,13 +22,15 @@

Uses sine-waves to produce LiveData

- +
- -

{JSON.stringify($accelerometerSynthesizer, null, 2)}

+ {#key keycnt} + + {/key} +

{JSON.stringify($liveDataSynthesizer, null, 2)}

diff --git a/src/components/playground/inputSynthesizer/LiveDataSynthesizer.ts b/src/components/playground/inputSynthesizer/LiveDataSynthesizer.ts new file mode 100644 index 000000000..d779bf821 --- /dev/null +++ b/src/components/playground/inputSynthesizer/LiveDataSynthesizer.ts @@ -0,0 +1,163 @@ +/** + * (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 { stores } from '../../../script/stores/Stores'; +import { SyntheticLiveData } from './SyntheticLiveData '; +import BaseVector from '../../../script/livedata/BaseVector'; + +type LiveDataSynthesizerOptions = { + intervalSpeed: number; + speeds: number[]; + isActive: boolean; + noOfAxes: number; +}; +const letters = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'L', + 'M', + 'N', + 'O', + 'P', +]; + +class LiveDataSynthesizer implements Readable { + private interval: NodeJS.Timeout | undefined = undefined; + private store: Writable; + private referenceStoreGetter: () => SyntheticLiveData; + + constructor() { + this.store = writable({ + intervalSpeed: this.getInitialIntervalValue(), + speeds: [this.getInitialSineSpeed()], + isActive: false, + noOfAxes: 1, + } as LiveDataSynthesizerOptions); + stores.setLiveData(new SyntheticLiveData([letters[0]])); + this.referenceStoreGetter = () => get(stores).liveData as SyntheticLiveData; + } + + public subscribe( + run: Subscriber, + invalidate?: ((value?: LiveDataSynthesizerOptions | undefined) => void) | undefined, + ): Unsubscriber { + return this.store.subscribe(run, invalidate); + } + + public updateInterval() { + if (this.interval) { + clearInterval(this.interval); + } + this.interval = setInterval(() => { + this.generateData(); + }, get(this.store).intervalSpeed); + } + + public getMinIntervalValue() { + return 5; + } + + public getMaxIntervalValue() { + return 300; + } + + public getInitialIntervalValue() { + return this.getMinIntervalValue(); + } + + public getInitialSineSpeed() { + return this.getMinSineSpeed() / 3000; + } + + public getMinSineSpeed() { + return 0.0; + } + + public getMaxSineSpeed() { + return 100; + } + + public setNoOfAxes(axes: number) { + this.store.update(e => { + if (e.noOfAxes !== axes) { + console.log('changed liveDatra'); + stores.setLiveData(new SyntheticLiveData(letters.slice(0, axes))); + } + e.noOfAxes = axes; + if (axes > e.speeds.length) { + e.speeds = [...e.speeds, ...new Array(axes - e.speeds.length).fill(0)]; + } else { + e.speeds = e.speeds.slice(0, axes); + } + return e; + }); + } + + public generateData() { + const val = new Date().getTime(); + + let newVector = new Array(get(this.store).noOfAxes).fill(0); + newVector = newVector.map((x, i) => Math.sin(val * get(this.store).speeds[i])); + const vectorLetters = letters.slice(0, newVector.length); + const newValue = new BaseVector(newVector, vectorLetters); + + this.referenceStoreGetter().put(newValue); + } + + public setSpeed(index: number, speed: number) { + this.store.update(s => { + s.speeds[index] = speed / 3000; + return s; + }); + } + + public setIntervalSpeed(value: number) { + this.store.update(updater => { + updater.intervalSpeed = value; + return updater; + }); + this.updateInterval(); + } + + public stop(): void { + clearInterval(this.interval); + this.store.update(updater => { + updater.isActive = false; + return updater; + }); + } + + public start(): void { + this.updateInterval(); + this.store.update(updater => { + updater.isActive = true; + return updater; + }); + } + + public isActive(): boolean { + return get(this).isActive; + } +} + +const liveDataSynthesizer = new LiveDataSynthesizer(); + +export default liveDataSynthesizer; diff --git a/src/components/playground/inputSynthesizer/NoOfAxesCounter.svelte b/src/components/playground/inputSynthesizer/NoOfAxesCounter.svelte new file mode 100644 index 000000000..bc7be5d7f --- /dev/null +++ b/src/components/playground/inputSynthesizer/NoOfAxesCounter.svelte @@ -0,0 +1,16 @@ + + + +

No of axes ({$liveDataSynthesizer.noOfAxes})

+ setNoOfAxes(e.detail.value)} /> diff --git a/src/components/playground/inputSynthesizer/SpeedSliders.svelte b/src/components/playground/inputSynthesizer/SpeedSliders.svelte index a328c4c4f..bc9418c13 100644 --- a/src/components/playground/inputSynthesizer/SpeedSliders.svelte +++ b/src/components/playground/inputSynthesizer/SpeedSliders.svelte @@ -4,38 +4,16 @@ SPDX-License-Identifier: MIT --> -

x Speed (Frequency)

- accelerometerSynthesizer.setXSpeed(e.detail.value)} /> - -

y Speed (Frequency)

- accelerometerSynthesizer.setYSpeed(e.detail.value)} /> - -

z Speed (Frequency)

- accelerometerSynthesizer.setZSpeed(e.detail.value)} /> +{#each values as val, index} +

Speed {index}

+ liveDataSynthesizer.setSpeed(index, e.detail.value)} /> +{/each} diff --git a/src/components/playground/inputSynthesizer/SynthesizerGraph.svelte b/src/components/playground/inputSynthesizer/SynthesizerGraph.svelte index a602c9921..c8747a6c0 100644 --- a/src/components/playground/inputSynthesizer/SynthesizerGraph.svelte +++ b/src/components/playground/inputSynthesizer/SynthesizerGraph.svelte @@ -3,15 +3,14 @@ SPDX-License-Identifier: MIT --> - + + + + + +
+ + +
diff --git a/src/components/playground/inputSynthesizer/SynthesizerToggleButton.svelte b/src/components/playground/inputSynthesizer/SynthesizerToggleButton.svelte index 46204d025..227581b1e 100644 --- a/src/components/playground/inputSynthesizer/SynthesizerToggleButton.svelte +++ b/src/components/playground/inputSynthesizer/SynthesizerToggleButton.svelte @@ -4,13 +4,13 @@ SPDX-License-Identifier: MIT --> @@ -18,8 +18,7 @@
+ on:click={toggleSynthesizer}> + {$liveDataSynthesizer.isActive ? 'Stop synthesizer' : 'Start synthesizer'} +
diff --git a/src/components/playground/inputSynthesizer/SyntheticLiveData .ts b/src/components/playground/inputSynthesizer/SyntheticLiveData .ts new file mode 100644 index 000000000..ac2ca271d --- /dev/null +++ b/src/components/playground/inputSynthesizer/SyntheticLiveData .ts @@ -0,0 +1,44 @@ +/** + * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ +import { + Subscriber, + Invalidator, + Unsubscriber, + Writable, + writable, + get, +} from 'svelte/store'; +import LiveDataBuffer from '../../../script/domain/LiveDataBuffer'; +import LiveData from '../../../script/domain/stores/LiveData'; +import BaseVector from '../../../script/livedata/BaseVector'; + +export class SyntheticLiveData implements LiveData { + private store: Writable; + private buffer: LiveDataBuffer; + public constructor(labels: string[]) { + this.store = writable(new BaseVector(new Array(labels.length).fill(0), labels)); + this.buffer = new LiveDataBuffer(200); + } + put(data: BaseVector): void { + this.store.set(data); + this.buffer.addValue(data); + } + getBuffer(): LiveDataBuffer { + return this.buffer; + } + getSeriesSize(): number { + return get(this.store).getSize(); + } + getLabels(): string[] { + return get(this.store).getLabels(); + } + subscribe( + run: Subscriber, + invalidate?: Invalidator | 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} +
+
+
+
+
+

{gesture.name}

+
+ {#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} + +
+ +
+

Neural Network

+
+ +
+

KNN Model

+
+
+
+{: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>; private static mlModel: Writable; private static filters: Filters; private static persistedFilters: PersistantWritable; private classifierFactory: ClassifierFactory; - constructor() { - const initialConfidence = new Map(); - LocalStorageClassifierRepository.confidences = writable(initialConfidence); + constructor(private confidences: Confidences) { LocalStorageClassifierRepository.mlModel = writable(undefined); LocalStorageClassifierRepository.persistedFilters = new PersistantWritable( - FilterTypes.toIterable(), + [FilterType.MAX, FilterType.MEAN, FilterType.MIN, FilterType.STD], LocalStorageClassifierRepository.PERSISTANT_FILTERS_KEY, ); LocalStorageClassifierRepository.filters = new Filters(this.getFilters()); @@ -70,6 +68,7 @@ class LocalStorageClassifierRepository implements ClassifierRepository { get(gestureRepository), LocalStorageClassifierRepository.filters, ); + const model = await trainer.trainModel(trainingData); LocalStorageClassifierRepository.mlModel.set(model); } @@ -78,13 +77,11 @@ class LocalStorageClassifierRepository implements ClassifierRepository { return (trainer: ModelTrainer) => this.trainModel(trainer); } - private setGestureConfidence(gestureId: GestureID, confidence: number) { + public setGestureConfidence(gestureId: GestureID, confidence: number) { if (confidence < 0 || confidence > 1) { throw new Error('Cannot set gesture confidence. Must be in the range 0.0-1.0'); } - const newConfidences = get(LocalStorageClassifierRepository.confidences); - newConfidences.set(gestureId, confidence); - LocalStorageClassifierRepository.confidences.set(newConfidences); + this.confidences.setConfidence(gestureId, confidence); } private getFilters(): Writable { @@ -113,21 +110,26 @@ class LocalStorageClassifierRepository implements ClassifierRepository { } public getGestureConfidence(gestureId: number): GestureConfidence { - const derivedConfidence = derived( - [LocalStorageClassifierRepository.confidences], - stores => { - const confidenceStore = stores[0]; - if (confidenceStore.has(gestureId)) { - return confidenceStore.get(gestureId) as number; - } - return 0; - }, - ); + const derivedConfidence = derived([this.confidences], stores => { + const confidenceStore = stores[0]; + if (confidenceStore.has(gestureId)) { + return confidenceStore.get(gestureId) as number; + } + throw new Error("No confidence found for gesture with id '" + gestureId + "'"); + }); return new GestureConfidence( StaticConfiguration.defaultRequiredConfidence, derivedConfidence, ); } + + public hasGestureConfidence(gestureId: number): boolean { + return get(this.confidences).has(gestureId); + } + + public getConfidences(): Confidences { + return this.confidences; + } } export default LocalStorageClassifierRepository; diff --git a/src/script/repository/LocalStorageGestureRepository.ts b/src/script/repository/LocalStorageGestureRepository.ts index ceef9630c..5bb5d65c2 100644 --- a/src/script/repository/LocalStorageGestureRepository.ts +++ b/src/script/repository/LocalStorageGestureRepository.ts @@ -6,15 +6,15 @@ import ControlledStorage from '../ControlledStorage'; import { Subscriber, Unsubscriber, Writable, get, writable } from 'svelte/store'; import LocalStorageClassifierRepository from './LocalStorageClassifierRepository'; -import { classifier } from '../stores/Stores'; import GestureRepository from '../domain/GestureRepository'; import Gesture from '../domain/stores/gesture/Gesture'; import { PersistantGestureData } from '../domain/stores/gesture/Gestures'; +import { stores } from '../stores/Stores'; class LocalStorageGestureRepository implements GestureRepository { private readonly LOCAL_STORAGE_KEY = 'gestureData'; private static gestureStore: Writable; - constructor(private modelRepository: LocalStorageClassifierRepository) { + constructor(private classifierRepository: LocalStorageClassifierRepository) { LocalStorageGestureRepository.gestureStore = writable([]); LocalStorageGestureRepository.gestureStore.set(this.getPersistedGestures()); } @@ -88,32 +88,40 @@ class LocalStorageGestureRepository implements GestureRepository { ID: gesture.getId(), name: gesture.getName(), recordings: gesture.getRecordings(), + color: gesture.getColor(), output: gesture.getOutput(), }; } private getPersistedGestures(): Gesture[] { const resultFromFetch: PersistantGestureData[] = this.getPersistedData(); - return resultFromFetch.map(persistedData => this.buildGesture(persistedData)); + return resultFromFetch.map((persistedData, index) => { + const gesture = this.buildGesture(persistedData); + return gesture; + }); } private buildGesture(persistedData: PersistantGestureData) { const store = this.buildPersistedGestureStore(persistedData); // TODO: The classifier object should be accessed through the repository, not the store. This cannot be done until the classifier is cached. - const onRecordingsChanged = () => classifier.getModel().markAsUntrained(); - return new Gesture( - store, - this.modelRepository.getGestureConfidence(get(store).ID), - onRecordingsChanged, - ); + const onRecordingsChanged = () => stores.getClassifier().getModel().markAsUntrained(); + + if (!this.classifierRepository.hasGestureConfidence(get(store).ID)) { + this.classifierRepository.setGestureConfidence(get(store).ID, 0); + } + const confidence = this.classifierRepository.getGestureConfidence(get(store).ID); + + return new Gesture(store, confidence, onRecordingsChanged); } private getPersistedData(): PersistantGestureData[] { - const result = localStorage.getItem(this.LOCAL_STORAGE_KEY); - if (!result) { + if (!ControlledStorage.hasValid(this.LOCAL_STORAGE_KEY)) { return []; } - return ControlledStorage.get(this.LOCAL_STORAGE_KEY); + const storedData = ControlledStorage.get( + this.LOCAL_STORAGE_KEY, + ); + return storedData; } } diff --git a/src/script/repository/LocalStorageRepositories.ts b/src/script/repository/LocalStorageRepositories.ts index 5020108d6..2e27b6188 100644 --- a/src/script/repository/LocalStorageRepositories.ts +++ b/src/script/repository/LocalStorageRepositories.ts @@ -6,6 +6,7 @@ import LocalStorageGestureRepository from './LocalStorageGestureRepository'; import LocalStorageClassifierRepository from './LocalStorageClassifierRepository'; import Repositories from '../domain/Repositories'; +import Confidences from '../domain/stores/Confidences'; class LocalStorageRepositories implements Repositories { private gestureRepository: LocalStorageGestureRepository; @@ -20,7 +21,8 @@ class LocalStorageRepositories implements Repositories { throw new Error('Could not instantiate repository. It is already instantiated!'); } LocalStorageRepositories.instance = this; - this.classifierRepository = new LocalStorageClassifierRepository(); + const confidences = new Confidences(); + this.classifierRepository = new LocalStorageClassifierRepository(confidences); this.gestureRepository = new LocalStorageGestureRepository(this.classifierRepository); } diff --git a/src/script/repository/PersistantWritable.ts b/src/script/repository/PersistantWritable.ts index 6b34a8fdb..d21df2326 100644 --- a/src/script/repository/PersistantWritable.ts +++ b/src/script/repository/PersistantWritable.ts @@ -21,7 +21,7 @@ class PersistantWritable implements Writable { initialValue: T, private key: string, ) { - if (ControlledStorage.has(key)) { + if (ControlledStorage.hasValid(key)) { const storedValue = ControlledStorage.get(key); this.store = writable(storedValue); } else { diff --git a/src/script/stores/Stores.ts b/src/script/stores/Stores.ts index 783400bf8..b6fdd071f 100644 --- a/src/script/stores/Stores.ts +++ b/src/script/stores/Stores.ts @@ -3,43 +3,101 @@ * * SPDX-License-Identifier: MIT */ -import LocalStorageRepositories from '../repository/LocalStorageRepositories'; -import PollingPredictorEngine from '../engine/PollingPredictorEngine'; -import MicrobitAccelerometerLiveData, { - MicrobitAccelerometerData, -} from '../livedata/MicrobitAccelerometerData'; -import LiveDataBuffer from '../domain/LiveDataBuffer'; -import StaticConfiguration from '../../StaticConfiguration'; + +import { + Invalidator, + Readable, + Subscriber, + Unsubscriber, + Writable, + derived, + get, + writable, +} from 'svelte/store'; import Repositories from '../domain/Repositories'; -import Gestures from '../domain/stores/gesture/Gestures'; import Classifier from '../domain/stores/Classifier'; import Engine from '../domain/stores/Engine'; import LiveData from '../domain/stores/LiveData'; -import { derived } from 'svelte/store'; - -const repositories: Repositories = new LocalStorageRepositories(); - -const gestures: Gestures = new Gestures(repositories.getGestureRepository()); -const classifier: Classifier = repositories.getClassifierRepository().getClassifier(); - -const accelerometerDataBuffer = new LiveDataBuffer( - StaticConfiguration.accelerometerLiveDataBufferSize, -); -const liveAccelerometerData: LiveData = - new MicrobitAccelerometerLiveData(accelerometerDataBuffer); - -const engine: Engine = new PollingPredictorEngine(classifier, liveAccelerometerData); - -// I'm not sure if this one should be -const confidences = derived([gestures, ...gestures.getGestures()], stores => { - const confidenceMap = new Map(); - - const [_, ...gestureStores] = stores; - gestureStores.forEach(store => { - confidenceMap.set(store.ID, store.confidence); - }); - return confidenceMap; -}); -// Export the stores here. Please be mindful when exporting stores, avoid whenever possible. -// This helps us avoid leaking too many objects, that aren't meant to be interacted with -export { engine, gestures, classifier, liveAccelerometerData, confidences }; +import { LiveDataVector } from '../domain/stores/LiveDataVector'; +import Gestures from '../domain/stores/gesture/Gestures'; +import PollingPredictorEngine from '../engine/PollingPredictorEngine'; +import LocalStorageRepositories from '../repository/LocalStorageRepositories'; +import Logger from '../utils/Logger'; +import Confidences from '../domain/stores/Confidences'; + +type StoresType = { + liveData: LiveData; +}; +/** + * Stores is a container object, that allows for management of global stores. + */ +class Stores implements Readable { + private liveData: Writable | undefined>; + private engine: Engine | undefined; + private classifier: Classifier; + private gestures: Gestures; + private confidences: Confidences; + + public constructor() { + this.liveData = writable(undefined); + this.engine = undefined; + const repositories: Repositories = new LocalStorageRepositories(); + this.classifier = repositories.getClassifierRepository().getClassifier(); + this.confidences = repositories.getClassifierRepository().getConfidences(); + this.gestures = new Gestures(repositories.getGestureRepository()); + } + + public subscribe( + run: Subscriber, + invalidate?: Invalidator | undefined, + ): Unsubscriber { + return derived([this.liveData], stores => { + if (!stores[0]) { + throw new Error( + 'Cannot subscribe to stores, livedata is null or undefined, set it user setLiveData(...) first', + ); + } + return { + liveData: stores[0], + }; + }).subscribe(run, invalidate); + } + + public setLiveData>(liveDataStore: T): T { + Logger.log('stores', 'setting live data'); + if (!liveDataStore) { + throw new Error('Cannot set live data store to undefined/null'); + } + this.liveData.set(liveDataStore); + + // We stop the previous engine from making predictions + if (this.engine) { + this.engine.stop(); + } + this.engine = new PollingPredictorEngine(this.classifier, liveDataStore); + return get(this.liveData) as T; + } + + public getClassifier(): Classifier { + return this.classifier; + } + + public getGestures(): Gestures { + return this.gestures; + } + + public getEngine(): Engine { + if (!this.engine) { + throw new Error( + 'Cannot get engine store, the liveData store has not been set. You must set it using setLiveData(...)', + ); + } + return this.engine; + } + + public getConfidences(): Confidences { + return this.confidences; + } +} + +export const stores = new Stores(); diff --git a/src/script/stores/knnConfig.ts b/src/script/stores/knnConfig.ts new file mode 100644 index 000000000..bb7256633 --- /dev/null +++ b/src/script/stores/knnConfig.ts @@ -0,0 +1,15 @@ +/** + * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ +import { persistantWritable } from './storeUtil'; +import StaticConfiguration from '../../StaticConfiguration'; + +export type KNNSettings = { + k: number; +}; + +export const knnConfig = persistantWritable('knnConfig', { + k: StaticConfiguration.defaultKnnNeighbourCount, +}); diff --git a/src/script/stores/storeUtil.ts b/src/script/stores/storeUtil.ts index 6417d8088..7457710a2 100644 --- a/src/script/stores/storeUtil.ts +++ b/src/script/stores/storeUtil.ts @@ -10,7 +10,7 @@ import { writable, type Writable } from 'svelte/store'; // Increment if stored information types are changed // or if a localstorage data needs to be wiped // (incrementing it, will overwrite all persistantWritable data stored in localstorage) -const persistVersion = 1; +const persistVersion = 3; // Creates a svelte store which automatically loads from localstorage, and // keeps localstorage up to date diff --git a/src/script/stores/uiStore.ts b/src/script/stores/uiStore.ts index 89322f890..4ef7466d1 100644 --- a/src/script/stores/uiStore.ts +++ b/src/script/stores/uiStore.ts @@ -13,11 +13,12 @@ import { t } from '../../i18n'; import { DeviceRequestStates } from './connectDialogStore'; import CookieManager from '../CookieManager'; import { isInputPatternValid } from './connectionStore'; -import { classifier } from './Stores'; import Gesture from '../domain/stores/gesture/Gesture'; import Axes from '../domain/Axes'; import PersistantWritable from '../repository/PersistantWritable'; import { DropdownOption } from '../../components/buttons/Buttons'; +import { stores } from './Stores'; +import ModelRegistry, { ModelInfo } from '../domain/ModelRegistry'; let text: (key: string, vars?: object) => string; t.subscribe(t => (text = t)); @@ -106,7 +107,7 @@ export function areActionsAllowed(actionAllowed = true, alertIfNotReady = true): function assessStateStatus(actionAllowed = true): { isReady: boolean; msg: string } { const currentState = get(state); - const model = classifier.getModel(); + const model = stores.getClassifier().getModel(); if (currentState.isRecording) return { isReady: false, msg: text('alert.isRecording') }; if (model.isTraining()) return { isReady: false, msg: text('alert.isTraining') }; @@ -127,39 +128,10 @@ export enum MicrobitInteractions { AB, } -export type ModelEntry = { - id: string; - title: string; - label: string; -}; - -export const availableModels: ModelEntry[] = [ - { - id: 'NN', - title: 'Neural network', - label: 'neural network', - }, - { - id: 'KNN', - title: 'KNN', - label: 'KNN', - }, -]; - -const defaultModel: ModelEntry | undefined = availableModels.find( - model => model.id === 'NN', -); - -if (!defaultModel) { - throw new Error('Default model not found!'); -} -// TODO: Should just be model id instead of dropdown option -export const preferredModel = new PersistantWritable( - { - id: defaultModel.id, - label: defaultModel.label, - }, - 'prefferedModel', +const defaultModel: ModelInfo = ModelRegistry.NeuralNetwork; +export const selectedModel = new PersistantWritable( + defaultModel, + 'selectedModel', ); // TODO: Should probably be elsewhere diff --git a/src/script/utils/Logger.ts b/src/script/utils/Logger.ts index 8db6900a6..5e0eda0fc 100644 --- a/src/script/utils/Logger.ts +++ b/src/script/utils/Logger.ts @@ -5,14 +5,47 @@ */ import Environment from '../Environment'; - class Logger { + constructor(private origin: any) {} + + public log(message: any, ...params: any[]) { + Logger.log(this.origin, message, params); + } + /** * Logs a message in development environment */ public static log(origin: any, message: any, ...params: any[]) { - Environment.isInDevelopment && console.log(`[${origin}] ${message}`, params); + if (!Environment.isInDevelopment) { + return; + } + if (!(window as typeof window & { hasLogged: boolean }).hasLogged) { + welcomeLog(); + } + const outputMessage = `[${origin}] ${message} ${params}`; + !(window as typeof window & { ns: boolean }).ns && console.trace(outputMessage); + (window as typeof window & { ns: boolean }).ns && console.log(outputMessage); } } +export const welcomeLog = () => { + if ( + !Environment.isInDevelopment || + (window as typeof window & { hasLogged: boolean }).hasLogged + ) { + return; + } + console.log(`⚙️ Development Mode : + Welcome to the ML-Machine development mode. + To disable stacktrace in logs, type ns=true or ns() in console + If you experience any bugs, please report them at https://github.com/microbit-foundation/cctd-ml-machine/issues`); + Object.assign(window, { hasLogged: true }); +}; + +if (!(window as typeof window & { ns: boolean }).ns) { + Object.assign(window, { + ns: false, + ds: () => ((window as typeof window & { ns: boolean }).ns = true), + }); +} export default Logger; diff --git a/src/views/OverlayView.svelte b/src/views/OverlayView.svelte index 522001c49..3bccd3682 100644 --- a/src/views/OverlayView.svelte +++ b/src/views/OverlayView.svelte @@ -11,6 +11,7 @@ import ReconnectPrompt from '../components/ReconnectPrompt.svelte'; import OutdatedMicrobitWarning from '../components/OutdatedMicrobitWarning.svelte'; import { isInputPatternValid } from '../script/stores/connectionStore'; + import FilterListFilterPreview from '../components/filters/FilterListFilterPreview.svelte'; // Helps show error messages on top of page let latestMessage = ''; @@ -55,4 +56,5 @@ {#if $state.isInputOutdated || $state.isOutputOutdated} {/if} +
diff --git a/windi.config.js b/windi.config.js index a5525d9a8..e41e5fe6e 100644 --- a/windi.config.js +++ b/windi.config.js @@ -10,16 +10,16 @@ export default { theme: { extend: { colors: { - primary: '#3a3a3a', + primary: '#2B5EA7', primarytext: '#000000', - secondary: '#a0a0a0', + secondary: '#2CCAC0', secondarytext: '#FFFFFF', info: '#98A2B3', backgrounddark: '#F5F5F5', backgroundlight: '#ffffff', infolight: '#93c5fd', link: '#258aff', - warning: '#ffaaaa', + warning: '#FF7777', disabled: '#8892A3', primaryborder: '#E5E7EB', infobglight: '#E7E5E4',