diff --git a/bun.lock b/bun.lock index 64a07089..c7b58315 100644 --- a/bun.lock +++ b/bun.lock @@ -616,7 +616,7 @@ "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], - "electron-to-chromium": ["electron-to-chromium@1.5.338", "", {}, "sha512-KVQQ3xko9/coDX3qXLUEEbqkKT8L+1DyAovrtu0Khtrt9wjSZ+7CZV4GVzxFy9Oe1NbrIU1oVXCwHJruIA1PNg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.339", "", {}, "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg=="], "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], diff --git a/package.json b/package.json index bb1a67ce..baef3281 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "0.20260416.3", + "version": "0.20260417.0", "engines": { "bun": ">=1.2.0" }, diff --git a/src/renderer/bun.lock b/src/renderer/bun.lock index c8a4ac8f..67fbe4b6 100644 --- a/src/renderer/bun.lock +++ b/src/renderer/bun.lock @@ -383,7 +383,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.338", "", {}, "sha512-KVQQ3xko9/coDX3qXLUEEbqkKT8L+1DyAovrtu0Khtrt9wjSZ+7CZV4GVzxFy9Oe1NbrIU1oVXCwHJruIA1PNg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.339", "", {}, "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -693,9 +693,9 @@ "@primevue/core/@primeuix/styled": ["@primeuix/styled@0.3.2", "", { "dependencies": { "@primeuix/utils": "^0.3.2" } }, "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], diff --git a/src/renderer/package.json b/src/renderer/package.json index ed8318a1..935934cf 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,7 +42,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "0.20260416.3", + "version": "0.20260417.0", "scripts": { "build": "vite build && bun scripts/generate.version.js", "build:lib": "vite build --config vite.lib.config.ts && bunx --bun vue-tsc --project tsconfig.lib.types.json", diff --git a/src/renderer/src/components/views/SimulationExperimentView.vue b/src/renderer/src/components/views/SimulationExperimentView.vue index 0c6b3f6a..4f84aee6 100644 --- a/src/renderer/src/components/views/SimulationExperimentView.vue +++ b/src/renderer/src/components/views/SimulationExperimentView.vue @@ -259,6 +259,7 @@ import * as externalData from '../../common/externalData'; import * as locCommon from '../../common/locCommon'; import * as locApi from '../../libopencor/locApi'; import * as locSedApi from '../../libopencor/locSedApi'; +import * as locUiJsonApi from '../../libopencor/locUiJsonApi'; import * as math from '../../common/math'; import type { ISimulationExperimentViewSettings } from '../dialogs/SimulationExperimentViewSettingsDialog.vue'; @@ -923,35 +924,91 @@ const addExternalData = async ( voiValues: new Float64Array(parsedCsv.columns[0]) }); - // Check the existing plots in our UI JSON and add additional traces for any plots that use model parameters that we - // have just replaced with external data IDs in the plot expressions, making sure to avoid adding duplicate traces - // if the same external data ID is used in multiple plot expressions. + // Go through the plots' main trace and additional traces and add a corresponding trace where the original model + // parameters are replaced with their corresponding external data. If an original trace has no corresponding + // external data then no corresponding trace is added. - for (const plot of interactiveUiJson.value.output.plots) { - const xExternalValue = replaceWithExternalDataIds(plot.xValue, dataIdToExternalDataId); - const yExternalValue = replaceWithExternalDataIds(plot.yValue, dataIdToExternalDataId); + for (let plotIndex = 0; plotIndex < interactiveUiJson.value.output.plots.length; ++plotIndex) { + const plot = interactiveUiJson.value.output.plots[plotIndex]; - if (xExternalValue === plot.xValue && yExternalValue === plot.yValue) { + if (!plot) { continue; } + // Initialise the additional traces array if it doesn't already exist. + if (!plot.additionalTraces) { plot.additionalTraces = []; } - const alreadyExists = plot.additionalTraces.some((additionalTrace) => { - return additionalTrace.xValue === xExternalValue && additionalTrace.yValue === yExternalValue; - }); + // Use a set of trace keys to determine whether a candidate trace already exists or not. - if (alreadyExists) { - continue; + const traceKey = (x: string, y: string): string => { + return `${x}::${y}`; + }; + const originalSimulationKeys = new Set(); + const originalAdditionalTraceCount = + nbOfOriginalAdditionalTracesInPlots[plotIndex] ?? plot.additionalTraces.length; + + originalSimulationKeys.add(traceKey(plot.xValue, plot.yValue)); + + for (let additionalTraceIndex = 0; additionalTraceIndex < originalAdditionalTraceCount; ++additionalTraceIndex) { + const additionalTrace = plot.additionalTraces[additionalTraceIndex]; + + if (!additionalTrace) { + continue; + } + + originalSimulationKeys.add(traceKey(additionalTrace.xValue, additionalTrace.yValue)); } - plot.additionalTraces.push({ - xValue: xExternalValue, - yValue: yExternalValue, - name: `${yExternalValue} vs. ${xExternalValue}` - }); + // Retrieve the candidate traces for this plot. + + const candidateTraces: Array<{ xValue: string; yValue: string }> = [ + { + xValue: plot.xValue, + yValue: plot.yValue + } + ]; + + for (let additionalTraceIndex = 0; additionalTraceIndex < originalAdditionalTraceCount; ++additionalTraceIndex) { + const additionalTrace = plot.additionalTraces[additionalTraceIndex]; + + if (!additionalTrace) { + continue; + } + + candidateTraces.push({ + xValue: additionalTrace.xValue, + yValue: additionalTrace.yValue + }); + } + + // Go through the candidate traces and add corresponding external data based traces where possible. + + for (let candidateTraceIndex = 0; candidateTraceIndex < candidateTraces.length; ++candidateTraceIndex) { + const candidateTrace = candidateTraces[candidateTraceIndex]; + + if (!candidateTrace) { + continue; + } + + // Make sure that the candidate trace is not one of our original traces (i.e. no replacements were made). + + const externalX = replaceWithExternalDataIds(candidateTrace.xValue, dataIdToExternalDataId); + const externalY = replaceWithExternalDataIds(candidateTrace.yValue, dataIdToExternalDataId); + + if (originalSimulationKeys.has(traceKey(externalX, externalY))) { + continue; + } + + // Add an additional trace for externalValue. + + plot.additionalTraces.push({ + xValue: externalX, + yValue: externalY + }); + } } // Update our interactive simulation and reset the plot margins to accommodate any new traces that have been added. @@ -1051,14 +1108,23 @@ if (interactiveInstanceTask) { updateInteractiveUi(); -// Watch for changes to our UI JSON and reset the compiled expressions when it changes. +// Watch for changes to our UI JSON and reset the compiled expressions when it changes, as well as keeping track of the +// number of additional traces in each plot. + +let nbOfOriginalAdditionalTracesInPlots: number[] = []; vue.watch( () => interactiveUiJson.value, () => { interactiveMath.resetCompiledExpressions(); + + nbOfOriginalAdditionalTracesInPlots = interactiveUiJson.value.output.plots.map( + (plot: locUiJsonApi.IUiJsonOutputPlot) => { + return plot.additionalTraces?.length ?? 0; + } + ); }, - { deep: true } + { deep: true, immediate: true } ); // A helper function to interpolate external data values at the VOI values of our simulation.