Skip to content

Commit

Permalink
feat: persist model time intervals in the backend
Browse files Browse the repository at this point in the history
- New Django model, `TimeInterval`, representing time intervals for secondary parameters.
- Update the combined model serialiser so that  it can save `model.time_intervals`.
- Update the OpenAPI schema in the backend and frontend.
- Update the React app to use the new OpenAPI models.
  • Loading branch information
eatyourgreens committed Nov 25, 2024
1 parent 8fc81d6 commit 906474f
Show file tree
Hide file tree
Showing 19 changed files with 325 additions and 64 deletions.
10 changes: 2 additions & 8 deletions frontend-v2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,19 @@ import "react-toastify/dist/ReactToastify.css";
import { SimulationContext } from "./contexts/SimulationContext";
import { SimulateResponse } from "./app/backendApi";
import { CollapsibleSidebarProvider } from "./shared/contexts/CollapsibleSidebarContext";
import { useModelTimeIntervals } from "./hooks/useModelTimeIntervals";

export type TimeInterval = {
start: number;
end: number;
unit: { [key: string]: string };
};
type Threshold = { lower: number; upper: number };
export type Thresholds = { [key: string]: Threshold };

const TIME_INTERVALS: TimeInterval[] = [];

const THRESHOLDS: Thresholds = {};

function App() {
const dispatch = useAppDispatch();
const isAuth = useSelector(isAuthenticated);
const error = useSelector((state: RootState) => state.login.error);
const [intervals, setIntervals] = useModelTimeIntervals();
const [simulations, setSimulations] = useState<SimulateResponse[]>([]);
const [intervals, setIntervals] = useState<TimeInterval[]>(TIME_INTERVALS);
const [thresholds, setThresholds] = useState<Thresholds>(THRESHOLDS);
const simulationContext = {
simulations,
Expand Down
33 changes: 33 additions & 0 deletions frontend-v2/src/app/backendApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1959,10 +1959,40 @@ export type DerivedVariableRead = {
/** base variable in PK part of model */
pk_variable: number;
};
export type TimeInterval = {
/** true if object has been stored */
read_only?: boolean;
/** datetime the object was stored. */
datetime?: string | null;
/** start time of interval */
start_time: number;
/** end time of interval */
end_time: number;
/** PKPD model that this time interval is for */
pkpd_model: number;
/** unit of interval */
unit: number;
};
export type TimeIntervalRead = {
id: number;
/** true if object has been stored */
read_only?: boolean;
/** datetime the object was stored. */
datetime?: string | null;
/** start time of interval */
start_time: number;
/** end time of interval */
end_time: number;
/** PKPD model that this time interval is for */
pkpd_model: number;
/** unit of interval */
unit: number;
};
export type CombinedModelSpeciesEnum = "H" | "R" | "N" | "M";
export type CombinedModel = {
mappings: PkpdMapping[];
derived_variables: DerivedVariable[];
time_intervals: TimeInterval[];
/** true if object has been stored */
read_only?: boolean;
/** datetime the object was stored. */
Expand Down Expand Up @@ -2001,6 +2031,7 @@ export type CombinedModelRead = {
id: number;
mappings: PkpdMappingRead[];
derived_variables: DerivedVariableRead[];
time_intervals: TimeIntervalRead[];
components: string;
variables: number[];
mmt: string;
Expand Down Expand Up @@ -2043,6 +2074,7 @@ export type CombinedModelRead = {
export type PatchedCombinedModel = {
mappings?: PkpdMapping[];
derived_variables?: DerivedVariable[];
time_intervals?: TimeInterval[];
/** true if object has been stored */
read_only?: boolean;
/** datetime the object was stored. */
Expand Down Expand Up @@ -2081,6 +2113,7 @@ export type PatchedCombinedModelRead = {
id?: number;
mappings?: PkpdMappingRead[];
derived_variables?: DerivedVariableRead[];
time_intervals?: TimeIntervalRead[];
components?: string;
variables?: number[];
mmt?: string;
Expand Down
9 changes: 5 additions & 4 deletions frontend-v2/src/contexts/SimulationContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { createContext } from "react";
import { SimulateResponse } from "../app/backendApi";
import { TimeInterval, Thresholds } from "../App";
import { SimulateResponse, TimeIntervalRead } from "../app/backendApi";
import { Thresholds } from "../App";
import { TimeIntervalUpdate } from "../hooks/useModelTimeIntervals";

export const SimulationContext = createContext({
simulations: [] as SimulateResponse[],
setSimulations: (simulations: SimulateResponse[]) => {},
intervals: [] as TimeInterval[],
setIntervals: (intervals: TimeInterval[]) => {},
intervals: [] as TimeIntervalRead[],
setIntervals: (intervals: TimeIntervalRead[] | TimeIntervalUpdate[]) => {},
thresholds: {} as Thresholds,
setThresholds: (thresholds: Thresholds) => {},
});
1 change: 1 addition & 0 deletions frontend-v2/src/features/model/Model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const Model: FC = () => {
project: projectIdOrZero,
mappings: [],
derived_variables: [],
time_intervals: [],
components: "",
variables: [],
mmt: "",
Expand Down
51 changes: 30 additions & 21 deletions frontend-v2/src/features/model/secondary/TimeIntervalsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ import Delete from "@mui/icons-material/Delete";
import { useSelector } from "react-redux";

import {
TimeIntervalRead,
useProjectRetrieveQuery,
useUnitListQuery,
} from "../../../app/backendApi";
import { RootState } from "../../../app/store";
import { SimulationContext } from "../../../contexts/SimulationContext";
import { TimeInterval } from "../../../App";

type TimeUnitSelectProps = {
interval: TimeInterval;
onChange: (interval: TimeInterval) => void;
interval: TimeIntervalRead;
onChange: (interval: TimeIntervalRead) => void;
};

function useTimeUnits() {
Expand All @@ -49,21 +49,20 @@ function useTimeUnits() {
function TimeUnitSelect({ interval, onChange }: TimeUnitSelectProps) {
const timeUnits = useTimeUnits();
const timeUnitOptions =
timeUnits?.map((unit) => ({ value: unit.symbol, label: unit.symbol })) ||
[];
const defaultTimeUnit = "h";
timeUnits?.map((unit) => ({ value: unit.id, label: unit.symbol })) || [];
const defaultTimeUnit = "9";

function onChangeUnit(event: SelectChangeEvent) {
const unit = timeUnits?.find((unit) => unit.symbol === event.target.value);
if (unit) {
onChange({ ...interval, unit });
onChange({ ...interval, unit: +unit.id });
}
}

return (
<FormControl>
<Select
value={interval.unit.symbol || defaultTimeUnit}
value={interval.unit.toString() || defaultTimeUnit}
onChange={onChangeUnit}
>
{timeUnitOptions.map((option) => (
Expand All @@ -77,30 +76,34 @@ function TimeUnitSelect({ interval, onChange }: TimeUnitSelectProps) {
}

type IntervalRowProps = {
interval: TimeInterval;
interval: TimeIntervalRead;
onDelete: () => void;
onUpdate: (interval: TimeInterval) => void;
onUpdate: (interval: TimeIntervalRead) => void;
};

function IntervalRow({ interval, onDelete, onUpdate }: IntervalRowProps) {
function onChangeStart(event: React.ChangeEvent<HTMLInputElement>) {
onUpdate({ ...interval, start: parseFloat(event.target.value) });
onUpdate({ ...interval, start_time: parseFloat(event.target.value) });
}
function onChangeEnd(event: React.ChangeEvent<HTMLInputElement>) {
onUpdate({ ...interval, end: parseFloat(event.target.value) });
onUpdate({ ...interval, end_time: parseFloat(event.target.value) });
}

return (
<TableRow>
<TableCell>
<TextField
type="number"
value={interval.start}
value={interval.start_time}
onChange={onChangeStart}
/>
</TableCell>
<TableCell>
<TextField type="number" value={interval.end} onChange={onChangeEnd} />
<TextField
type="number"
value={interval.end_time}
onChange={onChangeEnd}
/>
</TableCell>
<TableCell>
<TimeUnitSelect interval={interval} onChange={onUpdate} />
Expand All @@ -122,28 +125,34 @@ const TimeIntervalsTable: FC<TableProps> = (props) => {
function addInterval() {
const lastInterval = intervals[intervals.length - 1];
if (hourUnit) {
let newInterval: TimeInterval = { start: 0, end: 0, unit: hourUnit };
let newInterval = {
start_time: 0,
end_time: 0,
unit: +hourUnit.id,
};
if (lastInterval) {
const duration = lastInterval.end - lastInterval.start;
const duration = lastInterval.end_time - lastInterval.start_time;
newInterval = {
start: lastInterval.end,
end: lastInterval.end + duration,
start_time: lastInterval.end_time,
end_time: lastInterval.end_time + duration,
unit: lastInterval.unit,
};
}
setIntervals([...intervals, newInterval]);
}
}
function removeInterval(index: number) {
setIntervals(intervals.filter((_, i) => i !== index));
const newIntervals = intervals.filter((_, i) => i !== index);
console.log(newIntervals);
setIntervals(newIntervals);
}
function updateInterval(index: number, interval: TimeInterval) {
function updateInterval(index: number, interval: TimeIntervalRead) {
setIntervals(
intervals.map((i, iIndex) => (iIndex === index ? interval : i)),
);
}
const onDelete = (index: number) => () => removeInterval(index);
const onUpdate = (index: number) => (interval: TimeInterval) =>
const onUpdate = (index: number) => (interval: TimeIntervalRead) =>
updateInterval(index, interval);

return (
Expand Down
1 change: 1 addition & 0 deletions frontend-v2/src/features/projects/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const ProjectTable: FC = () => {
project: newProject.data.id,
mappings: [],
derived_variables: [],
time_intervals: [],
},
}).then((combinedModel) => {
if (combinedModel?.data) {
Expand Down
12 changes: 7 additions & 5 deletions frontend-v2/src/features/results/ResultsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import {
Typography,
} from "@mui/material";

import { TimeInterval } from "../../App";
import { VariableRead } from "../../app/backendApi";
import { TimeIntervalRead, VariableRead } from "../../app/backendApi";
import useSubjectGroups from "../../hooks/useSubjectGroups";
import { SimulationContext } from "../../contexts/SimulationContext";

Expand All @@ -28,13 +27,13 @@ export type FilterIndex = number | "rows" | "columns";
export type RowData =
| { name: string }[]
| Parameter[]
| TimeInterval[]
| TimeIntervalRead[]
| VariableRead[];

type Item =
| { name: string }
| Parameter
| (TimeInterval & { name: string })
| (TimeIntervalRead & { name: string })
| VariableRead;
type RowFilter = {
filter: (event: SelectChangeEvent) => void;
Expand Down Expand Up @@ -126,7 +125,10 @@ const ResultsTab: FC = () => {
const intervalSelect = {
filter: handleIntervalChange,
value: interval,
items: intervals.map((i) => ({ name: `${i.start} - ${i.end}`, ...i })),
items: intervals.map((i) => ({
name: `${i.start_time} - ${i.end_time}`,
...i,
})),
label: "Interval",
};
const variableSelect = {
Expand Down
4 changes: 3 additions & 1 deletion frontend-v2/src/features/results/ResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ export const ResultsTable: FC<ResultsTableProps> = ({
: variableIndex === "columns"
? concentrationVariables.map((variable) => variable.name)
: intervalIndex === "columns"
? intervals.map((interval) => `${interval.start}${interval.end}`)
? intervals.map(
(interval) => `${interval.start_time}${interval.end_time}`,
)
: groupIndex === "columns"
? groups
? [{ name: "Project" }, ...groups].map((group) => group.name)
Expand Down
13 changes: 8 additions & 5 deletions frontend-v2/src/features/results/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { SimulateResponse, VariableRead } from "../../app/backendApi";
import { TimeInterval } from "../../App";
import {
SimulateResponse,
TimeIntervalRead,
VariableRead,
} from "../../app/backendApi";

import { Parameter } from "./useParameters";

Expand All @@ -11,8 +14,8 @@ interface ParametersProps {
simulations: SimulateResponse[];
parameter?: Parameter;
parameters: Parameter[];
interval?: TimeInterval;
intervals: TimeInterval[];
interval?: TimeIntervalRead;
intervals: TimeIntervalRead[];
}

export function columns({
Expand All @@ -37,7 +40,7 @@ export function columns({
if (parameter && !interval) {
return intervals.map((interval) => {
return {
header: `${interval.start}${interval.end}`,
header: `${interval.start_time}${interval.end_time}`,
value: parameter.value,
};
});
Expand Down
10 changes: 7 additions & 3 deletions frontend-v2/src/features/results/useParameters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useContext } from "react";

import { SimulateResponse, VariableRead } from "../../app/backendApi";
import {
SimulateResponse,
TimeIntervalRead,
VariableRead,
} from "../../app/backendApi";
import {
formattedNumber,
timeOverThreshold,
Expand All @@ -9,7 +13,7 @@ import {
} from "./utils";
import { SimulationContext } from "../../contexts/SimulationContext";
import { useVariables } from "./useVariables";
import { Thresholds, TimeInterval } from "../../App";
import { Thresholds } from "../../App";

export type Parameter = {
name: string | JSX.Element;
Expand All @@ -22,7 +26,7 @@ export type Parameter = {
};

const variablePerInterval = (
intervals: TimeInterval[],
intervals: TimeIntervalRead[],
variable: VariableRead,
simulation: SimulateResponse,
intervalIndex: number,
Expand Down
4 changes: 2 additions & 2 deletions frontend-v2/src/features/results/useTableRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export function useTableRows({

return rows.map((row, index) => {
const header =
"start" in row && "end" in row && "unit" in row
? `${row.start}${row.end} [${row.unit.symbol}]`
"start_time" in row && "end_time" in row && "unit" in row
? `${row.start_time}${row.end_time} [${row.unit}]`
: "name" in row
? row.name
: "";
Expand Down
Loading

0 comments on commit 906474f

Please sign in to comment.