import json import requests import fastjsonschema from copy import deepcopy from typing import Optional, Union, List, Dict, Any from pathlib import Path from easygraph.classes.hypergraph import Hypergraph schema_url = "https://raw.githubusercontent.com/pszufe/HIF_validators/main/schemas/hif_schema_v0.1.0.json" class EasyGraphHIFError(Exception): """Custom exception for HIF conversion errors.""" pass _hif_validator = None def _get_hif_validator(): global _hif_validator if _hif_validator is None: try: resp = requests.get(schema_url, timeout=5) if resp.status_code == 200: schema = json.loads(resp.text) _hif_validator = fastjsonschema.compile(schema) except Exception: print("Warning: HIF Schema could not be fetched. Validation skipped.") _hif_validator = lambda x: True return _hif_validator if _hif_validator else (lambda x: True) def hypergraph_to_hif( hg: Hypergraph, filename: Optional[Union[str, Path]] = None, node_label: str = "name", edge_label: str = "name", ) -> dict: """ Converts an EasyGraph Hypergraph to HIF JSON. Correctly handles hg.e tuple structure ((edges), (weights), (props)). """ if hasattr(hg, "custom_hif_nodes"): nodj = hg.custom_hif_nodes else: nodj = [] num_v = hg.num_v if hasattr(hg, "num_v") else len(hg.v_property) if hasattr(hg, "v_property") else 0 v_props = getattr(hg, "v_property", [{} for _ in range(num_v)]) if not v_props and num_v > 0: v_props = [{} for _ in range(num_v)] for i in range(num_v): props = v_props[i] if i < len(v_props) and isinstance(v_props[i], dict) else {} p = props.copy() weight = p.pop("weight", 1.0) if node_label in p: node_id = str(p.get(node_label)) if node_label == "name": p.pop("name", None) else: node_id = p.pop("name", str(i)) nodj.append({"node": node_id, "weight": weight, "attrs": p}) e_structure = [] e_weights = [] e_props = [] if hasattr(hg, "e") and isinstance(hg.e, tuple) and len(hg.e) == 3 and \ isinstance(hg.e[0], (list, tuple)) and isinstance(hg.e[1], (list, tuple)): e_structure = hg.e[0] e_weights = hg.e[1] e_props = hg.e[2] elif hasattr(hg, "e_list") and hg.e_list: e_structure = hg.e_list e_weights = getattr(hg, "e_weight", [1.0] * len(e_structure)) e_props = getattr(hg, "e_property_full", [{} for _ in range(len(e_structure))]) elif hasattr(hg, "e") and isinstance(hg.e, (list, tuple)): e_structure = hg.e e_weights = getattr(hg, "e_weight", [1.0] * len(e_structure)) e_props = getattr(hg, "e_property_full", [{} for _ in range(len(e_structure))]) num_e = len(e_structure) if len(e_weights) < num_e: e_weights = [1.0] * num_e if len(e_props) < num_e: e_props = [{} for _ in range(num_e)] if hasattr(hg, "custom_hif_edges"): edgj = hg.custom_hif_edges else: edgj = [] for i in range(num_e): props = e_props[i].copy() if isinstance(e_props[i], dict) else {} # edge_id = props.pop("name", str(i)) weight = e_weights[i] props.pop("weight", None) if edge_label in props: edge_id = str(props.get(edge_label)) if edge_label == "name": props.pop("name", None) else: edge_id = props.pop("name", str(i)) edgj.append({"edge": edge_id, "weight": weight, "attrs": props}) if hasattr(hg, "custom_hif_incidences"): incj = hg.custom_hif_incidences else: incj = [] node_id_list = [n["node"] for n in nodj] edge_id_list = [e["edge"] for e in edgj] for e_idx, nodes_in_edge in enumerate(e_structure): if e_idx >= len(edge_id_list): break edge_name = edge_id_list[e_idx] flat_nodes = [] if isinstance(nodes_in_edge, (list, tuple)): for item in nodes_in_edge: if isinstance(item, (list, tuple)): flat_nodes.extend(item) else: flat_nodes.append(item) else: flat_nodes = [nodes_in_edge] for n_idx in flat_nodes: try: n_idx_int = int(n_idx) if 0 <= n_idx_int < len(node_id_list): incj.append({ "edge": edge_name, "node": node_id_list[n_idx_int], "weight": 1.0, }) except (ValueError, TypeError): continue metadata = getattr(hg, "metadata", {}) network_type = getattr(hg, "network_type", "undirected") hif = { "nodes": nodj, "edges": edgj, "incidences": incj, "network-type": network_type, "metadata": metadata } try: validator = _get_hif_validator() validator(hif) except Exception as e: print(f"Validation Warning: {e}") if filename: with open(filename, "w", encoding='utf-8') as f: json.dump(hif, f, indent=4, ensure_ascii=False) return hif def hif_to_hypergraph( hif: dict = None, filename: Optional[Union[str, Path]] = None, node_label: str = "name", edge_label: str = "name", ): """ Reads HIF JSON and returns an EasyGraph Hypergraph. Attaches original JSON parts to 'custom_hif_*' attributes to preserve structure during round-trips. """ if hif is None: if filename is None: raise EasyGraphHIFError("No HIF data or filename provided.") try: with open(filename, "r", encoding='utf-8') as f: hif = json.load(f) except Exception as e: raise EasyGraphHIFError(f"Failed to load HIF file {filename}: {e}") nodes_list = hif.get("nodes", []) node_name_to_idx = {rec["node"]: i for i, rec in enumerate(nodes_list)} num_v = len(nodes_list) edges_list = hif.get("edges", []) edge_name_to_idx = {rec["edge"]: i for i, rec in enumerate(edges_list)} num_e = len(edges_list) v_property = [{} for _ in range(num_v)] for rec in nodes_list: idx = node_name_to_idx.get(rec["node"]) if idx is not None: prop = rec.get("attrs", {}).copy() if node_label in prop: prop["name"] = str(prop[node_label]) else: prop["name"] = rec["node"] prop["weight"] = rec.get("weight", 1.0) v_property[idx] = prop e_property_full = [{} for _ in range(num_e)] e_weight = [1.0] * num_e for rec in edges_list: idx = edge_name_to_idx.get(rec["edge"]) if idx is not None: prop = rec.get("attrs", {}).copy() # if "name" not in prop: # prop["name"] = rec["edge"] if edge_label in prop: prop["name"] = str(prop[edge_label]) else: prop["name"] = rec["edge"] prop["weight"] = rec.get("weight", 1.0) e_property_full[idx] = prop e_weight[idx] = prop["weight"] raw_groups = [[] for _ in range(num_e)] incidences_list = hif.get("incidences", []) for inc in incidences_list: e_name = inc.get("edge") n_name = inc.get("node") e_idx = edge_name_to_idx.get(e_name) n_idx = node_name_to_idx.get(n_name) if e_idx is not None and n_idx is not None: raw_groups[e_idx].append(n_idx) hg = Hypergraph( num_v=num_v, e_list=raw_groups, e_weight=e_weight, v_property=v_property ) hg.node_label_index = {} for i in range(num_v): name = v_property[i].get("name") if name: hg.node_label_index[name] = i hg.edge_label_index = {} for i in range(num_e): name = e_property_full[i].get("name") if name: hg.edge_label_index[name] = i hg.custom_hif_nodes = deepcopy(nodes_list) hg.custom_hif_edges = deepcopy(edges_list) hg.custom_hif_incidences = deepcopy(incidences_list) if "metadata" in hif: hg.metadata = deepcopy(hif["metadata"]) else: hg.metadata = {} if "network-type" in hif: hg.network_type = hif["network-type"] hg.e_property_full = e_property_full return hg