Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: funman graph improvements #5105

Merged
merged 12 commits into from
Oct 16, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,13 @@
<MultiSelect
ref="columnSelect"
class="w-full mt-1 mb-2"
:modelValue="variablesOfInterest"
:options="requestParameters.map((d: any) => d.name)"
:model-value="variablesOfInterest"
:options="requestParameters"
option-label="name"
option-disabled="disabled"
:show-toggle-all="false"
@update:modelValue="onToggleVariableOfInterest"
placeholder="Select variables"
@update:model-value="onToggleVariableOfInterest"
/>
<div class="mb-2 timespan">
<div class="timespan-input">
Expand Down Expand Up @@ -248,17 +250,13 @@ const outputs = computed(() => {
const activeOutput = ref<WorkflowOutput<FunmanOperationState> | null>(null);
const variablesOfInterest = ref<string[]>([]);
const onToggleVariableOfInterest = (vals: string[]) => {
variablesOfInterest.value = vals;
const variablesOfInterest = ref();
const onToggleVariableOfInterest = (event: any[]) => {
variablesOfInterest.value = event;
const namesOfInterest = event.map((d) => d.name);
requestParameters.value.forEach((d) => {
if (variablesOfInterest.value.includes(d.name)) {
d.label = 'all';
} else {
d.label = 'any';
}
d.label = namesOfInterest.includes(d.name) ? 'all' : 'any';
});
const state = _.cloneDeep(props.node.state);
state.requestParameters = _.cloneDeep(requestParameters.value);
emit('update-state', state);
Expand Down Expand Up @@ -330,7 +328,7 @@ const runMakeQuery = async () => {
model: configuredModel.value,
request: {
constraints,
parameters: requestParameters.value,
parameters: requestParameters.value.map(({ disabled, ...rest }) => rest), // Remove the disabled property from the request (it's only used for UI)
structure_parameters: [
{
name: 'schedules',
Expand Down Expand Up @@ -459,7 +457,7 @@ const setModelOptions = async () => {
if (configuredModel.value.semantics?.ode.parameters) {
setRequestParameters(configuredModel.value.semantics?.ode.parameters);
variablesOfInterest.value = requestParameters.value.filter((d: any) => d.label === 'all').map((d: any) => d.name);
variablesOfInterest.value = requestParameters.value.filter((d: any) => d.label === 'all');
} else {
toast.error('', 'Provided model has no parameters');
}
Expand All @@ -477,18 +475,24 @@ const setRequestParameters = (modelParameters: ModelParameter[]) => {
});
requestParameters.value = modelParameters.map((ele) => {
let interval = { lb: ele.value, ub: ele.value };
const name = ele.id;
const param = {
name,
label: (labelMap.get(name) as string) ?? 'any',
interval: { lb: ele.value, ub: ele.value },
disabled: false
};
if (ele.distribution) {
interval = {
param.interval = {
lb: ele.distribution.parameters.minimum,
ub: ele.distribution.parameters.maximum
};
} else {
param.disabled = true; // Disable if constant
}
const param = { name: ele.id, interval, label: 'any' };
if (labelMap.has(param.name)) {
param.label = labelMap.get(param.name) as string;
}
return param;
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@
<Button label="Save for reuse" outlined severity="secondary" />
</div>
</header>

<Accordion multiple :active-index="[0, 1, 2, 3]">
<AccordionTab header="Summary"> Summary text </AccordionTab>
<AccordionTab>
<template #header> State variables<i class="pi pi-info-circle" /> </template>
<!--TODO: Will put these checkbox options in output settings later-->
<div class="flex align-items-center gap-2 ml-4 mb-3">
<Checkbox v-model="onlyShowLatestResults" binary @change="renderCharts" />
<label>Only show furthest results</label>
</div>
shawnyama marked this conversation as resolved.
Show resolved Hide resolved
<div class="flex align-items-center gap-2 ml-4 mb-4">
<Checkbox v-model="focusOnModelChecks" binary @change="updateStateChart" /> <label>Focus on model checks</label>
</div>
<template v-if="stateChart">
<Dropdown v-model="selectedState" :options="stateOptions" @update:model-value="updateStateChart" />
<Dropdown class="ml-4" v-model="selectedState" :options="stateOptions" @update:model-value="updateStateChart" />
<vega-chart :visualization-spec="stateChart" :are-embed-actions-visible="false" />
</template>
<span class="ml-4" v-else> No boxes were generated. </span>
Expand Down Expand Up @@ -61,6 +70,7 @@
<script setup lang="ts">
import { isEmpty } from 'lodash';
import { ref, watch } from 'vue';
import Checkbox from 'primevue/checkbox';
import TeraObservables from '@/components/model/model-parts/tera-observables.vue';
import TeraInitialTable from '@/components/model/petrinet/tera-initial-table.vue';
import TeraParameterTable from '@/components/model/petrinet/tera-parameter-table.vue';
Expand All @@ -69,7 +79,7 @@ import {
type FunmanConstraintsResponse,
processFunman
} from '@/services/models/funman-service';
import { createFunmanStateChart, createFunmanParameterChart } from '@/services/charts';
import { createFunmanStateChart, createFunmanParameterCharts } from '@/services/charts';
import VegaChart from '@/components/widgets/VegaChart.vue';
import { getRunResult } from '@/services/models/simulation-service';
import Dropdown from 'primevue/dropdown';
Expand All @@ -93,6 +103,7 @@ const emit = defineEmits(['update:trajectoryState']);
let processedFunmanResult: ProcessedFunmanResult | null = null;
let constraintsResponse: FunmanConstraintsResponse[] = [];
let mmt: MiraModel = emptyMiraModel();
let funmanResult: any = {};

// Model configuration stuff
const model = ref<Model | null>(null);
Expand All @@ -103,31 +114,34 @@ const calibratedConfigObservables = ref<Observable[]>([]);

const stateOptions = ref<string[]>([]);
const selectedState = ref<string>('');
const onlyShowLatestResults = ref(false);
const focusOnModelChecks = ref(false);

const stateChart = ref();
const parameterCharts = ref();

const initalize = async () => {
const rawFunmanResult = await getRunResult(props.runId, 'validation.json');
if (!rawFunmanResult) {
logger.error('Failed to fetch funman result');
return;
}
const funmanResult = JSON.parse(rawFunmanResult);
constraintsResponse = funmanResult.request.constraints;
stateOptions.value = funmanResult.model.petrinet.model.states.map(({ id }) => id);
validatedModelConfiguration.value = funmanResult.modelConfiguration;
function updateStateChart() {
if (!processedFunmanResult) return;
emit('update:trajectoryState', selectedState.value);
stateChart.value = createFunmanStateChart(
processedFunmanResult,
constraintsResponse,
selectedState.value,
focusOnModelChecks.value
);
}

processedFunmanResult = processFunman(funmanResult);
async function renderCharts() {
processedFunmanResult = processFunman(funmanResult, onlyShowLatestResults.value);

// State chart
selectedState.value = props.trajectoryState ?? stateOptions.value[0];
updateStateChart();

// Parameter charts
const parametersOfInterest = funmanResult.request.parameters.filter((d: any) => d.label === 'all');
const distributionParameters = funmanResult.request.parameters.filter((d: any) => d.interval.lb !== d.interval.ub); // TODO: This conditional may change as funman will return constants soon
if (processedFunmanResult.boxes) {
parameterCharts.value = createFunmanParameterChart(parametersOfInterest, processedFunmanResult.boxes);
parameterCharts.value = createFunmanParameterCharts(distributionParameters, processedFunmanResult.boxes);
}

// For displaying model/model configuration
Expand All @@ -152,14 +166,22 @@ const initalize = async () => {
expression
})
);
};

function updateStateChart() {
if (!processedFunmanResult) return;
emit('update:trajectoryState', selectedState.value);
stateChart.value = createFunmanStateChart(processedFunmanResult, constraintsResponse, selectedState.value);
}

const initalize = async () => {
const rawFunmanResult = await getRunResult(props.runId, 'validation.json');
if (!rawFunmanResult) {
logger.error('Failed to fetch funman result');
return;
}
funmanResult = JSON.parse(rawFunmanResult);
constraintsResponse = funmanResult.request.constraints;
stateOptions.value = funmanResult.model.petrinet.model.states.map(({ id }) => id);
validatedModelConfiguration.value = funmanResult.modelConfiguration;

renderCharts();
};

watch(
() => props.runId,
() => {
Expand Down
58 changes: 45 additions & 13 deletions packages/client/hmi-client/src/services/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,17 +817,29 @@ enum FunmanChartLegend {
ModelChecks = 'Model checks'
}

function getBoundType(label: string): string {
switch (label) {
case 'true':
return FunmanChartLegend.Satisfactory;
case 'false':
return FunmanChartLegend.Unsatisfactory;
default:
return FunmanChartLegend.Ambiguous;
}
}

export function createFunmanStateChart(
data: ProcessedFunmanResult,
constraints: FunmanConstraintsResponse[],
stateId: string
stateId: string,
focusOnModelChecks: boolean
) {
if (isEmpty(data.trajs)) return null;

const globalFont = 'Figtree';

const boxLines = data.trajs.map((traj) => {
const legendItem = traj.label === 'true' ? FunmanChartLegend.Satisfactory : FunmanChartLegend.Unsatisfactory;
const legendItem = getBoundType(traj.label);
return { timepoints: traj.timestep, value: traj[stateId], legendItem };
});

Expand All @@ -842,8 +854,8 @@ export function createFunmanStateChart(
startX: c.timepoints.lb,
endX: c.timepoints.ub,
// If the interval bounds are within the min/max values of the line plot use them, otherwise use the min/max values
startY: Math.max(c.additive_bounds.lb ?? minY, minY),
endY: Math.min(c.additive_bounds.ub ?? maxY, maxY)
startY: focusOnModelChecks ? c.additive_bounds.lb : Math.max(c.additive_bounds.lb ?? minY, minY),
endY: focusOnModelChecks ? c.additive_bounds.ub : Math.min(c.additive_bounds.ub ?? maxY, maxY)
}));

return {
Expand Down Expand Up @@ -881,7 +893,7 @@ export function createFunmanStateChart(
x: { title: 'Timepoints' },
y: {
title: `${stateId} (persons)`,
scale: { domain: [minY, maxY] }
scale: focusOnModelChecks ? {} : { domain: [minY, maxY] }
},
color: {
field: 'legendItem',
Expand All @@ -900,38 +912,45 @@ export function createFunmanStateChart(
};
}

export function createFunmanParameterChart(
parametersOfInterest: { label: 'all'; name: string; interval: FunmanInterval }[],
export function createFunmanParameterCharts(
distributionParameters: { label: string; name: string; interval: FunmanInterval }[],
boxes: FunmanBox[]
) {
const parameterRanges: { parameterId: string; boundType: string; lb?: number; ub?: number }[] = [];
const parameterRanges: { parameterId: string; boundType: string; lb?: number; ub?: number; tick?: number }[] = [];
const distributionParameterIds: string[] = [];

// Widest range (model configuration ranges)
parametersOfInterest.forEach(({ name, interval }) => {
distributionParameters.forEach(({ name, interval }) => {
parameterRanges.push({
parameterId: name,
boundType: 'length',
lb: interval.lb,
ub: interval.ub
});
distributionParameterIds.push(name);
});

// Ranges determined by the true/false boxes
boxes.forEach(({ label, parameters }) => {
Object.keys(parameters).forEach((key) => {
if (!distributionParameterIds.includes(key)) return;
parameterRanges.push({
parameterId: key,
boundType: label === 'true' ? FunmanChartLegend.Satisfactory : FunmanChartLegend.Unsatisfactory,
boundType: getBoundType(label),
lb: parameters[key].lb,
ub: parameters[key].ub
ub: parameters[key].ub,
tick: parameters[key].point
});
});
});

const globalFont = 'Figtree';
return {
$schema: VEGALITE_SCHEMA,
config: { font: globalFont },
config: {
font: globalFont,
tick: { thickness: 2 }
},
width: 600,
height: 50, // Height per facet
data: {
Expand Down Expand Up @@ -968,7 +987,7 @@ export function createFunmanParameterChart(
{
mark: {
type: 'bar', // Use a bar to represent ranges
opacity: 0.3 // FIXME: This opacity shouldn't be applied to the legend
opacity: 0.4 // FIXME: This opacity shouldn't be applied to the legend
},
encoding: {
x: {
Expand Down Expand Up @@ -996,6 +1015,19 @@ export function createFunmanParameterChart(
}
}
}
},
{
mark: {
type: 'tick',
size: 20
},
encoding: {
x: {
field: 'tick',
type: 'quantitative',
title: null
}
}
}
]
}
Expand Down
Loading