diff --git a/frontend-v2/src/app/backendApi.ts b/frontend-v2/src/app/backendApi.ts index 16a4ec19..efa95f7f 100644 --- a/frontend-v2/src/app/backendApi.ts +++ b/frontend-v2/src/app/backendApi.ts @@ -3533,6 +3533,8 @@ export type Variable = { display?: boolean; /** False/True if biomarker type displayed on LHS/RHS axis */ axis?: boolean; + /** unit for the threshold values */ + threshold_unit?: number | null; /** variable values are in this unit (note this might be different from the unit in the stored sbml) */ unit?: number | null; /** pharmacodynamic model */ @@ -3583,6 +3585,8 @@ export type VariableRead = { display?: boolean; /** False/True if biomarker type displayed on LHS/RHS axis */ axis?: boolean; + /** unit for the threshold values */ + threshold_unit?: number | null; /** variable values are in this unit (note this might be different from the unit in the stored sbml) */ unit?: number | null; /** pharmacodynamic model */ @@ -3632,6 +3636,8 @@ export type PatchedVariable = { display?: boolean; /** False/True if biomarker type displayed on LHS/RHS axis */ axis?: boolean; + /** unit for the threshold values */ + threshold_unit?: number | null; /** variable values are in this unit (note this might be different from the unit in the stored sbml) */ unit?: number | null; /** pharmacodynamic model */ @@ -3682,6 +3688,8 @@ export type PatchedVariableRead = { display?: boolean; /** False/True if biomarker type displayed on LHS/RHS axis */ axis?: boolean; + /** unit for the threshold values */ + threshold_unit?: number | null; /** variable values are in this unit (note this might be different from the unit in the stored sbml) */ unit?: number | null; /** pharmacodynamic model */ diff --git a/frontend-v2/src/features/model/secondary/ThresholdsTable.tsx b/frontend-v2/src/features/model/secondary/ThresholdsTable.tsx index 344a7a1c..13d60d16 100644 --- a/frontend-v2/src/features/model/secondary/ThresholdsTable.tsx +++ b/frontend-v2/src/features/model/secondary/ThresholdsTable.tsx @@ -69,6 +69,7 @@ function VariableRow({ variable: VariableRead; unit: UnitRead | undefined; }) { + const units = useUnits(); const [updateVariable] = useVariableUpdateMutation(); const [unitSymbol, setUnitSymbol] = useState( unit?.symbol, @@ -99,6 +100,16 @@ function VariableRow({ } function onChangeUnit(event: SelectChangeEvent) { setUnitSymbol(event.target.value as string); + const unit = units?.find((unit) => unit.symbol === event.target.value); + if (unit) { + updateVariable({ + id: variable.id, + variable: { + ...variable, + threshold_unit: unit.id, + }, + }); + } } return ( @@ -158,7 +169,9 @@ const ThresholdsTable: FC = (props) => { unit.id === variable.unit)} + unit={units?.find( + (unit) => unit.id === (variable.threshold_unit || variable.unit), + )} /> ))} diff --git a/frontend-v2/src/features/results/useParameters.tsx b/frontend-v2/src/features/results/useParameters.tsx index c9f62d2e..5b2ca9fb 100644 --- a/frontend-v2/src/features/results/useParameters.tsx +++ b/frontend-v2/src/features/results/useParameters.tsx @@ -1,6 +1,7 @@ import { SimulateResponse, TimeIntervalRead, + VariableListApiResponse, VariableRead, } from "../../app/backendApi"; import { @@ -41,6 +42,30 @@ function useNormalisedIntervals(intervals: TimeIntervalRead[]) { }); } +function useNormalisedVariables(variables: VariableListApiResponse) { + const units = useUnits(); + return variables.map((variable) => { + const variableUnit = units?.find( + (unit) => unit.id === variable.threshold_unit, + ); + const defaultUnit = variableUnit?.compatible_units.find( + (u) => u.symbol === "pmol/L", + ); + const conversionFactor = parseFloat(defaultUnit?.conversion_factor || "1"); + const lower_threshold = variable.lower_threshold + ? variable.lower_threshold * conversionFactor + : null; + const upper_threshold = variable.upper_threshold + ? variable.upper_threshold * conversionFactor + : null; + return { + ...variable, + lower_threshold, + upper_threshold, + }; + }); +} + const variablePerInterval = ( intervals: TimeIntervalRead[], variable: VariableRead, @@ -78,8 +103,9 @@ const timeOverUpperThresholdPerInterval = ( export function useParameters() { const [baseIntervals] = useModelTimeIntervals(); - const variables = useVariables(); + const baseVariables = useVariables(); const intervals = useNormalisedIntervals(baseIntervals); + const variables = useNormalisedVariables(baseVariables || []); return [ { name: "Min", @@ -147,23 +173,26 @@ export function useParameters() { value( intervalIndex: number, simulation: SimulateResponse, - variable: VariableRead, + baseVariable: VariableRead, ) { - const [intervalValues, intervalTimes] = variablePerInterval( - intervals, - variable, - simulation, - intervalIndex, - ); - return intervalValues - ? formattedNumber( - timeOverLowerThresholdPerInterval( - intervalValues, - intervalTimes, - variable, - ), - ) - : 0; + const variable = variables.find((v) => v.id === baseVariable.id); + if (variable) { + const [intervalValues, intervalTimes] = variablePerInterval( + intervals, + variable, + simulation, + intervalIndex, + ); + return intervalValues + ? formattedNumber( + timeOverLowerThresholdPerInterval( + intervalValues, + intervalTimes, + variable, + ), + ) + : 0; + } }, }, { @@ -175,23 +204,26 @@ export function useParameters() { value( intervalIndex: number, simulation: SimulateResponse, - variable: VariableRead, + baseVariable: VariableRead, ) { - const [intervalValues, intervalTimes] = variablePerInterval( - intervals, - variable, - simulation, - intervalIndex, - ); - return intervalValues - ? formattedNumber( - timeOverUpperThresholdPerInterval( - intervalValues, - intervalTimes, - variable, - ), - ) - : 0; + const variable = variables.find((v) => v.id === baseVariable.id); + if (variable) { + const [intervalValues, intervalTimes] = variablePerInterval( + intervals, + variable, + simulation, + intervalIndex, + ); + return intervalValues + ? formattedNumber( + timeOverUpperThresholdPerInterval( + intervalValues, + intervalTimes, + variable, + ), + ) + : 0; + } }, }, { @@ -203,28 +235,31 @@ export function useParameters() { value( intervalIndex: number, simulation: SimulateResponse, - variable: VariableRead, + baseVariable: VariableRead, ) { - const [intervalValues, intervalTimes] = variablePerInterval( - intervals, - variable, - simulation, - intervalIndex, - ); - return intervalValues - ? formattedNumber( - timeOverLowerThresholdPerInterval( - intervalValues, - intervalTimes, - variable, - ) - - timeOverUpperThresholdPerInterval( + const variable = variables.find((v) => v.id === baseVariable.id); + if (variable) { + const [intervalValues, intervalTimes] = variablePerInterval( + intervals, + variable, + simulation, + intervalIndex, + ); + return intervalValues + ? formattedNumber( + timeOverLowerThresholdPerInterval( intervalValues, intervalTimes, variable, - ), - ) - : 0; + ) - + timeOverUpperThresholdPerInterval( + intervalValues, + intervalTimes, + variable, + ), + ) + : 0; + } }, }, ] as Parameter[]; diff --git a/pkpdapp/pkpdapp/migrations/0018_variable_threshold_unit.py b/pkpdapp/pkpdapp/migrations/0018_variable_threshold_unit.py new file mode 100644 index 00000000..c7f1d631 --- /dev/null +++ b/pkpdapp/pkpdapp/migrations/0018_variable_threshold_unit.py @@ -0,0 +1,31 @@ +# +# This file is part of PKPDApp (https://github.com/pkpdapp-team/pkpdapp) which +# is released under the BSD 3-clause license. See accompanying LICENSE.md for +# copyright notice and full license details. +# +# Generated by Django 3.2.25 on 2024-11-25 16:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pkpdapp', '0017_auto_20241125_1415'), + ] + + operations = [ + migrations.AddField( + model_name='variable', + name='threshold_unit', + field=models.ForeignKey( + blank=True, + help_text='unit for the threshold values', + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='thresholds', + to='pkpdapp.unit' + ), + ), + ] diff --git a/pkpdapp/pkpdapp/models/variable.py b/pkpdapp/pkpdapp/models/variable.py index cff0eb1c..1ecfe36d 100644 --- a/pkpdapp/pkpdapp/models/variable.py +++ b/pkpdapp/pkpdapp/models/variable.py @@ -40,6 +40,14 @@ class Variable(StoredModel): upper_threshold = models.FloatField( blank=True, null=True, help_text="upper threshold for this variable" ) + threshold_unit = models.ForeignKey( + Unit, + on_delete=models.PROTECT, + blank=True, + null=True, + related_name="thresholds", + help_text="unit for the threshold values", + ) is_log = models.BooleanField( default=False, diff --git a/pkpdapp/schema.yml b/pkpdapp/schema.yml index 341e9fd6..0924501a 100644 --- a/pkpdapp/schema.yml +++ b/pkpdapp/schema.yml @@ -4902,6 +4902,10 @@ components: axis: type: boolean description: False/True if biomarker type displayed on LHS/RHS axis + threshold_unit: + type: integer + nullable: true + description: unit for the threshold values unit: type: integer nullable: true @@ -5721,6 +5725,10 @@ components: axis: type: boolean description: False/True if biomarker type displayed on LHS/RHS axis + threshold_unit: + type: integer + nullable: true + description: unit for the threshold values unit: type: integer nullable: true