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: make model parameters editable #1499

Merged
merged 15 commits into from
Jul 19, 2023
Merged
168 changes: 141 additions & 27 deletions packages/client/hmi-client/src/components/models/tera-model.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
:project="project"
:dialog-flavour="'model'"
/>
<Accordion multiple :active-index="[0, 1, 2, 3, 4, 5, 6]" @click="editSection">
<Accordion multiple :active-index="[0, 1, 2, 3, 4, 5, 6]">
<!-- Description -->
<AccordionTab>
<template #header>Description</template>
Expand All @@ -105,29 +105,43 @@
<template #header>
Parameters<span class="artifact-amount">({{ parameters?.length }})</span>
</template>
<main v-if="parameters.length > 0" class="datatable" style="--columns: 4">
<main v-if="parameters.length > 0" class="datatable" style="--columns: 5">
<header>
<div>ID</div>
<div>Value</div>
<div>Distribution</div>
<div>Extractions</div>
</header>
<section
v-for="parameter in parameters"
v-for="(parameter, i) in parameters"
:key="parameter.id"
:class="[
{ active: isSectionEditable === `parameter-${parameter.id}` },
`parameter-${parameter.id}`
]"
>
<template v-if="isSectionEditable === `parameter-${parameter.id}`">
<div><input type="text" :value="parameter?.name ?? '--'" /></div>
<div><input type="text" :value="parameter?.value ?? '--'" /></div>
<template v-if="isRowEditable === `parameter-${parameter.id}`">
<div>
<input
type="text"
:value="parameter?.id ?? '--'"
@input="updateTable('parameters', i, 'id', $event.target?.['value'])"
/>
</div>
<div>
<input
type="text"
:value="parameter?.value ?? '--'"
@input="updateTable('parameters', i, 'value', $event.target?.['value'])"
/>
</div>
<div>--</div>
<div>
<!-- TODO: needs to make those button active -->
<Button icon="pi pi-check" text rounded aria-label="Save" />
<Button icon="pi pi-times" text rounded aria-label="Discard" />
<template v-if="parameter?.distribution?.parameters">
[{{ round(parameter?.distribution?.parameters.minimum, 4) }},
{{ round(parameter?.distribution?.parameters.maximum, 4) }}]
</template>
<template v-else>--</template>
</div>
<div v-if="extractions?.[parameter?.id]" style="grid-column: 1 / span 4">
<tera-model-extraction :extractions="extractions[parameter.id]" />
Expand All @@ -150,6 +164,13 @@
<template v-else>--</template>
</div>
</template>
<div v-if="!isRowEditable">
<Button icon="pi pi-file-edit" text @click="editRow" />
</div>
<div v-else-if="isRowEditable === `parameter-${parameter.id}`">
<Button icon="pi pi-check" text rounded aria-label="Save" @click="confirmEdit" />
<Button icon="pi pi-times" text rounded aria-label="Discard" @click="cancelEdit" />
</div>
</section>
</main>
</AccordionTab>
Expand All @@ -159,7 +180,7 @@
<template #header>
State variables<span class="artifact-amount">({{ states.length }})</span>
</template>
<main v-if="states.length > 0" class="datatable" style="--columns: 5">
<main v-if="states.length > 0" class="datatable" style="--columns: 6">
<header>
<div>Id</div>
<div>Name</div>
Expand All @@ -168,19 +189,32 @@
<div>Extractions</div>
</header>
<section
v-for="state in states"
v-for="(state, i) in states"
:key="state.id"
:class="[{ active: isSectionEditable === `state-${state.id}` }, `state-${state.id}`]"
:class="[{ active: isRowEditable === `state-${state.id}` }, `state-${state.id}`]"
>
<template v-if="isSectionEditable === `state-${state.id}`">
<div><input type="text" :value="state.id ?? '--'" /></div>
<div><input type="text" :value="state?.name ?? '--'" /></div>
<template v-if="isRowEditable === `state-${state.id}`">
<div>
<input
type="text"
:value="state?.id ?? '--'"
@input="updateTable('states', i, 'id', $event.target?.['value'])"
/>
</div>
<div>
<input
type="text"
:value="state?.name ?? '--'"
@input="updateTable('states', i, 'name', $event.target?.['value'])"
/>
</div>
<div><input type="text" :value="state?.units?.expression ?? '--'" /></div>
<div>Identifiers</div>
<div>
<!-- TODO: needs to make those button active -->
<Button icon="pi pi-check" text rounded aria-label="Save" />
<Button icon="pi pi-times" text rounded aria-label="Discard" />
<template v-if="extractions?.[state?.id]">
<Tag :value="extractions?.[state?.id].length" />
</template>
<template v-else>--</template>
</div>
<div v-if="extractions?.[state?.id]" style="grid-column: 1 / span 4">
<tera-model-extraction :extractions="extractions[state.id]" />
Expand Down Expand Up @@ -217,6 +251,13 @@
<template v-else>--</template>
</div>
</template>
<div v-if="!isRowEditable">
<Button icon="pi pi-file-edit" text @click="editRow" />
</div>
<div v-else-if="isRowEditable === `state-${state.id}`">
<Button icon="pi pi-check" text rounded aria-label="Save" @click="confirmEdit" />
<Button icon="pi pi-times" text rounded aria-label="Discard" @click="cancelEdit" />
</div>
</section>
</main>
</AccordionTab>
Expand Down Expand Up @@ -524,7 +565,11 @@ import Textarea from 'primevue/textarea';
import TeraAsset from '@/components/asset/tera-asset.vue';
import TeraRelatedPublications from '@/components/widgets/tera-related-publications.vue';
import TeraModal from '@/components/widgets/tera-modal.vue';
import { convertToAMRModel } from '@/model-representation/petrinet/petrinet-service';
import {
convertToAMRModel,
updateConfigFields,
updateParameterId
} from '@/model-representation/petrinet/petrinet-service';
import { RouteName } from '@/router/routes';
import { getCuriesEntities } from '@/services/concept';
import { createModel, addModelToProject, getModel, updateModel } from '@/services/model';
Expand Down Expand Up @@ -702,6 +747,14 @@ const time = computed(() =>
);
const states = computed(() => model.value?.model?.states ?? []);

// Used to keep track of the values of the current row being edited
interface ModelTableTypes {
tableType: string;
idx: number;
updateProperty: { [key: string]: string };
}
const transientTableValue = ref<ModelTableTypes | null>(null);

// Model Transitions
const transitions = computed(() => {
const results: any[] = [];
Expand Down Expand Up @@ -742,6 +795,7 @@ const otherConcepts = computed(() => {
return [];
});
const isSectionEditable = ref<string | null>();
const isRowEditable = ref<string | null>();

const relatedTerariumModels = computed(
() => relatedTerariumArtifacts.value.filter((d) => isModel(d)) as Model[]
Expand Down Expand Up @@ -865,20 +919,80 @@ async function updateModelName() {
if (model.value) {
const modelClone = cloneDeep(model.value);
modelClone.name = newModelName.value;
updateModel(modelClone);
isRenamingModel.value = false;
await updateModel(modelClone);
model.value = await getModel(props.assetId);
// FIXME: Names aren't updated in sidebar
}
isRenamingModel.value = false;
}

// Toggle rows to become editable, display extractions to choose from.
function editSection(event: Event) {
// Toggle rows to become editable
function editRow(event: Event) {
if (!event?.target) return;
const section = (event.target as HTMLElement).closest('.datatable section');
if (!section) return;
isSectionEditable.value =
isSectionEditable.value === section.className ? null : section.className;
const row = (event.target as HTMLElement).closest('.datatable section');
if (!row) return;
isRowEditable.value = isRowEditable.value === row.className ? null : row.className;
}

async function confirmEdit() {
if (model.value && transientTableValue.value) {
const { tableType, idx, updateProperty } = transientTableValue.value;
const modelClone = cloneDeep(model.value);

switch (tableType) {
case 'parameters':
if (model.value.semantics?.ode.parameters) {
Object.entries(updateProperty).forEach(([key, value]) => {
modelClone.semantics!.ode.parameters![idx][key] = value;

if (key === 'id') {
const ode = model.value!.semantics!.ode;
// update the parameter id in the model (as well as rate expression and expression_mathml)
updateParameterId(modelClone, ode.parameters![idx][key], value as string);

// note that this is making a call to an async function to update the different model configs
// but we don't need to wait for it to finish because we don't need immediate access to the model configs
updateConfigFields(model.value!.id, ode.parameters![idx][key], value as string);
}
});
}
break;
case 'states':
Object.entries(updateProperty).forEach(([key, value]) => {
if (key !== 'unit') {
// TODO: remove this condition when we have proper editing of unit
modelClone.model.states[idx][key] = value;
}
// TODO: update all of the properties affected by state id
});
break;
default:
logger.info(`${tableType} not recognized`);
}
mwdchang marked this conversation as resolved.
Show resolved Hide resolved

await updateModel(modelClone);
model.value = await getModel(props.assetId);
}

isRowEditable.value = null;
transientTableValue.value = null;
}

function cancelEdit() {
isRowEditable.value = null;
transientTableValue.value = null;
}

function updateTable(tableType: string, idx: number, key: string, value: string) {
transientTableValue.value = {
...transientTableValue.value,
mwdchang marked this conversation as resolved.
Show resolved Hide resolved
tableType,
idx,
updateProperty: {
...transientTableValue.value?.updateProperty,
[key]: value
}
};
}
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import API from '@/api/api';
import { IGraph } from '@graph-scaffolder/types';
import { PetriNetModel, Model, PetriNetTransition, TypingSemantics } from '@/types/Types';
import { PetriNet } from '@/petrinet/petrinet-service';
import { getModelConfigurations } from '@/services/model';
import { updateModelConfiguration } from '@/services/model-configurations';

export interface NodeData {
type: string;
Expand Down Expand Up @@ -416,6 +418,114 @@ export const updateTransition = (
});
};

const replaceExactString = (str: string, wordToReplace: string, replacementWord: string): string =>
str.trim() === wordToReplace.trim() ? str.replace(wordToReplace, replacementWord) : str;

const replaceValuesInExpression = (
expression: string,
wordToReplace: string,
replaceWord: string
): string => {
let expressionBuilder = '';
let isOperator = false;
let content = '';

[...expression].forEach((c) => {
// not sure if this is an exhaustive list of operators or if it includes any operators it shouldn't
if ([',', '(', ')', '+', '-', '*', '/', '^'].includes(c)) {
isOperator = true;
} else {
isOperator = false;
}

if (isOperator) {
expressionBuilder += replaceExactString(content, wordToReplace, replaceWord);
content = '';
expressionBuilder += c;
}
if (!isOperator) {
content += c;
}
});

// if we reach the end of an expression and it doesn't end with an operator, we need to add the updated content
if (!isOperator) {
expressionBuilder += replaceExactString(content, wordToReplace, replaceWord);
}

return expressionBuilder;
};

// function to replace the content inside the tags of a mathml expression
const replaceValuesInMathML = (
mathmlExpression: string,
wordToReplace: string,
replaceWord: string
): string => {
let expressionBuilder = '';
let isTag = false;
let content = '';

[...mathmlExpression].forEach((c) => {
if (!isTag && c === '<') {
isTag = true;
}
if (isTag && c === '>') {
isTag = false;
}

if (isTag) {
expressionBuilder += replaceExactString(content, wordToReplace, replaceWord);
content = '';
expressionBuilder += c;
}
if (!isTag) {
// this only works if there is no '>' literal in the non-tag content
if (c !== '>') {
content += c;
} else {
expressionBuilder += c;
}
}
});

return expressionBuilder;
};

export const updateParameterId = (amr: Model, id: string, newId: string) => {
if (amr.semantics?.ode.parameters) {
amr.semantics.ode.parameters.forEach((param) => {
if (param.id === id) {
param.id = newId;
}
});

// update the expression and expression_mathml fields
amr.semantics.ode.rates.forEach((rate) => {
rate.expression = replaceValuesInExpression(rate.expression, id, newId);
if (rate.expression_mathml) {
rate.expression_mathml = replaceValuesInMathML(rate.expression_mathml, id, newId);
}
});

// if there's a timeseries field with the old parameter id then update it to the new id
if (amr.metadata?.timeseries) {
delete Object.assign(amr.metadata.timeseries, { [newId]: amr.metadata.timeseries[id] })[id];
}
mwdchang marked this conversation as resolved.
Show resolved Hide resolved
}
};

export const updateConfigFields = async (modelId: string, id: string, newId: string) => {
const modelConfigs = await getModelConfigurations(modelId);

modelConfigs.forEach((config) => {
updateParameterId(config.configuration, id, newId);
// note that this is making an async call but we don't need to wait for it to finish
// since we don't immediately need the updated configs
updateModelConfiguration(config);
});
};

// Replace typing semantics
export const addTyping = (amr: Model, typing: TypingSemantics) => {
if (amr.semantics) {
Expand Down
1 change: 1 addition & 0 deletions packages/client/hmi-client/src/types/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export interface ModelMetadata {
variable_statements?: VariableStatement[];
annotations?: Annotations;
attributes: any[];
timeseries?: { [index: string]: any };
}

export interface ModelGrounding {
Expand Down
Loading