Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/nervous-points-move.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/common/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
97 changes: 43 additions & 54 deletions packages/core/src/common/util/converters/converters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ExperimentType,
ScoreVariableType,
ValueVariableType,
scoreName,
scoreNames,
} from '@core/common/types'
import { initialState } from '@core/context'
import {
Expand All @@ -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 },
Expand All @@ -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 },
Expand Down Expand Up @@ -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
)
Expand All @@ -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
)
Expand Down Expand Up @@ -397,7 +397,7 @@ describe('converters', () => {
},
{
type: 'score',
name: 'score',
name: scoreNames[0],
value: 1,
},
],
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -469,7 +468,7 @@ describe('converters', () => {
},
{
type: 'score',
name: 'score',
name: scoreNames[0],
value: 1,
},
],
Expand Down Expand Up @@ -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)
})
Expand All @@ -531,7 +529,7 @@ describe('converters', () => {
},
{
type: 'score',
name: 'score',
name: scoreNames[0],
value: 2,
},
],
Expand All @@ -550,7 +548,7 @@ describe('converters', () => {
},
{
type: 'score',
name: 'score',
name: scoreNames[0],
value: 0,
},
],
Expand All @@ -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)
})
Expand Down Expand Up @@ -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[] = [
Expand Down Expand Up @@ -647,7 +649,7 @@ describe('converters', () => {
},
{
type: 'score',
name: 'score',
name: scoreNames[0],
value: 1,
},
],
Expand Down Expand Up @@ -677,7 +679,7 @@ describe('converters', () => {
},
{
type: 'score',
name: 'score',
name: scoreNames[0],
value: 2,
},
],
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 },
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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' },
Expand All @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/common/util/converters/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SpaceType,
ValueVariableType,
dataPointSchema,
isValidScoreName,
} from '@core/common/types'

/**
Expand Down Expand Up @@ -78,7 +79,10 @@ export const calculateData = (
it.type === 'numeric' ? Number(it.value) : it.value
) as Array<string | number>, // 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),
})
Expand Down
Loading
Loading