lang-main/src/lang_main/render/cytoscape.py
2024-08-07 20:06:06 +02:00

441 lines
15 KiB
Python

import time
from collections.abc import Iterable
from pathlib import Path
from typing import Literal, cast
import py4cytoscape as p4c
from networkx import DiGraph, Graph
from py4cytoscape.exceptions import CyError
from requests.exceptions import RequestException
from lang_main.constants import (
CYTO_BASE_NETWORK_NAME,
CYTO_COLLECTION_NAME,
CYTO_ITER_NEIGHBOUR_DEPTH,
CYTO_LAYOUT_NAME,
CYTO_LAYOUT_PROPERTIES,
CYTO_NETWORK_ZOOM_FACTOR,
CYTO_NUMBER_SUBGRAPHS,
CYTO_PATH_STYLESHEET,
CYTO_SANDBOX_NAME,
CYTO_SELECTION_PROPERTY,
CYTO_STYLESHEET_NAME,
PROPERTY_NAME_DEGREE_WEIGHTED,
SAVE_PATH_FOLDER,
)
from lang_main.loggers import logger_rendering as logger
from lang_main.types import (
CytoExportFileTypes,
CytoExportPageSizes,
CytoLayoutProperties,
CytoLayouts,
CytoNodeID,
)
# ** Cytoscape API related, using py4cytoscape
def verify_connection():
"""Cytoscape: checks if CyREST and Cytoscape versions are compatible nad
if Cytoscape API endpoint is reachable
Raises
------
CyError
incompatible CyREST or Cytoscape versions
RequestException
API endpoint not reachable
"""
try:
p4c.cytoscape_ping()
except CyError as error:
logger.error('[CyError] CyREST or Cytoscape version not supported.')
raise error
except RequestException as error:
logger.error('[CytoAPIConnection] Connection to CyREST API failed.')
raise error
def import_to_cytoscape(
graph: DiGraph | Graph,
network_name: str = CYTO_BASE_NETWORK_NAME,
sandbox_name: str = CYTO_SANDBOX_NAME,
reinitialise_sandbox: bool = True,
) -> None:
"""Cytoscape: import NetworkX graph as new network collection
Parameters
----------
graph : DiGraph | Graph
NetworkX graph object
"""
logger.debug('Checking Cytoscape connection...')
verify_connection()
logger.debug('Setting Cytoscape sandbox...')
p4c.sandbox_set(
sandbox_name=sandbox_name,
reinitialize=reinitialise_sandbox,
copy_samples=False,
)
logger.debug('Importing to and analysing network in Cytoscape...')
p4c.delete_all_networks()
p4c.create_network_from_networkx(
graph,
title=network_name,
collection=CYTO_COLLECTION_NAME,
)
analyse_network(network_name=network_name)
logger.debug('Import and analysis of network to Cytoscape successful.')
def verify_table_property(
property: str,
table_type: Literal['node', 'edge', 'network'] = 'node',
network_name: str = CYTO_BASE_NETWORK_NAME,
) -> bool:
table = p4c.get_table_columns(table=table_type, network=network_name)
return property in table.columns
def analyse_network(
property_degree_weighted: str = PROPERTY_NAME_DEGREE_WEIGHTED,
network_name: str = CYTO_BASE_NETWORK_NAME,
) -> None:
node_table = p4c.get_table_columns(table='node', network=network_name)
net_analyse_possible: bool = True
if len(node_table) < 4:
net_analyse_possible = False
if net_analyse_possible:
p4c.analyze_network(directed=False)
node_table = p4c.get_table_columns(table='node', network=network_name)
node_table['stress_norm'] = node_table['Stress'] / node_table['Stress'].max()
node_table[CYTO_SELECTION_PROPERTY] = (
node_table[property_degree_weighted]
* node_table['BetweennessCentrality']
* node_table['stress_norm']
)
else:
node_table[CYTO_SELECTION_PROPERTY] = 1
p4c.load_table_data(node_table, data_key_column='name', network=network_name)
def reset_current_network_to_base() -> None:
"""resets to currently selected network in Cytoscape back to the base one"""
p4c.set_current_network(CYTO_BASE_NETWORK_NAME)
def fit_content(
zoom_factor: float = CYTO_NETWORK_ZOOM_FACTOR,
network_name: str = CYTO_BASE_NETWORK_NAME,
) -> None:
p4c.hide_all_panels()
p4c.fit_content(selected_only=False, network=network_name)
zoom_current = p4c.get_network_zoom(network=network_name)
zoom_new = zoom_current * zoom_factor
p4c.set_network_zoom_bypass(zoom_new, bypass=False, network=network_name)
def export_network_to_image(
filename: str,
target_folder: Path = SAVE_PATH_FOLDER,
filetype: CytoExportFileTypes = 'SVG',
network_name: str = CYTO_BASE_NETWORK_NAME,
pdf_export_page_size: CytoExportPageSizes = 'A4',
sandbox_name: str = CYTO_SANDBOX_NAME,
) -> None:
"""Cytoscape: export current selected view as image
Parameters
----------
filename : str
export filename
filetype : CytoExportFileTypes, optional
export filetype supported by Cytoscape, by default 'SVG'
network_name : str, optional
network to export, by default CYTO_BASE_NETWORK_NAME
pdf_export_page_size : CytoExportPageSizes, optional
page size which should be used for PDF exports supported by Cytoscape,
by default 'A4'
"""
logger.debug('Exporting image to file...')
if not target_folder.exists():
target_folder.mkdir(parents=True)
dst_file_pth = (target_folder / filename).with_suffix(f'.{filetype.lower()}')
text_as_font = True
if filetype == 'SVG':
text_as_font = False
# close non-necessary windows and fit graph in frame before image display
fit_content(network_name=network_name)
# image is generated in sandbox directory and transferred to target destination
# (preparation for remote instances of Cytoscape)
p4c.export_image(
filename=filename,
type=filetype,
network=network_name,
overwrite_file=True,
all_graphics_details=True,
export_text_as_font=text_as_font,
page_size=pdf_export_page_size,
)
# TODO remove if Cytoscape >= 3.10.* is running in container
# p4c.export_image(
# filename=filename,
# type=filetype,
# network=network_name,
# overwrite_file=True,
# )
logger.debug('Exported image to sandbox.')
logger.debug('Transferring image from sandbox to target destination...')
sandbox_filename = f'{filename}.{filetype.lower()}'
p4c.sandbox_get_from(
source_file=sandbox_filename,
dest_file=str(dst_file_pth),
overwrite=True,
sandbox_name=sandbox_name,
)
logger.debug('Transfer of image from sandbox to target destination successful.')
def layout_network(
layout_name: CytoLayouts = CYTO_LAYOUT_NAME,
layout_properties: CytoLayoutProperties = CYTO_LAYOUT_PROPERTIES,
network_name: str = CYTO_BASE_NETWORK_NAME,
) -> None:
"""Cytoscape: apply a supported layout algorithm to currently selected
network
Parameters
----------
layout_name : CytoLayouts, optional
layout algorithm supported by Cytoscape (name of the CyREST API, does not
necessarily match the name in the Cytoscape UI),
by default CYTO_LAYOUT_NAME
layout_properties : CytoLayoutProperties, optional
configuration of parameters for the given layout algorithm,
by default CYTO_LAYOUT_PROPERTIES
network_name : str, optional
network to apply the layout algorithm on, by default CYTO_BASE_NETWORK_NAME
"""
logger.debug('Applying layout to network...')
p4c.set_layout_properties(layout_name, layout_properties)
p4c.layout_network(layout_name=layout_name, network=network_name)
fit_content(network_name=network_name)
logger.debug('Layout application to network successful.')
def apply_style_to_network(
style_name: str = CYTO_STYLESHEET_NAME,
pth_to_stylesheet: Path = CYTO_PATH_STYLESHEET,
network_name: str = CYTO_BASE_NETWORK_NAME,
node_size_property: str = 'node_selection',
min_node_size: int = 15,
max_node_size: int = 40,
sandbox_name: str = CYTO_SANDBOX_NAME,
) -> None:
"""Cytoscape: apply a chosen Cytoscape style to the defined network
Parameters
----------
style_name : str, optional
Cytoscape name of the style which should be applied,
by default CYTO_STYLESHEET_NAME
pth_to_stylesheet : Path, optional
path where the stylesheet definition in Cytoscape's XML format can
be found,
by default CYTO_PATH_STYLESHEET
network_name : str, optional
network to apply the style on, by default CYTO_BASE_NETWORK_NAME
Raises
------
FileNotFoundError
if provided stylesheet can not be found under the provided path
"""
logger.debug('Applying style to network...')
styles_avail = cast(list[str], p4c.get_visual_style_names())
if style_name not in styles_avail:
if not pth_to_stylesheet.exists():
# existence for standard path verified at import, but not for other
# provided paths
raise FileNotFoundError(
f'Visual stylesheet for Cytoscape not found under: >>{pth_to_stylesheet}<<'
)
# send to sandbox
sandbox_filename = pth_to_stylesheet.name
p4c.sandbox_send_to(
source_file=pth_to_stylesheet,
dest_file=sandbox_filename,
overwrite=True,
sandbox_name=sandbox_name,
)
# load stylesheet
p4c.import_visual_styles(sandbox_filename)
p4c.set_visual_style(style_name, network=network_name)
# node size mapping, only if needed property is available
# TODO check removal
# size_prop_available = verify_table_property(
# property=node_size_property,
# network_name=network_name,
# )
# if size_prop_available:
scheme = p4c.scheme_c_number_continuous(
start_value=min_node_size, end_value=max_node_size
)
node_size_map = p4c.gen_node_size_map(
node_size_property,
number_scheme=scheme,
mapping_type='c',
style_name=style_name,
default_number=min_node_size,
)
p4c.set_node_size_mapping(**node_size_map)
# TODO removal
# else:
# node_table = p4c.get_table_columns(table='node', network=network_name)
# nodes_SUID = node_table['SUID'].to_list()
# p4c.set_node_size_bypass(nodes_SUID, new_sizes=min_node_size, network=network_name)
# p4c.set_visual_style(style_name, network=network_name)
# time.sleep(1) # if not waited image export could be without applied style
fit_content(network_name=network_name)
logger.debug('Style application to network successful.')
def get_subgraph_node_selection(
network_name: str = CYTO_BASE_NETWORK_NAME,
num_subgraphs: int = CYTO_NUMBER_SUBGRAPHS,
) -> list[CytoNodeID]:
"""Cytoscape: obtain the relevant nodes for iterative subgraph generation
Parameters
----------
network_name : str, optional
network to retrieve the nodes from, by default CYTO_BASE_NETWORK_NAME
property_degree_weighted : str, optional
property name which contains the weighted degree,
by default PROPERTY_NAME_DEGREE_WEIGHTED
num_subgraphs : int, optional
number of relevant nodes which form the basis to generate subgraphs from,
by default CYTO_NUMBER_SUBGRAPHS
Returns
-------
list[CytoNodeID]
list containing all relevant Cytoscape nodes
"""
logger.debug('Selecting nodes for subgraph generation...')
node_table = p4c.get_table_columns(table='node', network=network_name)
node_table = node_table.sort_values(by=CYTO_SELECTION_PROPERTY, ascending=False)
p4c.load_table_data(node_table, data_key_column='name', network=network_name)
node_table_choice = node_table.iloc[:num_subgraphs, :]
logger.debug('Selection of nodes for subgraph generation successful.')
return node_table_choice['SUID'].to_list()
def select_neighbours_of_node(
node: CytoNodeID,
neighbour_iter_depth: int = CYTO_ITER_NEIGHBOUR_DEPTH,
network_name: str = CYTO_BASE_NETWORK_NAME,
) -> None:
"""Cytoscape: iterative selection of a node's neighbouring nodes and
their connecting edges
Parameters
----------
node : CytoNodeID
node which neighbours should be selected
neighbour_iter_depth : int, optional
indicates how many levels of neighbours should be choosen, e.g. 1 --> only
first-level neighbours are considered which are directly connected to the node,
2 --> all nodes with iteration depth of 1 are chosen and additionally their
direct neighbours,
by default CYTO_ITER_NEIGHBOUR_DEPTH
network_name : str, optional
network to perform action on, by default CYTO_BASE_NETWORK_NAME
"""
logger.debug('Selecting node neighbours for %s...', node)
p4c.clear_selection(network=network_name)
p4c.select_nodes(node, network=network_name)
for _ in range(neighbour_iter_depth):
_ = p4c.select_first_neighbors(network=network_name)
_ = p4c.select_edges_connecting_selected_nodes()
logger.debug('Selection of node neighbours for %s successful.', node)
def make_subnetwork(
index: int,
network_name: str = CYTO_BASE_NETWORK_NAME,
export_image: bool = True,
target_folder: Path = SAVE_PATH_FOLDER,
) -> None:
"""Cytoscape: generate a new subnetwork based on the currently
selected nodes and edges
Parameters
----------
index : int
id-like property to identify the subnetwork relative to its parent
network_name : str, optional
network to generate subnetwork from, by default CYTO_BASE_NETWORK_NAME
export_image : bool, optional
trigger image export of newly generated subnetwork, by default True
"""
logger.debug('Generating subnetwork with index %d...', index)
subnetwork_name = network_name + f'_sub_{index+1}'
p4c.create_subnetwork(
nodes='selected',
edges='selected',
subnetwork_name=subnetwork_name,
network=network_name,
)
p4c.set_current_network(subnetwork_name)
if export_image:
time.sleep(1)
export_network_to_image(
filename=subnetwork_name,
target_folder=target_folder,
network_name=subnetwork_name,
)
logger.debug('Generation of subnetwork with index %d successful.', index)
def build_subnetworks(
nodes_to_analyse: Iterable[CytoNodeID],
network_name: str = CYTO_BASE_NETWORK_NAME,
export_image: bool = True,
target_folder: Path = SAVE_PATH_FOLDER,
) -> None:
"""Cytoscape: iteratively build subnetworks from a collection of nodes
and their respective neighbouring nodes
Parameters
----------
nodes_to_analyse : Iterable[CytoNodeID]
collection of nodes to make subnetworks from, for each node a dedicated
subnetwork will be generated
network_name : str, optional
network which contains the provided nodes,
by default CYTO_BASE_NETWORK_NAME
export_image : bool, optional
trigger image export of newly generated subnetworks, by default True
"""
logger.debug('Generating all subnetworks for node selection...')
for idx, node in enumerate(nodes_to_analyse):
select_neighbours_of_node(node=node, network_name=network_name)
make_subnetwork(
index=idx,
network_name=network_name,
export_image=export_image,
target_folder=target_folder,
)
logger.debug('Generation of all subnetworks for node selection successful.')