1+ import json
2+ import requests
3+ import fastjsonschema
4+ from copy import deepcopy
5+ from typing import Optional , Union , List , Dict , Any
6+ from pathlib import Path
7+ from easygraph .classes .hypergraph import Hypergraph
8+
9+ schema_url = "https://raw.githubusercontent.com/pszufe/HIF_validators/main/schemas/hif_schema_v0.1.0.json"
10+
11+ class EasyGraphHIFError (Exception ):
12+ """Custom exception for HIF conversion errors."""
13+ pass
14+
15+ _hif_validator = None
16+
17+ def _get_hif_validator ():
18+ global _hif_validator
19+ if _hif_validator is None :
20+ try :
21+ resp = requests .get (schema_url , timeout = 5 )
22+ if resp .status_code == 200 :
23+ schema = json .loads (resp .text )
24+ _hif_validator = fastjsonschema .compile (schema )
25+ except Exception :
26+ print ("Warning: HIF Schema could not be fetched. Validation skipped." )
27+ _hif_validator = lambda x : True
28+
29+ return _hif_validator if _hif_validator else (lambda x : True )
30+
31+ def hypergraph_to_hif (
32+ hg : Hypergraph ,
33+ filename : Optional [Union [str , Path ]] = None ,
34+ node_label : str = "name" ,
35+ edge_label : str = "name" ,
36+ ) -> dict :
37+ """
38+ Converts an EasyGraph Hypergraph to HIF JSON.
39+ Correctly handles hg.e tuple structure ((edges), (weights), (props)).
40+ """
41+
42+ if hasattr (hg , "custom_hif_nodes" ):
43+ nodj = hg .custom_hif_nodes
44+ else :
45+ nodj = []
46+ num_v = hg .num_v if hasattr (hg , "num_v" ) else len (hg .v_property ) if hasattr (hg , "v_property" ) else 0
47+ v_props = getattr (hg , "v_property" , [{} for _ in range (num_v )])
48+ if not v_props and num_v > 0 : v_props = [{} for _ in range (num_v )]
49+
50+ for i in range (num_v ):
51+ props = v_props [i ] if i < len (v_props ) and isinstance (v_props [i ], dict ) else {}
52+ p = props .copy ()
53+ weight = p .pop ("weight" , 1.0 )
54+ if node_label in p :
55+ node_id = str (p .get (node_label ))
56+ if node_label == "name" :
57+ p .pop ("name" , None )
58+ else :
59+ node_id = p .pop ("name" , str (i ))
60+ nodj .append ({"node" : node_id , "weight" : weight , "attrs" : p })
61+
62+ e_structure = []
63+ e_weights = []
64+ e_props = []
65+
66+ if hasattr (hg , "e" ) and isinstance (hg .e , tuple ) and len (hg .e ) == 3 and \
67+ isinstance (hg .e [0 ], (list , tuple )) and isinstance (hg .e [1 ], (list , tuple )):
68+ e_structure = hg .e [0 ]
69+ e_weights = hg .e [1 ]
70+ e_props = hg .e [2 ]
71+
72+ elif hasattr (hg , "e_list" ) and hg .e_list :
73+ e_structure = hg .e_list
74+ e_weights = getattr (hg , "e_weight" , [1.0 ] * len (e_structure ))
75+ e_props = getattr (hg , "e_property_full" , [{} for _ in range (len (e_structure ))])
76+
77+ elif hasattr (hg , "e" ) and isinstance (hg .e , (list , tuple )):
78+ e_structure = hg .e
79+ e_weights = getattr (hg , "e_weight" , [1.0 ] * len (e_structure ))
80+ e_props = getattr (hg , "e_property_full" , [{} for _ in range (len (e_structure ))])
81+
82+ num_e = len (e_structure )
83+
84+ if len (e_weights ) < num_e : e_weights = [1.0 ] * num_e
85+ if len (e_props ) < num_e : e_props = [{} for _ in range (num_e )]
86+
87+ if hasattr (hg , "custom_hif_edges" ):
88+ edgj = hg .custom_hif_edges
89+ else :
90+ edgj = []
91+ for i in range (num_e ):
92+ props = e_props [i ].copy () if isinstance (e_props [i ], dict ) else {}
93+ # edge_id = props.pop("name", str(i))
94+ weight = e_weights [i ]
95+ props .pop ("weight" , None )
96+ if edge_label in props :
97+ edge_id = str (props .get (edge_label ))
98+ if edge_label == "name" :
99+ props .pop ("name" , None )
100+ else :
101+ edge_id = props .pop ("name" , str (i ))
102+ edgj .append ({"edge" : edge_id , "weight" : weight , "attrs" : props })
103+
104+ if hasattr (hg , "custom_hif_incidences" ):
105+ incj = hg .custom_hif_incidences
106+ else :
107+ incj = []
108+ node_id_list = [n ["node" ] for n in nodj ]
109+ edge_id_list = [e ["edge" ] for e in edgj ]
110+
111+ for e_idx , nodes_in_edge in enumerate (e_structure ):
112+ if e_idx >= len (edge_id_list ): break
113+ edge_name = edge_id_list [e_idx ]
114+
115+ flat_nodes = []
116+ if isinstance (nodes_in_edge , (list , tuple )):
117+ for item in nodes_in_edge :
118+ if isinstance (item , (list , tuple )):
119+ flat_nodes .extend (item )
120+ else :
121+ flat_nodes .append (item )
122+ else :
123+ flat_nodes = [nodes_in_edge ]
124+
125+ for n_idx in flat_nodes :
126+ try :
127+ n_idx_int = int (n_idx )
128+ if 0 <= n_idx_int < len (node_id_list ):
129+ incj .append ({
130+ "edge" : edge_name ,
131+ "node" : node_id_list [n_idx_int ],
132+ "weight" : 1.0 ,
133+ })
134+ except (ValueError , TypeError ):
135+ continue
136+
137+ metadata = getattr (hg , "metadata" , {})
138+ network_type = getattr (hg , "network_type" , "undirected" )
139+
140+ hif = {
141+ "nodes" : nodj ,
142+ "edges" : edgj ,
143+ "incidences" : incj ,
144+ "network-type" : network_type ,
145+ "metadata" : metadata
146+ }
147+
148+ try :
149+ validator = _get_hif_validator ()
150+ validator (hif )
151+ except Exception as e :
152+ print (f"Validation Warning: { e } " )
153+
154+ if filename :
155+ with open (filename , "w" , encoding = 'utf-8' ) as f :
156+ json .dump (hif , f , indent = 4 , ensure_ascii = False )
157+
158+ return hif
159+
160+
161+ def hif_to_hypergraph (
162+ hif : dict = None ,
163+ filename : Optional [Union [str , Path ]] = None ,
164+ node_label : str = "name" ,
165+ edge_label : str = "name" ,
166+ ):
167+ """
168+ Reads HIF JSON and returns an EasyGraph Hypergraph.
169+ Attaches original JSON parts to 'custom_hif_*' attributes to preserve
170+ structure during round-trips.
171+ """
172+ if hif is None :
173+ if filename is None :
174+ raise EasyGraphHIFError ("No HIF data or filename provided." )
175+ try :
176+ with open (filename , "r" , encoding = 'utf-8' ) as f :
177+ hif = json .load (f )
178+ except Exception as e :
179+ raise EasyGraphHIFError (f"Failed to load HIF file { filename } : { e } " )
180+
181+ nodes_list = hif .get ("nodes" , [])
182+ node_name_to_idx = {rec ["node" ]: i for i , rec in enumerate (nodes_list )}
183+ num_v = len (nodes_list )
184+
185+ edges_list = hif .get ("edges" , [])
186+ edge_name_to_idx = {rec ["edge" ]: i for i , rec in enumerate (edges_list )}
187+ num_e = len (edges_list )
188+
189+ v_property = [{} for _ in range (num_v )]
190+ for rec in nodes_list :
191+ idx = node_name_to_idx .get (rec ["node" ])
192+ if idx is not None :
193+
194+ prop = rec .get ("attrs" , {}).copy ()
195+ if node_label in prop :
196+ prop ["name" ] = str (prop [node_label ])
197+ else :
198+ prop ["name" ] = rec ["node" ]
199+ prop ["weight" ] = rec .get ("weight" , 1.0 )
200+ v_property [idx ] = prop
201+
202+ e_property_full = [{} for _ in range (num_e )]
203+ e_weight = [1.0 ] * num_e
204+
205+ for rec in edges_list :
206+ idx = edge_name_to_idx .get (rec ["edge" ])
207+ if idx is not None :
208+ prop = rec .get ("attrs" , {}).copy ()
209+ # if "name" not in prop:
210+ # prop["name"] = rec["edge"]
211+ if edge_label in prop :
212+ prop ["name" ] = str (prop [edge_label ])
213+ else :
214+ prop ["name" ] = rec ["edge" ]
215+ prop ["weight" ] = rec .get ("weight" , 1.0 )
216+ e_property_full [idx ] = prop
217+ e_weight [idx ] = prop ["weight" ]
218+
219+ raw_groups = [[] for _ in range (num_e )]
220+
221+ incidences_list = hif .get ("incidences" , [])
222+
223+ for inc in incidences_list :
224+ e_name = inc .get ("edge" )
225+ n_name = inc .get ("node" )
226+
227+ e_idx = edge_name_to_idx .get (e_name )
228+ n_idx = node_name_to_idx .get (n_name )
229+
230+ if e_idx is not None and n_idx is not None :
231+ raw_groups [e_idx ].append (n_idx )
232+
233+ hg = Hypergraph (
234+ num_v = num_v ,
235+ e_list = raw_groups ,
236+ e_weight = e_weight ,
237+ v_property = v_property
238+ )
239+
240+ hg .node_label_index = {}
241+ for i in range (num_v ):
242+ name = v_property [i ].get ("name" )
243+ if name :
244+ hg .node_label_index [name ] = i
245+
246+ hg .edge_label_index = {}
247+ for i in range (num_e ):
248+ name = e_property_full [i ].get ("name" )
249+ if name :
250+ hg .edge_label_index [name ] = i
251+
252+ hg .custom_hif_nodes = deepcopy (nodes_list )
253+ hg .custom_hif_edges = deepcopy (edges_list )
254+ hg .custom_hif_incidences = deepcopy (incidences_list )
255+
256+ if "metadata" in hif :
257+ hg .metadata = deepcopy (hif ["metadata" ])
258+ else :
259+ hg .metadata = {}
260+
261+ if "network-type" in hif :
262+ hg .network_type = hif ["network-type" ]
263+
264+ hg .e_property_full = e_property_full
265+
266+ return hg
0 commit comments