Fix workstation startup

Update registry
This commit is contained in:
Xuwznln
2025-10-13 15:00:50 +08:00
parent 8c8359fab3
commit aed39b648d
7 changed files with 171 additions and 1731 deletions

View File

@@ -134,3 +134,5 @@ WORKFLOW_STEP_IDS = {
"observe": "" "observe": ""
} }
} }
LOCATION_MAPPING = {}

View File

@@ -54,7 +54,7 @@ workstation.bioyond_dispensing_station:
percent_90_3_target_weigh: '' percent_90_3_target_weigh: ''
speed: '' speed: ''
temperature: '' temperature: ''
handles: [] handles: {}
result: result:
return_info: return_info return_info: return_info
schema: schema:
@@ -174,7 +174,7 @@ workstation.bioyond_dispensing_station:
target_weigh: '' target_weigh: ''
temperature: '' temperature: ''
volume: '' volume: ''
handles: [] handles: {}
result: result:
return_info: return_info return_info: return_info
schema: schema:
@@ -230,12 +230,23 @@ workstation.bioyond_dispensing_station:
title: DispenStationSolnPrep title: DispenStationSolnPrep
type: object type: object
type: DispenStationSolnPrep type: DispenStationSolnPrep
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispendsingStation module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
status_types: {} status_types: {}
type: python type: python
config_info: [] config_info: []
description: '' description: ''
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema:
config:
properties:
config:
type: string
required:
- config
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0 version: 1.0.0

View File

@@ -1361,8 +1361,7 @@ laiyu_liquid:
mix_liquid_height: 0.0 mix_liquid_height: 0.0
mix_rate: 0 mix_rate: 0
mix_stage: '' mix_stage: ''
mix_times: mix_times: 0
- 0
mix_vol: 0 mix_vol: 0
none_keys: none_keys:
- '' - ''
@@ -1492,11 +1491,9 @@ laiyu_liquid:
mix_stage: mix_stage:
type: string type: string
mix_times: mix_times:
items: maximum: 2147483647
maximum: 2147483647 minimum: -2147483648
minimum: -2147483648 type: integer
type: integer
type: array
mix_vol: mix_vol:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648

View File

@@ -3994,8 +3994,7 @@ liquid_handler:
mix_liquid_height: 0.0 mix_liquid_height: 0.0
mix_rate: 0 mix_rate: 0
mix_stage: '' mix_stage: ''
mix_times: mix_times: 0
- 0
mix_vol: 0 mix_vol: 0
none_keys: none_keys:
- '' - ''
@@ -4151,11 +4150,9 @@ liquid_handler:
mix_stage: mix_stage:
type: string type: string
mix_times: mix_times:
items: maximum: 2147483647
maximum: 2147483647 minimum: -2147483648
minimum: -2147483648 type: integer
type: integer
type: array
mix_vol: mix_vol:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
@@ -5015,8 +5012,7 @@ liquid_handler.biomek:
mix_liquid_height: 0.0 mix_liquid_height: 0.0
mix_rate: 0 mix_rate: 0
mix_stage: '' mix_stage: ''
mix_times: mix_times: 0
- 0
mix_vol: 0 mix_vol: 0
none_keys: none_keys:
- '' - ''
@@ -5159,11 +5155,9 @@ liquid_handler.biomek:
mix_stage: mix_stage:
type: string type: string
mix_times: mix_times:
items: maximum: 2147483647
maximum: 2147483647 minimum: -2147483648
minimum: -2147483648 type: integer
type: integer
type: array
mix_vol: mix_vol:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
@@ -7807,8 +7801,7 @@ liquid_handler.prcxi:
mix_liquid_height: 0.0 mix_liquid_height: 0.0
mix_rate: 0 mix_rate: 0
mix_stage: '' mix_stage: ''
mix_times: mix_times: 0
- 0
mix_vol: 0 mix_vol: 0
none_keys: none_keys:
- '' - ''
@@ -7937,11 +7930,9 @@ liquid_handler.prcxi:
mix_stage: mix_stage:
type: string type: string
mix_times: mix_times:
items: maximum: 2147483647
maximum: 2147483647 minimum: -2147483648
minimum: -2147483648 type: integer
type: integer
type: array
mix_vol: mix_vol:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648

View File

@@ -4,11 +4,11 @@ reaction_station.bioyond:
- reaction_station_bioyond - reaction_station_bioyond
class: class:
action_value_mappings: action_value_mappings:
auto-add_material: auto-append_to_workflow_sequence:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
material_data: null web_workflow_name: null
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -18,22 +18,21 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
properties: properties:
material_data: web_workflow_name:
type: object type: string
required: required:
- material_data - web_workflow_name
type: object type: object
result: {} result: {}
required: required:
- goal - goal
title: add_material参数 title: append_to_workflow_sequence参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-create_90_10_vial_feeding_task: auto-clear_workflows:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default: {}
task_data: null
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -42,470 +41,13 @@ reaction_station.bioyond:
properties: properties:
feedback: {} feedback: {}
goal: goal:
properties: properties: {}
task_data:
type: string
required:
- task_data
type: object
result: {}
required:
- goal
title: create_90_10_vial_feeding_task参数
type: object
type: UniLabJsonCommand
auto-create_batch_90_10_vial_feeding_task:
feedback: {}
goal: {}
goal_default:
batch_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
batch_data:
type: string
required:
- batch_data
type: object
result: {}
required:
- goal
title: create_batch_90_10_vial_feeding_task参数
type: object
type: UniLabJsonCommand
auto-create_batch_diamine_solution_task:
feedback: {}
goal: {}
goal_default:
batch_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
batch_data:
type: string
required:
- batch_data
type: object
result: {}
required:
- goal
title: create_batch_diamine_solution_task参数
type: object
type: UniLabJsonCommand
auto-create_diamine_solution_task:
feedback: {}
goal: {}
goal_default:
solution_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
solution_data:
type: string
required:
- solution_data
type: object
result: {}
required:
- goal
title: create_diamine_solution_task参数
type: object
type: UniLabJsonCommand
auto-create_order:
feedback: {}
goal: {}
goal_default:
parameters: null
task_name: null
workflow_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
parameters:
type: object
task_name:
type: string
workflow_name:
type: string
required:
- workflow_name
- task_name
type: object
result: {}
required:
- goal
title: create_order参数
type: object
type: UniLabJsonCommand
auto-create_resource:
feedback: {}
goal: {}
goal_default:
resource_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
resource_data:
type: string
required:
- resource_data
type: object
result: {}
required:
- goal
title: create_resource参数
type: object
type: UniLabJsonCommand
auto-delete_material:
feedback: {}
goal: {}
goal_default:
material_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_data:
type: string
required:
- material_data
type: object
result: {}
required:
- goal
title: delete_material参数
type: object
type: UniLabJsonCommand
auto-device_operation:
feedback: {}
goal: {}
goal_default:
device_id: null
operation: null
parameters: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
device_id:
type: string
operation:
type: string
parameters:
type: object
required:
- device_id
- operation
type: object
result: {}
required:
- goal
title: device_operation参数
type: object
type: UniLabJsonCommand
auto-dispensing_material_inbound:
feedback: {}
goal: {}
goal_default:
material_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_data:
type: string
required:
- material_data
type: object
result: {}
required:
- goal
title: dispensing_material_inbound参数
type: object
type: UniLabJsonCommand
auto-dispensing_material_outbound:
feedback: {}
goal: {}
goal_default:
material_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_data:
type: string
required:
- material_data
type: object
result: {}
required:
- goal
title: dispensing_material_outbound参数
type: object
type: UniLabJsonCommand
auto-drip_back:
feedback: {}
goal: {}
goal_default:
assign_material_name: Reactor
temperature: 25.0
time: '0'
torque_variation: '1'
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assign_material_name:
default: Reactor
type: string
temperature:
default: 25.0
type: number
time:
default: '0'
type: string
torque_variation:
default: '1'
type: string
required: [] required: []
type: object type: object
result: {} result: {}
required: required:
- goal - goal
title: drip_back参数 title: clear_workflows参数
type: object
type: UniLabJsonCommand
auto-execute_bioyond_sync_workflow:
feedback: {}
goal: {}
goal_default:
parameters: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
parameters:
type: object
required:
- parameters
type: object
result: {}
required:
- goal
title: execute_bioyond_sync_workflow参数
type: object
type: UniLabJsonCommandAsync
auto-execute_bioyond_update_workflow:
feedback: {}
goal: {}
goal_default:
parameters: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
parameters:
type: object
required:
- parameters
type: object
result: {}
required:
- goal
title: execute_bioyond_update_workflow参数
type: object
type: UniLabJsonCommandAsync
auto-liquid_feeding_beaker:
feedback: {}
goal: {}
goal_default:
material_name: ''
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: liquid_feeding_beaker参数
type: object
type: UniLabJsonCommand
auto-liquid_feeding_solvents:
feedback: {}
goal: {}
goal_default:
material_name: ''
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: liquid_feeding_solvents参数
type: object
type: UniLabJsonCommand
auto-liquid_feeding_titration:
feedback: {}
goal: {}
goal_default:
material_name: ''
time: '120'
titration_type: '1'
torque_variation: '2'
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
time:
default: '120'
type: string
titration_type:
default: '1'
type: string
torque_variation:
default: '2'
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: liquid_feeding_titration参数
type: object
type: UniLabJsonCommand
auto-liquid_feeding_vials_non_titration:
feedback: {}
goal: {}
goal_default:
material_name: ''
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: liquid_feeding_vials_non_titration参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-load_bioyond_data_from_file: auto-load_bioyond_data_from_file:
@@ -533,74 +75,11 @@ reaction_station.bioyond:
title: load_bioyond_data_from_file参数 title: load_bioyond_data_from_file参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-material_inbound:
feedback: {}
goal: {}
goal_default:
location_name: null
material_id: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
location_name:
type: string
material_id:
type: string
required:
- material_id
- location_name
type: object
result: {}
required:
- goal
title: material_inbound参数
type: object
type: UniLabJsonCommand
auto-material_outbound:
feedback: {}
goal: {}
goal_default:
location_name: null
material_id: null
quantity: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
location_name:
type: string
material_id:
type: string
quantity:
type: integer
required:
- material_id
- location_name
- quantity
type: object
result: {}
required:
- goal
title: material_outbound参数
type: object
type: UniLabJsonCommand
auto-merge_workflow_with_parameters: auto-merge_workflow_with_parameters:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
name: null json_str: null
workflows: null
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -610,15 +89,10 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
properties: properties:
name: json_str:
type: string type: string
workflows:
items:
type: object
type: array
required: required:
- name - json_str
- workflows
type: object type: object
result: {} result: {}
required: required:
@@ -626,31 +100,6 @@ reaction_station.bioyond:
title: merge_workflow_with_parameters参数 title: merge_workflow_with_parameters参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-order_query:
feedback: {}
goal: {}
goal_default:
query_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
query_data:
type: string
required:
- query_data
type: object
result: {}
required:
- goal
title: order_query参数
type: object
type: UniLabJsonCommand
auto-post_init: auto-post_init:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -676,33 +125,11 @@ reaction_station.bioyond:
title: post_init参数 title: post_init参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-reactor_taken_in: auto-process_web_workflows:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: reactor_taken_in参数
type: object
type: UniLabJsonCommand
auto-reactor_taken_out:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
order_id: '' json_str: null
preintake_id: ''
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -712,18 +139,15 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
properties: properties:
order_id: json_str:
default: ''
type: string type: string
preintake_id: required:
default: '' - json_str
type: string
required: []
type: object type: object
result: {} result: {}
required: required:
- goal - goal
title: reactor_taken_out参数 title: process_web_workflows参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-reset_workstation: auto-reset_workstation:
@@ -747,11 +171,11 @@ reaction_station.bioyond:
title: reset_workstation参数 title: reset_workstation参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-sample_waste_removal: auto-resource_tree_add:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
waste_data: null resources: null
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -761,119 +185,42 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
properties: properties:
waste_data: resources:
items:
type: object
type: array
required:
- resources
type: object
result: {}
required:
- goal
title: resource_tree_add参数
type: object
type: UniLabJsonCommand
auto-set_workflow_sequence:
feedback: {}
goal: {}
goal_default:
json_str: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
json_str:
type: string type: string
required: required:
- waste_data - json_str
type: object type: object
result: {} result: {}
required: required:
- goal - goal
title: sample_waste_removal参数 title: set_workflow_sequence参数
type: object
type: UniLabJsonCommand
auto-solid_feeding_vials:
feedback: {}
goal: {}
goal_default:
material_name: ''
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: solid_feeding_vials参数
type: object
type: UniLabJsonCommand
auto-start_scheduler:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: start_scheduler参数
type: object
type: UniLabJsonCommand
auto-stock_material:
feedback: {}
goal: {}
goal_default:
location: null
material_id: null
quantity: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
location:
type: string
material_id:
type: string
quantity:
type: integer
required:
- material_id
- location
- quantity
type: object
result: {}
required:
- goal
title: stock_material参数
type: object
type: UniLabJsonCommand
auto-stop_scheduler:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: stop_scheduler参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-transfer_resource_to_another: auto-transfer_resource_to_another:
@@ -1064,33 +411,6 @@ reaction_station.bioyond:
title: transfer_resource_to_another参数 title: transfer_resource_to_another参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-validate_workflow_parameters:
feedback: {}
goal: {}
goal_default:
workflows: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
workflows:
items:
type: object
type: array
required:
- workflows
type: object
result: {}
required:
- goal
title: validate_workflow_parameters参数
type: object
type: UniLabJsonCommand
bioyond_sync: bioyond_sync:
feedback: {} feedback: {}
goal: goal:
@@ -1407,11 +727,8 @@ reaction_station.bioyond:
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
protocol_type: [] protocol_type: []
status_types: status_types:
all_workflows: dict
bioyond_status: dict bioyond_status: dict
device_list: dict
scheduler_status: dict
station_info: dict
workflow_parameter_template: dict
workstation_status: dict workstation_status: dict
type: python type: python
config_info: [] config_info: []
@@ -1431,24 +748,15 @@ reaction_station.bioyond:
type: object type: object
data: data:
properties: properties:
all_workflows:
type: object
bioyond_status: bioyond_status:
type: object type: object
device_list:
type: object
scheduler_status:
type: object
station_info:
type: object
workflow_parameter_template:
type: object
workstation_status: workstation_status:
type: object type: object
required: required:
- station_info
- bioyond_status - bioyond_status
- workflow_parameter_template - all_workflows
- scheduler_status
- device_list
- workstation_status - workstation_status
type: object type: object
version: 1.0.0 version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,14 @@
import importlib import importlib
import inspect import inspect
import json import json
import os.path
import traceback import traceback
from typing import Union, Any, Dict, List, Tuple from typing import Union, Any, Dict, List, Tuple
import networkx as nx import networkx as nx
from pylabrobot.resources import ResourceHolder from pylabrobot.resources import ResourceHolder
from unilabos_msgs.msg import Resource from unilabos_msgs.msg import Resource
from unilabos.config.config import BasicConfig
from unilabos.resources.container import RegularContainer from unilabos.resources.container import RegularContainer
from unilabos.resources.itemized_carrier import ItemizedCarrier from unilabos.resources.itemized_carrier import ItemizedCarrier
from unilabos.ros.msgs.message_converter import convert_to_ros_msg from unilabos.ros.msgs.message_converter import convert_to_ros_msg
@@ -45,6 +47,29 @@ def canonicalize_nodes_data(
if node.get("label") is not None: if node.get("label") is not None:
node_id = node.pop("label") node_id = node.pop("label")
node["id"] = node["name"] = node_id node["id"] = node["name"] = node_id
if not isinstance(node.get("config"), dict):
node["config"] = {}
if not node.get("type"):
node["type"] = "device"
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'type', defaulting to 'device'", "warning")
if not node.get("name"):
node["name"] = node.get("id")
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'name', defaulting to {node['name']}", "warning")
if not isinstance(node.get("position"), dict):
node["position"] = {"position": {}}
x = node.pop("x", None)
if x is not None:
node["position"]["position"]["x"] = x
y = node.pop("y", None)
if y is not None:
node["position"]["position"]["y"] = y
z = node.pop("z", None)
if z is not None:
node["position"]["position"]["z"] = z
for k in list(node.keys()):
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data"]:
v = node.pop(k)
node["config"][k] = v
# 第二步处理parent_relation # 第二步处理parent_relation
id2idx = {node["id"]: idx for idx, node in enumerate(nodes)} id2idx = {node["id"]: idx for idx, node in enumerate(nodes)}
@@ -302,6 +327,10 @@ def read_graphml(graphml_file: str) -> tuple[nx.Graph, ResourceTreeSet, List[Dic
"nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes], "nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes],
"links": standardized_links, "links": standardized_links,
} }
dump_json_path = os.path.join(BasicConfig.working_dir, os.path.basename(graphml_file).rsplit(".")[0] + ".json")
with open(dump_json_path, "w", encoding="utf-8") as f:
f.write(json.dumps(graph_data, indent=4, ensure_ascii=False))
print_status(f"GraphML converted to JSON and saved to {dump_json_path}", "info")
physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False) physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False)
handle_communications(physical_setup_graph) handle_communications(physical_setup_graph)