From 3afd8efac813ddea7d40c8c8e302e10ef0c6f4ff Mon Sep 17 00:00:00 2001 From: emilinmathew Date: Thu, 22 Aug 2024 14:40:30 -0700 Subject: [PATCH] Made pr changes --- .github/workflows/dirgraph.yml | 2 +- .gitignore | 2 +- .../dirgraph_connections.py | 12 - .../dirgraph_visualization/dirgraph_main.py | 48 ++-- .../dirgraph_visualization/dirgraph_utils.py | 239 ++++++++++++------ docs/pages/developer_guide.md | 2 + docs/pages/main.md | 2 +- docs/pages/visualization.md | 71 +++++- tests/test_dirgraph.py | 7 +- 9 files changed, 257 insertions(+), 128 deletions(-) diff --git a/.github/workflows/dirgraph.yml b/.github/workflows/dirgraph.yml index 2f3e6f864..7929b7bfc 100644 --- a/.github/workflows/dirgraph.yml +++ b/.github/workflows/dirgraph.yml @@ -1,4 +1,4 @@ -name: Test Dirgraph +name: Test Visualization Application on: [push, pull_request] diff --git a/.gitignore b/.gitignore index e8b4f64a6..2fa074b08 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,5 @@ build .settings *.swp -#Node Modules +# Node modules (for directed graph visualization) node_modules/ diff --git a/applications/dirgraph_visualization/dirgraph_connections.py b/applications/dirgraph_visualization/dirgraph_connections.py index 9913e89f9..a102a7f27 100644 --- a/applications/dirgraph_visualization/dirgraph_connections.py +++ b/applications/dirgraph_visualization/dirgraph_connections.py @@ -60,9 +60,6 @@ def connect_blocks_by_inblock_list( # Check if connection definition is consistent for bA in block_list: for bBnm in bA.connecting_block_list: - # print("Current block name being searched:", bBnm) - # print(bA.connecting_block_list) - bB = block_list[bnames.index(bBnm)] check_block_pair_flow_consistency(bA, bB) @@ -77,14 +74,11 @@ def connect_blocks_by_inblock_list( if bA.flow_directions[i] == +1 and (id_bA, id_bB) not in connectivity: name_wire = bA.name + '_' + bB.name connecting_elements = (block_list[id_bA], block_list[id_bB]) - # wire_dict[name_wire] = wire(connecting_elements,name=name_wire) connectivity.append((id_bA, id_bB)) # connectivity stores pair-wise tuples of indices of the blocks that are connected; basically, if block 1 is connected to block 2 and the flow goes from block 1 to block 2, then connectivity will store a 2-element tuple, where the first element is the index at which block 1 is stored in block_list and the 2nd element is the index at which block 2 is stored in block_list. if the flow goes from block 2 to block 1, then connectivity will store a 2-element tuple, where the first element is the index at which block 2 is stored in block_list and the 2nd element is the index at which block 1 is stored in block_list. elif bA.flow_directions[i] == -1: name_wire = bB.name + '_' + bA.name connecting_elements = (block_list[id_bB], block_list[id_bA]) - # block_list[id_bA].add_connecting_wire(name_wire) - # block_list[id_bB].add_connecting_wire(name_wire) else: continue # if this line is executed, then the next two lines (wire_dict[name_wire] = ... and block_list[id_bA] = ...) will not be executed wire_dict[name_wire] = wire(connecting_elements, name=name_wire) @@ -113,12 +107,6 @@ def connect_blocks_by_connectivity_list(block_list, connectivity): if e1name not in block_list[e2].connecting_block_list: block_list[e2].add_connecting_wire(name_wire) block_list[e2].add_connecting_block(e1name, -1) - - # print name_wire - # print block_list[e1].name, block_list[e1].flow_directions - # print block_list[e2].name, block_list[e2].flow_directions - - # print wire_dict return wire_dict diff --git a/applications/dirgraph_visualization/dirgraph_main.py b/applications/dirgraph_visualization/dirgraph_main.py index 435b22545..f605546f6 100644 --- a/applications/dirgraph_visualization/dirgraph_main.py +++ b/applications/dirgraph_visualization/dirgraph_main.py @@ -1,3 +1,4 @@ +import sys import pysvzerod import pandas as pd import matplotlib.pyplot as plt @@ -15,27 +16,29 @@ This file enables the visualization of 0D simulation results in a web app that displays the 0D network as a directed graph. Users can interactively select nodes to view their parameters and simulation results. -Simply provide the filepath for your simulation input JSON file and specify the directory where -you want to save the output directed graph. +Enter the filepath for your simulation input JSON file and the directory where +you want to save the output directed graph as command line arguments to run the script. +If you want to save the raw svZeroDSolver simulation results, add "export-csv" as the third command line argument. -Created by Emilin Mathew (emilinm@stanford.edu) ''' - -def dirgraph(filepath, output_dir): +def dirgraph(filepath, output_dir, export_csv): solver = pysvzerod.Solver(filepath) solver.run() results = pd.DataFrame(solver.get_full_result()) - # If you want to export the simulation results as a csv - # results.to_csv('insert_file_name', sep=',', index=False, encoding='utf-8') + + if export_csv: + results.to_csv('results.csv', sep=',', index=False, encoding='utf-8') + print(f"Results exported to results.csv") + with open(filepath, 'r') as infile: parameters = json.load(infile) set_up_0d_network( filepath, - name_type='id', # Will take either 'id' or 'name', - draw_directed_graph=False, # Enter false if you don't want to save the directed graph - output_dir=output_dir + name_type='id', # Options 'name' or 'id' specifies whether vessel names or ids should be used for each node + draw_directed_graph= False, # Enter True if you want to save the directed graph + output_dir= output_dir ) base_name = filepath.rsplit('/', 1)[-1] output_file = os.path.join(output_dir, os.path.splitext(base_name)[0] + "_directed_graph.dot") @@ -43,13 +46,28 @@ def dirgraph(filepath, output_dir): return results, parameters, G -# Input filepath for 0d simulation & specify the output directory. -results, parameters, G = dirgraph( - filepath='' - , output_dir='') +def main(): + # Check if the correct number of arguments is provided + if len(sys.argv) < 3: + print("Please pass in at least two arguments: 1) svZeroDSolver input (json file) 2) Output directory to store the results.") + sys.exit(1) + + # Retrieve arguments + filepath = sys.argv[1] + output_dir = sys.argv[2] + export_csv = False + + if len(sys.argv) > 3 and sys.argv[3] == 'export_csv': + export_csv = True + + # Call the dirgraph function with the provided arguments + return dirgraph(filepath, output_dir, export_csv) + +if __name__ == '__main__': + results, parameters, G = main() -# Mapping vessel names to IDs +# Mapping vessel names to IDs. The block parameters are obtained from the user's input json file. vessel_name_to_vessel_id_map = {} vessel_id_to_vessel_name_map = {} vessel_params = {} diff --git a/applications/dirgraph_visualization/dirgraph_utils.py b/applications/dirgraph_visualization/dirgraph_utils.py index 4cb882208..19bf1baff 100644 --- a/applications/dirgraph_visualization/dirgraph_utils.py +++ b/applications/dirgraph_visualization/dirgraph_utils.py @@ -24,12 +24,12 @@ import matplotlib.pyplot as plt # only needed if you want to visualize the 0d model as a directed graph except ImportError: print( - "\nmatplotlib.pyplot not found. matplotlib.pyplot is needed only if you want to visualize your 0d model as a directed graph.") + "\nmatplotlib.pyplot not found. matplotlib.pyplot is needed for visualizing the 0D model.") try: import networkx as nx # only needed if you want to visualize the 0d model as a directed graph except ImportError: - print("\nnetworkx not found. networkx is needed only if you want to visualize your 0d model as a directed graph.") + print("\nnetworkx not found. networkx is needed for visualizing the 0D model.") try: from profilehooks import profile # only needed if you want to profile this script @@ -46,13 +46,27 @@ from collections import defaultdict # Loads a json file and extracts necessary information to draw the directed graph -def load_json_input_file(fpath, name_type, inlet_block_type): +def load_json_input_file(fpath, name_type): + """ + Purpose: + Parses through the input JSON file to create pandas DataFrames for each 0D element type + and prepares data for further processing. + + Inputs: + fpath (str): Path to the JSON file. + name_type (str): How to name nodes, either "id" or "name". + + Returns: + d: A dictionary containing pandas DataFrames for each 0D element type and updated entries of blocks & block_names + """ + with open(fpath, 'rb') as fp: d = json.load(fp) blocks = {} # {block_name : block_object} d.update({"blocks": blocks}) dirgraph_steady_bc.create_block_to_boundary_condition_map(d) d.update({"block_names": list(d["blocks"].keys())}) + vessel_id_map = get_vessel_name_to_vessel_id_map(d) if 'vessels' not in d or len(d['vessels']) == 0: df_vessels = pd.DataFrame() @@ -71,22 +85,44 @@ def load_json_input_file(fpath, name_type, inlet_block_type): df_junctions_expanded = pd.DataFrame() else: - if inlet_block_type: - inlet = 'inlet_blocks' - outlet = 'outlet_blocks' - else: - inlet = 'inlet_vessels' - outlet = 'outlet_vessels' - - # Create separate lists for inlets and outlets + # Create separate lists for inlets and outlets junction_inlets = [] junction_outlets = [] + for junction in d['junctions']: junction_name = junction['junction_name'] - for block in junction.get(inlet, []): - junction_inlets.append({'junction_name': junction_name, 'block_name': "V" + str(block), 'direction': 'inlet'}) - for block in junction.get(outlet, []): - junction_outlets.append({'junction_name': junction_name, 'block_name': "V" + str(block), 'direction': 'outlet'}) + + # Check for inlet and outlet blocks + if 'inlet_blocks' in junction: + for block in junction['inlet_blocks']: + if block in vessel_id_map: + junction_inlets.append( + {'junction_name': junction_name, 'block_name': "V" + str(vessel_id_map[block]), + 'direction': 'inlet', 'type': 'vessel'}) + else: + junction_inlets.append( + {'junction_name': junction_name, 'block_name': block, + 'direction': 'inlet', 'type': 'block'}) + for block in junction['outlet_blocks']: + if block in vessel_id_map: + junction_inlets.append( + {'junction_name': junction_name, 'block_name': "V" + str(vessel_id_map[block]), + 'direction': 'outlet', 'type': 'vessel'}) + else: + junction_inlets.append( + {'junction_name': junction_name, 'block_name': block, + 'direction': 'outlet', 'type': 'block'}) + + # Check for inlet and outlet vessels + if 'inlet_vessels' in junction: + for vessel in junction['inlet_vessels']: + junction_inlets.append( + {'junction_name': junction_name, 'block_name': "V" + str(vessel), 'direction': 'inlet', + 'type': 'vessel'}) + for vessel in junction['outlet_vessels']: + junction_outlets.append( + {'junction_name': junction_name, 'block_name': "V" + str(vessel), 'direction': 'outlet', + 'type': 'vessel'}) # Create DataFrames from the lists df_junctions_inlets = pd.DataFrame(junction_inlets) @@ -119,7 +155,6 @@ def load_json_input_file(fpath, name_type, inlet_block_type): if 'chambers' not in d: df_chambers= pd.DataFrame(columns=['name', 'type']) - else: chambers = d['chambers'] df_chambers = pd.DataFrame(dict( @@ -137,6 +172,20 @@ def load_json_input_file(fpath, name_type, inlet_block_type): def create_valve_blocks(d, df_vessels, df_valves, name_type): + """ + Purpose: + Create the valve blocks for the 0d model. + Inputs: + d = dict of parameters + df_vessels = df of vessel specific data (because valves are often connected to vessels) + df_valves = df ov valve specific data + name_type: How to name nodes, either "id" or "name". + Returns: + void, but updates d["blocks"] to include the valve blocks, where + d["blocks"] = {block_name : block_object} + """ + if df_valves.empty: + return valve_blocks = {} # {block_name: block_object} if name_type == 'id': @@ -195,15 +244,17 @@ def create_chamber_blocks(d, df_chambers, df_junctions_expanded, df_valves): Purpose: Create the chamber blocks for the 0d model. Inputs: - dict parameters - = created from function utils.extract_info_from_solver_input_file - str name_type - = str specified by either 'name' or 'id' that specifies whether - vessel names or ids should be used for each node + d = dict of parameters + df_chambers = df of chamber specific data + df_junctions_expanded = df of junctions (modified to have inlets & outlets clearly structured) + df_valves = df ov valve specific data + Returns: - void, but updates parameters["blocks"] to include the chamber, where - parameters["blocks"] = {block_name : block_object} + void, but updates d["blocks"] to include the chamber blocks, where + d["blocks"] = {block_name : block_object} """ + if df_chambers.empty: + return chamber_blocks = {} # {block_name: block_object} def process_chamber(row): @@ -250,10 +301,9 @@ def create_junction_blocks(d, df_junctions_expanded, name_type): Purpose: Create the junction blocks for the 0d model. Inputs: - dict parameters - = created from function utils.extract_info_from_solver_input_file - str name_type - = str specified by either 'name' or 'id' that specifies whether + d = dict of parameters + df_junctions_expanded = df of junctions (modified to have inlets & outlets clearly structured) + name_type = str specified by either 'name' or 'id' that specifies whether vessel names or ids should be used for each node Returns: void, but updates parameters["blocks"] to include the junction_blocks, where @@ -306,6 +356,16 @@ def process_junction(row): def get_vessel_list(parameters, name_type): + """ + Purpose: + Returns a list of all vessel ids or names based on name_type. + Inputs: + parameters = dict of parameters + name_type = str specified by either 'name' or 'id' that specifies whether + vessel names or ids should be used to iterate through parameters["vessels"] + Returns: + returns a list of vessel ids or vessel names + """ vessel_id_list = [] for vessel in parameters["vessels"]: if name_type == 'id': @@ -320,11 +380,12 @@ def get_vessel_block_helpers(d, df_vessels, df_junctions_expanded, df_valves, na Purpose: Create helper dictionaries to support the creation of the vessel blocks. Inputs: - dict parameters - -- created from function utils.extract_info_from_solver_input_file - str name_type - = str specified by either 'name' or 'id' that specifies whether - vessel names or ids should be used for each node + d = dict of parameters + df_vessels = df of vessel specific data (because valves are often connected to vessels) + df_junctions_expanded = df of junctions (modified to have inlets & outlets clearly structured) + df_valves = df ov valve specific data + name_type: How to name nodes, either "id" or "name". + Returns: dict vessel_blocks_connecting_block_lists = {vessel_id : connecting_block_list} @@ -365,15 +426,25 @@ def get_vessel_block_helpers(d, df_vessels, df_junctions_expanded, df_valves, na df_vessels.drop(columns=['bc_block_name', 'flow_direction'], inplace=True) - for _, row in df_junctions_expanded.iterrows(): - vessel_id = row['block_name'] - id = int(vessel_id[1:]) junction_name = row['junction_name'] + block_or_vessel_name = row['block_name'] # This is the generic 'block_name' that could be a vessel or a block direction = +1 if row['direction'] == 'inlet' else -1 - df_vessels.loc[df_vessels['vessel_id'] == id, 'connecting_block_list'].apply(lambda x: x.append(junction_name)) - df_vessels.loc[df_vessels['vessel_id'] == id, 'flow_directions'].apply(lambda x: x.append(direction)) + if row['type'] == 'vessel': + vessel_id = int(block_or_vessel_name[1:]) # Strip the 'V' and convert to integer + # Update the connecting_block_list and flow_directions for the vessel + df_vessels.loc[df_vessels['vessel_id'] == vessel_id, 'connecting_block_list'] = df_vessels.loc[ + df_vessels['vessel_id'] == vessel_id, 'connecting_block_list'].apply(lambda x: x + [junction_name]) + df_vessels.loc[df_vessels['vessel_id'] == vessel_id, 'flow_directions'] = df_vessels.loc[ + df_vessels['vessel_id'] == vessel_id, 'flow_directions'].apply(lambda x: x + [direction]) + elif row['type'] == 'block': + # Update the connecting_block_list and flow_directions for the block + df_vessels.loc[df_vessels['name'] == block_or_vessel_name, 'connecting_block_list'] = df_vessels.loc[ + df_vessels['name'] == block_or_vessel_name, 'connecting_block_list'].apply( + lambda x: x + [junction_name]) + df_vessels.loc[df_vessels['name'] == block_or_vessel_name, 'flow_directions'] = df_vessels.loc[ + df_vessels['name'] == block_or_vessel_name, 'flow_directions'].apply(lambda x: x + [direction]) # Process valves for _, row in df_valves.iterrows(): @@ -400,13 +471,11 @@ def create_vessel_blocks(d, df_vessels, df_junctions_expanded, df_valves, name_t Purpose: Create the vessel blocks for the 0d model. Inputs: - dict parameters - -- created from function utils.extract_info_from_solver_input_file - module custom_0d_elements_arguments - = module to call custom 0d element arguments from - str name_type - = str specified by either 'name' or 'id' that specifies whether - vessel names or ids should be used for each node + d = dict of parameters + df_vessels = df of vessel specific data (because valves are often connected to vessels) + df_junctions_expanded = df of junctions (modified to have inlets & outlets clearly structured) + df_valves = df ov valve specific data + name_type: How to name nodes, either "id" or "name". Returns: void, but updates parameters["blocks"] to include the vessel_blocks, where parameters["blocks"] = {block_name : block_object} @@ -424,7 +493,6 @@ def create_vessel_blocks(d, df_vessels, df_junctions_expanded, df_valves, name_t name=block_name, flow_directions=flow_directions ) - d["blocks"].update(vessel_blocks) @@ -433,13 +501,9 @@ def create_outlet_bc_blocks(d, df_valves, name_type): Purpose: Create the outlet bc (boundary condition) blocks for the 0d model. Inputs: - dict parameters - -- created from function utils.extract_info_from_solver_input_file - module custom_0d_elements_arguments - = module to call custom 0d element arguments from - str name_type - = str specified by either 'name' or 'id' that specifies whether - vessel names or ids should be used for each node + d = dict of parameters + df_valves = df ov valve specific data + name_type: How to name nodes, either "id" or "name". Returns: void, but updates parameters["blocks"] to include the outlet_bc_blocks, where parameters["blocks"] = {block_name : block_object} @@ -477,15 +541,13 @@ def create_inlet_bc_blocks(d, df_valves, name_type): Purpose: Create the inlet bc (boundary condition) blocks for the 0d model. Inputs: - dict parameters - -- created from function utils.extract_info_from_solver_input_file - module custom_0d_elements_arguments - = module to call custom 0d element arguments from + d = dict of parameters + df_valves = df ov valve specific data + name_type: How to name nodes, either "id" or "name". Returns: void, but updates parameters["blocks"] to include the inlet_bc_blocks, where parameters["blocks"] = {block_name : block_object} """ - name_type = name_type.lower() inlet_bc_blocks = {} inlet_vessels_of_model = dirgraph_steady_bc.get_ids_of_cap_vessels(d, "inlet") @@ -493,7 +555,6 @@ def create_inlet_bc_blocks(d, df_valves, name_type): vessel_name_get = get_vessel_id_to_vessel_name_map(d) # Process valves - for _, valve in df_valves.iterrows(): valve_name = valve['name'] if valve_name not in block_to_boundary_condition_map or 'inlet' not in block_to_boundary_condition_map[ @@ -519,7 +580,6 @@ def create_inlet_bc_blocks(d, df_valves, name_type): name=block_name, flow_directions=flow_directions ) - d["blocks"].update(inlet_bc_blocks) @@ -527,14 +587,11 @@ def create_inlet_bc_blocks(d, df_valves, name_type): def run_network_util(zero_d_solver_input_file_path, d, draw_directed_graph, output_dir): """ Purpose: - Run functions from network_util_NR to execute the 0d simulation and generate simulation results (pressure, flow rates). + Creates and saves the directed graph. Will open an image if draw_directed_graph is set to true. Inputs: - string zero_d_solver_input_file_path - = path to the 0d solver input file - dict parameters - -- created from function utils.extract_info_from_solver_input_file - boolean draw_directed_graph - = True to visualize the 0d model as a directed graph using networkx -- saves the graph to a .png file (hierarchical graph layout) and a networkx .dot file; False, otherwise. .dot file can be opened with neato from graphviz to visualize the directed in a different format. + string zero_d_solver_input_file_path = path to the 0d solver input file + d = dict of parameters + boolean draw_directed_graph = True to visualize the 0d model as a directed graph using networkx Returns: list block_list = list of blocks (nodes) @@ -544,14 +601,20 @@ def run_network_util(zero_d_solver_input_file_path, d, draw_directed_graph, outp block_list = list(d["blocks"].values()) connect_list, wire_dict = dirgraph_connections.connect_blocks_by_inblock_list(block_list) case_name = zero_d_solver_input_file_path.rsplit('/', 1)[-1] - # directed_graph_file_path = output_dir + '/' + os.path.splitext(case_name)[0] + "_directed_graph" directed_graph_file_path = os.path.join(output_dir, os.path.splitext(case_name)[0] + "_directed_graph") save_directed_graph(block_list, connect_list, directed_graph_file_path, draw_directed_graph) - return block_list, connect_list def get_vessel_id_to_length_map(parameters): + """ + Purpose: + Creates a map with key vessel id and value vessel length. + Inputs: + parameters = dict of parameters + Returns: + vessel_id_to_length_map + """ vessel_id_to_length_map = {} for vessel in parameters["vessels"]: vessel_id = vessel["vessel_id"] @@ -561,6 +624,14 @@ def get_vessel_id_to_length_map(parameters): def get_vessel_id_to_vessel_name_map(parameters): + """ + Purpose: + Creates a map with key vessel id and value vessel name. + Inputs: + parameters = dict of parameters + Returns: + vessel_id_to_vessel_name_map + """ vessel_id_to_vessel_name_map = {} for vessel in parameters["vessels"]: vessel_id = vessel["vessel_id"] @@ -569,6 +640,14 @@ def get_vessel_id_to_vessel_name_map(parameters): return vessel_id_to_vessel_name_map def get_vessel_name_to_vessel_id_map(parameters): + """ + Purpose: + Creates a map with key vessel name and value vessel id. + Inputs: + parameters = dict of parameters + Returns: + vessel_name_to_vessel_id_map + """ vessel_name_to_vessel_id_map = {} for vessel in parameters["vessels"]: vessel_name = vessel["vessel_name"] @@ -580,7 +659,7 @@ def get_vessel_name_to_vessel_id_map(parameters): def save_directed_graph(block_list, connect_list, directed_graph_file_path, draw_directed_graph): """ Purpose: - Visualize the 0d model as a directed graph -- save the graph in a hierarchical graph layout to a .png file; also save a networkx .dot file that can be opened with neato via graphviz to visualize the graph in a different layout. + Visualize and saves the 0d model as a directed graph Inputs: list block_list = [list of all of the 0d LPNBlock objects] @@ -589,8 +668,10 @@ def save_directed_graph(block_list, connect_list, directed_graph_file_path, draw where blockA and blockB are connected to each other, and blockA_index and blockB_index are the index locations at which the blockA and blockB objects are stored in block_list string directed_graph_file_path = name of the hierarchical graph .png file and networkx .dot file that will be saved + draw_directed_graph (bool): Determines if the directed graph should be displayed. Returns: - void, but saves a .png file visualizing the 0d model as a directed graph, as well as a networkx .dot file that can be opened with neato via graphviz to visualize the graph in a different layout + void, but saves a .png file visualizing the 0d model as a directed graph if draw_directed_graph = True, + as well as a networkx .dot file """ G = nx.DiGraph() G.add_edges_from([(block_list[tpl[0]].name, block_list[tpl[1]].name) for tpl in connect_list]) @@ -616,32 +697,26 @@ def save_directed_graph(block_list, connect_list, directed_graph_file_path, draw # Drawing the graph nx.nx_pydot.write_dot(G, directed_graph_file_path + ".dot") - - -def set_up_0d_network(zero_d_solver_input_file_path: object, output_dir, name_type: object, inlet_block: object = False, draw_directed_graph: object = False) -> object: +def set_up_0d_network(zero_d_solver_input_file_path: object, output_dir, name_type: object, draw_directed_graph: object = False) -> object: """ Purpose: - Create all network_util_NR::LPNBlock objects for the 0d model and run the 0d simulation. + Create all network_util_NR::LPNBlock objects for the 0d model. Inputs: string zero_d_solver_input_file_path = path to the 0d solver input file + string output_dir = path to save the directed graph dot file + directed graph png. str name_type = str specified by either 'name' or 'id' that specifies whether vessel names or ids should be used for each node boolean draw_directed_graph = True to visualize the 0d model as a directed graph using networkx -- saves the graph to a .png file (hierarchical graph layout) and a networkx .dot file; False, otherwise. .dot file can be opened with neato from graphviz to visualize the directed in a different format. - boolean use_custom_0d_elements - = True to use user-defined, custom 0d elements in the 0d model; False, otherwire - string custom_0d_elements_arguments_file_path - = path to user-defined custom 0d element file - Caveats: - The save_results_branch option works only for 0d models with the branching structure where each vessel is modeled as a single branch with 1 or multiple sub-segments + Returns: void """ - d = load_json_input_file(zero_d_solver_input_file_path, name_type, inlet_block) + d = load_json_input_file(zero_d_solver_input_file_path, name_type) block_list, connect_list = run_network_util( zero_d_solver_input_file_path, d, draw_directed_graph=draw_directed_graph, output_dir=output_dir - ) + ) \ No newline at end of file diff --git a/docs/pages/developer_guide.md b/docs/pages/developer_guide.md index 204f6ebe7..230f5341e 100644 --- a/docs/pages/developer_guide.md +++ b/docs/pages/developer_guide.md @@ -54,6 +54,8 @@ The modular architecture of svZeroDSolver relies on "blocks", such as blood vess Detailed steps required to implement a new block in svZeroDSolver are available [here](@ref add_block). +Steps required to visualize a new block with svZeroDSolver Visualization application are available [here](@ref visualization)." + # Code Style We follow the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html). diff --git a/docs/pages/main.md b/docs/pages/main.md index d01e6ba8d..295e5960e 100644 --- a/docs/pages/main.md +++ b/docs/pages/main.md @@ -19,7 +19,7 @@ blood vessel parameters to recapitulate given time-varying flow and pressure mea (for example, from a high-fidelity 3D simulation). This allows users to build accurate 0D models that reflect observed hemodynamics. * The svZeroDVisualization application enables users to visualize their 0D network and interactively select nodes to view simulation results. -* The svZeroDGUI application allows users to generate input files for the svZeroDSolver by drawing the network on an easy-to-use GUI. This provides an alternative to manually creating files and is useful for users without access to a 3D model. +* The svZeroDGUI application allows users to generate input files for svZeroDSolver by drawing the network on an easy-to-use GUI. This provides an alternative to manually creating files and is useful for users without access to a 3D model. Zero-dimensional (0D) models diff --git a/docs/pages/visualization.md b/docs/pages/visualization.md index 5bc7c1476..ce92f7ade 100644 --- a/docs/pages/visualization.md +++ b/docs/pages/visualization.md @@ -11,24 +11,73 @@ This application is available in the `applications` folder. # Architecture svZeroDVisualization is built using a robust architecture that includes: - Frontend: Utilizes HTML, CSS, Dash, and Plotly for creating a dynamic and interactive user interface. This setup allows for effective visualization and interaction with the 0D network and simulation results. -- Backend: Powered by a Flask application that handles the server-side logic. It leverages NetworkX for managing and visualizing the network graph and a Python version of svZeroDSolver to draw connections. +- Backend: Powered by a Flask application that handles the server-side logic. It leverages NetworkX for managing and visualizing the network graph and a Python code to determine network connections. + +# Installing Dependencies +1. We recommend using a virtual environment to help manage project-specific +dependencies and avoid conflicts with other projects. +- Using venv: +```bash +python -m venv venv +source venv/bin/activate # On Windows use `venv\Scripts\activate` +``` +- Using Conda: +```bash +conda create --name myenv python=3.12 # Replace with your desired Python version +conda activate myenv +``` + +2. Install the necessary packages: +```bash +pysvzerod +pandas +matplotlib +networkx +dash +plotly +numpy +``` # How to Use -1. Navigate to the `applications` folder and then into the `dirgraph_visualization`. subdirectory. +1. Navigate to the `applications` folder and then into the `dirgraph_visualization` subdirectory. 2. Open the dirgraph_main.py file. -3. In the file, locate line 47 where the dirgraph function is called. -Pass the filepath to your input JSON file and specify the output_directory where you want the visualization to be saved. +3. Pass the filepath to your input JSON file and the output_directory where you want the visualization to be saved as command line arguments. -```bash -results, parameters, G = dirgraph( filepath='' , output_dir='') -``` - -4. Run the script. It will execute the svZeroDSolver, generate a directed graph visualization of your network, parse simulation results, +4. Run the script. It will execute svZeroDSolver, generate a directed graph visualization of your network, parse simulation results, and display the results along with the corresponding nodes on a local Flask server. -5. Once the server is open, you can select a node you want to inspect further. The data for that node will be displayed, including the simulation parameters input for that node, pressure/flow data, and any internal variables if present. +5. Once the server is open, you can click on a node to inspect further. The data for that node will be displayed, including the simulation parameters input for that node, pressure/flow data, and any internal variables if present. + +6. Additional features include the ability to download figures and use the trace function +for more detailed inspection of network elements. The trace feature allows users to filter the +view by specific element types, such as isolating and examining only the blood vessels or +identifying the locations of the chambers within the network. This functionality enhances the +ability to focus on and analyze particular components of the network with precision. + + +# How to Visualize a New Block +1. **Update JSON Parsing**: + - When parsing the JSON file, ensure that the new block type is included. + - Update the `load_json_input_file` function to create a new pandas DataFrame for the new block type. + +2. **Create a New Function for the Block Type**: + - Develop a function that processes the new block type. This function should take in: + - `d` (the dictionary of parameters loaded from the JSON file) + - Any relevant DataFrames (e.g., `df_vessels`, `junctions_expanded`) + - Within this function: + - Initialize a dictionary to hold all blocks of the new type. + - Iterate through potential connectors for the new block type. + - For each connector, update the `connecting_block_list` and determine the `flow_directions` (use +1 for downstream and -1 for upstream). + +3. **Update the Parameter Dictionary**: + - Add the newly created blocks to the `d["blocks"]` dictionary. + +4. **Update Existing Functions**: +- Ensure that the new block type is integrated into the existing block structure, allowing it to interact with other components in the visualization. +- For instance, if the new block type can be connected to vessels, make sure to update functions like `create_vessel_blocks` to handle connections involving the new block type. +This includes updating any relevant functions that create or manage connections between blocks. -6. Additional actions include downloading figures and using the trace feature to further inspect elements by their type (e.g., view only blood vessels or junctions in the network). \ No newline at end of file +By following these steps, you will ensure that the new block type is properly parsed, processed, and integrated into the visualization system. diff --git a/tests/test_dirgraph.py b/tests/test_dirgraph.py index aa5e45dae..fc47b1b62 100644 --- a/tests/test_dirgraph.py +++ b/tests/test_dirgraph.py @@ -43,7 +43,6 @@ def run_program(zero_d_solver_input_file_path, tmp_path): inlet_block=False, draw_directed_graph=False, output_dir = tmp_path - ) @@ -56,10 +55,8 @@ def test_directed_graph_generation(setup_files): generated_dot_file_path = tmp_path / (os.path.splitext(os.path.basename(input_file_path))[0] + "_directed_graph.dot") - # Compare the generated file with the expected file - assert filecmp.cmp(generated_dot_file_path, - expected_dot_file_path), "The generated dot file does not match the expected dot file." - + assert filecmp.cmp(generated_dot_file_path, expected_dot_file_path), \ + f"The generated dot file '{generated_dot_file_path}' does not match the expected dot file '{expected_dot_file_path}'." if __name__ == "__main__": pytest.main() \ No newline at end of file