From b35b8c4b70f2bade59a366bf0030ff46a62ebe44 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:32:53 +0000 Subject: [PATCH 01/11] Introduce UI labels for score variables --- README.md | 2 ++ packages/core/src/common/types/common.ts | 4 +++ .../src/common/util/migration/schemas/17.json | 3 +++ .../context/experiment/experiment-context.tsx | 16 +++++++++--- .../context/experiment/experiment-reducers.ts | 7 +++-- packages/core/src/context/experiment/store.ts | 25 ++++++++++++++++-- .../editable-table-expanded-row.tsx | 2 +- .../core/editable-table/editable-table.tsx | 26 +++++++++++-------- .../src/features/core/editable-table/types.ts | 1 + .../src/features/data-points/useDataPoints.ts | 19 +++++++++----- sample-app/src/components/home/home.tsx | 18 +++++++++++++ sample-app/src/experiment-container.tsx | 8 +++++- 12 files changed, 105 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 3bfe926a..a873748a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ npm run dev:app 3. Open [http://localhost:5173](http://localhost:5173) with your browser to see the result. +Note: To build and watch a package, e.g. in /packages/ui, run `npm run build -- --watch` to make the ui package build automatically. + ## Build and run production docker image ```bash diff --git a/packages/core/src/common/types/common.ts b/packages/core/src/common/types/common.ts index 61100e9a..46059eb9 100644 --- a/packages/core/src/common/types/common.ts +++ b/packages/core/src/common/types/common.ts @@ -6,6 +6,9 @@ import { z } from 'zod' export const currentVersion = '17' export const scoreName = 'Quality (0-5)' +export const scoreNames = [scoreName, scoreName + ' 2'] +// Label is shown in UI, name is used in data +export const scoreLabels = ['Quality (0-5)', 'Cost (0-5)'] const infoSchema = z.object({ name: z.string(), @@ -43,6 +46,7 @@ const valueVariableSchema = z.object({ const scoreVariableSchema = z.object({ name: z.string(), + label: z.optional(z.string()), description: z.string(), enabled: z.boolean(), }) diff --git a/packages/core/src/common/util/migration/schemas/17.json b/packages/core/src/common/util/migration/schemas/17.json index 6115def9..ca95086a 100644 --- a/packages/core/src/common/util/migration/schemas/17.json +++ b/packages/core/src/common/util/migration/schemas/17.json @@ -137,6 +137,9 @@ "name": { "type": "string" }, + "label": { + "type": "string" + }, "description": { "type": "string" }, diff --git a/packages/core/src/context/experiment/experiment-context.tsx b/packages/core/src/context/experiment/experiment-context.tsx index 22193cf3..48547f11 100644 --- a/packages/core/src/context/experiment/experiment-context.tsx +++ b/packages/core/src/context/experiment/experiment-context.tsx @@ -3,7 +3,12 @@ import { useLocalStorageReducer } from '@core/storage' import { DefaultApi } from '@boostv/process-optimizer-frontend-api' import { Dispatch, rootReducer } from './reducers' import { migrate } from '@core/common' -import { initialState, State, useApi } from '@core/context/experiment' +import { + initialState, + initialStateMultiObjective, + State, + useApi, +} from '@core/context/experiment' import { ExperimentType } from '@core/common/types' import { fetchExperimentResult } from '@core/context/experiment/api' @@ -21,17 +26,22 @@ type ExperimentProviderProps = { experimentId: string children?: React.ReactNode storage?: Storage + isMultiObjective?: boolean } export const ExperimentProvider = ({ experimentId, children, storage, + isMultiObjective, }: ExperimentProviderProps) => { const storageKey = experimentId === undefined ? 'unknown' : experimentId + const initialStateToUse = isMultiObjective + ? initialStateMultiObjective + : initialState const initialExperimentState = { - ...initialState, - experiment: { ...initialState.experiment, id: experimentId }, + ...initialStateToUse, + experiment: { ...initialStateToUse.experiment, id: experimentId }, } const [state, dispatch] = useLocalStorageReducer( rootReducer, diff --git a/packages/core/src/context/experiment/experiment-reducers.ts b/packages/core/src/context/experiment/experiment-reducers.ts index 056ea831..af79e75d 100644 --- a/packages/core/src/context/experiment/experiment-reducers.ts +++ b/packages/core/src/context/experiment/experiment-reducers.ts @@ -7,7 +7,9 @@ import { ScoreVariableType, ValueVariableType, experimentSchema, + scoreLabels, scoreName, + scoreNames as scoreNamesState, } from '@core/common/types' import { produce } from 'immer' import md5 from 'md5' @@ -399,8 +401,9 @@ export const experimentReducer = produce( if (state.scoreVariables.length < 2) { state.scoreVariables.push({ - name: scoreName + ' 2', - description: scoreName + ' 2', + name: scoreNamesState[1] ?? scoreName + ' 2', + label: scoreLabels[1], + description: scoreNamesState[1] ?? scoreName + ' 2', enabled: true, }) const scoreNames = state.scoreVariables.map(it => it.name) diff --git a/packages/core/src/context/experiment/store.ts b/packages/core/src/context/experiment/store.ts index bee6b2f3..cdc30452 100644 --- a/packages/core/src/context/experiment/store.ts +++ b/packages/core/src/context/experiment/store.ts @@ -1,5 +1,5 @@ import { versionInfo } from '@core/common' -import { currentVersion, scoreName } from '@core/common/types' +import { currentVersion, scoreLabels, scoreName } from '@core/common/types' import { ExperimentType } from '@core/common/types' export const emptyExperiment: ExperimentType = { @@ -19,7 +19,8 @@ export const emptyExperiment: ExperimentType = { scoreVariables: [ { name: scoreName, - description: scoreName, + label: scoreLabels[0], + description: '', enabled: true, }, ], @@ -58,3 +59,23 @@ export type State = { export const initialState: State = { experiment: emptyExperiment, } + +export const initialStateMultiObjective: State = { + experiment: { + ...emptyExperiment, + scoreVariables: [ + { + name: scoreName, + label: scoreLabels[0], + description: '', + enabled: true, + }, + { + name: scoreName + ' 2', + label: scoreLabels[1], + description: '', + enabled: true, + }, + ], + }, +} diff --git a/packages/ui/src/features/core/editable-table/editable-table-expanded-row.tsx b/packages/ui/src/features/core/editable-table/editable-table-expanded-row.tsx index 6d815e47..6d8a010e 100644 --- a/packages/ui/src/features/core/editable-table/editable-table-expanded-row.tsx +++ b/packages/ui/src/features/core/editable-table/editable-table-expanded-row.tsx @@ -76,7 +76,7 @@ export const EditableTableExpandedRow = ({ key={'header' + i} className={classes.rowHeaderCell} > - {d.name} + {d.label ?? d.name} ))} diff --git a/packages/ui/src/features/core/editable-table/editable-table.tsx b/packages/ui/src/features/core/editable-table/editable-table.tsx index b7a08ddb..dccba503 100644 --- a/packages/ui/src/features/core/editable-table/editable-table.tsx +++ b/packages/ui/src/features/core/editable-table/editable-table.tsx @@ -157,9 +157,11 @@ export const EditableTable = ({ # - {rows[0]?.dataPoints.map((item, index) => ( - {item.name} - ))} + {rows + .find(r => r.isNew) + ?.dataPoints.map((item, index) => ( + {item.label ?? item.name} + ))} Edit @@ -204,14 +206,16 @@ export const EditableTable = ({ - {rows[0]?.dataPoints.map((item, index) => ( - - {item.name} - - ))} + {rows + .find(r => r.isNew) + ?.dataPoints.map((item, index) => ( + + {item.label ?? item.name} + + ))} Edit diff --git a/packages/ui/src/features/core/editable-table/types.ts b/packages/ui/src/features/core/editable-table/types.ts index f1e7ac1e..1d01b567 100644 --- a/packages/ui/src/features/core/editable-table/types.ts +++ b/packages/ui/src/features/core/editable-table/types.ts @@ -2,6 +2,7 @@ export type TableDataPointType = 'numeric' | 'options' | 'string' | 'rating' export type TableDataPoint = { name: string + label?: string value?: string tooltip?: string options?: string[] | undefined diff --git a/packages/ui/src/features/data-points/useDataPoints.ts b/packages/ui/src/features/data-points/useDataPoints.ts index eb4eff95..0e324dba 100644 --- a/packages/ui/src/features/data-points/useDataPoints.ts +++ b/packages/ui/src/features/data-points/useDataPoints.ts @@ -18,7 +18,10 @@ export const useDataPoints = ( dataPoints: DataEntry[] ) => { const scoreNames = useMemo( - () => scoreVariables.filter(it => it.enabled).map(it => it.name), + () => + scoreVariables + .filter(it => it.enabled) + .map(it => ({ name: it.name, label: it.label })), [scoreVariables] ) @@ -246,7 +249,7 @@ const mapDataPointToTableType = ( const buildEmptyRow = ( valueVariables: ValueVariableType[], categoricalVariables: CategoricalVariableType[], - scoreNames: string[] + scoreNames: { name: string; label?: string }[] ) => { return { dataPoints: buildCombinedVariables(valueVariables, categoricalVariables) @@ -259,7 +262,8 @@ const buildEmptyRow = ( })) .concat( scoreNames.map((s, i) => ({ - name: s, + name: s.name, + label: s.label, value: undefined, options: undefined, tooltip: undefined, @@ -280,7 +284,7 @@ export const formatScore = (value: number | string) => const buildRows = ( valueVariables: ValueVariableType[], categoricalVariables: CategoricalVariableType[], - scoreNames: string[], + scoreNames: { name: string; label?: string }[], dataPoints: DataEntry[] ) => { const combinedVariables = buildCombinedVariables( @@ -314,17 +318,20 @@ const buildRows = ( } }) scoreNames.forEach((v, i) => { - const existingData = item.data.find(d => d.name === v) + const existingData = item.data.find(d => d.name === v.name) const type = i === 0 ? 'rating' : 'numeric' + const label = v.label if (existingData !== undefined) { vars.push({ ...existingData, value: formatScore(existingData.value), + label, type, }) } else { vars.push({ - name: v, + name: v.name, + label, value: undefined, type, }) diff --git a/sample-app/src/components/home/home.tsx b/sample-app/src/components/home/home.tsx index b09f6cae..c7302270 100644 --- a/sample-app/src/components/home/home.tsx +++ b/sample-app/src/components/home/home.tsx @@ -107,6 +107,11 @@ export default function Home() { navigate('/experiment/' + uuid()) } + const createNewExperimentMultiobjective = () => { + deleteExperiments() + navigate('/experiment/' + uuid() + '?multiObjective=true') + } + const openSavedExperiment = (key: string) => { deleteExperiments() console.log('TODO route to ' + key) @@ -185,6 +190,19 @@ export default function Home() { + + + + + createNewExperimentMultiobjective()} + > + diff --git a/sample-app/src/experiment-container.tsx b/sample-app/src/experiment-container.tsx index c1eb054e..6d0b90d5 100644 --- a/sample-app/src/experiment-container.tsx +++ b/sample-app/src/experiment-container.tsx @@ -3,13 +3,18 @@ import TabbedExperiment from '@sample/components/experiment/tabbed-experiment' import Experiment from '@sample/components/experiment/experiment' import DebugExperiment from '@sample/components/experiment/debug-experiment' import { useGlobal } from '@sample/context/global' -import { useParams } from 'react-router-dom' +import { useParams, useSearchParams } from 'react-router-dom' import { LoadingExperiment } from '@sample/components/experiment/loading-experiment' import JsonEditor from '@sample/components/json-editor/json-editor' import { useEffect } from 'react' export default function ExperimentContainer() { const { experimentId } = useParams() + const [searchParams] = useSearchParams() + const isMultiObjective = searchParams.get('multiObjective') === 'true' + + console.log('ExperimentContainer render', { experimentId, isMultiObjective }) + const { dispatch, state: { debug, showJsonEditor, focus }, @@ -34,6 +39,7 @@ export default function ExperimentContainer() { experimentId={ Array.isArray(experimentId) ? (experimentId[0] ?? '') : experimentId } + isMultiObjective={isMultiObjective} > {focus === 'legacy' ? : } {debug && } From 6caf8f470a9a3ce4041add2b2f6d93d91fa318b5 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:37:04 +0000 Subject: [PATCH 02/11] Remove console log --- sample-app/src/experiment-container.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/sample-app/src/experiment-container.tsx b/sample-app/src/experiment-container.tsx index 6d0b90d5..2584201f 100644 --- a/sample-app/src/experiment-container.tsx +++ b/sample-app/src/experiment-container.tsx @@ -13,8 +13,6 @@ export default function ExperimentContainer() { const [searchParams] = useSearchParams() const isMultiObjective = searchParams.get('multiObjective') === 'true' - console.log('ExperimentContainer render', { experimentId, isMultiObjective }) - const { dispatch, state: { debug, showJsonEditor, focus }, From f4fba354cbe578826dd0f04142c740a41ae5cdfb Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:09:05 +0000 Subject: [PATCH 03/11] Add changeset --- .changeset/nervous-points-move.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/nervous-points-move.md diff --git a/.changeset/nervous-points-move.md b/.changeset/nervous-points-move.md new file mode 100644 index 00000000..0c7d19ae --- /dev/null +++ b/.changeset/nervous-points-move.md @@ -0,0 +1,7 @@ +--- +'@boostv/process-optimizer-frontend-core': minor +'@boostv/process-optimizer-frontend-ui': minor +'@boostv/process-optimizer-frontend-sample-app': minor +--- + +Add multiobjective experiment type to UI From 4732d708429750ba5adc47aea6cab3e7b35c03a0 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:19:19 +0100 Subject: [PATCH 04/11] Change spelling Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sample-app/src/components/home/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-app/src/components/home/home.tsx b/sample-app/src/components/home/home.tsx index c7302270..aa7d66a0 100644 --- a/sample-app/src/components/home/home.tsx +++ b/sample-app/src/components/home/home.tsx @@ -202,7 +202,7 @@ export default function Home() { From a8616c93bb691a3bc20a1aab0cfa9505779f6828 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:51:37 +0000 Subject: [PATCH 05/11] Add migration 18 with changes to score vars --- packages/core/src/common/types/common.ts | 5 +- .../common/util/converters/converters.test.ts | 22 +- .../util/migration/data-formats/18.json | 103 +++++ .../common/util/migration/migration.test.ts | 146 +++++- .../src/common/util/migration/migration.ts | 4 +- .../common/util/migration/migrations/index.ts | 2 + .../util/migration/migrations/migrateToV17.ts | 63 +-- .../util/migration/migrations/migrateToV18.ts | 46 ++ .../src/common/util/migration/schemas/18.json | 419 ++++++++++++++++++ .../context/experiment/experiment-reducers.ts | 5 +- packages/core/src/context/experiment/store.ts | 8 +- .../result-data/single-data-point.tsx | 4 +- 12 files changed, 765 insertions(+), 62 deletions(-) create mode 100644 packages/core/src/common/util/migration/data-formats/18.json create mode 100644 packages/core/src/common/util/migration/migrations/migrateToV18.ts create mode 100644 packages/core/src/common/util/migration/schemas/18.json diff --git a/packages/core/src/common/types/common.ts b/packages/core/src/common/types/common.ts index 46059eb9..4469257a 100644 --- a/packages/core/src/common/types/common.ts +++ b/packages/core/src/common/types/common.ts @@ -3,10 +3,9 @@ import { z } from 'zod' // Change the current version when doing structural // changes to any types belonging to ExperimentType -export const currentVersion = '17' +export const currentVersion = '18' -export const scoreName = 'Quality (0-5)' -export const scoreNames = [scoreName, scoreName + ' 2'] +export const scoreNames = ['Quality', 'Cost'] // Label is shown in UI, name is used in data export const scoreLabels = ['Quality (0-5)', 'Cost (0-5)'] diff --git a/packages/core/src/common/util/converters/converters.test.ts b/packages/core/src/common/util/converters/converters.test.ts index 19428e3a..e46fe583 100644 --- a/packages/core/src/common/util/converters/converters.test.ts +++ b/packages/core/src/common/util/converters/converters.test.ts @@ -6,7 +6,7 @@ import { ExperimentType, ScoreVariableType, ValueVariableType, - scoreName, + scoreNames, } from '@core/common/types' import { initialState } from '@core/context' import { @@ -26,14 +26,14 @@ describe('converters', () => { { type: 'numeric', name: 'Peber', value: 982 }, { type: 'numeric', name: 'Hvedemel', value: 632 }, { type: 'categorical', name: 'Kunde', value: 'Mus' }, - { type: 'score', name: scoreName, value: 0.1 }, + { type: 'score', name: scoreNames[0] ?? '', value: 0.1 }, ] satisfies DataPointType[], [ { type: 'numeric', name: 'Sukker', value: 15 }, { type: 'numeric', name: 'Peber', value: 123 }, { type: 'numeric', name: 'Hvedemel', value: 324 }, { type: 'categorical', name: 'Kunde', value: 'Ræv' }, - { type: 'score', name: scoreName, value: 0.2 }, + { type: 'score', name: scoreNames[0] ?? '', value: 0.2 }, ] satisfies DataPointType[], ].map((data, idx) => ({ meta: { enabled: true, id: idx + 1, valid: true }, @@ -45,16 +45,16 @@ describe('converters', () => { { type: 'numeric', name: 'Peber', value: 982 }, { type: 'numeric', name: 'Hvedemel', value: 632 }, { type: 'categorical', name: 'Kunde', value: 'Mus' }, - { type: 'score', name: scoreName, value: 0.1 }, - { type: 'score', name: scoreName + ' 2', value: 0.3 }, + { type: 'score', name: scoreNames[0] ?? '', value: 0.1 }, + { type: 'score', name: scoreNames[1] ?? '', value: 0.3 }, ] satisfies DataPointType[], [ { type: 'numeric', name: 'Sukker', value: 15 }, { type: 'numeric', name: 'Peber', value: 123 }, { type: 'numeric', name: 'Hvedemel', value: 324 }, { type: 'categorical', name: 'Kunde', value: 'Ræv' }, - { type: 'score', name: scoreName, value: 0.2 }, - { type: 'score', name: scoreName + ' 2', value: 0.4 }, + { type: 'score', name: scoreNames[0] ?? '', value: 0.2 }, + { type: 'score', name: scoreNames[1] ?? '', value: 0.4 }, ] satisfies DataPointType[], ].map((data, idx) => ({ meta: { enabled: true, id: idx + 1, valid: true }, @@ -248,8 +248,8 @@ describe('converters', () => { sampleExperiment.categoricalVariables, sampleExperiment.valueVariables, [ - { name: scoreName, description: '', enabled: true }, - { name: scoreName + ' 2', description: '', enabled: true }, + { name: scoreNames[0] ?? '', description: '', enabled: true }, + { name: scoreNames[1] ?? '', description: '', enabled: true }, ], sampleMultiObjectiveDataPoints ) @@ -265,8 +265,8 @@ describe('converters', () => { sampleExperiment.categoricalVariables, sampleExperiment.valueVariables, [ - { name: scoreName, description: '', enabled: true }, - { name: scoreName + ' 2', description: '', enabled: false }, + { name: scoreNames[0] ?? '', description: '', enabled: true }, + { name: scoreNames[1] ?? '', description: '', enabled: false }, ], sampleMultiObjectiveDataPoints ) diff --git a/packages/core/src/common/util/migration/data-formats/18.json b/packages/core/src/common/util/migration/data-formats/18.json new file mode 100644 index 00000000..b25513dc --- /dev/null +++ b/packages/core/src/common/util/migration/data-formats/18.json @@ -0,0 +1,103 @@ +{ + "id": "1234", + "changedSinceLastEvaluation": true, + "lastEvaluationHash": "never-calculated", + "info": { + "dataFormatVersion": "18", + "swVersion": "v1.2.0-16", + "name": "Cake", + "description": "Yummy", + "version": 0, + "extras": {} + }, + "categoricalVariables": [ + { + "name": "Icing", + "description": "Sugary", + "options": ["White", "Brown"], + "enabled": true + } + ], + "valueVariables": [ + { + "name": "name1", + "description": "desc1", + "min": 10, + "max": 100, + "type": "discrete", + "enabled": true + }, + { + "name": "name2", + "description": "desc2", + "min": 10.2, + "max": 100.3, + "type": "continuous", + "enabled": true + } + ], + "scoreVariables": [ + { + "name": "Quality", + "label": "Quality (0-5)", + "description": "", + "enabled": true + } + ], + "constraints": [ + { + "type": "sum", + "dimensions": [], + "value": 0 + } + ], + "optimizerConfig": { + "baseEstimator": "GP", + "acqFunc": "EI", + "initialPoints": 3, + "kappa": 1.96, + "xi": 0.01 + }, + "results": { + "id": "", + "next": [[]], + "plots": [], + "pickled": "", + "expectedMinimum": [], + "extras": {} + }, + "dataPoints": [ + { + "meta": { + "enabled": true, + "valid": true, + "id": 1 + }, + "data": [ + { + "type": "categorical", + "name": "Icing", + "value": "Brown" + }, + { + "type": "numeric", + "name": "name1", + "value": 10 + }, + { + "type": "numeric", + "name": "name2", + "value": 10.2 + }, + { + "type": "score", + "name": "Quality", + "value": 0.5 + } + ] + } + ], + "extras": { + "experimentSuggestionCount": 1 + } +} diff --git a/packages/core/src/common/util/migration/migration.test.ts b/packages/core/src/common/util/migration/migration.test.ts index a6016a59..3b297f3b 100644 --- a/packages/core/src/common/util/migration/migration.test.ts +++ b/packages/core/src/common/util/migration/migration.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { JSONSchemaFaker } from 'json-schema-faker' import { migrate, _migrate, MIGRATIONS } from './migration' +import version18 from './data-formats/18.json' import version17 from './data-formats/17.json' import version16 from './data-formats/16.json' import version3 from './data-formats/3.json' @@ -17,10 +18,12 @@ import { ExperimentType, ScoreVariableType, experimentSchema, - scoreName, + scoreLabels, + scoreNames, } from '@core/common/types' import { storeLatestSchema, loadTestData } from './test-utils' -import { migrateToV17 } from './migrations/migrateToV17' +import { scoreName17 } from './migrations/migrateToV17' +import { migrateToV17, migrateToV18 } from './migrations' describe('Migration of data format', () => { storeLatestSchema() @@ -170,7 +173,7 @@ describe('Migration of data format', () => { { name: 'score2', description: 'score', - enabled: true, + enabled: false, }, ] satisfies ScoreVariableType[] @@ -184,13 +187,13 @@ describe('Migration of data format', () => { } as unknown as ExperimentType expect(migrateToV17(experiment16).scoreVariables).toEqual([ { - name: scoreName, - description: scoreName, + name: scoreName17, + description: scoreName17, enabled: scoreVariables[0]?.enabled, }, { - name: scoreName + ' 2', - description: scoreName, + name: scoreName17 + ' 2', + description: scoreName17, enabled: scoreVariables[1]?.enabled, }, ]) @@ -243,7 +246,7 @@ describe('Migration of data format', () => { }, { type: 'score', - name: scoreName, + name: scoreName17, value: 0.5, }, ].concat( @@ -251,7 +254,7 @@ describe('Migration of data format', () => { ? [ { type: 'score', - name: scoreName + ' 2', + name: scoreName17 + ' 2', value: 2, }, ] @@ -264,14 +267,133 @@ describe('Migration of data format', () => { ) }) + describe('migrateToV18', () => { + const scoreVarsMultiObjective = [ + { + name: 'Quality (0-5)', + description: 'Quality (0-5)', + enabled: true, + }, + { + name: 'Quality (0-5) 2', + description: 'Quality (0-5) 2', + enabled: true, + }, + ] satisfies ScoreVariableType[] + + const scoreVarsMultiObjectiveDisabled = [ + { + name: 'Quality (0-5)', + description: 'Quality (0-5)', + enabled: true, + }, + { + name: 'Quality (0-5) 2', + description: 'Quality (0-5) 2', + enabled: false, + }, + ] satisfies ScoreVariableType[] + + it.each([ + ['multiobjective, all enabled', scoreVarsMultiObjective], + ['multiobjective, one disabled', scoreVarsMultiObjectiveDisabled], + ])('scoreVariables should be updated, %s', (_, scoreVariables) => { + const experiment17 = { + ...version17, + scoreVariables, + } as unknown as ExperimentType + expect(migrateToV18(experiment17).scoreVariables).toEqual([ + { + name: scoreNames[0], + label: scoreLabels[0] ?? '', + description: '', + enabled: scoreVariables[0]?.enabled, + }, + { + name: scoreNames[1], + label: scoreLabels[1] ?? '', + description: '', + enabled: scoreVariables[1]?.enabled, + }, + ]) + }) + + const dataPointsSingle = [...version17.dataPoints] + const dataPointsMulti = [...version17.dataPoints].map(dp => ({ + ...dp, + data: [...dp.data].concat([ + { + type: 'score', + name: scoreNames[1] ?? '', + value: 2, + }, + ]), + })) + + it.each([ + ['one score exists', dataPointsSingle, false], + ['two scores exist', dataPointsMulti, true], + ])( + 'data points should be renamed, %s', + (_, dataPoints, isMultiObjective) => { + const experiment17 = { + ...version17, + dataPoints, + } as unknown as ExperimentType + const actual = [ + { + meta: { + enabled: true, + valid: true, + id: 1, + }, + data: [ + { + type: 'categorical', + name: 'Icing', + value: 'Brown', + }, + { + type: 'numeric', + name: 'name1', + value: 10, + }, + { + type: 'numeric', + name: 'name2', + value: 10.2, + }, + { + type: 'score', + name: scoreNames[0], + value: 0.5, + }, + ].concat( + isMultiObjective + ? [ + { + type: 'score', + name: scoreNames[1], + value: 2, + }, + ] + : [] + ), + }, + ] + expect(migrateToV18(experiment17).dataPoints).toEqual(actual) + } + ) + }) + describe('experiment properties', () => { - //TODO: More/better tests - maybe this can be mabe obsolete by schema testing + //TODO: More/better tests - maybe this can be made obsolete by schema testing it('newest data format json should match default empty experiment', () => { expect(Object.keys(emptyExperiment).length).toBe( - Object.keys(version17).length + Object.keys(version18).length ) Object.keys(emptyExperiment).forEach(p => - expect(version17).toHaveProperty(p) + expect(version18).toHaveProperty(p) ) }) }) diff --git a/packages/core/src/common/util/migration/migration.ts b/packages/core/src/common/util/migration/migration.ts index 3b87d0e3..7c37fc87 100644 --- a/packages/core/src/common/util/migration/migration.ts +++ b/packages/core/src/common/util/migration/migration.ts @@ -17,8 +17,9 @@ import { migrateToV14, migrateToV15, migrateToV16, + migrateToV17, + migrateToV18, } from './migrations' -import { migrateToV17 } from './migrations/migrateToV17' export const migrate = (json: any): ExperimentType => { const migrated = _migrate( @@ -100,4 +101,5 @@ export const MIGRATIONS: Migration[] = [ { version: '15', converter: migrateToV15 }, { version: '16', converter: migrateToV16 }, { version: '17', converter: migrateToV17 }, + { version: '18', converter: migrateToV18 }, ] diff --git a/packages/core/src/common/util/migration/migrations/index.ts b/packages/core/src/common/util/migration/migrations/index.ts index dfc49358..ad863e51 100644 --- a/packages/core/src/common/util/migration/migrations/index.ts +++ b/packages/core/src/common/util/migration/migrations/index.ts @@ -12,3 +12,5 @@ export { migrateToV13 } from './migrateToV13' export { migrateToV14 } from './migrateToV14' export { migrateToV15 } from './migrateToV15' export { migrateToV16 } from './migrateToV16' +export { migrateToV17 } from './migrateToV17' +export { migrateToV18 } from './migrateToV18' diff --git a/packages/core/src/common/util/migration/migrations/migrateToV17.ts b/packages/core/src/common/util/migration/migrations/migrateToV17.ts index 6f86b312..326f03d2 100644 --- a/packages/core/src/common/util/migration/migrations/migrateToV17.ts +++ b/packages/core/src/common/util/migration/migrations/migrateToV17.ts @@ -1,33 +1,44 @@ -import { ExperimentType, scoreName } from '@core/common/types' +import { ExperimentType } from '@core/common/types' import { produce } from 'immer' +export const scoreName17 = 'Quality (0-5)' + export const migrateToV17 = (json: ExperimentType): ExperimentType => { // renames all scores scores to ["scoreName", "scoreName 2"...] - return produce(json, draft => { - draft.info.dataFormatVersion = '17' - draft.scoreVariables = json.scoreVariables.map((s, i) => ({ - name: getScoreName(scoreName, i), - description: scoreName, - enabled: s.enabled, - })) - draft.dataPoints = json.dataPoints.map(dp => { - let scoreIndex = 0 - return { - ...dp, - data: dp.data.map(d => { - let newName = d.name - if (d.type === 'score') { - newName = getScoreName(scoreName, scoreIndex) - scoreIndex++ - } - return { - ...d, - name: newName, - } - }), - } - }) - }) + return produce( + json, + (draft: { + info: { dataFormatVersion: string } + scoreVariables: { name: string; description: string; enabled: boolean }[] + dataPoints: { + data: { name: string; type: string }[] + }[] + }) => { + draft.info.dataFormatVersion = '17' + draft.scoreVariables = json.scoreVariables.map((s, i) => ({ + name: getScoreName(scoreName17, i), + description: scoreName17, + enabled: s.enabled, + })) + draft.dataPoints = json.dataPoints.map(dp => { + let scoreIndex = 0 + return { + ...dp, + data: dp.data.map(d => { + let newName = d.name + if (d.type === 'score') { + newName = getScoreName(scoreName17, scoreIndex) + scoreIndex++ + } + return { + ...d, + name: newName, + } + }), + } + }) + } + ) } const getScoreName = (name: string, index: number) => diff --git a/packages/core/src/common/util/migration/migrations/migrateToV18.ts b/packages/core/src/common/util/migration/migrations/migrateToV18.ts new file mode 100644 index 00000000..3b26fb8a --- /dev/null +++ b/packages/core/src/common/util/migration/migrations/migrateToV18.ts @@ -0,0 +1,46 @@ +import { ExperimentType, scoreLabels, scoreNames } from '@core/common/types' +import { produce } from 'immer' + +// rename scores, change description to empty string, add labels +export const migrateToV18 = (json: ExperimentType): ExperimentType => { + return produce( + json, + (draft: { + info: { dataFormatVersion: string } + scoreVariables: { + name: string + label?: string + description: string + enabled: boolean + }[] + dataPoints: { + data: { name: string; type: string }[] + }[] + }) => { + draft.info.dataFormatVersion = '18' + draft.scoreVariables = json.scoreVariables.map((s, i) => ({ + name: scoreNames[i] ?? '', + label: scoreLabels[i] ?? '', + description: '', + enabled: s.enabled, + })) + draft.dataPoints = json.dataPoints.map(dp => { + let scoreIndex = 0 + return { + ...dp, + data: dp.data.map(d => { + let newName = d.name + if (d.type === 'score') { + newName = scoreNames[scoreIndex] ?? '' + scoreIndex++ + } + return { + ...d, + name: newName, + } + }), + } + }) + } + ) +} diff --git a/packages/core/src/common/util/migration/schemas/18.json b/packages/core/src/common/util/migration/schemas/18.json new file mode 100644 index 00000000..6a2af9c1 --- /dev/null +++ b/packages/core/src/common/util/migration/schemas/18.json @@ -0,0 +1,419 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lastEvaluationHash": { + "type": "string" + }, + "changedSinceLastEvaluation": { + "type": "boolean" + }, + "info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "swVersion": { + "type": "string" + }, + "dataFormatVersion": { + "type": "string", + "const": "18" + }, + "version": { + "type": "number" + }, + "extras": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": [ + "name", + "description", + "swVersion", + "dataFormatVersion", + "version", + "extras" + ], + "additionalProperties": false + }, + "extras": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "categoricalVariables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "name", + "description", + "options", + "enabled" + ], + "additionalProperties": false + } + }, + "valueVariables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "discrete" + }, + { + "type": "string", + "const": "continuous" + } + ] + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "min": { + "type": "number" + }, + "max": { + "type": "number" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "type", + "name", + "description", + "min", + "max", + "enabled" + ], + "additionalProperties": false + } + }, + "scoreVariables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "name", + "description", + "enabled" + ], + "additionalProperties": false + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "sum" + }, + "value": { + "type": "number" + }, + "dimensions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "value", + "dimensions" + ], + "additionalProperties": false + } + }, + "optimizerConfig": { + "type": "object", + "properties": { + "baseEstimator": { + "type": "string" + }, + "acqFunc": { + "type": "string" + }, + "initialPoints": { + "type": "number" + }, + "kappa": { + "type": "number" + }, + "xi": { + "type": "number" + } + }, + "required": [ + "baseEstimator", + "acqFunc", + "initialPoints", + "kappa", + "xi" + ], + "additionalProperties": false + }, + "results": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "plots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "plot": { + "type": "string" + } + }, + "required": [ + "id", + "plot" + ], + "additionalProperties": false + } + }, + "next": { + "type": "array", + "items": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "pickled": { + "type": "string" + }, + "expectedMinimum": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, + { + "type": "number" + } + ] + } + }, + "extras": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": [ + "id", + "plots", + "next", + "pickled", + "expectedMinimum", + "extras" + ], + "additionalProperties": false + }, + "dataPoints": { + "type": "array", + "items": { + "type": "object", + "properties": { + "meta": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "enabled": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "enabled", + "valid" + ], + "additionalProperties": false + }, + "data": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "numeric" + }, + "name": { + "type": "string" + }, + "value": { + "type": "number" + } + }, + "required": [ + "type", + "name", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "categorical" + }, + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "name", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "score" + }, + "name": { + "type": "string" + }, + "value": { + "type": "number" + } + }, + "required": [ + "type", + "name", + "value" + ], + "additionalProperties": false + } + ] + } + } + }, + "required": [ + "meta", + "data" + ], + "additionalProperties": false + } + } + }, + "required": [ + "id", + "changedSinceLastEvaluation", + "info", + "extras", + "categoricalVariables", + "valueVariables", + "scoreVariables", + "constraints", + "optimizerConfig", + "results", + "dataPoints" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/packages/core/src/context/experiment/experiment-reducers.ts b/packages/core/src/context/experiment/experiment-reducers.ts index af79e75d..fb3fe682 100644 --- a/packages/core/src/context/experiment/experiment-reducers.ts +++ b/packages/core/src/context/experiment/experiment-reducers.ts @@ -8,7 +8,6 @@ import { ValueVariableType, experimentSchema, scoreLabels, - scoreName, scoreNames as scoreNamesState, } from '@core/common/types' import { produce } from 'immer' @@ -401,9 +400,9 @@ export const experimentReducer = produce( if (state.scoreVariables.length < 2) { state.scoreVariables.push({ - name: scoreNamesState[1] ?? scoreName + ' 2', + name: scoreNamesState[1] ?? '', label: scoreLabels[1], - description: scoreNamesState[1] ?? scoreName + ' 2', + description: '', enabled: true, }) const scoreNames = state.scoreVariables.map(it => it.name) diff --git a/packages/core/src/context/experiment/store.ts b/packages/core/src/context/experiment/store.ts index cdc30452..a5263ab1 100644 --- a/packages/core/src/context/experiment/store.ts +++ b/packages/core/src/context/experiment/store.ts @@ -1,5 +1,5 @@ import { versionInfo } from '@core/common' -import { currentVersion, scoreLabels, scoreName } from '@core/common/types' +import { currentVersion, scoreLabels, scoreNames } from '@core/common/types' import { ExperimentType } from '@core/common/types' export const emptyExperiment: ExperimentType = { @@ -18,7 +18,7 @@ export const emptyExperiment: ExperimentType = { valueVariables: [], scoreVariables: [ { - name: scoreName, + name: scoreNames[0] ?? '', label: scoreLabels[0], description: '', enabled: true, @@ -65,13 +65,13 @@ export const initialStateMultiObjective: State = { ...emptyExperiment, scoreVariables: [ { - name: scoreName, + name: scoreNames[0] ?? '', label: scoreLabels[0], description: '', enabled: true, }, { - name: scoreName + ' 2', + name: scoreNames[1] ?? '', label: scoreLabels[1], description: '', enabled: true, diff --git a/packages/ui/src/features/result-data/single-data-point.tsx b/packages/ui/src/features/result-data/single-data-point.tsx index 95887006..4726bcdd 100644 --- a/packages/ui/src/features/result-data/single-data-point.tsx +++ b/packages/ui/src/features/result-data/single-data-point.tsx @@ -14,7 +14,7 @@ import { import useStyles from './single-data-point.style' import { PNGPlot } from '@boostv/process-optimizer-frontend-plots' import { useState } from 'react' -import { scoreName } from '@boostv/process-optimizer-frontend-core' +import { scoreNames } from '@boostv/process-optimizer-frontend-core' interface SingleDataPointProps { title: string @@ -47,7 +47,7 @@ export const SingleDataPoint = ({ {headers - .concat([scoreName + ' (95 % credibility interval)']) + .concat([(scoreNames[0] ?? '') + ' (95 % credibility interval)']) .map((h, idx) => ( {h} From 4ae7209f2e4b568bbaaa8fd7456cddaabef5c4e9 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:52:25 +0000 Subject: [PATCH 06/11] Change score names to const --- packages/core/src/common/types/common.ts | 6 +- .../__snapshots__/converters.test.ts.snap | 2 +- .../common/util/converters/converters.test.ts | 95 ++++----- .../src/common/util/converters/converters.ts | 5 +- .../util/migration/data-formats/18.json | 4 +- .../common/util/migration/migration.test.ts | 180 ++++++++---------- .../util/migration/migrations/migrateToV18.ts | 87 ++++++--- .../src/common/util/migration/schemas/18.json | 12 +- .../context/experiment/experiment-reducers.ts | 10 +- .../src/context/experiment/reducers.test.ts | 48 +++-- packages/core/src/context/experiment/store.ts | 12 +- .../core/src/context/experiment/test-utils.ts | 12 +- .../src/context/experiment/validation.test.ts | 23 ++- .../result-data/single-data-point.tsx | 10 +- 14 files changed, 267 insertions(+), 239 deletions(-) diff --git a/packages/core/src/common/types/common.ts b/packages/core/src/common/types/common.ts index 4469257a..101bf03e 100644 --- a/packages/core/src/common/types/common.ts +++ b/packages/core/src/common/types/common.ts @@ -5,7 +5,7 @@ import { z } from 'zod' export const currentVersion = '18' -export const scoreNames = ['Quality', 'Cost'] +export const scoreNames = ['quality', 'cost'] as const // Label is shown in UI, name is used in data export const scoreLabels = ['Quality (0-5)', 'Cost (0-5)'] @@ -44,8 +44,8 @@ const valueVariableSchema = z.object({ }) const scoreVariableSchema = z.object({ - name: z.string(), - label: z.optional(z.string()), + name: z.literal(scoreNames[0]).or(z.literal(scoreNames[1])), + label: z.string(), description: z.string(), enabled: z.boolean(), }) diff --git a/packages/core/src/common/util/converters/__snapshots__/converters.test.ts.snap b/packages/core/src/common/util/converters/__snapshots__/converters.test.ts.snap index 13ddf97c..9f46277c 100644 --- a/packages/core/src/common/util/converters/__snapshots__/converters.test.ts.snap +++ b/packages/core/src/common/util/converters/__snapshots__/converters.test.ts.snap @@ -2,7 +2,7 @@ exports[`converters > csvToDataPoints > should fail if duplicate ids are supplied 1`] = `[Error: Duplicate or missing IDs detected in input data]`; -exports[`converters > csvToDataPoints > should fail if header is missing 1`] = `[Error: Headers does not match Sukker,Peber,Hvedemel,Kunde,score !== Sukker,Hvedemel,Kunde,score]`; +exports[`converters > csvToDataPoints > should fail if header is missing 1`] = `[Error: Headers does not match Sukker,Peber,Hvedemel,Kunde,quality !== Sukker,Hvedemel,Kunde,quality]`; exports[`converters > csvToDataPoints > should fail if types does not match schema 1`] = ` [ZodError: [ diff --git a/packages/core/src/common/util/converters/converters.test.ts b/packages/core/src/common/util/converters/converters.test.ts index e46fe583..9f98b9fc 100644 --- a/packages/core/src/common/util/converters/converters.test.ts +++ b/packages/core/src/common/util/converters/converters.test.ts @@ -26,14 +26,14 @@ describe('converters', () => { { type: 'numeric', name: 'Peber', value: 982 }, { type: 'numeric', name: 'Hvedemel', value: 632 }, { type: 'categorical', name: 'Kunde', value: 'Mus' }, - { type: 'score', name: scoreNames[0] ?? '', value: 0.1 }, + { type: 'score', name: scoreNames[0], value: 0.1 }, ] satisfies DataPointType[], [ { type: 'numeric', name: 'Sukker', value: 15 }, { type: 'numeric', name: 'Peber', value: 123 }, { type: 'numeric', name: 'Hvedemel', value: 324 }, { type: 'categorical', name: 'Kunde', value: 'Ræv' }, - { type: 'score', name: scoreNames[0] ?? '', value: 0.2 }, + { type: 'score', name: scoreNames[0], value: 0.2 }, ] satisfies DataPointType[], ].map((data, idx) => ({ meta: { enabled: true, id: idx + 1, valid: true }, @@ -45,16 +45,16 @@ describe('converters', () => { { type: 'numeric', name: 'Peber', value: 982 }, { type: 'numeric', name: 'Hvedemel', value: 632 }, { type: 'categorical', name: 'Kunde', value: 'Mus' }, - { type: 'score', name: scoreNames[0] ?? '', value: 0.1 }, - { type: 'score', name: scoreNames[1] ?? '', value: 0.3 }, + { type: 'score', name: scoreNames[0], value: 0.1 }, + { type: 'score', name: scoreNames[1], value: 0.3 }, ] satisfies DataPointType[], [ { type: 'numeric', name: 'Sukker', value: 15 }, { type: 'numeric', name: 'Peber', value: 123 }, { type: 'numeric', name: 'Hvedemel', value: 324 }, { type: 'categorical', name: 'Kunde', value: 'Ræv' }, - { type: 'score', name: scoreNames[0] ?? '', value: 0.2 }, - { type: 'score', name: scoreNames[1] ?? '', value: 0.4 }, + { type: 'score', name: scoreNames[0], value: 0.2 }, + { type: 'score', name: scoreNames[1], value: 0.4 }, ] satisfies DataPointType[], ].map((data, idx) => ({ meta: { enabled: true, id: idx + 1, valid: true }, @@ -248,8 +248,8 @@ describe('converters', () => { sampleExperiment.categoricalVariables, sampleExperiment.valueVariables, [ - { name: scoreNames[0] ?? '', description: '', enabled: true }, - { name: scoreNames[1] ?? '', description: '', enabled: true }, + { name: scoreNames[0], description: '', enabled: true, label: '' }, + { name: scoreNames[1], description: '', enabled: true, label: '' }, ], sampleMultiObjectiveDataPoints ) @@ -265,8 +265,8 @@ describe('converters', () => { sampleExperiment.categoricalVariables, sampleExperiment.valueVariables, [ - { name: scoreNames[0] ?? '', description: '', enabled: true }, - { name: scoreNames[1] ?? '', description: '', enabled: false }, + { name: scoreNames[0], description: '', enabled: true, label: '' }, + { name: scoreNames[1], description: '', enabled: false, label: '' }, ], sampleMultiObjectiveDataPoints ) @@ -397,7 +397,7 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 1, }, ], @@ -431,14 +431,13 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 2, }, ], }, ] - const expected = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled;valid\n1;28;982;632;Mus;1;true;true\n3;15;986;5;Mus;2;false;true' + const expected = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled;valid\n1;28;982;632;Mus;1;true;true\n3;15;986;5;Mus;2;false;true` const actual = dataPointsToCSV(input) expect(actual).toEqual(expected) }) @@ -469,7 +468,7 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 1, }, ], @@ -503,14 +502,13 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 2, }, ], }, ] - const expected = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled;valid\n1;28;;632;Mus;1;true;true\n3;15;986;5;Mus;2;false;true' + const expected = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled;valid\n1;28;;632;Mus;1;true;true\n3;15;986;5;Mus;2;false;true` const actual = dataPointsToCSV(input) expect(actual).toEqual(expected) }) @@ -531,7 +529,7 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 2, }, ], @@ -550,7 +548,7 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 0, }, ], @@ -569,14 +567,13 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 1, }, ], }, ] - const expected = - 'id;Sukker;score;enabled;valid\n3;282;2;true;true\n1;280;0;true;true\n2;281;1;true;true' + const expected = `id;Sukker;${scoreNames[0]};enabled;valid\n3;282;2;true;true\n1;280;0;true;true\n2;281;1;true;true` const actual = dataPointsToCSV(input) expect(actual).toEqual(expected) }) @@ -618,7 +615,12 @@ describe('converters', () => { }, ] const scoreVariables: ScoreVariableType[] = [ - { name: 'score', description: '', enabled: true }, + { + name: scoreNames[0], + label: 'qualitylabel', + description: '', + enabled: true, + }, ] const sampleDataPoints: DataEntry[] = [ @@ -647,7 +649,7 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 1, }, ], @@ -677,7 +679,7 @@ describe('converters', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 2, }, ], @@ -692,8 +694,7 @@ describe('converters', () => { }) it('should convert known value', () => { - const input = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled;valid\n1;28;982;632;Mus;1;true;true\n2;15;986;5;Mus;2;false;true' + const input = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled;valid\n1;28;982;632;Mus;1;true;true\n2;15;986;5;Mus;2;false;true` const expected = sampleDataPoints const actual = csvToDataPoints( input, @@ -705,12 +706,9 @@ describe('converters', () => { }) it('should interpret enabled field as case insensitive', () => { - const inputWithLowerCase = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled;valid\n1;28;982;632;Mus;1;true\n2;15;986;5;Mus;2;false;true' - const inputWithUpperCase = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled;valid\n1;28;982;632;Mus;1;TRUE\n2;15;986;5;Mus;2;FALSE;true' - const inputWithMixedCase = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled;valid\n1;28;982;632;Mus;1;True\n2;15;986;5;Mus;2;False;true' + const inputWithLowerCase = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled;valid\n1;28;982;632;Mus;1;true\n2;15;986;5;Mus;2;false;true` + const inputWithUpperCase = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled;valid\n1;28;982;632;Mus;1;TRUE\n2;15;986;5;Mus;2;FALSE;true` + const inputWithMixedCase = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled;valid\n1;28;982;632;Mus;1;True\n2;15;986;5;Mus;2;False;true` const expected = sampleDataPoints const actual = [ inputWithLowerCase, @@ -730,8 +728,7 @@ describe('converters', () => { }) it('should use ID column from CSV', () => { - const input = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled\n42;28;982;632;Mus;1;true\n16;15;986;5;Mus;2;false' + const input = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled\n42;28;982;632;Mus;1;true\n16;15;986;5;Mus;2;false` const ids = [42, 16] const expected = sampleDataPoints.map((dp, idx) => ({ ...dp, @@ -747,8 +744,7 @@ describe('converters', () => { }) it('should fail if duplicate ids are supplied', () => { - const input = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled\n2;28;982;632;Mus;1;true\n2;15;986;5;Mus;2;false' + const input = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled\n2;28;982;632;Mus;1;true\n2;15;986;5;Mus;2;false` expect(() => csvToDataPoints( input, @@ -760,8 +756,7 @@ describe('converters', () => { }) it('should fail if types does not match schema', () => { - const input = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled\n1;fisk;982;632;Mus;1;true\n2;15;986;5;Mus;2;false' + const input = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled\n1;fisk;982;632;Mus;1;true\n2;15;986;5;Mus;2;false` expect(() => csvToDataPoints( input, @@ -773,8 +768,7 @@ describe('converters', () => { }) it('should work with no meta data columns (ID is generated based on line order)', () => { - const input = - 'Sukker;Peber;Hvedemel;Kunde;score\n28;982;632;Mus;1\n15;986;5;Mus;2' + const input = `Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]}\n28;982;632;Mus;1\n15;986;5;Mus;2` const expected = sampleDataPoints.map((dp, idx) => ({ ...dp, meta: { enabled: true, id: idx + 1, valid: true }, @@ -789,8 +783,7 @@ describe('converters', () => { }) it('should accept shuffled columns', () => { - const input = - 'Sukker;score;id;Hvedemel;enabled;Peber;Kunde\n28;1;1;632;true;982;Mus\n15;2;2;5;false;986;Mus' + const input = `Sukker;${scoreNames[0]};id;Hvedemel;enabled;Peber;Kunde\n28;1;1;632;true;982;Mus\n15;2;2;5;false;986;Mus` const expected = sampleDataPoints const actual = csvToDataPoints( input, @@ -802,7 +795,7 @@ describe('converters', () => { }) it('should fail if header is missing', () => { - const input = 'Sukker;Hvedemel;Kunde;score\n28;632;Mus;1\n15;5;Mus;2' + const input = `Sukker;Hvedemel;Kunde;${scoreNames[0]}\n28;632;Mus;1\n15;5;Mus;2` expect(() => csvToDataPoints( input, @@ -814,8 +807,7 @@ describe('converters', () => { }) it('should not fail if there are extra headers', () => { - const input = - 'Sukker;Peber;Hvedemel;Halm;Kunde;score\n28;982;632;007;Mus;1\n15;986;5;008;Mus;2' + const input = `Sukker;Peber;Hvedemel;Halm;Kunde;${scoreNames[0]}\n28;982;632;007;Mus;1\n15;986;5;008;Mus;2` const expected = sampleDataPoints.map(d => d.data) const actual = csvToDataPoints( input, @@ -827,8 +819,7 @@ describe('converters', () => { }) it('should accept whitespace in headers', () => { - const input = - 'id;Sukker ;Peber;Hvedemel; Kunde;score;enabled;valid\n1;28;982;632;Mus;1;true;true\n2;15;986;5;Mus;2;false;true' + const input = `id;Sukker ;Peber;Hvedemel; Kunde;${scoreNames[0]};enabled;valid\n1;28;982;632;Mus;1;true;true\n2;15;986;5;Mus;2;false;true` const expected = sampleDataPoints const actual = csvToDataPoints( input, @@ -840,8 +831,7 @@ describe('converters', () => { }) it('should add extra headers to meta', () => { - const input = - 'Sukker;Peber;Hvedemel;Halm;Kunde;score;enabled;valid\n28;982;632;008;Mus;1;true;true\n15;986;5;008;Mus;2;false;true' + const input = `Sukker;Peber;Hvedemel;Halm;Kunde;${scoreNames[0]};enabled;valid\n28;982;632;008;Mus;1;true;true\n15;986;5;008;Mus;2;false;true` const expected = sampleDataPoints.map(d => ({ ...d, meta: { ...d.meta, halm: '008' }, @@ -856,8 +846,7 @@ describe('converters', () => { }) it('should parse optional meta data field (description)', () => { - const input = - 'id;Sukker;Peber;Hvedemel;Kunde;score;enabled;valid;description\n1;28;982;632;Mus;1;true;true;I am a description\n2;15;986;5;Mus;2;false;true;I am also a description' + const input = `id;Sukker;Peber;Hvedemel;Kunde;${scoreNames[0]};enabled;valid;description\n1;28;982;632;Mus;1;true;true;I am a description\n2;15;986;5;Mus;2;false;true;I am also a description` const actual = csvToDataPoints( input, valueVariables, diff --git a/packages/core/src/common/util/converters/converters.ts b/packages/core/src/common/util/converters/converters.ts index 77b7f631..5df5859e 100644 --- a/packages/core/src/common/util/converters/converters.ts +++ b/packages/core/src/common/util/converters/converters.ts @@ -8,6 +8,7 @@ import { SpaceType, ValueVariableType, dataPointSchema, + scoreNames, } from '@core/common/types' /** @@ -78,7 +79,9 @@ export const calculateData = ( it.type === 'numeric' ? Number(it.value) : it.value ) as Array, // This type cast is valid here because only scores can be number[] and they are filtered out yi: run - .filter(it => enabledScoreNames.includes(it.name)) + .filter(it => + enabledScoreNames.includes(it.name as (typeof scoreNames)[number]) + ) .map(it => it.value) .map(it => Number(it) * -1), }) diff --git a/packages/core/src/common/util/migration/data-formats/18.json b/packages/core/src/common/util/migration/data-formats/18.json index b25513dc..4340e47a 100644 --- a/packages/core/src/common/util/migration/data-formats/18.json +++ b/packages/core/src/common/util/migration/data-formats/18.json @@ -38,7 +38,7 @@ ], "scoreVariables": [ { - "name": "Quality", + "name": "quality", "label": "Quality (0-5)", "description": "", "enabled": true @@ -91,7 +91,7 @@ }, { "type": "score", - "name": "Quality", + "name": "quality", "value": 0.5 } ] diff --git a/packages/core/src/common/util/migration/migration.test.ts b/packages/core/src/common/util/migration/migration.test.ts index 3b297f3b..f5871356 100644 --- a/packages/core/src/common/util/migration/migration.test.ts +++ b/packages/core/src/common/util/migration/migration.test.ts @@ -16,7 +16,6 @@ import { emptyExperiment } from '@core/context/experiment' import { formatNext } from './migrations/migrateToV9' import { ExperimentType, - ScoreVariableType, experimentSchema, scoreLabels, scoreNames, @@ -24,6 +23,7 @@ import { import { storeLatestSchema, loadTestData } from './test-utils' import { scoreName17 } from './migrations/migrateToV17' import { migrateToV17, migrateToV18 } from './migrations' +import { ExperimentTypeV17 } from './migrations/migrateToV18' describe('Migration of data format', () => { storeLatestSchema() @@ -162,7 +162,7 @@ describe('Migration of data format', () => { description: 'score', enabled: true, }, - ] satisfies ScoreVariableType[] + ] const scoreVarsMultiObjectiveDisabled = [ { @@ -175,7 +175,7 @@ describe('Migration of data format', () => { description: 'score', enabled: false, }, - ] satisfies ScoreVariableType[] + ] it.each([ ['multiobjective, all enabled', scoreVarsMultiObjective], @@ -268,122 +268,94 @@ describe('Migration of data format', () => { }) describe('migrateToV18', () => { - const scoreVarsMultiObjective = [ - { - name: 'Quality (0-5)', - description: 'Quality (0-5)', - enabled: true, - }, - { - name: 'Quality (0-5) 2', - description: 'Quality (0-5) 2', - enabled: true, - }, - ] satisfies ScoreVariableType[] + const experiment17 = { + ...version17, + scoreVariables: [ + { + name: 'Quality (0-5)', + description: 'Quality (0-5)', + enabled: true, + }, + ], + } as ExperimentTypeV17 - const scoreVarsMultiObjectiveDisabled = [ - { - name: 'Quality (0-5)', - description: 'Quality (0-5)', - enabled: true, - }, - { - name: 'Quality (0-5) 2', - description: 'Quality (0-5) 2', - enabled: false, - }, - ] satisfies ScoreVariableType[] + it('should convert single score variable name and add label', () => { + const result = migrateToV18(experiment17) + expect(result.scoreVariables).toEqual([ + { + name: scoreNames[0], + label: scoreLabels[0], + description: '', + enabled: true, + }, + ]) + }) - it.each([ - ['multiobjective, all enabled', scoreVarsMultiObjective], - ['multiobjective, one disabled', scoreVarsMultiObjectiveDisabled], - ])('scoreVariables should be updated, %s', (_, scoreVariables) => { - const experiment17 = { + it('should convert multiple score variables with correct indices', () => { + const multiObj = { ...version17, - scoreVariables, - } as unknown as ExperimentType - expect(migrateToV18(experiment17).scoreVariables).toEqual([ + scoreVariables: [ + { + name: 'Quality (0-5)', + description: 'Quality (0-5)', + enabled: true, + }, + { + name: 'Quality (0-5) 2', + description: 'Quality (0-5) 2', + enabled: false, + }, + ], + } as ExperimentTypeV17 + + const result = migrateToV18(multiObj) + expect(result.scoreVariables).toEqual([ { name: scoreNames[0], - label: scoreLabels[0] ?? '', + label: scoreLabels[0], description: '', - enabled: scoreVariables[0]?.enabled, + enabled: true, }, { name: scoreNames[1], - label: scoreLabels[1] ?? '', + label: scoreLabels[1], description: '', - enabled: scoreVariables[1]?.enabled, + enabled: false, }, ]) }) - const dataPointsSingle = [...version17.dataPoints] - const dataPointsMulti = [...version17.dataPoints].map(dp => ({ - ...dp, - data: [...dp.data].concat([ - { - type: 'score', - name: scoreNames[1] ?? '', - value: 2, - }, - ]), - })) + it('should update score names in dataPoints for single objective', () => { + const result = migrateToV18(experiment17) + const scoreData = result.dataPoints[0]?.data.find(d => d.type === 'score') + expect(scoreData?.name).toEqual(scoreNames[0]) + }) - it.each([ - ['one score exists', dataPointsSingle, false], - ['two scores exist', dataPointsMulti, true], - ])( - 'data points should be renamed, %s', - (_, dataPoints, isMultiObjective) => { - const experiment17 = { - ...version17, - dataPoints, - } as unknown as ExperimentType - const actual = [ - { - meta: { - enabled: true, - valid: true, - id: 1, - }, - data: [ - { - type: 'categorical', - name: 'Icing', - value: 'Brown', - }, - { - type: 'numeric', - name: 'name1', - value: 10, - }, - { - type: 'numeric', - name: 'name2', - value: 10.2, - }, - { - type: 'score', - name: scoreNames[0], - value: 0.5, - }, - ].concat( - isMultiObjective - ? [ - { - type: 'score', - name: scoreNames[1], - value: 2, - }, - ] - : [] - ), - }, - ] - expect(migrateToV18(experiment17).dataPoints).toEqual(actual) - } - ) + it('should update score names in dataPoints for multi-objective', () => { + const multiObj = { + ...version17, + scoreVariables: [ + { name: 'Quality (0-5)', description: '', enabled: true }, + { name: 'Quality (0-5) 2', description: '', enabled: true }, + ], + dataPoints: version17.dataPoints.map(dp => ({ + ...dp, + data: [ + ...dp.data, + { type: 'score', name: 'Quality (0-5) 2', value: 2 }, + ], + })), + } as ExperimentTypeV17 + + const result = migrateToV18(multiObj) + const scoreData = result.dataPoints[0]?.data.filter( + d => d.type === 'score' + ) + expect(scoreData?.map(s => s.name)).toEqual([ + scoreNames[0], + scoreNames[1], + ]) + }) }) describe('experiment properties', () => { diff --git a/packages/core/src/common/util/migration/migrations/migrateToV18.ts b/packages/core/src/common/util/migration/migrations/migrateToV18.ts index 3b26fb8a..10f6d97d 100644 --- a/packages/core/src/common/util/migration/migrations/migrateToV18.ts +++ b/packages/core/src/common/util/migration/migrations/migrateToV18.ts @@ -1,46 +1,71 @@ -import { ExperimentType, scoreLabels, scoreNames } from '@core/common/types' +import { + ExperimentType, + scoreLabels, + scoreNames, + ScoreVariableType, +} from '@core/common/types' import { produce } from 'immer' +// TODO Multiobjective - do not use ScoreVariableType because this could change in the future? +type ScoreVariableTypeV17 = Omit & { + name: string +} + +// TODO Multiobjective - do not use Experiment because this could change in the future? +export type ExperimentTypeV17 = Omit< + ExperimentType, + 'info' | 'scoreVariables' +> & { + info: Omit & { + dataFormatVersion: string + } + scoreVariables: ScoreVariableTypeV17[] +} + // rename scores, change description to empty string, add labels -export const migrateToV18 = (json: ExperimentType): ExperimentType => { - return produce( - json, - (draft: { - info: { dataFormatVersion: string } - scoreVariables: { - name: string - label?: string - description: string - enabled: boolean - }[] - dataPoints: { - data: { name: string; type: string }[] - }[] - }) => { - draft.info.dataFormatVersion = '18' - draft.scoreVariables = json.scoreVariables.map((s, i) => ({ - name: scoreNames[i] ?? '', - label: scoreLabels[i] ?? '', +export const migrateToV18 = (json: ExperimentTypeV17): ExperimentType => { + return produce(json, draft => { + draft.info.dataFormatVersion = '18' + draft.scoreVariables = json.scoreVariables + .slice(0, scoreNames.length) + .map((s, i) => ({ + name: scoreNames[i] as (typeof scoreNames)[number], + label: scoreLabels[i] ?? 'score' + (i + 1), description: '', enabled: s.enabled, })) - draft.dataPoints = json.dataPoints.map(dp => { - let scoreIndex = 0 - return { - ...dp, - data: dp.data.map(d => { - let newName = d.name + draft.dataPoints = json.dataPoints.map(dp => { + let scoreIndex = 0 + return { + ...dp, + data: dp.data + .filter(d => { + // only map score data points up to scoreNames.length if (d.type === 'score') { - newName = scoreNames[scoreIndex] ?? '' + if (scoreIndex >= scoreNames.length) { + return false + } scoreIndex++ } + return true + }) + .map(d => { + let newName = d.name + if (d.type === 'score') { + // reset scoreIndex for mapping + const currentScoreIndex = dp.data + .slice(0, dp.data.indexOf(d)) + .filter(item => item.type === 'score').length + newName = scoreNames[ + currentScoreIndex + ] as (typeof scoreNames)[number] + } return { ...d, name: newName, } }), - } - }) - } - ) + } + }) + }) as unknown as ExperimentType } diff --git a/packages/core/src/common/util/migration/schemas/18.json b/packages/core/src/common/util/migration/schemas/18.json index 6a2af9c1..370c1eca 100644 --- a/packages/core/src/common/util/migration/schemas/18.json +++ b/packages/core/src/common/util/migration/schemas/18.json @@ -135,7 +135,16 @@ "type": "object", "properties": { "name": { - "type": "string" + "anyOf": [ + { + "type": "string", + "const": "quality" + }, + { + "type": "string", + "const": "cost" + } + ] }, "label": { "type": "string" @@ -149,6 +158,7 @@ }, "required": [ "name", + "label", "description", "enabled" ], diff --git a/packages/core/src/context/experiment/experiment-reducers.ts b/packages/core/src/context/experiment/experiment-reducers.ts index fb3fe682..20a3ee6b 100644 --- a/packages/core/src/context/experiment/experiment-reducers.ts +++ b/packages/core/src/context/experiment/experiment-reducers.ts @@ -400,8 +400,8 @@ export const experimentReducer = produce( if (state.scoreVariables.length < 2) { state.scoreVariables.push({ - name: scoreNamesState[1] ?? '', - label: scoreLabels[1], + name: scoreNamesState[1], + label: scoreLabels[1] ?? 'score2', description: '', enabled: true, }) @@ -409,7 +409,11 @@ export const experimentReducer = produce( state.dataPoints.forEach(dataEntry => { const dp = dataEntry.data const containedScores = dp - .filter(it => scoreNames.includes(it.name)) + .filter( + it => + it.type === 'score' && + scoreNames.includes(it.name as (typeof scoreNames)[number]) + ) .map(it => it.name) scoreNames.forEach(scoreName => { if (!containedScores.includes(scoreName)) diff --git a/packages/core/src/context/experiment/reducers.test.ts b/packages/core/src/context/experiment/reducers.test.ts index e7855469..9179750e 100644 --- a/packages/core/src/context/experiment/reducers.test.ts +++ b/packages/core/src/context/experiment/reducers.test.ts @@ -7,6 +7,7 @@ import { ExperimentResultType, ExperimentType, OptimizerConfig, + scoreNames, ValueVariableType, } from '@core/common/types' import { emptyExperiment, State } from '@core/context/experiment' @@ -54,7 +55,8 @@ describe('experiment reducer', () => { ], scoreVariables: [ { - name: 'score', + name: scoreNames[0], + label: 'qualitylabel', description: 'score', enabled: true, }, @@ -97,7 +99,7 @@ describe('experiment reducer', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 10, }, ], @@ -372,7 +374,7 @@ describe('experiment reducer', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 10, }, ]) @@ -510,7 +512,7 @@ describe('experiment reducer', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 10, }, ], @@ -606,7 +608,7 @@ describe('experiment reducer', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 10, }, ]) @@ -792,7 +794,7 @@ describe('experiment reducer', () => { () => { const values = ['value1', 'value2', 'value3'] const cats = ['cat1', 'cat2', 'cat3'] - const scores = ['score', 'score2'] + const scores = scoreNames const testState = produce(initState, draft => { draft.experiment.valueVariables = values.map(name => @@ -817,7 +819,7 @@ describe('experiment reducer', () => { 1, values, cats, - scores, + [...scoreNames], true ).map(dr => ({ ...dr, @@ -835,8 +837,8 @@ describe('experiment reducer', () => { 'cat1', 'cat2', 'cat3', - 'score', - 'score2', + scoreNames[0], + scoreNames[1], ] const actual = rootReducer(testState, action).experiment.dataPoints.map( dr => dr.data.map(d => d.name) @@ -949,12 +951,14 @@ describe('experiment reducer', () => { ...initState.experiment, scoreVariables: [ { - name: 'score', + name: scoreNames[0], + label: 'qualitylabel', description: 'score', enabled: true, }, { - name: 'score2', + name: scoreNames[1], + label: 'costlabel', description: 'score 2', enabled: true, }, @@ -986,12 +990,14 @@ describe('experiment reducer', () => { ...initState.experiment, scoreVariables: [ { - name: 'score', + name: scoreNames[0], + label: 'qualitylabel', description: 'score', enabled: true, }, { - name: 'score2', + name: scoreNames[1], + label: 'costlabel', description: 'score 2', enabled: false, }, @@ -1024,7 +1030,7 @@ describe('experiment reducer', () => { scores.length, ['Water'], ['Icing'], - ['score', 'score2'], + [...scoreNames], true, scores ) @@ -1035,12 +1041,14 @@ describe('experiment reducer', () => { ...initState.experiment, scoreVariables: [ { - name: 'score', + name: scoreNames[0], + label: 'qualitylabel', description: 'score', enabled: true, }, { - name: 'score2', + name: scoreNames[1], + label: 'costlabel', description: 'score 2', enabled: true, }, @@ -1062,7 +1070,7 @@ describe('experiment reducer', () => { scores.length, ['Water'], ['Icing'], - ['score', 'score2'], + [...scoreNames], true, scores ) @@ -1073,12 +1081,14 @@ describe('experiment reducer', () => { ...initState.experiment, scoreVariables: [ { - name: 'score', + name: scoreNames[0], + label: 'qualitylabel', description: 'score', enabled: true, }, { - name: 'score2', + name: scoreNames[1], + label: 'costlabel', description: 'score 2', enabled: true, }, diff --git a/packages/core/src/context/experiment/store.ts b/packages/core/src/context/experiment/store.ts index a5263ab1..b4f5eeb6 100644 --- a/packages/core/src/context/experiment/store.ts +++ b/packages/core/src/context/experiment/store.ts @@ -18,8 +18,8 @@ export const emptyExperiment: ExperimentType = { valueVariables: [], scoreVariables: [ { - name: scoreNames[0] ?? '', - label: scoreLabels[0], + name: scoreNames[0], + label: scoreLabels[0] ?? scoreNames[0], description: '', enabled: true, }, @@ -65,14 +65,14 @@ export const initialStateMultiObjective: State = { ...emptyExperiment, scoreVariables: [ { - name: scoreNames[0] ?? '', - label: scoreLabels[0], + name: scoreNames[0], + label: scoreLabels[0] ?? scoreNames[0], description: '', enabled: true, }, { - name: scoreNames[1] ?? '', - label: scoreLabels[1], + name: scoreNames[1], + label: scoreLabels[1] ?? scoreNames[1], description: '', enabled: true, }, diff --git a/packages/core/src/context/experiment/test-utils.ts b/packages/core/src/context/experiment/test-utils.ts index 12009e04..c05c75f5 100644 --- a/packages/core/src/context/experiment/test-utils.ts +++ b/packages/core/src/context/experiment/test-utils.ts @@ -3,6 +3,7 @@ import { DataEntry, DataPointType, ExperimentResultType, + scoreNames, ScoreVariableType, ValueVariableType, } from '@core/common' @@ -29,9 +30,12 @@ export const createCategoricalVariable = ( enabled: input.enabled ?? true, }) satisfies CategoricalVariableType -export const createScoreVariable = (input: Partial) => +export const createScoreVariable = ( + input: Partial & Pick +) => ({ - name: input.name ?? 'name', + name: input.name ?? 'quality', + label: input.label ?? 'label', description: input.description ?? '', enabled: input.enabled ?? true, }) satisfies ScoreVariableType @@ -40,7 +44,7 @@ export const createDataPoints = ( count: number, values = ['Water'], categorical = ['Icing'], - scores = ['score'], + scores: (typeof scoreNames)[number][] = [scoreNames[0]], randomize = false, scoreValues: number[] | undefined = undefined ): DataEntry[] => { @@ -67,7 +71,7 @@ export const createDataPoints = ( return data.map((dp, i) => ({ ...dp, data: dp.data.map(d => { - if (d.type === 'score' && d.name === 'score') { + if (d.type === 'score' && d.name === 'quality') { const score = scoreValues[i] return { ...d, diff --git a/packages/core/src/context/experiment/validation.test.ts b/packages/core/src/context/experiment/validation.test.ts index 01b4e165..608bdc85 100644 --- a/packages/core/src/context/experiment/validation.test.ts +++ b/packages/core/src/context/experiment/validation.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { ExperimentType } from 'common' import { emptyExperiment } from './store' import { ValidationViolations, @@ -12,6 +11,7 @@ import { validateLowerBoundary, validateUpperBoundary, } from './validation' +import { ExperimentType, scoreNames } from '@core/common' describe('validateUpperBoundary', () => { it('should return empty array if no violations exist', () => { @@ -330,7 +330,8 @@ describe('validateDataPointsUndefined', () => { ...emptyExperiment, scoreVariables: [ { - name: 'score', + name: 'quality', + label: 'qualitylabel', description: '', enabled: true, }, @@ -344,7 +345,7 @@ describe('validateDataPointsUndefined', () => { }, data: [ { type: 'numeric', name: 'Water', value: 10 }, - { type: 'score', name: 'score', value: 1 }, + { type: 'score', name: 'quality', value: 1 }, ], }, ], @@ -357,7 +358,8 @@ describe('validateDataPointsUndefined', () => { ...emptyExperiment, scoreVariables: [ { - name: 'score', + name: 'quality', + label: 'qualitylabel', description: '', enabled: true, }, @@ -391,7 +393,8 @@ describe('validateDataPointsUndefined', () => { ], scoreVariables: [ { - name: 'score', + name: 'quality', + label: 'qualitylabel', description: '', enabled: true, }, @@ -420,7 +423,7 @@ describe('validateDataPointsUndefined', () => { data: [ { type: 'score', - name: 'score', + name: 'quality', value: 1, }, ], @@ -435,12 +438,14 @@ describe('validateDataPointsUndefined', () => { ...emptyExperiment, scoreVariables: [ { - name: 'score', + name: 'quality', + label: 'qualitylabel', description: '', enabled: true, }, { - name: 'score 2', + name: 'cost', + label: 'costlabel', description: '', enabled: false, }, @@ -460,7 +465,7 @@ describe('validateDataPointsUndefined', () => { }, { type: 'score', - name: 'score', + name: scoreNames[0], value: 1, }, ], diff --git a/packages/ui/src/features/result-data/single-data-point.tsx b/packages/ui/src/features/result-data/single-data-point.tsx index 4726bcdd..54ade73e 100644 --- a/packages/ui/src/features/result-data/single-data-point.tsx +++ b/packages/ui/src/features/result-data/single-data-point.tsx @@ -14,7 +14,10 @@ import { import useStyles from './single-data-point.style' import { PNGPlot } from '@boostv/process-optimizer-frontend-plots' import { useState } from 'react' -import { scoreNames } from '@boostv/process-optimizer-frontend-core' +import { + scoreLabels, + scoreNames, +} from '@boostv/process-optimizer-frontend-core' interface SingleDataPointProps { title: string @@ -47,7 +50,10 @@ export const SingleDataPoint = ({ {headers - .concat([(scoreNames[0] ?? '') + ' (95 % credibility interval)']) + .concat([ + (scoreLabels[0] ?? scoreNames[0]) + + ' (95 % credibility interval)', + ]) .map((h, idx) => ( {h} From 392ff415c9a27c58078dd7a1ffb724bb5b603ba1 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:32:30 +0000 Subject: [PATCH 07/11] Add entire v17 type in migration --- .../util/migration/migrations/migrateToV18.ts | 198 ++++++++++++------ 1 file changed, 136 insertions(+), 62 deletions(-) diff --git a/packages/core/src/common/util/migration/migrations/migrateToV18.ts b/packages/core/src/common/util/migration/migrations/migrateToV18.ts index 10f6d97d..6cc617b2 100644 --- a/packages/core/src/common/util/migration/migrations/migrateToV18.ts +++ b/packages/core/src/common/util/migration/migrations/migrateToV18.ts @@ -1,71 +1,145 @@ -import { - ExperimentType, - scoreLabels, - scoreNames, - ScoreVariableType, -} from '@core/common/types' +import { ExperimentType, scoreLabels, scoreNames } from '@core/common/types' import { produce } from 'immer' -// TODO Multiobjective - do not use ScoreVariableType because this could change in the future? -type ScoreVariableTypeV17 = Omit & { - name: string -} - -// TODO Multiobjective - do not use Experiment because this could change in the future? -export type ExperimentTypeV17 = Omit< - ExperimentType, - 'info' | 'scoreVariables' -> & { - info: Omit & { - dataFormatVersion: string +// Avoid using current ExperimentType as that can change in the future +export type ExperimentTypeV17 = { + id: string + lastEvaluationHash?: string + changedSinceLastEvaluation: boolean + info: { + name: string + description: string + swVersion: string + dataFormatVersion: '17' + version: number + extras: Record + } + extras: Record + categoricalVariables: { + name: string + description: string + options: string[] + enabled: boolean + }[] + valueVariables: { + type: 'discrete' | 'continuous' + name: string + description: string + min: number + max: number + enabled: boolean + }[] + scoreVariables: { + name: string + label?: string + description: string + enabled: boolean + }[] + constraints: { + type: 'sum' + value: number + dimensions: string[] + }[] + optimizerConfig: { + baseEstimator: string + acqFunc: string + initialPoints: number + kappa: number + xi: number } - scoreVariables: ScoreVariableTypeV17[] + results: { + id: string + plots: { + id: string + plot: string + }[] + next: (number | string)[][] + pickled: string + expectedMinimum: ((number | string)[] | number)[] + extras: Record + } + dataPoints: { + meta: { + id: number + enabled: boolean + valid: boolean + description?: string + } + data: ( + | { + type: 'numeric' + name: string + value: number + } + | { + type: 'categorical' + name: string + value: string + } + | { + type: 'score' + name: string + value: number + } + )[] + }[] } // rename scores, change description to empty string, add labels export const migrateToV18 = (json: ExperimentTypeV17): ExperimentType => { - return produce(json, draft => { - draft.info.dataFormatVersion = '18' - draft.scoreVariables = json.scoreVariables - .slice(0, scoreNames.length) - .map((s, i) => ({ - name: scoreNames[i] as (typeof scoreNames)[number], - label: scoreLabels[i] ?? 'score' + (i + 1), - description: '', - enabled: s.enabled, - })) - draft.dataPoints = json.dataPoints.map(dp => { - let scoreIndex = 0 - return { - ...dp, - data: dp.data - .filter(d => { - // only map score data points up to scoreNames.length - if (d.type === 'score') { - if (scoreIndex >= scoreNames.length) { - return false + return produce( + json, + (draft: { + info: { dataFormatVersion: string } + scoreVariables: { + label?: string + }[] + dataPoints: { + data: { name: string; type: string }[] + }[] + }) => { + draft.info.dataFormatVersion = '18' + draft.scoreVariables = json.scoreVariables + .slice(0, scoreNames.length) + .map((s, i) => ({ + name: scoreNames[i] as (typeof scoreNames)[number], + label: scoreLabels[i] ?? scoreNames[i], + description: '', + enabled: s.enabled, + })) + draft.dataPoints = json.dataPoints.map(dp => { + let scoreIndex = 0 + return { + ...dp, + data: dp.data + .filter(d => { + // only map score data points up to scoreNames.length + if (d.type === 'score') { + if (scoreIndex >= scoreNames.length) { + return false + } + scoreIndex++ + } + return true + }) + .map(d => { + let newName = d.name + if (d.type === 'score') { + // reset scoreIndex for mapping + const currentScoreIndex = dp.data + .slice(0, dp.data.indexOf(d)) + .filter(item => item.type === 'score').length + newName = scoreNames[ + currentScoreIndex + ] as (typeof scoreNames)[number] + } + return { + ...d, + name: newName, } - scoreIndex++ - } - return true - }) - .map(d => { - let newName = d.name - if (d.type === 'score') { - // reset scoreIndex for mapping - const currentScoreIndex = dp.data - .slice(0, dp.data.indexOf(d)) - .filter(item => item.type === 'score').length - newName = scoreNames[ - currentScoreIndex - ] as (typeof scoreNames)[number] - } - return { - ...d, - name: newName, - } - }), - } - }) - }) as unknown as ExperimentType + }), + } + }) + } + ) as unknown as ExperimentType } From 2020f10535122644151f88edbfee33f365671ba1 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:06:26 +0000 Subject: [PATCH 08/11] Add type guard, revert change to 17, simplify 18 migration --- packages/core/src/common/types/common.ts | 6 +++ .../src/common/util/converters/converters.ts | 7 ++-- .../util/migration/migrations/migrateToV18.ts | 42 +++++++------------ .../src/common/util/migration/schemas/17.json | 3 -- .../context/experiment/experiment-reducers.ts | 9 ++-- .../core/src/context/experiment/test-utils.ts | 2 +- 6 files changed, 29 insertions(+), 40 deletions(-) diff --git a/packages/core/src/common/types/common.ts b/packages/core/src/common/types/common.ts index 101bf03e..a67666d0 100644 --- a/packages/core/src/common/types/common.ts +++ b/packages/core/src/common/types/common.ts @@ -9,6 +9,12 @@ export const scoreNames = ['quality', 'cost'] as const // Label is shown in UI, name is used in data export const scoreLabels = ['Quality (0-5)', 'Cost (0-5)'] +export const isValidScoreName = ( + name: string +): name is (typeof scoreNames)[number] => { + return (scoreNames as readonly string[]).includes(name) +} + const infoSchema = z.object({ name: z.string(), description: z.string(), diff --git a/packages/core/src/common/util/converters/converters.ts b/packages/core/src/common/util/converters/converters.ts index 5df5859e..67c47373 100644 --- a/packages/core/src/common/util/converters/converters.ts +++ b/packages/core/src/common/util/converters/converters.ts @@ -8,7 +8,7 @@ import { SpaceType, ValueVariableType, dataPointSchema, - scoreNames, + isValidScoreName, } from '@core/common/types' /** @@ -79,8 +79,9 @@ export const calculateData = ( it.type === 'numeric' ? Number(it.value) : it.value ) as Array, // This type cast is valid here because only scores can be number[] and they are filtered out yi: run - .filter(it => - enabledScoreNames.includes(it.name as (typeof scoreNames)[number]) + .filter( + it => + isValidScoreName(it.name) && enabledScoreNames.includes(it.name) ) .map(it => it.value) .map(it => Number(it) * -1), diff --git a/packages/core/src/common/util/migration/migrations/migrateToV18.ts b/packages/core/src/common/util/migration/migrations/migrateToV18.ts index 6cc617b2..f8943cca 100644 --- a/packages/core/src/common/util/migration/migrations/migrateToV18.ts +++ b/packages/core/src/common/util/migration/migrations/migrateToV18.ts @@ -85,6 +85,7 @@ export type ExperimentTypeV17 = { }[] } +// TODO: multiobjective, try without immer // rename scores, change description to empty string, add labels export const migrateToV18 = (json: ExperimentTypeV17): ExperimentType => { return produce( @@ -111,33 +112,20 @@ export const migrateToV18 = (json: ExperimentTypeV17): ExperimentType => { let scoreIndex = 0 return { ...dp, - data: dp.data - .filter(d => { - // only map score data points up to scoreNames.length - if (d.type === 'score') { - if (scoreIndex >= scoreNames.length) { - return false - } - scoreIndex++ - } - return true - }) - .map(d => { - let newName = d.name - if (d.type === 'score') { - // reset scoreIndex for mapping - const currentScoreIndex = dp.data - .slice(0, dp.data.indexOf(d)) - .filter(item => item.type === 'score').length - newName = scoreNames[ - currentScoreIndex - ] as (typeof scoreNames)[number] - } - return { - ...d, - name: newName, - } - }), + data: dp.data.flatMap(d => { + if (d.type === 'score') { + // empty result is filtered out by flatMap + return scoreIndex < scoreNames.length + ? { + ...d, + name: scoreNames[ + scoreIndex++ + ] as (typeof scoreNames)[number], + } + : [] + } + return d + }), } }) } diff --git a/packages/core/src/common/util/migration/schemas/17.json b/packages/core/src/common/util/migration/schemas/17.json index ca95086a..6115def9 100644 --- a/packages/core/src/common/util/migration/schemas/17.json +++ b/packages/core/src/common/util/migration/schemas/17.json @@ -137,9 +137,6 @@ "name": { "type": "string" }, - "label": { - "type": "string" - }, "description": { "type": "string" }, diff --git a/packages/core/src/context/experiment/experiment-reducers.ts b/packages/core/src/context/experiment/experiment-reducers.ts index 20a3ee6b..41048e39 100644 --- a/packages/core/src/context/experiment/experiment-reducers.ts +++ b/packages/core/src/context/experiment/experiment-reducers.ts @@ -7,6 +7,7 @@ import { ScoreVariableType, ValueVariableType, experimentSchema, + isValidScoreName, scoreLabels, scoreNames as scoreNamesState, } from '@core/common/types' @@ -401,7 +402,7 @@ export const experimentReducer = produce( if (state.scoreVariables.length < 2) { state.scoreVariables.push({ name: scoreNamesState[1], - label: scoreLabels[1] ?? 'score2', + label: scoreLabels[1] ?? scoreNamesState[1], description: '', enabled: true, }) @@ -409,11 +410,7 @@ export const experimentReducer = produce( state.dataPoints.forEach(dataEntry => { const dp = dataEntry.data const containedScores = dp - .filter( - it => - it.type === 'score' && - scoreNames.includes(it.name as (typeof scoreNames)[number]) - ) + .filter(it => it.type === 'score' && isValidScoreName(it.name)) .map(it => it.name) scoreNames.forEach(scoreName => { if (!containedScores.includes(scoreName)) diff --git a/packages/core/src/context/experiment/test-utils.ts b/packages/core/src/context/experiment/test-utils.ts index c05c75f5..5708c82f 100644 --- a/packages/core/src/context/experiment/test-utils.ts +++ b/packages/core/src/context/experiment/test-utils.ts @@ -34,7 +34,7 @@ export const createScoreVariable = ( input: Partial & Pick ) => ({ - name: input.name ?? 'quality', + name: input.name, label: input.label ?? 'label', description: input.description ?? '', enabled: input.enabled ?? true, From 592b2b024b08d89300eea78b9ff6d9000be8f58d Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Fri, 23 Jan 2026 07:07:43 +0000 Subject: [PATCH 09/11] Derive type from sample json --- .../util/migration/migrations/migrateToV18.ts | 158 +++++------------- 1 file changed, 46 insertions(+), 112 deletions(-) diff --git a/packages/core/src/common/util/migration/migrations/migrateToV18.ts b/packages/core/src/common/util/migration/migrations/migrateToV18.ts index f8943cca..3d8ecce4 100644 --- a/packages/core/src/common/util/migration/migrations/migrateToV18.ts +++ b/packages/core/src/common/util/migration/migrations/migrateToV18.ts @@ -1,73 +1,22 @@ import { ExperimentType, scoreLabels, scoreNames } from '@core/common/types' -import { produce } from 'immer' +import sampleV17 from '../data-formats/17.json' +// Use 17.json to define ExperimentTypeV17. Explicitly define string literals to make ts happy. // Avoid using current ExperimentType as that can change in the future -export type ExperimentTypeV17 = { - id: string - lastEvaluationHash?: string - changedSinceLastEvaluation: boolean - info: { - name: string - description: string - swVersion: string - dataFormatVersion: '17' - version: number - extras: Record - } - extras: Record - categoricalVariables: { - name: string - description: string - options: string[] - enabled: boolean - }[] - valueVariables: { +type ExperimentTypeV17 = Omit< + typeof sampleV17, + 'valueVariables' | 'constraints' | 'dataPoints' +> & { + valueVariables: (Omit<(typeof sampleV17.valueVariables)[number], 'type'> & { type: 'discrete' | 'continuous' - name: string - description: string - min: number - max: number - enabled: boolean - }[] - scoreVariables: { - name: string - label?: string - description: string - enabled: boolean - }[] - constraints: { + })[] + constraints: (Omit<(typeof sampleV17.constraints)[number], 'type'> & { type: 'sum' - value: number - dimensions: string[] - }[] - optimizerConfig: { - baseEstimator: string - acqFunc: string - initialPoints: number - kappa: number - xi: number - } - results: { - id: string - plots: { - id: string - plot: string - }[] - next: (number | string)[][] - pickled: string - expectedMinimum: ((number | string)[] | number)[] - extras: Record - } - dataPoints: { - meta: { - id: number - enabled: boolean - valid: boolean - description?: string - } + })[] + dataPoints: (Omit<(typeof sampleV17.dataPoints)[number], 'data'> & { data: ( | { - type: 'numeric' + type: 'numeric' | 'score' name: string value: number } @@ -76,58 +25,43 @@ export type ExperimentTypeV17 = { name: string value: string } - | { - type: 'score' - name: string - value: number - } )[] - }[] + })[] } -// TODO: multiobjective, try without immer // rename scores, change description to empty string, add labels export const migrateToV18 = (json: ExperimentTypeV17): ExperimentType => { - return produce( - json, - (draft: { - info: { dataFormatVersion: string } - scoreVariables: { - label?: string - }[] - dataPoints: { - data: { name: string; type: string }[] - }[] - }) => { - draft.info.dataFormatVersion = '18' - draft.scoreVariables = json.scoreVariables - .slice(0, scoreNames.length) - .map((s, i) => ({ - name: scoreNames[i] as (typeof scoreNames)[number], - label: scoreLabels[i] ?? scoreNames[i], - description: '', - enabled: s.enabled, - })) - draft.dataPoints = json.dataPoints.map(dp => { - let scoreIndex = 0 - return { - ...dp, - data: dp.data.flatMap(d => { - if (d.type === 'score') { - // empty result is filtered out by flatMap - return scoreIndex < scoreNames.length - ? { - ...d, - name: scoreNames[ - scoreIndex++ - ] as (typeof scoreNames)[number], - } - : [] - } - return d - }), - } - }) - } - ) as unknown as ExperimentType + return { + ...json, + info: { + ...json.info, + dataFormatVersion: '18', + }, + scoreVariables: json.scoreVariables + .slice(0, scoreNames.length) + .map((s, i) => ({ + name: scoreNames[i] as (typeof scoreNames)[number], + label: scoreLabels[i] ?? (scoreNames[i] as (typeof scoreNames)[number]), + description: '', + enabled: s.enabled, + })), + dataPoints: json.dataPoints.map(dp => { + let scoreIndex = 0 + return { + ...dp, + data: dp.data.flatMap(d => { + if (d.type === 'score') { + // empty result is filtered out by flatMap + return scoreIndex < scoreNames.length + ? { + ...d, + name: scoreNames[scoreIndex++] as (typeof scoreNames)[number], + } + : [] + } + return d + }), + } + }), + } } From ca271c6ddda18f312f5dd4d28e72a29a1e21e33b Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Fri, 23 Jan 2026 07:34:20 +0000 Subject: [PATCH 10/11] Remove complex typing --- .../util/migration/migrations/migrateToV18.ts | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/packages/core/src/common/util/migration/migrations/migrateToV18.ts b/packages/core/src/common/util/migration/migrations/migrateToV18.ts index 3d8ecce4..0f1eaa09 100644 --- a/packages/core/src/common/util/migration/migrations/migrateToV18.ts +++ b/packages/core/src/common/util/migration/migrations/migrateToV18.ts @@ -1,36 +1,8 @@ import { ExperimentType, scoreLabels, scoreNames } from '@core/common/types' -import sampleV17 from '../data-formats/17.json' - -// Use 17.json to define ExperimentTypeV17. Explicitly define string literals to make ts happy. -// Avoid using current ExperimentType as that can change in the future -type ExperimentTypeV17 = Omit< - typeof sampleV17, - 'valueVariables' | 'constraints' | 'dataPoints' -> & { - valueVariables: (Omit<(typeof sampleV17.valueVariables)[number], 'type'> & { - type: 'discrete' | 'continuous' - })[] - constraints: (Omit<(typeof sampleV17.constraints)[number], 'type'> & { - type: 'sum' - })[] - dataPoints: (Omit<(typeof sampleV17.dataPoints)[number], 'data'> & { - data: ( - | { - type: 'numeric' | 'score' - name: string - value: number - } - | { - type: 'categorical' - name: string - value: string - } - )[] - })[] -} // rename scores, change description to empty string, add labels -export const migrateToV18 = (json: ExperimentTypeV17): ExperimentType => { +// Note: Json is v17 but casted as ExperimentType +export const migrateToV18 = (json: ExperimentType): ExperimentType => { return { ...json, info: { From 9d3291e9fd0b99733d293ec93bf5cfd74dfc550d Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Fri, 23 Jan 2026 07:58:51 +0000 Subject: [PATCH 11/11] Hardcode v17 type --- .../util/migration/migrations/migrateToV18.ts | 86 ++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/core/src/common/util/migration/migrations/migrateToV18.ts b/packages/core/src/common/util/migration/migrations/migrateToV18.ts index 0f1eaa09..cd03ab52 100644 --- a/packages/core/src/common/util/migration/migrations/migrateToV18.ts +++ b/packages/core/src/common/util/migration/migrations/migrateToV18.ts @@ -1,8 +1,90 @@ import { ExperimentType, scoreLabels, scoreNames } from '@core/common/types' +export type ExperimentTypeV17 = { + id: string + lastEvaluationHash?: string + changedSinceLastEvaluation: boolean + info: { + name: string + description: string + swVersion: string + dataFormatVersion: '17' + version: number + extras: Record + } + extras: Record + categoricalVariables: { + name: string + description: string + options: string[] + enabled: boolean + }[] + valueVariables: { + type: 'discrete' | 'continuous' + name: string + description: string + min: number + max: number + enabled: boolean + }[] + scoreVariables: { + name: string + label?: string + description: string + enabled: boolean + }[] + constraints: { + type: 'sum' + value: number + dimensions: string[] + }[] + optimizerConfig: { + baseEstimator: string + acqFunc: string + initialPoints: number + kappa: number + xi: number + } + results: { + id: string + plots: { + id: string + plot: string + }[] + next: (number | string)[][] + pickled: string + expectedMinimum: ((number | string)[] | number)[] + extras: Record + } + dataPoints: { + meta: { + id: number + enabled: boolean + valid: boolean + description?: string + } + data: ( + | { + type: 'numeric' + name: string + value: number + } + | { + type: 'categorical' + name: string + value: string + } + | { + type: 'score' + name: string + value: number + } + )[] + }[] +} + // rename scores, change description to empty string, add labels -// Note: Json is v17 but casted as ExperimentType -export const migrateToV18 = (json: ExperimentType): ExperimentType => { +export const migrateToV18 = (json: ExperimentTypeV17): ExperimentType => { return { ...json, info: {