Skip to content

Commit f968faa

Browse files
Merge pull request #946 from arayabrain/feature/porting_finish_workflow_without_run
Porting finish-workflow-without-run
2 parents 1f7b350 + 09477ba commit f968faa

9 files changed

Lines changed: 189 additions & 12 deletions

File tree

frontend/src/components/Workspace/FlowChart/FlowChartNode/AlgorithmNode.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { NodeData, NodeIdProps } from "store/slice/FlowElement/FlowElementType"
4343
import {
4444
selectPipelineLatestUid,
4545
selectPipelineNodeResultMessage,
46+
selectPipelineNodeResultOutputPathsExists,
4647
selectPipelineNodeResultStatus,
4748
selectPipelineStatus,
4849
} from "store/slice/Pipeline/PipelineSelectors"
@@ -108,6 +109,10 @@ const AlgorithmNodeImple = memo(function AlgorithmNodeImple({
108109
const isUpdated = useSelector(selectAlgorithmIsUpdated(nodeId))
109110
const isParentParamsUpdated = useSelector(isParentNodeUpdatedParams(nodeId))
110111

112+
const nodeResultOutputPathsExists = useSelector(
113+
selectPipelineNodeResultOutputPathsExists(nodeId),
114+
)
115+
111116
const updated =
112117
typeof workflowId !== "undefined" &&
113118
(isUpdated || ancestorIsUpdated || isParentParamsUpdated)
@@ -118,7 +123,7 @@ const AlgorithmNodeImple = memo(function AlgorithmNodeImple({
118123
"suite2p_roi",
119124
"caiman_cnmf",
120125
"lccd_cell_detection",
121-
"vacant_roi",
126+
"vacant_roi",
122127
"caiman_cnmfe",
123128
"cnmf_multisession",
124129
].includes(data.label),
@@ -158,7 +163,10 @@ const AlgorithmNodeImple = memo(function AlgorithmNodeImple({
158163
<Button
159164
size="small"
160165
onClick={onClickOutputButton}
161-
disabled={status !== NODE_RESULT_STATUS.SUCCESS}
166+
disabled={
167+
status !== NODE_RESULT_STATUS.SUCCESS ||
168+
!nodeResultOutputPathsExists
169+
}
162170
>
163171
Output
164172
</Button>
@@ -167,7 +175,9 @@ const AlgorithmNodeImple = memo(function AlgorithmNodeImple({
167175
size="small"
168176
onClick={onClickFilterButton}
169177
disabled={
170-
status !== NODE_RESULT_STATUS.SUCCESS || isParentParamsUpdated
178+
status !== NODE_RESULT_STATUS.SUCCESS ||
179+
!nodeResultOutputPathsExists ||
180+
isParentParamsUpdated
171181
}
172182
style={{ backgroundColor: isUpdateFilterParams ? "#ff98004d" : "" }}
173183
>

frontend/src/store/slice/DisplayData/DisplayDataType.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ export type DisplayData = {
6464
}
6565

6666
export const DATA_TYPE_SET = {
67-
TIME_SERIES: "timeSeries",
68-
HEAT_MAP: "heatMap",
67+
EMPTY: "empty",
6968
IMAGE: "image",
7069
CSV: "csv",
7170
ROI: "roi",
71+
TIME_SERIES: "timeSeries",
72+
HEAT_MAP: "heatMap",
7273
SCATTER: "scatter",
7374
BAR: "bar",
7475
HDF5: "hdf5",

frontend/src/store/slice/DisplayData/DisplayDataUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
*/
99
export function toDataType(value: string): DATA_TYPE {
1010
switch (value) {
11+
case "empty":
12+
return DATA_TYPE_SET.EMPTY
1113
case "images":
1214
return DATA_TYPE_SET.IMAGE
1315
case "timeseries":

frontend/src/store/slice/Pipeline/PipelineSelectors.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DATA_TYPE_SET } from "store/slice/DisplayData/DisplayDataType"
12
import {
23
NodeResult,
34
NodeResultPending,
@@ -165,13 +166,27 @@ const selectPipelineNodeResultOutputPaths =
165166
return nodeResult.outputPaths
166167
}
167168
}
168-
throw new Error(`key error. nodeId:${nodeId}`)
169+
return null
170+
}
171+
172+
export const selectPipelineNodeResultOutputPathsExists =
173+
(nodeId: string) => (state: RootState) => {
174+
const outputPaths = selectPipelineNodeResultOutputPaths(nodeId)(state)
175+
176+
if (outputPaths == null) {
177+
return false
178+
}
179+
180+
const firstOutputPath = Object.values(outputPaths).at(0)
181+
const outputPathsExists =
182+
firstOutputPath != null && firstOutputPath.type !== DATA_TYPE_SET.EMPTY
183+
return outputPathsExists
169184
}
170185

171186
export const selectPipelineNodeResultOutputFilePath =
172187
(nodeId: string, outputKey: string) => (state: RootState) => {
173188
const outputPaths = selectPipelineNodeResultOutputPaths(nodeId)(state)
174-
if (Object.keys(outputPaths).includes(outputKey)) {
189+
if (outputPaths && Object.keys(outputPaths).includes(outputKey)) {
175190
return outputPaths[outputKey].path
176191
} else {
177192
throw new Error(`key error. outputKey:${outputKey}`)
@@ -182,7 +197,7 @@ export const selectOutputFilePathCellRoi =
182197
(nodeId?: string | null) => (state: RootState) => {
183198
if (!nodeId) return ""
184199
const outputPaths = selectPipelineNodeResultOutputPaths(nodeId)(state)
185-
if (Object.keys(outputPaths).includes("cell_roi")) {
200+
if (outputPaths && Object.keys(outputPaths).includes("cell_roi")) {
186201
return outputPaths["cell_roi"].path
187202
}
188203
return ""
@@ -191,7 +206,7 @@ export const selectOutputFilePathCellRoi =
191206
export const selectPipelineNodeResultOutputFileDataType =
192207
(nodeId: string, outputKey: string) => (state: RootState) => {
193208
const outputPaths = selectPipelineNodeResultOutputPaths(nodeId)(state)
194-
if (Object.keys(outputPaths).includes(outputKey)) {
209+
if (outputPaths && Object.keys(outputPaths).includes(outputKey)) {
195210
return outputPaths[outputKey].type
196211
} else {
197212
throw new Error(`key error. outputKey:${outputKey}`)

frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import {
1313
setNewDisplayDataPath,
1414
} from "store/slice/VisualizeItem/VisualizeItemActions"
1515
import {
16-
HeatMapItem,
16+
EmptyItem,
1717
ImageItem,
1818
CsvItem,
1919
TimeSeriesItem,
2020
RoiItem,
21+
HeatMapItem,
2122
ScatterItem,
2223
VisualaizeItem,
2324
VISUALIZE_ITEM_TYPE_SET,
@@ -62,6 +63,10 @@ const displayDataCommonInitialValue = {
6263
saveFileName: "newPlot",
6364
saveFormat: "png",
6465
}
66+
const emptyItemInitialValue: EmptyItem = {
67+
...displayDataCommonInitialValue,
68+
dataType: DATA_TYPE_SET.EMPTY,
69+
}
6570
const imageItemInitialValue: ImageItem = {
6671
...displayDataCommonInitialValue,
6772
dataType: DATA_TYPE_SET.IMAGE,
@@ -178,6 +183,8 @@ const matlabItemInitialValue: MatlabItem = {
178183

179184
function getDisplayDataItemInitialValue(dataType: DATA_TYPE) {
180185
switch (dataType) {
186+
case DATA_TYPE_SET.EMPTY:
187+
return emptyItemInitialValue
181188
case DATA_TYPE_SET.IMAGE:
182189
return imageItemInitialValue
183190
case DATA_TYPE_SET.HEAT_MAP:

frontend/src/store/slice/VisualizeItem/VisualizeItemType.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type VISUALIZE_ITEM_TYPE =
3939
(typeof VISUALIZE_ITEM_TYPE_SET)[keyof typeof VISUALIZE_ITEM_TYPE_SET]
4040

4141
export type DisplayDataItem =
42+
| EmptyItem
4243
| ImageItem
4344
| TimeSeriesItem
4445
| HeatMapItem
@@ -65,6 +66,10 @@ export interface DisplayDataItemBaseType extends ItemBaseType<"displayData"> {
6566
saveFormat: string
6667
}
6768

69+
export interface EmptyItem extends DisplayDataItemBaseType {
70+
dataType: typeof DATA_TYPE_SET.EMPTY
71+
}
72+
6873
export interface ImageItem extends DisplayDataItemBaseType {
6974
dataType: typeof DATA_TYPE_SET.IMAGE
7075
activeIndex: number

studio/app/common/core/workflow/workflow.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def check_nodetype_from_filetype(file_type: str) -> str:
107107

108108
@dataclass
109109
class OutputType:
110+
EMPTY: str = "empty"
110111
IMAGE: str = "images"
111112
TIMESERIES: str = "timeseries"
112113
HEATMAP: str = "heatmap"

studio/app/common/core/workflow/workflow_runner.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import uuid
22
from dataclasses import asdict
3+
from datetime import datetime
34
from typing import Dict, List
45

6+
from fastapi import BackgroundTasks
7+
8+
from studio.app.common.core.experiment.experiment_reader import ExptConfigReader
9+
from studio.app.common.core.experiment.experiment_record_services import (
10+
ExperimentRecordService,
11+
)
512
from studio.app.common.core.experiment.experiment_writer import ExptConfigWriter
613
from studio.app.common.core.rules.runner import Runner
714
from studio.app.common.core.snakemake.smk import FlowConfig, Rule, SmkParam
@@ -12,9 +19,17 @@
1219
from studio.app.common.core.snakemake.snakemake_reader import SmkParamReader
1320
from studio.app.common.core.snakemake.snakemake_rule import SmkRule
1421
from studio.app.common.core.snakemake.snakemake_writer import SmkConfigWriter
15-
from studio.app.common.core.workflow.workflow import NodeType, NodeTypeUtil, RunItem
22+
from studio.app.common.core.workflow.workflow import (
23+
NodeType,
24+
NodeTypeUtil,
25+
OutputPath,
26+
OutputType,
27+
RunItem,
28+
WorkflowRunStatus,
29+
)
1630
from studio.app.common.core.workflow.workflow_params import get_typecheck_params
1731
from studio.app.common.core.workflow.workflow_writer import WorkflowConfigWriter
32+
from studio.app.const import DATE_FORMAT
1833

1934

2035
class WorkflowRunner:
@@ -47,7 +62,7 @@ def create_workflow_unique_id() -> str:
4762
new_unique_id = str(uuid.uuid4())[:8]
4863
return new_unique_id
4964

50-
def run_workflow(self, background_tasks):
65+
def run_workflow(self, background_tasks: BackgroundTasks):
5166
self.set_smk_config()
5267

5368
snakemake_params: SmkParam = get_typecheck_params(
@@ -68,6 +83,54 @@ def run_workflow(self, background_tasks):
6883
snakemake_execute, self.workspace_id, self.unique_id, snakemake_params
6984
)
7085

86+
def finish_workflow_without_run(
87+
self, status: WorkflowRunStatus = WorkflowRunStatus.SUCCESS
88+
):
89+
"""
90+
Saves the settings and finishes the workflow without actually running it.
91+
- Function solely for creating experiment record.
92+
"""
93+
94+
# Load current configs
95+
expt_config = ExptConfigReader.read(self.workspace_id, self.unique_id)
96+
97+
# Construct update data (ExptConfig.*)
98+
update_expt_config = ExptConfigReader.create_empty_experiment_config()
99+
now = datetime.now().strftime(DATE_FORMAT)
100+
update_expt_config.success = status.value
101+
update_expt_config.finished_at = now
102+
update_expt_config.data_usage = 0
103+
104+
# Construct update data (ExptConfig.function)
105+
update_expt_config.function = {}
106+
for node_id, function in expt_config.function.items():
107+
function.success = WorkflowRunStatus.SUCCESS.value
108+
function.outputPaths = {
109+
"empty": OutputPath(
110+
path="empty",
111+
type=OutputType.EMPTY,
112+
max_index=1,
113+
)
114+
}
115+
116+
update_expt_config.function[node_id] = function
117+
118+
# Prepare data (dict variable) for overwriting the config file
119+
update_expt_config_dict = {
120+
k: v for k, v in asdict(update_expt_config).items() if v is not None
121+
}
122+
123+
# Overwrite config file
124+
ExptConfigWriter(self.workspace_id, self.unique_id).overwrite(
125+
update_expt_config_dict
126+
)
127+
128+
# Update experiment database record
129+
if ExperimentRecordService.is_available():
130+
ExperimentRecordService.regist_record_on_workflow_completed(
131+
self.workspace_id, self.unique_id
132+
)
133+
71134
def set_smk_config(self):
72135
rules, last_output = self.rulefile()
73136

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import os
2+
import shutil
3+
4+
from studio.app.common.core.experiment.experiment_reader import ExptConfigReader
5+
from studio.app.common.core.workflow.workflow import (
6+
Edge,
7+
Node,
8+
NodeData,
9+
RunItem,
10+
WorkflowRunStatus,
11+
)
12+
from studio.app.common.core.workflow.workflow_runner import WorkflowRunner
13+
from studio.app.dir_path import DIRPATH
14+
15+
workspace_id = "default"
16+
unique_id = "workflow_test"
17+
18+
node_data = NodeData(label="a", param={}, path="", type="")
19+
20+
nodeDict = {
21+
"test1": Node(
22+
id="node_id",
23+
type="a",
24+
data=node_data,
25+
position={"x": 0, "y": 0},
26+
style={
27+
"border": None,
28+
"borderRadius": 0,
29+
"height": 100,
30+
"padding": 0,
31+
"width": 180,
32+
},
33+
)
34+
}
35+
36+
edgeDict = {
37+
"test2": Edge(
38+
id="edge_id",
39+
type="a",
40+
animated=False,
41+
source="",
42+
sourceHandle="",
43+
target="",
44+
targetHandle="",
45+
style={},
46+
)
47+
}
48+
49+
50+
dirpath = f"{DIRPATH.OUTPUT_DIR}/{workspace_id}/{unique_id}"
51+
52+
53+
def test_finish_workflow_without_run():
54+
if os.path.exists(dirpath):
55+
shutil.rmtree(dirpath)
56+
57+
runItem = RunItem(
58+
name="New Flow",
59+
nodeDict=nodeDict,
60+
edgeDict=edgeDict,
61+
snakemakeParam={},
62+
nwbParam={},
63+
forceRunList=[],
64+
)
65+
66+
WorkflowRunner(workspace_id, unique_id, runItem).finish_workflow_without_run()
67+
68+
assert os.path.exists(f"{dirpath}/experiment.yaml")
69+
assert os.path.exists(f"{dirpath}/workflow.yaml")
70+
71+
exp_config = ExptConfigReader.read(workspace_id, unique_id)
72+
73+
assert exp_config.success == WorkflowRunStatus.SUCCESS.value

0 commit comments

Comments
 (0)