Skip to content

Commit

Permalink
show math expressions in funman input panel (#4905)
Browse files Browse the repository at this point in the history
  • Loading branch information
shawnyama authored Sep 25, 2024
1 parent 67d68c3 commit ec2f327
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 53 deletions.
6 changes: 6 additions & 0 deletions packages/client/hmi-client/src/assets/css/reset.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ a, div, em, figure, footer, form, h1, h2, h3, h4, h5, h6, header, img, li, ol, p
vertical-align: baseline;
}

/* Prevent font inheritance for KaTeX elements */
.katex * {
font-family: 'KaTeX_Main', 'Times New Roman', serif;
font-size: 1.1rem;
}

img {
display: block;
max-width: 100%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export enum Constraint {

export enum ConstraintType {
LessThan = 'less than',
LessThanOrEqualTo = 'less than or equal to',
GreaterThan = 'greater than',
GreaterThanOrEqualTo = 'greater than or equal to',
Increasing = 'increasing',
Decreasing = 'decreasing',
LinearlyConstrained = 'linearly constrained',
Expand All @@ -24,7 +26,7 @@ export interface ConstraintGroup {
constraint: Constraint;
constraintType: ConstraintType;
variables: string[];
weights?: number[]; // 1 to 1 mapping with variables
weights: number[]; // 1 to 1 mapping with variables
timepoints: FunmanInterval;
interval: FunmanInterval;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
should be
<Dropdown
:model-value="config.constraintType"
:options="Object.values(ConstraintType)"
:options="constraintTypeOptions"
@update:model-value="
($event: ConstraintType) => {
// Disable isActive if constraintType is Following
Expand All @@ -51,19 +51,30 @@
"
/>
<tera-input-number
v-if="config.constraintType === ConstraintType.LessThan"
v-if="
config.constraintType === ConstraintType.LessThan ||
config.constraintType === ConstraintType.LessThanOrEqualTo
"
auto-width
:model-value="config.interval.ub"
@update:model-value="emit('update-self', { key: 'interval', value: { lb: config.interval.lb, ub: $event } })"
/>
<tera-input-number
v-if="config.constraintType === ConstraintType.GreaterThan"
v-else-if="
config.constraintType === ConstraintType.GreaterThan ||
config.constraintType === ConstraintType.GreaterThanOrEqualTo
"
auto-width
:model-value="config.interval.lb"
@update:model-value="emit('update-self', { key: 'interval', value: { lb: $event, ub: config.interval.ub } })"
/>
<template
v-if="config.constraintType === ConstraintType.LessThan || config.constraintType === ConstraintType.GreaterThan"
v-if="
config.constraintType === ConstraintType.LessThan ||
config.constraintType === ConstraintType.LessThanOrEqualTo ||
config.constraintType === ConstraintType.GreaterThan ||
config.constraintType === ConstraintType.GreaterThanOrEqualTo
"
>
persons
</template>
Expand Down Expand Up @@ -101,28 +112,54 @@
<!--TODO: should be based on time variable in model semantics-->
days.
</p>
<ul v-if="config.weights && !isEmpty(config.weights)">
<li v-for="(variable, index) of config.variables" :key="index">
<!--TODO: Add dataset column -> variable mapping for following option-->
<div
v-if="config.constraintType === ConstraintType.LinearlyConstrained"
class="flex flex-wrap align-items-center gap-2 mt-3"
>
<tera-input-number
auto-width
:model-value="config.interval.lb"
@update:model-value="emit('update-self', { key: 'interval', value: { lb: $event, ub: config.interval.ub } })"
/>
<katex-element :expression="stringToLatexExpression(`\\leq [`)" />
<template v-for="(variable, index) in config.variables" :key="index">
<tera-input-number
:label="variable + ' Weight'"
:placeholder="variable"
auto-width
:model-value="config.weights[index]"
@update:model-value="
($event) => {
if (isNaN($event)) return; // Don't accept empty value
const newWeights = cloneDeep(config.weights);
if (!newWeights) return;
newWeights[index] = $event;
emit('update-self', { key: 'weights', value: newWeights });
}
"
/>
</li>
</ul>
<katex-element
:expression="stringToLatexExpression(`${variable} ${index === config.variables.length - 1 ? '' : '\\ +'}`)"
/>
</template>
<katex-element :expression="stringToLatexExpression(`] \\leq`)" />
<tera-input-number
auto-width
:model-value="config.interval.ub"
@update:model-value="emit('update-self', { key: 'interval', value: { lb: config.interval.lb, ub: $event } })"
/>
<katex-element
:expression="stringToLatexExpression(`\\forall \\ t \\in [${config.timepoints.lb}, ${config.timepoints.ub}]`)"
/>
</div>
<katex-element
class="mt-3"
v-else-if="config.constraintType !== ConstraintType.Following"
:expression="stringToLatexExpression(generateConstraintExpression(config))"
/>
</section>
</template>

<script setup lang="ts">
import { isEmpty, cloneDeep } from 'lodash';
import { cloneDeep } from 'lodash';
import { computed } from 'vue';
import TeraToggleableInput from '@/components/widgets/tera-toggleable-input.vue';
import MultiSelect from 'primevue/multiselect';
Expand All @@ -131,6 +168,8 @@ import InputSwitch from 'primevue/inputswitch';
import Button from 'primevue/button';
import { ConstraintGroup, Constraint, ConstraintType } from '@/components/workflow/ops/funman/funman-operation';
import TeraInputNumber from '@/components/widgets/tera-input-number.vue';
import { stringToLatexExpression } from '@/services/model';
import { generateConstraintExpression } from '@/services/models/funman-service';
const props = defineProps<{
stateIds: string[];
Expand All @@ -141,6 +180,17 @@ const props = defineProps<{
const emit = defineEmits(['delete-self', 'update-self']);
// Not supporting GreaterThan
const constraintTypeOptions = [
ConstraintType.LessThan,
ConstraintType.LessThanOrEqualTo,
ConstraintType.GreaterThanOrEqualTo,
ConstraintType.Increasing,
ConstraintType.Decreasing,
ConstraintType.LinearlyConstrained,
ConstraintType.Following
];
const variableOptions = computed(() => {
switch (props.config.constraint) {
case Constraint.State:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,21 @@
<InputSwitch class="mr-3" v-model="knobs.compartmentalConstraint.isActive" />
</div>
</header>
<div class="section-row">
<katex-element :expression="stringToLatexExpression(`${stateIds.join(' + ')} = ${mass}`)" />
<span v-for="v of stateIds" :key="v"> {{ v }} &#8805; 0, </span>
<div class="flex align-items-center gap-6">
<katex-element
:expression="
stringToLatexExpression(
stateIds
.map((s, index) => `${s}${index === stateIds.length - 1 ? `\\geq 0` : ','}`)
.join('')
)
"
/>
<katex-element
:expression="
stringToLatexExpression(`${stateIds.join('+')} = ${displayNumber(mass)} \\ \\forall \\ t`)
"
/>
</div>
</section>
</li>
Expand Down Expand Up @@ -179,6 +191,7 @@ import TeraFunmanOutput from '@/components/workflow/ops/funman/tera-funman-outpu
import TeraConstraintGroupForm from '@/components/workflow/ops/funman/tera-constraint-group-form.vue';
import { DrilldownTabs } from '@/types/common';
import { stringToLatexExpression } from '@/services/model';
import { displayNumber } from '@/utils/number';
import { FunmanOperationState, Constraint, ConstraintType, CompartmentalConstraint } from './funman-operation';
const props = defineProps<{
Expand Down Expand Up @@ -259,48 +272,57 @@ const runMakeQuery = async () => {
}
const constraints = props.node.state.constraintGroups
?.map((ele) => {
if (!ele.isActive) return null;
?.map((constraintGroup) => {
if (!constraintGroup.isActive) return null;
const { name, constraintType, variables, timepoints } = constraintGroup;
// Use inputted weights when linearly constrained otherwise use implicit weights
const weights =
constraintType === ConstraintType.LinearlyConstrained
? constraintGroup.weights
: Array<number>(variables.length).fill(1.0);
// Increasing/descreasing (monotonicity)
if (ele.constraintType === ConstraintType.Increasing || ele.constraintType === ConstraintType.Decreasing) {
const weights = ele.weights ?? [1.0];
if (constraintType === ConstraintType.Increasing || constraintType === ConstraintType.Decreasing) {
return {
soft: true,
name: ele.name,
timepoints: null,
additive_bounds: {
lb: 0.0,
original_width: MAX
},
variables: ele.variables,
name,
timepoints,
additive_bounds: { lb: 0.0, original_width: MAX },
variables,
weights:
ele.constraintType === ConstraintType.Increasing
constraintGroup.constraintType === ConstraintType.Increasing
? weights.map((d) => Math.abs(d))
: weights.map((d) => -Math.abs(d)),
derivative: true
};
}
if (ele.timepoints) {
ele.timepoints.closed_upper_bound = true;
}
// Use bounds needed that are saved in the UI
const interval: FunmanInterval = {};
if (ele.constraintType === ConstraintType.LessThan) {
interval.ub = ele.interval.ub;
} else if (ele.constraintType === ConstraintType.GreaterThan) {
interval.lb = ele.interval.lb;
} else if (ele.constraintType === ConstraintType.LinearlyConstrained) {
interval.lb = ele.interval.lb;
interval.ub = ele.interval.ub;
if (constraintType === ConstraintType.LessThan || constraintType === ConstraintType.LessThanOrEqualTo) {
interval.ub = constraintGroup.interval.ub;
} else if (
constraintType === ConstraintType.GreaterThan ||
constraintType === ConstraintType.GreaterThanOrEqualTo
) {
interval.lb = constraintGroup.interval.lb;
} else if (constraintType === ConstraintType.LinearlyConstrained) {
interval.lb = constraintGroup.interval.lb;
interval.ub = constraintGroup.interval.ub;
}
if (constraintType === ConstraintType.LessThanOrEqualTo) {
interval.closed_upper_bound = true;
}
timepoints.closed_upper_bound = true;
return {
name: ele.name,
variables: ele.variables,
weights: ele.weights,
name,
variables,
weights,
additive_bounds: interval,
timepoints: ele.timepoints
timepoints
};
})
.filter(Boolean); // Removes falsey values
Expand Down Expand Up @@ -348,6 +370,7 @@ const addConstraintForm = () => {
interval: { lb: 0, ub: 100 },
constraint: Constraint.State,
variables: [],
weights: [],
constraintType: ConstraintType.LessThan
});
emit('update-state', state);
Expand All @@ -362,7 +385,7 @@ const deleteConstraintGroupForm = (index: number) => {
const updateConstraintGroupForm = (index: number, key: string, value: any) => {
const state = _.cloneDeep(props.node.state);
// Changing constraint type resets settings
// Changing constraint resets settings
if (key === 'constraint') {
state.constraintGroups[index].variables = [];
state.constraintGroups[index].weights = [];
Expand All @@ -373,7 +396,7 @@ const updateConstraintGroupForm = (index: number, key: string, value: any) => {
state.constraintGroups[index][key] = value;
// Make sure weights makes sense
const weightLength = state.constraintGroups[index].weights?.length ?? 0;
const weightLength = state.constraintGroups[index].weights.length;
const variableLength = state.constraintGroups[index].variables.length;
if (weightLength !== variableLength) {
state.constraintGroups[index].weights = Array<number>(variableLength).fill(1.0);
Expand Down Expand Up @@ -611,15 +634,6 @@ ul {
border: 1px solid var(--surface-border-light);
border-radius: var(--border-radius);
}
& .section-row {
display: flex;
padding: 0.5rem 0rem;
align-items: center;
gap: 0.5rem;
align-self: stretch;
flex-wrap: wrap;
}
}
.timespan-list {
Expand Down
30 changes: 30 additions & 0 deletions packages/client/hmi-client/src/services/models/funman-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import API from '@/api/api';
import type { FunmanPostQueriesRequest } from '@/types/Types';
import * as d3 from 'd3';
import { Dictionary, groupBy } from 'lodash';
import { ConstraintGroup, ConstraintType } from '@/components/workflow/ops/funman/funman-operation';

// Partially typing Funman response
interface FunmanBound {
Expand Down Expand Up @@ -43,6 +44,35 @@ export async function makeQueries(body: FunmanPostQueriesRequest) {
}
}

export function generateConstraintExpression(config: ConstraintGroup) {
const { constraintType, interval, variables, timepoints } = config;
let expression = '';
for (let i = 0; i < variables.length; i++) {
let expressionPart = `${variables[i]}(t)`;
if (constraintType === ConstraintType.Increasing || constraintType === ConstraintType.Decreasing) {
expressionPart = `d/dt ${expressionPart}`;
}
if (i === variables.length - 1) {
if (
constraintType === ConstraintType.LessThan ||
constraintType === ConstraintType.LessThanOrEqualTo ||
constraintType === ConstraintType.Decreasing
) {
expressionPart += constraintType === ConstraintType.LessThan ? `<` : `\\leq`;
expressionPart += constraintType === ConstraintType.Decreasing ? '0' : `${interval?.ub ?? 0}`;
} else {
expressionPart += constraintType === ConstraintType.GreaterThan ? `>` : `\\geq`;
expressionPart += constraintType === ConstraintType.Increasing ? '0' : `${interval?.lb ?? 0}`;
}
} else {
expressionPart += ',';
}
expression += expressionPart;
}
// Adding the "for all in timepoints" in the same expression helps with text alignment
return `${expression} \\ \\forall \\ t \\in [${timepoints.lb}, ${timepoints.ub}]`;
}

export const processFunman = (result: any) => {
// List of states and parameters
const states = result.model.petrinet.model.states.map((s) => s.id);
Expand Down
Loading

0 comments on commit ec2f327

Please sign in to comment.