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

fix(funman): limit y range and round timepoints to third decimal place in preview #5308

Merged
merged 11 commits into from
Oct 30, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -85,38 +85,43 @@
Settings
<i class="pi pi-info-circle pl-2" v-tooltip="validateParametersToolTip" />
</template>
<label>Select parameters of interest</label>
<MultiSelect
ref="columnSelect"
class="w-full mt-1 mb-2"
:model-value="variablesOfInterest"
:options="requestParameters"
option-label="name"
option-disabled="disabled"
:show-toggle-all="false"
placeholder="Select variables"
@update:model-value="onToggleVariableOfInterest"
/>
<span class="timespan mb-2">
<div>
<label>Start time</label>
<tera-input-number class="w-12" v-model="knobs.currentTimespan.start" />
</div>
<div>
<label>End time</label>
<tera-input-number class="w-12" v-model="knobs.currentTimespan.end" />
</div>
<div>
<label>Number of timesteps</label>
<tera-input-number class="w-12" v-model="knobs.numberOfSteps" />
<section class="flex flex-column gap-2">
<label>Select parameters of interest</label>
<MultiSelect
ref="columnSelect"
class="w-full"
:model-value="variablesOfInterest"
:options="requestParameters"
option-label="name"
option-disabled="disabled"
:show-toggle-all="false"
placeholder="Select variables"
@update:model-value="onToggleVariableOfInterest"
/>
<span class="timespan">
<div>
<label>Start time</label>
<tera-input-number class="w-12" v-model="knobs.currentTimespan.start" />
</div>
<div>
<label>End time</label>
<tera-input-number class="w-12" v-model="knobs.currentTimespan.end" />
</div>
<div>
<label>Number of timesteps</label>
<tera-input-number class="w-12" v-model="knobs.numberOfSteps" />
</div>
</span>
<label>Timepoints</label>
<code>
{{ stepList.map((step) => Number(step.toFixed(3))).join(', ') }}
</code>
<label>Tolerance</label>
<div class="input-tolerance fadein animation-ease-in-out animation-duration-350">
<tera-input-number v-model="knobs.tolerance" />
<Slider v-model="knobs.tolerance" :min="0" :max="1" :step="0.01" class="w-full mr-2" />
</div>
</span>
<tera-input-text :disabled="true" class="mb-2" v-model="requestStepListString" />
<label>Tolerance</label>
<div class="mt-1 input-tolerance fadein animation-ease-in-out animation-duration-350">
<tera-input-number v-model="knobs.tolerance" />
<Slider v-model="knobs.tolerance" :min="0" :max="1" :step="0.01" class="w-full mr-2" />
</div>
</section>
</AccordionTab>
</Accordion>
</main>
Expand Down Expand Up @@ -293,11 +298,10 @@
</template>

<script setup lang="ts">
import _, { floor, isEmpty } from 'lodash';
import { isEmpty, cloneDeep } from 'lodash';
import { computed, ref, watch } from 'vue';
import { logger } from '@/utils/logger';
import Button from 'primevue/button';
import TeraInputText from '@/components/widgets/tera-input-text.vue';
import TeraInputNumber from '@/components/widgets/tera-input-number.vue';
import Slider from 'primevue/slider';
import MultiSelect from 'primevue/multiselect';
Expand Down Expand Up @@ -390,9 +394,6 @@ const isSliderOpen = ref(true);

const mass = ref('0');

const requestStepList = computed(() => getStepList());
const requestStepListString = computed(() => requestStepList.value.join()); // Just used to display. dont like this but need to be quick

const requestParameters = ref<any[]>([]);
const configuredModel = ref<Model | null>();

Expand All @@ -402,7 +403,7 @@ const observableIds = ref<string[]>([]);

const selectedOutputId = ref<string>();
const outputs = computed(() => {
if (!_.isEmpty(props.node.outputs)) {
if (!isEmpty(props.node.outputs)) {
return [
{
label: 'Select an output',
Expand All @@ -422,11 +423,19 @@ const onToggleVariableOfInterest = (event: any[]) => {
requestParameters.value.forEach((d) => {
d.label = namesOfInterest.includes(d.name) ? 'all' : 'any';
});
const state = _.cloneDeep(props.node.state);
state.requestParameters = _.cloneDeep(requestParameters.value);
const state = cloneDeep(props.node.state);
state.requestParameters = cloneDeep(requestParameters.value);
emit('update-state', state);
};

const stepList = computed(() => {
const { start, end } = knobs.value.currentTimespan;
const steps = knobs.value.numberOfSteps;

const stepSize = (end - start) / steps;
return [start, ...Array.from({ length: steps - 1 }, (_, i) => (i + 1) * stepSize), end];
});

const runMakeQuery = async () => {
if (!configuredModel.value) {
toast.error('', 'No Model provided for request');
Expand Down Expand Up @@ -497,7 +506,7 @@ const runMakeQuery = async () => {
structure_parameters: [
{
name: 'schedules',
schedules: [{ timepoints: requestStepList.value }]
schedules: [{ timepoints: stepList.value }]
}
],
config: {
Expand All @@ -513,13 +522,13 @@ const runMakeQuery = async () => {
const response = await makeQueries(request, originalModelId);

// Setup the in-progress id
const state = _.cloneDeep(props.node.state);
const state = cloneDeep(props.node.state);
state.inProgressId = response.id;
emit('update-state', state);
};

const addConstraintForm = () => {
const state = _.cloneDeep(props.node.state);
const state = cloneDeep(props.node.state);
state.constraintGroups.push({
name: `Constraint ${state.constraintGroups.length + 1}`,
isActive: true,
Expand All @@ -534,13 +543,13 @@ const addConstraintForm = () => {
};

const deleteConstraintGroupForm = (index: number) => {
const state = _.cloneDeep(props.node.state);
const state = cloneDeep(props.node.state);
state.constraintGroups.splice(index, 1);
emit('update-state', state);
};

const updateConstraintGroupForm = (index: number, key: string, value: any) => {
const state = _.cloneDeep(props.node.state);
const state = cloneDeep(props.node.state);

// Changing constraint resets settings
if (key === 'constraint') {
Expand All @@ -561,22 +570,6 @@ const updateConstraintGroupForm = (index: number, key: string, value: any) => {
emit('update-state', state);
};

// Used to set requestStepList.
// Grab startTime, endTime, numberOfSteps and create list.
function getStepList() {
const start = knobs.value.currentTimespan.start;
const end = knobs.value.currentTimespan.end;
const steps = knobs.value.numberOfSteps;

const aList = [start];
const stepSize = floor((end - start) / steps);
for (let i = 1; i < steps; i++) {
aList[i] = i * stepSize;
}
aList.push(end);
return aList;
}

const initialize = async () => {
const modelConfigurationId = props.node.inputs[0].value?.[0];
if (!modelConfigurationId) return;
Expand Down Expand Up @@ -614,9 +607,9 @@ const setModelOptions = async () => {
if (ode.observables) observableIds.value = ode.observables.map((d) => d.id);
}

const state = _.cloneDeep(props.node.state);
const state = cloneDeep(props.node.state);
knobs.value.numberOfSteps = state.numSteps;
knobs.value.currentTimespan = _.cloneDeep(state.currentTimespan);
knobs.value.currentTimespan = cloneDeep(state.currentTimespan);
knobs.value.tolerance = state.tolerance;
knobs.value.compartmentalConstraint = state.compartmentalConstraint;

Expand All @@ -627,7 +620,7 @@ const setModelOptions = async () => {
toast.error('', 'Provided model has no parameters');
}

state.requestParameters = _.cloneDeep(requestParameters.value);
state.requestParameters = cloneDeep(requestParameters.value);
emit('update-state', state);
};

Expand Down Expand Up @@ -682,7 +675,7 @@ watch(
watch(
() => knobs.value,
() => {
const state = _.cloneDeep(props.node.state);
const state = cloneDeep(props.node.state);
state.tolerance = knobs.value.tolerance;
state.currentTimespan.start = knobs.value.currentTimespan.start;
state.currentTimespan.end = knobs.value.currentTimespan.end;
Expand Down Expand Up @@ -871,7 +864,7 @@ watch(
.map(({ name }) => name);

// Initialize default output settings
const state = _.cloneDeep(props.node.state);
const state = cloneDeep(props.node.state);
state.chartSettings = updateChartSettingsBySelectedVariables([], ChartSettingType.VARIABLE, stateOptions.value);
state.chartSettings = updateChartSettingsBySelectedVariables(
state.chartSettings,
Expand Down Expand Up @@ -915,6 +908,18 @@ watch(
position: relative;
}

code {
background-color: var(--gray-50);
color: var(--text-color-subdued);
border-radius: var(--border-radius);
border: 1px solid var(--surface-border);
padding: var(--gap-2);
overflow-wrap: break-word;
font-size: var(--font-caption);
max-height: 10rem;
overflow: auto;
}

.timespan {
display: flex;
align-items: end;
Expand All @@ -924,7 +929,7 @@ watch(
& > div {
display: flex;
flex-direction: column;
gap: var(--gap-1);
gap: var(--gap-2);
}
}

Expand Down
55 changes: 40 additions & 15 deletions packages/client/hmi-client/src/services/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -875,22 +875,45 @@ export function createFunmanStateChart(
) {
if (isEmpty(trajectories)) return null;

const threshold = 1e25;
const globalFont = 'Figtree';

// Find min/max values to set an appropriate viewing range for y-axis
const minY = Math.floor(Math.min(...trajectories.map((d) => d.values[stateId])));
const maxY = Math.ceil(Math.max(...trajectories.map((d) => d.values[stateId])));

// Show checks for the selected state
const stateIdConstraints = constraints.filter((c) => c.variables.includes(stateId));
const modelChecks = stateIdConstraints.map((c) => ({
legendItem: FunmanChartLegend.ModelChecks,
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: 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)
}));
// Find min/max values to set an appropriate viewing range for y-axis
// Limit by threshold as very large numbers cause the chart to have NaN in the y domain
const lowerBounds = stateIdConstraints.map((c) => {
let lb = c.additive_bounds.lb;
const ub = c.additive_bounds.ub;
if (lb && ub && lb < -threshold) lb = -Math.abs(ub) * 0.5 + ub;
return lb;
});
const upperBounds = stateIdConstraints.map((c) => {
const lb = c.additive_bounds.lb;
let ub = c.additive_bounds.ub;
if (ub && lb && ub > threshold) ub = Math.abs(lb) * 0.5 + lb;
return ub;
});
const yPoints = trajectories.map((t) => t.values[stateId]);

const potentialMinYs = focusOnModelChecks && !isEmpty(lowerBounds) ? [...yPoints, ...lowerBounds] : yPoints;
const potentialMaxYs = focusOnModelChecks && !isEmpty(upperBounds) ? [...yPoints, ...upperBounds] : yPoints;
const minY = Math.floor(Math.min(...potentialMinYs));
const maxY = Math.ceil(Math.max(...potentialMaxYs));

const modelChecks = stateIdConstraints.map((c) => {
let startY = c.additive_bounds.lb ?? minY;
let endY = c.additive_bounds.ub ?? maxY;
// Fallback to min/max if the bounds exceed the threshold
if (startY < -threshold) startY = minY;
if (endY > threshold) endY = maxY;
return {
legendItem: FunmanChartLegend.ModelChecks,
startX: c.timepoints.lb,
endX: c.timepoints.ub,
startY,
endY
};
});

return {
$schema: VEGALITE_SCHEMA,
Expand All @@ -915,7 +938,9 @@ export function createFunmanStateChart(
{
mark: {
type: 'rect',
clip: true
clip: true,
stroke: '#A4CEFF',
strokeWidth: 1
},
data: { values: modelChecks },
encoding: {
Expand Down Expand Up @@ -948,7 +973,7 @@ export function createFunmanStateChart(
x: { title: 'Timepoints' },
y: {
title: `${stateId} (persons)`,
scale: focusOnModelChecks ? {} : { domain: [minY, maxY] }
scale: { domain: [minY, maxY] }
},
color: {
field: 'legendItem',
Expand Down