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 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..a67666d0 100644 --- a/packages/core/src/common/types/common.ts +++ b/packages/core/src/common/types/common.ts @@ -3,9 +3,17 @@ 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 = ['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(), @@ -42,7 +50,8 @@ const valueVariableSchema = z.object({ }) const scoreVariableSchema = z.object({ - name: 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 19428e3a..9f98b9fc 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, label: '' }, + { name: scoreNames[1], description: '', enabled: true, label: '' }, ], 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, 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..67c47373 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, + isValidScoreName, } from '@core/common/types' /** @@ -78,7 +79,10 @@ 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 => + 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/data-formats/18.json b/packages/core/src/common/util/migration/data-formats/18.json new file mode 100644 index 00000000..4340e47a --- /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..f5871356 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' @@ -15,12 +16,14 @@ import { emptyExperiment } from '@core/context/experiment' import { formatNext } from './migrations/migrateToV9' 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' +import { ExperimentTypeV17 } from './migrations/migrateToV18' describe('Migration of data format', () => { storeLatestSchema() @@ -159,7 +162,7 @@ describe('Migration of data format', () => { description: 'score', enabled: true, }, - ] satisfies ScoreVariableType[] + ] const scoreVarsMultiObjectiveDisabled = [ { @@ -170,9 +173,9 @@ describe('Migration of data format', () => { { name: 'score2', description: 'score', - enabled: true, + enabled: false, }, - ] satisfies ScoreVariableType[] + ] it.each([ ['multiobjective, all enabled', scoreVarsMultiObjective], @@ -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,105 @@ describe('Migration of data format', () => { ) }) + describe('migrateToV18', () => { + const experiment17 = { + ...version17, + scoreVariables: [ + { + name: 'Quality (0-5)', + description: 'Quality (0-5)', + enabled: true, + }, + ], + } as ExperimentTypeV17 + + 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('should convert multiple score variables with correct indices', () => { + const multiObj = { + ...version17, + 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], + description: '', + enabled: true, + }, + { + name: scoreNames[1], + label: scoreLabels[1], + description: '', + enabled: false, + }, + ]) + }) + + 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('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', () => { - //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..cd03ab52 --- /dev/null +++ b/packages/core/src/common/util/migration/migrations/migrateToV18.ts @@ -0,0 +1,121 @@ +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 +export const migrateToV18 = (json: ExperimentTypeV17): 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 + }), + } + }), + } +} 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..370c1eca --- /dev/null +++ b/packages/core/src/common/util/migration/schemas/18.json @@ -0,0 +1,429 @@ +{ + "$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": { + "anyOf": [ + { + "type": "string", + "const": "quality" + }, + { + "type": "string", + "const": "cost" + } + ] + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "name", + "label", + "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-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..41048e39 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, - scoreName, + isValidScoreName, + scoreLabels, + scoreNames as scoreNamesState, } from '@core/common/types' import { produce } from 'immer' import md5 from 'md5' @@ -399,15 +401,16 @@ export const experimentReducer = produce( if (state.scoreVariables.length < 2) { state.scoreVariables.push({ - name: scoreName + ' 2', - description: scoreName + ' 2', + name: scoreNamesState[1], + label: scoreLabels[1] ?? scoreNamesState[1], + description: '', enabled: true, }) const scoreNames = state.scoreVariables.map(it => it.name) state.dataPoints.forEach(dataEntry => { const dp = dataEntry.data const containedScores = dp - .filter(it => scoreNames.includes(it.name)) + .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/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 bee6b2f3..b4f5eeb6 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, scoreNames } from '@core/common/types' import { ExperimentType } from '@core/common/types' export const emptyExperiment: ExperimentType = { @@ -18,8 +18,9 @@ export const emptyExperiment: ExperimentType = { valueVariables: [], scoreVariables: [ { - name: scoreName, - description: scoreName, + name: scoreNames[0], + label: scoreLabels[0] ?? scoreNames[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: scoreNames[0], + label: scoreLabels[0] ?? scoreNames[0], + description: '', + enabled: true, + }, + { + 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..5708c82f 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, + 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/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/packages/ui/src/features/result-data/single-data-point.tsx b/packages/ui/src/features/result-data/single-data-point.tsx index 95887006..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 { scoreName } 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([scoreName + ' (95 % credibility interval)']) + .concat([ + (scoreLabels[0] ?? scoreNames[0]) + + ' (95 % credibility interval)', + ]) .map((h, idx) => ( {h} diff --git a/sample-app/src/components/home/home.tsx b/sample-app/src/components/home/home.tsx index b09f6cae..aa7d66a0 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..2584201f 100644 --- a/sample-app/src/experiment-container.tsx +++ b/sample-app/src/experiment-container.tsx @@ -3,13 +3,16 @@ 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' + const { dispatch, state: { debug, showJsonEditor, focus }, @@ -34,6 +37,7 @@ export default function ExperimentContainer() { experimentId={ Array.isArray(experimentId) ? (experimentId[0] ?? '') : experimentId } + isMultiObjective={isMultiObjective} > {focus === 'legacy' ? : } {debug && }