diff --git a/gklearn/experiments/ged/__init__.py b/gklearn/experiments/ged/__init__.py new file mode 100644 index 0000000000..29ac2e1917 --- /dev/null +++ b/gklearn/experiments/ged/__init__.py @@ -0,0 +1,6 @@ +""" +__init__ + +@Author: jajupmochi +@Date: May 22 2025 +""" diff --git a/gklearn/experiments/ged/ged_model/__init__.py b/gklearn/experiments/ged/ged_model/__init__.py new file mode 100644 index 0000000000..1c29e1ea12 --- /dev/null +++ b/gklearn/experiments/ged/ged_model/__init__.py @@ -0,0 +1,6 @@ +""" +__init__.py + +@Author: jajupmochi +@Date: May 22 2025 +""" diff --git a/gklearn/experiments/ged/ged_model/compare_gedlib_with_coords_in_string_and_attr_format.py b/gklearn/experiments/ged/ged_model/compare_gedlib_with_coords_in_string_and_attr_format.py new file mode 100644 index 0000000000..e90930c34d --- /dev/null +++ b/gklearn/experiments/ged/ged_model/compare_gedlib_with_coords_in_string_and_attr_format.py @@ -0,0 +1,470 @@ +""" +@File: compare_gedlib_with_coords_in_string_and_attr_format.py + +@Author: jajupmochi +@Date: May 22 2025 +""" +from typing import List + +import networkx as nx +import numpy as np + +ISSUE_TAG = "\033[91m[issue]\033[0m " # Red +INFO_TAG = "\033[94m[info]\033[0m " # Blue +SUCCESS_TAG = "\033[92m[success]\033[0m " # Green + + +def fit_model_ged( + graphs_X: List[nx.Graph], + graphs_Y: List[nx.Graph] = None, + ged_options: dict = None, + parallel: bool = None, + n_jobs: int = None, + chunksize: int = None, + copy_graphs: bool = True, + read_resu_from_file: int = 1, + output_dir: str = None, + params_idx: str = None, + reorder_graphs: bool = False, + verbose: int = 2, + **kwargs +): + # if read_resu_from_file >= 1: + # fn_model = os.path.join( + # output_dir, 'metric_model.params_{}.pkl'.format( + # params_idx + # ) + # ) + # # Load model from file if it exists: + # if os.path.exists(fn_model) and os.path.getsize(fn_model) > 0: + # print('\nLoading model from file...') + # resu = pickle.load(open(fn_model, 'rb')) + # return resu['model'], resu['history'], resu['model'].dis_matrix + + # Reorder graphs if specified: + if reorder_graphs: + graphs_X = reorder_graphs_by_index(graphs_X, idx_key='id') + if graphs_Y is not None: + graphs_Y = reorder_graphs_by_index(graphs_Y, idx_key='id') + + # Compute metric matrix otherwise: + print(f'{INFO_TAG}Computing metric matrix...') + all_graphs = graphs_X + graphs_Y if graphs_Y else graphs_X + nl_names = list( + all_graphs[0].nodes[list(all_graphs[0].nodes)[0]].keys() + ) if graphs_X else [] + if not all_graphs: + el_names = [] + else: + idx_edge = ( + np.where(np.array([nx.number_of_edges(g) for g in all_graphs]) > 0)[0] + ) + if len(idx_edge) == 0: + el_names = [] + else: + el_names = list( + all_graphs[idx_edge[0]].edges[ + list(all_graphs[idx_edge[0]].edges)[0]].keys() + ) + + from gklearn.experiments.ged.ged_model.ged_model_parallel import GEDModel + + if parallel is False: + parallel = None + elif parallel is True: + parallel = 'imap_unordered' + + model = GEDModel( + # env_type=ged_options['env_type'], + ed_method=ged_options['method'], + edit_cost_fun=ged_options['edit_cost_fun'], + init_edit_cost_constants=ged_options['edit_costs'], + edit_cost_config=ged_options.get('edit_cost_config', {}), + optim_method=ged_options['optim_method'], + node_labels=nl_names, edge_labels=el_names, + parallel=parallel, + n_jobs=n_jobs, + chunksize=chunksize, + copy_graphs=copy_graphs, + # make sure it is a full deep copy. and faster! + verbose=verbose + ) + + # Train model. + try: + if graphs_Y is None: + # Compute the distance matrix for the same set of graphs: + matrix = model.fit_transform( + graphs_X, y=graphs_Y, + save_dm_train=True, repeats=ged_options['repeats'], + ) + else: + model.fit(graphs_X, repeats=ged_options['repeats']) + matrix = model.transform( + graphs_Y, + save_dm_test=True, repeats=ged_options['repeats'], + ) + + except OSError as exception: + if 'GLIBC_2.23' in exception.args[0]: + msg = \ + 'This error is very likely due to the low version of GLIBC ' \ + 'on your system. ' \ + 'The required version of GLIBC is 2.23. This may happen on the ' \ + 'CentOS 7 system, where the highest version of GLIBC is 2.17. ' \ + 'You may check your CLIBC version by bash command `rpm -q glibc`. ' \ + 'The `graphkit-learn` library comes with GLIBC_2.23, which you can ' \ + 'install by enable the `--build-gedlib` option: ' \ + '`python3 setup.py install --build-gedlib`. This will compile the C++ ' \ + 'module `gedlib`, which requires a C++ compiler and CMake.' + raise AssertionError(msg) from exception + else: + assert False, exception + except Exception as exception: + assert False, exception + + # Save history: + # For graph kernels it is n * (n - 1) / 2: + if graphs_Y is None: + n_pairs = len(graphs_X) * (len(graphs_X) - 1) / 2 + else: + n_pairs = len(graphs_X) * len(graphs_Y) + # history = {'run_time': AverageMeter()} + # history['run_time'].update(model.run_time / n_pairs, n_pairs) + + # # Save model and history to file: + # if read_resu_from_file >= 1: + # os.makedirs(os.path.dirname(fn_model), exist_ok=True) + # pickle.dump({'model': model, 'history': history}, open(fn_model, 'wb')) + + # Print out the information: + params_msg = f' for parameters {params_idx}' if params_idx else '' + print( + f'{SUCCESS_TAG}Computed metric matrix of size {matrix.shape} in {model.run_time:.3f} ' + f'seconds ({(model.run_time / n_pairs):.9f} s per pair){params_msg}.' + ) + + stats = { + 'n_pairs': n_pairs, + 'matrix_shape': matrix.shape, + 'run_time': model.run_time, + 'run_time_per_pair': model.run_time / n_pairs, + } + + return model, matrix, stats + + +def show_some_graphs(graphs): + """ + Show some graphs from the list of graphs. + """ + print(f'{INFO_TAG}Showing some graphs:') + for i, g in enumerate(graphs[:3]): + print(f'Graph {i}:') + print('Number of nodes:', g.number_of_nodes()) + print('Number of edges:', g.number_of_edges()) + print('Nodes:', g.nodes(data=True)) + print('Edges:', g.edges(data=True)) + print() + + +def convert_graphs_coords_from_attr_to_string(graphs: List[nx.Graph]): + """ + Convert the coordinates of nodes in graphs from the attribute format `AttrLabel` to the string format `GXLLabel`. + """ + for g in graphs: + for node in g.nodes(data=True): + if 'coords' in node[1]: + # Convert the coordinates to string format and store them in "x" and "y" keys: + coords = node[1]['coords'] + node[1]['x'] = str(coords[0]) + node[1]['y'] = str(coords[1]) + for idx in range(2, len(coords)): + # If there are more than 2 coordinates, store them with extra keys: + node[1][f'coord_{idx}'] = str(coords[idx]) + del node[1]['coords'] + print(f'{INFO_TAG}Converted coordinates from attribute format to string format.') + + +def fit_model_attr_version( + seed: int = 42, n_graphs: int = 100, n_emb_dim: int = 2, parallel: bool = False +) -> (np.array, float): + """ + Fit the GED model with graphs that have coordinates on nodes in attribute format `AttrLabel`. + """ + print( + f'\n{INFO_TAG}Fitting model with graphs with coordinates in attribute format...' + ) + + from gklearn.experiments.ged.ged_model.graph_generator import GraphGenerator + generator = GraphGenerator( + num_graphs=n_graphs, + max_num_nodes=20, + min_num_nodes=10, + max_num_edges=50, + min_num_edges=20, + node_feat_type='float', + edge_feat_type=None, + with_discrete_n_features=False, + with_discrete_e_features=False, + with_continuous_n_features=True, + with_continuous_e_features=False, + continuous_n_feature_key='coords', + continuous_n_feature_dim=n_emb_dim, + continuous_e_feature_dim=0, + seed=seed + ) + graphs = generator.generate_graphs() + # Check graph node label format: + one_n_labels = graphs[0].nodes[list(graphs[0].nodes)[0]] + assert 'coords' in one_n_labels and isinstance(one_n_labels['coords'], np.ndarray) and ( + len(one_n_labels['coords']) > 0 and one_n_labels['coords'].dtype in [ + np.float64, np.float32] + ), ( + 'The node labels should contain "coords" key with a numpy array as value.' + ) + print( + f'{INFO_TAG}Generated {len(graphs)} graphs with coordinates in string format.' + ) + show_some_graphs(graphs) + + # Set GED options: + ged_options = { + 'env_type': 'attr', # Use the attribute-based environment + 'method': 'BIPARTITE', + 'edit_cost_fun': 'GEOMETRIC', + 'edit_costs': [3, 3, 1, 3, 3, 1], + 'edit_cost_config': { + 'node_coord_metric': 'euclidean', + 'node_embed_metric': 'cosine_distance', + 'edge_weight_metric': 'euclidean', + 'edge_embed_metric': 'cosine_distance', + }, + 'optim_method': 'init', + 'repeats': 1, + } + + fit_settings = { + 'parallel': parallel, # Use parallel processing if specified + 'n_jobs': 10, # min(12, max(os.cpu_count() - 2, 0)), + 'chunksize': None, # None == automatic determination + 'copy_graphs': True, + 'reorder_graphs': False, + } + + # Fit model and compute GED matrix: + model, matrix, stats = fit_model_ged( + graphs, + graphs_Y=None, + ged_options=ged_options, + read_resu_from_file=0, + output_dir=None, + params_idx=None, + verbose=2, + **fit_settings + ) + print("Model:", model) + print("Matrix shape:", matrix.shape) + print("Run time:", stats['run_time']) + + return matrix, stats['run_time'] + + +def fit_model_string_version( + seed: int = 42, n_graphs: int = 100, n_emb_dim: int = 2, parallel: bool = False +) -> (np.array, float): + """ + Fit the GED model with graphs that have coordinates on nodes in string format `GXLLabel`. + """ + print(f'\n{INFO_TAG}Fitting model with graphs with coordinates in string format...') + + from gklearn.experiments.ged.ged_model.graph_generator import GraphGenerator + generator = GraphGenerator( + num_graphs=n_graphs, + max_num_nodes=20, + min_num_nodes=10, + max_num_edges=50, + min_num_edges=20, + node_feat_type='float', + edge_feat_type=None, + with_discrete_n_features=False, + with_discrete_e_features=False, + with_continuous_n_features=True, + with_continuous_e_features=False, + continuous_n_feature_key='coords', + continuous_n_feature_dim=n_emb_dim, + continuous_e_feature_dim=0, + seed=seed + ) + graphs = generator.generate_graphs() + convert_graphs_coords_from_attr_to_string(graphs) + # Check graph node label format: + one_n_labels = graphs[0].nodes[list(graphs[0].nodes)[0]] + assert 'x' in one_n_labels and 'y' in one_n_labels and isinstance( + one_n_labels['x'], str) and isinstance(one_n_labels['y'], str), ( + 'The node labels should contain "x" and "y" keys with string values.' + ) + print( + f'{INFO_TAG}Generated {len(graphs)} graphs with coordinates in string format.' + ) + show_some_graphs(graphs) + + # Set GED options: + ged_options = { + 'env_type': 'gxl', # Use the GXLLabel environment + 'method': 'BIPARTITE', + 'edit_cost_fun': 'NON_SYMBOLIC', + 'edit_costs': [3, 3, 1, 3, 3, 1], + 'optim_method': 'init', + 'repeats': 1 + } + + fit_settings = { + 'parallel': parallel, # Use parallel processing if specified + 'n_jobs': 10, # min(12, max(os.cpu_count() - 2, 0)), + 'chunksize': None, # None == automatic determination + 'copy_graphs': True, + 'reorder_graphs': False, + } + + # Fit model and compute GED matrix: + model, matrix, stats = fit_model_ged( + graphs, + graphs_Y=None, + ged_options=ged_options, + read_resu_from_file=0, + output_dir=None, + params_idx=None, + verbose=2, + **fit_settings + ) + print("Model:", model) + print("Matrix shape:", matrix.shape) + print("Run time:", stats['run_time']) + + return matrix, stats['run_time'] + + +def compare_gedlib_with_coords_in_string_and_attr_format( + seed: int = 42, n_graphs: int = 100, n_emb_dim: int = 2, parallel: bool = False +) -> (np.array, np.array): + """ + Compare the output and the performance of GEDLIB with the same graphs with coordinates on nodes, + but one is in string format `GXLLabel` and the other is in the complex attribute format `AttrLabel`. + """ + cost_matrix_a, run_time_a = fit_model_attr_version( + seed=seed, n_graphs=n_graphs, n_emb_dim=n_emb_dim, parallel=parallel + ) + cost_matrix_s, run_time_s = fit_model_string_version( + seed=seed, n_graphs=n_graphs, n_emb_dim=n_emb_dim, parallel=parallel + ) + if not np.allclose(cost_matrix_s, cost_matrix_a, rtol=1e-9): + print( + f'{ISSUE_TAG}The cost matrices are not equal! ' + f'String version: {cost_matrix_s.shape}, ' + f'Attribute version: {cost_matrix_a.shape}, ' + f'Relevant tolerance: 1e-9.' + ) + else: + print( + f'{SUCCESS_TAG}The cost matrices are equal! ' + f'String version: {cost_matrix_s.shape}, ' + f'Attribute version: {cost_matrix_a.shape}, ' + f'Relevant tolerance: 1e-9.' + ) + + # Print the first 5 rows and columns of the matrices: + print('First 5 rows and columns of the string version cost matrix:') + print(cost_matrix_s[:5, :5]) + print('First 5 rows and columns of the attribute version cost matrix:') + print(cost_matrix_a[:5, :5]) + + # Print the run times: + print(f'String version run time: {run_time_s:.3f} seconds.') + print(f'Attribute version run time: {run_time_a:.3f} seconds.') + + # Print the run time per pair: + n_pairs = cost_matrix_s.shape[0] * (cost_matrix_s.shape[0] - 1) / 2 + print( + f'String version run time per pair: {run_time_s / n_pairs:.9f} seconds.' + ) + print( + f'Attribute version run time per pair: {run_time_a / n_pairs:.9f} seconds.' + ) + + return cost_matrix_s, cost_matrix_a + + +if __name__ == '__main__': + # Test the class + # feat_type = 'str' + seed = 42 + n_graphs = 500 + n_emb_dim = 100 + parellel = True + compare_gedlib_with_coords_in_string_and_attr_format( + seed=seed, n_graphs=n_graphs, n_emb_dim=n_emb_dim, parallel=parellel + ) + + # # Comparison of the two versions: + # + # General Settings: + # - n_graphs: 500 + # - node numbers: 10-20 + # - edge numbers: 20-50 + # - Regenerate GEDEnv for each pair of computation (not optimized). + # - Coordinates as labels of strings in GXLLabel or one label of np.array in AttrLabel, + # where the latter is optimized by the Eigen C++ library for vectorized operations. + # + # ## Without parallelization: + # + # ### n_emb_dim = 2: + # - String version run time: 7.4e-4 s per pair (92.3 s total). + # - Attribute version run time: 5.0e-4 s per pair (62.4 s total). + # The Attr version is ~ 1.5x faster than the String version. + # + # ### n_emb_dim = 20: + # - String version run time: 5.4e-3 s per pair (675.1 s total). + # - Attribute version run time: 5.5e-4 s per pair (69.0 s total). + # The Attr version is ~ 10x faster than the String version. + # + # ### n_emb_dim = 100: + # - String version run time: too long to compute (over 1 h ~ 3698.5 s). + # - Attribute version run time: 8.0e-4 s per pair (99.9 s total). + # The Attr version is ~ 37x faster than the String version. + # + # ### Conclusion: + # - The Attribute version is faster than the String version. + # - With the increase of the dimensionality of the coordinates (n_emb_dim): + # -- Attribute version takes almost the same level of time to compute pairwise + # distances (e.g., ~ 1.6x slower when n_emb_dim = 100 than 2). + # -- String version becomes unusable (~ 40x slower when n_emb_dim = 100 than 2), + # and ~ 37x slower than the Attribute version with n_emb_dim = 100. + # + # ## With parallelization (n_jobs=10): + # + # ### n_emb_dim = 2: + # - String version run time: 3.6e-4 s per pair (45.3 s total). + # - Attribute version run time: 3.6e-4 s per pair (45.3 s total). + # The two versions are almost equal in terms of run time. + # + # ### n_emb_dim = 20: + # - String version run time: 9.8e-4 s per pair (122.4 s total). + # - Attribute version run time: 4.1e-4 s per pair (50.7 s total). + # The Attribute version is ~ 2.4x faster than the String version. + # + # ### n_emb_dim = 100: + # - String version run time: 5.3e-3 s per pair (664.3 s total). + # - Attribute version run time: 4.4e-4 s per pair (54.3 s total). + # The Attribute version is ~ 12.2x faster than the String version. + # + # ### Conclusion: + # - The Attribute version is still faster than the String version. + # - The parallelization helps to reduce the run time of both versions, + # but the improvement on the String version is much more significant, + # e.g., ~ x faster than the non-parallelized version with n_emb_dim = 100 + # - On the other hand, the improvement brought by parallelization is not so significant + # for the Attribute version, e.g., ~ 1.8x faster than the non-parallelized version + # with n_emb_dim = 100. + # -- I assume the reason is that the construction of the GEDEnvAttr and the + # Python-C++ interface conversion becomes the bottleneck of the process. diff --git a/gklearn/experiments/ged/ged_model/fit_ged_model.py b/gklearn/experiments/ged/ged_model/fit_ged_model.py new file mode 100644 index 0000000000..8b34fd8c2c --- /dev/null +++ b/gklearn/experiments/ged/ged_model/fit_ged_model.py @@ -0,0 +1,236 @@ +""" +fit_ged_model + +@Author: jajupmochi +@Date: May 22 2025 +""" +from typing import List + +import networkx as nx +import numpy as np + +ISSUE_TAG = "\033[91m[issue]\033[0m " # Red +INFO_TAG = "\033[94m[info]\033[0m " # Blue +SUCCESS_TAG = "\033[92m[success]\033[0m " # Green + + +def fit_model_ged( + graphs_X: List[nx.Graph], + graphs_Y: List[nx.Graph] = None, + ged_options: dict = None, + parallel: bool = None, + n_jobs: int = None, + chunksize: int = None, + copy_graphs: bool = True, + read_resu_from_file: int = 1, + output_dir: str = None, + params_idx: str = None, + reorder_graphs: bool = False, + verbose: int = 2, + **kwargs +): + # if read_resu_from_file >= 1: + # fn_model = os.path.join( + # output_dir, 'metric_model.params_{}.pkl'.format( + # params_idx + # ) + # ) + # # Load model from file if it exists: + # if os.path.exists(fn_model) and os.path.getsize(fn_model) > 0: + # print('\nLoading model from file...') + # resu = pickle.load(open(fn_model, 'rb')) + # return resu['model'], resu['history'], resu['model'].dis_matrix + + # Reorder graphs if specified: + if reorder_graphs: + graphs_X = reorder_graphs_by_index(graphs_X, idx_key='id') + if graphs_Y is not None: + graphs_Y = reorder_graphs_by_index(graphs_Y, idx_key='id') + + # Compute metric matrix otherwise: + print(f'{INFO_TAG}Computing metric matrix...') + all_graphs = graphs_X + graphs_Y if graphs_Y else graphs_X + nl_names = list( + all_graphs[0].nodes[list(all_graphs[0].nodes)[0]].keys() + ) if graphs_X else [] + if not all_graphs: + el_names = [] + else: + idx_edge = ( + np.where(np.array([nx.number_of_edges(g) for g in all_graphs]) > 0)[0] + ) + if len(idx_edge) == 0: + el_names = [] + else: + el_names = list( + all_graphs[idx_edge[0]].edges[ + list(all_graphs[idx_edge[0]].edges)[0]].keys() + ) + + from gklearn.experiments.ged.ged_model.ged_model_parallel import GEDModel + + if parallel is False: + parallel = None + elif parallel is True: + parallel = 'imap_unordered' + + model = GEDModel( + ed_method=ged_options['method'], + edit_cost_fun=ged_options['edit_cost_fun'], + init_edit_cost_constants=ged_options['edit_costs'], + optim_method=ged_options['optim_method'], + node_labels=nl_names, edge_labels=el_names, + parallel=parallel, + n_jobs=n_jobs, + chunksize=chunksize, + copy_graphs=copy_graphs, + # make sure it is a full deep copy. and faster! + verbose=verbose + ) + + # Train model. + try: + if graphs_Y is None: + # Compute the distance matrix for the same set of graphs: + matrix = model.fit_transform( + graphs_X, y=graphs_Y, + save_dm_train=True, repeats=ged_options['repeats'], + ) + else: + model.fit(graphs_X, repeats=ged_options['repeats']) + matrix = model.transform( + graphs_Y, + save_dm_test=True, repeats=ged_options['repeats'], + ) + + except OSError as exception: + if 'GLIBC_2.23' in exception.args[0]: + msg = \ + 'This error is very likely due to the low version of GLIBC ' \ + 'on your system. ' \ + 'The required version of GLIBC is 2.23. This may happen on the ' \ + 'CentOS 7 system, where the highest version of GLIBC is 2.17. ' \ + 'You may check your CLIBC version by bash command `rpm -q glibc`. ' \ + 'The `graphkit-learn` library comes with GLIBC_2.23, which you can ' \ + 'install by enable the `--build-gedlib` option: ' \ + '`python3 setup.py install --build-gedlib`. This will compile the C++ ' \ + 'module `gedlib`, which requires a C++ compiler and CMake.' + raise AssertionError(msg) from exception + else: + assert False, exception + except Exception as exception: + assert False, exception + + # Save history: + # For graph kernels it is n * (n - 1) / 2: + if graphs_Y is None: + n_pairs = len(graphs_X) * (len(graphs_X) - 1) / 2 + else: + n_pairs = len(graphs_X) * len(graphs_Y) + # history = {'run_time': AverageMeter()} + # history['run_time'].update(model.run_time / n_pairs, n_pairs) + + # # Save model and history to file: + # if read_resu_from_file >= 1: + # os.makedirs(os.path.dirname(fn_model), exist_ok=True) + # pickle.dump({'model': model, 'history': history}, open(fn_model, 'wb')) + + # Print out the information: + params_msg = f' for parameters {params_idx}' if params_idx else '' + print( + f'{SUCCESS_TAG}Computed metric matrix of size {matrix.shape} in {model.run_time:.3f} ' + f'seconds ({(model.run_time / n_pairs):.9f} s per pair){params_msg}.' + ) + + stats = { + 'n_pairs': n_pairs, + 'matrix_shape': matrix.shape, + 'run_time': model.run_time, + 'run_time_per_pair': model.run_time / n_pairs, + } + + return model, matrix, stats + + +def fit_model_ged_test(feat_type: str = 'str'): + # Example usage: + from gklearn.experiments.ged.ged_model.graph_generator import GraphGenerator + if feat_type in ['str', 'int']: + generator = GraphGenerator( + num_graphs=10, + max_num_nodes=5, + min_num_nodes=3, + max_num_edges=10, + min_num_edges=5, + node_feat_type=feat_type, + edge_feat_type=feat_type, + with_discrete_n_features=True, + with_discrete_e_features=True, + with_continuous_n_features=False, + with_continuous_e_features=False, + continuous_n_feature_dim=1, + continuous_e_feature_dim=1, + + ) + else: + generator = GraphGenerator( + num_graphs=10, + max_num_nodes=5, + min_num_nodes=3, + max_num_edges=10, + min_num_edges=5, + with_discrete_n_features=True, + with_discrete_e_features=True, + with_continuous_n_features=True, + with_continuous_e_features=True, + continuous_n_feature_dim=5, + continuous_e_feature_dim=3, + # node_features=['color', 'shape'], + # edge_features=['weight'], + # node_feature_values={'color': ['red', 'blue'], 'shape': ['circle', 'square']}, + # edge_feature_values={'weight': [1, 2, 3]}, + ) + run_fit(generator) + + +def run_fit(graph_generator): + graphs = graph_generator.generate_graphs() + + # Set GED options: + ged_options = { + 'method': 'BIPARTITE', + 'edit_cost_fun': 'NON_SYMBOLIC', + 'edit_costs': [3, 3, 1, 3, 3, 1], + 'optim_method': 'init', + 'repeats': 1 + } + + fit_settings = { + 'parallel': None, + 'n_jobs': 1, # min(12, max(os.cpu_count() - 2, 0)), + 'chunksize': None, # None == automatic determination + 'copy_graphs': True, + 'reorder_graphs': False, + } + + # Fit model and compute GED matrix: + model, matrix, stats = fit_model_ged( + graphs, + graphs_Y=None, + ged_options=ged_options, + read_resu_from_file=0, + output_dir=None, + params_idx=None, + verbose=2, + **fit_settings + ) + print("Model:", model) + print("Matrix shape:", matrix.shape) + + + +if __name__ == '__main__': + # Test the class + # feat_type = 'str' + feat_type = 'int' + fit_model_ged_test(feat_type=feat_type) diff --git a/gklearn/experiments/ged/ged_model/ged_model_parallel.py b/gklearn/experiments/ged/ged_model/ged_model_parallel.py new file mode 100644 index 0000000000..7841ffd380 --- /dev/null +++ b/gklearn/experiments/ged/ged_model/ged_model_parallel.py @@ -0,0 +1,1334 @@ +""" +basic + +@Author: jajupmochi +@Date: May 22 2025 +""" +import gc +import multiprocessing +import os +import sys +import time +from concurrent.futures import ProcessPoolExecutor +from contextlib import contextmanager +from functools import partial +from itertools import combinations, product +from multiprocessing import shared_memory, Manager + +import joblib +import networkx as nx +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.exceptions import NotFittedError +from sklearn.utils.validation import check_is_fitted +from tqdm import tqdm + +from gklearn.ged.model.distances import euclid_d +from gklearn.ged.util import pairwise_ged +from gklearn.utils import get_iters + + +# @TODO: it should be faster if creating a global env variable. +class GEDModel(BaseEstimator): # , ABC): + """The graph edit distance model class compatible with `scikit-learn`. + + Attributes + ---------- + _graphs : list + Stores the input graphs on fit input data. + Default format of the list objects is `NetworkX` graphs. + **We don't guarantee that the input graphs remain unchanged during the + computation.** + + Notes + ----- + This class uses the `gedlibpy` module to compute the graph edit distance. + + References + ---------- + https://ysig.github.io/GraKeL/0.1a8/_modules/grakel/kernels/kernel.html#Kernel. + """ + + + def __init__( + self, + env_type: str | None = None, + ed_method='BIPARTITE', + edit_cost_fun='CONSTANT', + init_edit_cost_constants=[3, 3, 1, 3, 3, 1], + edit_cost_config: dict = {}, + optim_method='init', + optim_options={'y_distance': euclid_d, 'mode': 'reg'}, + node_labels=[], + edge_labels=[], + parallel=None, + n_jobs=None, + chunksize=None, + # normalize=True, + copy_graphs=True, # make sure it is a full deep copy. and faster! + verbose=2 + ): + """`__init__` for `GEDModel` object. + + Parameters + ---------- + env_type : str, optional + The type of the GED environment. Default is None. If None, try to determine + the type automatically based on the given graph node / edge labels. + + Available types are: + + - 'attr': Attribute-based environment (with complex node and edge labels). + Each node or edge can have multiple key-value label pairs, and each value can + be of the following types: int, float, str, list/np.ndarray of int or float. + This is the default type if no node or edge labels are provided. + + - 'gxl' or 'str': GXLLabel environment (with string labels). Each node or + edge can have multiple key-value label pairs, but all values must be strings. + The type will be set to GXL only if at least one node or edge label is + provided. + """ + # @todo: the default settings of the parameters are different from those in the self.compute method. + # self._graphs = None + self.env_type = env_type + self.ed_method = ed_method + self.edit_cost_fun = edit_cost_fun + self.init_edit_cost_constants = init_edit_cost_constants + self.edit_cost_config = edit_cost_config + self.optim_method = optim_method + self.optim_options = optim_options + self.node_labels = node_labels + self.edge_labels = edge_labels + self.parallel = parallel + self.n_jobs = ( + (multiprocessing.cpu_count() - 1) if n_jobs is None else n_jobs) + self.chunksize = chunksize + # self.normalize = normalize + self.copy_graphs = copy_graphs + self.verbose = verbose + + + # self._run_time = 0 + # self._gram_matrix = None + # self._gram_matrix_unnorm = None + + ########################################################################## + # The following is the 1st paradigm to compute GED distance matrix, which is + # compatible with `scikit-learn`. + ########################################################################## + + def fit(self, X, y=None, **kwargs): + """Fit a graph dataset for a transformer. + + Parameters + ---------- + X : iterable + DESCRIPTION. + + y : None, optional + There is no need of a target in a transformer, yet the `scikit-learn` + pipeline API requires this parameter. + + Returns + ------- + object + Returns self. + + """ + # self._is_tranformed = False + + # Clear any prior attributes stored on the estimator, # @todo: unless warm_start is used; + self.clear_attributes() + + # Validate parameters for the transformer. + self.validate_parameters() + + # Validate the input. + self._graphs = self.validate_input(X) + if y is not None: + self._targets = y + # self._targets = self.validate_input(y) + + # Compute edit cost constants. + self.compute_edit_costs(**kwargs) + + # self._X = X + # self._kernel = self._get_kernel_instance() + + # Return the transformer. + return self + + + def transform( + self, + X=None, + return_dm_train=False, + save_dm_test=False, + return_dm_test=False, + **kwargs + ): + """Compute the graph kernel matrix between given and fitted data. + + Parameters + ---------- + X : TYPE + DESCRIPTION. + + Raises + ------ + ValueError + DESCRIPTION. + + Returns + ------- + None. + + """ + # If `return_dm_train`, return the fitted GED distance matrix of training data. + if return_dm_train: + check_is_fitted(self, '_dm_train') + self._is_transformed = True + return self._dm_train # @TODO: copy or not? + + if return_dm_test: + check_is_fitted(self, '_dm_test') + return self._dm_test # @TODO: copy or not? + + # Check if method "fit" had been called. + check_is_fitted(self, '_graphs') + + # Validate the input. + Y = self.validate_input(X) + + # Transform: compute the graph kernel matrix. + dis_matrix = self.compute_distance_matrix(Y, **kwargs) + self._Y = Y + + # Self transform must appear before the diagonal call on normalization. + self._is_transformed = True # @TODO: When to set this to True? When return dm test? + # if self.normalize: + # X_diag, Y_diag = self.diagonals() + # old_settings = np.seterr(invalid='raise') # Catch FloatingPointError: invalid value encountered in sqrt. + # try: + # kernel_matrix /= np.sqrt(np.outer(Y_diag, X_diag)) + # except: + # raise + # finally: + # np.seterr(**old_settings) + + if save_dm_test: + self._dm_test = dis_matrix + # If the model is retransformed and the `save_dm_test` flag is not set, + # then remove the previously computed dm_test to prevent conflicts. + else: + if hasattr(self, '_dm_test'): + delattr(self, '_dm_test') + + return dis_matrix + + + def fit_transform( + self, + X, + y=None, + save_dm_train=False, + save_mm_train: bool = False, + **kwargs + ): + """Fit and transform: compute GED distance matrix on the same data. + + Parameters + ---------- + X : list of graphs + Input graphs. + + Returns + ------- + dis_matrix : numpy array, shape = [len(X), len(X)] + The distance matrix of X. + + """ + self.fit(X, y, **kwargs) + + # Transform: compute Gram matrix. + dis_matrix = self.compute_distance_matrix(**kwargs) + + # # Normalize. + # if self.normalize: + # self._X_diag = np.diagonal(gram_matrix).copy() + # old_settings = np.seterr(invalid='raise') # Catch FloatingPointError: invalid value encountered in sqrt. + # try: + # gram_matrix /= np.sqrt(np.outer(self._X_diag, self._X_diag)) + # except: + # raise + # finally: + # np.seterr(**old_settings) + + if save_mm_train or save_dm_train: + self._dm_train = dis_matrix + # If the model is refitted and the `save_dm_train` flag is not set, then + # remove the previously computed dm_train to prevent conflicts. + else: + if hasattr(self, '_dm_train'): + delattr(self, '_dm_train') + + return dis_matrix + + + def get_params(self): + pass + + + def set_params(self): + pass + + + def clear_attributes(self): # @todo: update + # if hasattr(self, '_X_diag'): + # delattr(self, '_X_diag') + if hasattr(self, '_graphs'): + delattr(self, '_graphs') + if hasattr(self, '_Y'): + delattr(self, '_Y') + if hasattr(self, '_run_time'): + delattr(self, '_run_time') + if hasattr(self, '_test_run_time'): + delattr(self, '_test_run_time') + + + def validate_parameters(self): + """Validate all parameters for the transformer. + + Returns + ------- + None. + + """ + if self.parallel == False: + self.parallel = None + elif self.parallel == True: + self.parallel = 'imap_unordered' + if self.parallel is not None and self.parallel not in [ + 'imap_unordered', 'multiprocessing', 'joblib', 'concurrent' + ]: + raise ValueError('Parallel mode is not set correctly.') + + if self.parallel == 'imap_unordered' and self.n_jobs is None: + self.n_jobs = multiprocessing.cpu_count() + + + def validate_input(self, X): + """Validate the given input and raise errors if it is invalid. + + Parameters + ---------- + X : list + The input to check. Should be a list of graph. + + Raises + ------ + ValueError + Raise if the input is not correct. + + Returns + ------- + X : list + The input. A list of graph. + + """ + if X is None: + raise ValueError('Please add graphs before computing.') + elif not isinstance(X, list): + raise ValueError('Cannot detect graphs. The input must be a list.') + elif len(X) == 0: + raise ValueError( + 'The graph list given is empty. No computation will be performed.' + ) + + return X + + + def compute_distance_matrix(self, Y=None, **kwargs): + """Compute the distance matrix between a given target graphs (Y) and + the fitted graphs (X / self._graphs) or the distance matrix for the fitted + graphs (X / self._graphs). + + Parameters + ---------- + Y : list of graphs, optional + The target graphs. The default is None. If None distance is computed + between X and itself. + + Returns + ------- + dis_matrix : numpy array, shape = [n_targets, n_inputs] + The computed distance matrix. + + """ + if Y is None: + # Compute metric matrix for self._graphs (X). + dis_matrix = self._compute_X_distance_matrix(**kwargs) + # self._gram_matrix_unnorm = np.copy(self._gram_matrix) + + else: + # Compute metric matrix between Y and self._graphs (X). + Y_copy = ([g.copy() for g in Y] if self.copy_graphs else Y) + graphs_copy = ( + [g.copy() for g in self._graphs] + if self.copy_graphs else self._graphs + ) + + start_time = time.time() + + if self.parallel in [ + 'imap_unordered', 'joblib', 'concurrent', 'multiprocessing' + ]: + dis_matrix = self._compute_cross_distance_matrix_parallel( + Y_copy, graphs_copy, **kwargs + ) + + elif self.parallel is None: + dis_matrix = self._compute_cross_distance_matrix_series( + Y_copy, graphs_copy, **kwargs + ) + else: + raise Exception('Parallel mode is not set correctly.') + + self._run_time = time.time() - start_time + + if self.verbose: + print( + 'Distance matrix of size (%d, %d) built in %s seconds.' + % (len(Y), len(self._graphs), self._run_time) + ) + + return dis_matrix + + + def diagonals(self): + """Compute the kernel matrix diagonals of the fit/transformed data. + + Returns + ------- + X_diag : numpy array + The diagonal of the kernel matrix between the fitted data. + This consists of each element calculated with itself. + + Y_diag : numpy array + The diagonal of the kernel matrix, of the transform. + This consists of each element calculated with itself. + + """ + # Check if method "fit" had been called. + check_is_fitted(self, ['_graphs']) + + # Check if the diagonals of X exist. + try: + check_is_fitted(self, ['_X_diag']) + except NotFittedError: + # Compute diagonals of X. + self._X_diag = np.empty(shape=(len(self._graphs),)) + graphs = ([g.copy() for g in + self._graphs] if self.copy_graphs else self._graphs) + for i, x in enumerate(graphs): + self._X_diag[i] = self.pairwise_kernel(x, x) # @todo: parallel? + + try: + # If transform has happened, return both diagonals. + check_is_fitted(self, ['_Y']) + self._Y_diag = np.empty(shape=(len(self._Y),)) + Y = ([g.copy() for g in self._Y] if self.copy_graphs else self._Y) + for (i, y) in enumerate(Y): + self._Y_diag[i] = self.pairwise_kernel(y, y) # @todo: parallel? + + return self._X_diag, self._Y_diag + except NotFittedError: + # Else just return both X_diag + return self._X_diag + + + # @abstractmethod + def pairwise_distance(self, x, y): + """Compute pairwise kernel between two graphs. + + Parameters + ---------- + x, y : NetworkX Graph. + Graphs bewteen which the kernel is computed. + + Returns + ------- + kernel: float + The computed kernel. + +# Notes +# ----- +# This method is abstract and must be implemented by a subclass. + + """ + raise NotImplementedError( + 'Pairwise kernel computation is not implemented!' + ) + + + def compute_edit_costs(self, Y=None, Y_targets=None, **kwargs): + """Compute edit cost constants. When optimizing method is `fiited`, + apply Jia2021's metric learning method by using a given target graphs (Y) + the fitted graphs (X / self._graphs). + + Parameters + ---------- + Y : TYPE, optional + DESCRIPTION. The default is None. + + Returns + ------- + None. + + """ + # Get or compute. + if self.optim_method == 'random': + self._edit_cost_constants = np.random.rand(6) + + elif self.optim_method == 'init': + self._edit_cost_constants = self.init_edit_cost_constants + + elif self.optim_method == 'expert': + self._edit_cost_constants = [3, 3, 1, 3, 3, 1] + + elif self.optim_method == 'fitted': # Jia2021 method + # Get proper inputs. + if Y is None: + check_is_fitted(self, ['_graphs']) + check_is_fitted(self, ['_targets']) + graphs = ([g.copy() for g in + self._graphs] if self.copy_graphs else self._graphs) + targets = self._targets + else: + graphs = ([g.copy() for g in Y] if self.copy_graphs else Y) + targets = Y_targets + + # Get optimization options. + node_labels = self.node_labels + edge_labels = self.edge_labels + unlabeled = (len(node_labels) == 0 and len(edge_labels) == 0) + repeats = kwargs.get('repeats', 1) + from gklearn.ged.model.optim_costs import compute_optimal_costs + self._edit_cost_constants = compute_optimal_costs( + graphs, targets, + node_labels=node_labels, edge_labels=edge_labels, + unlabeled=unlabeled, + init_costs=self.init_edit_cost_constants, + ed_method=self.ed_method, + edit_cost_fun=self.edit_cost_fun, + repeats=repeats, + rescue_optim_failure=False, + verbose=(self.verbose >= 2), + **self.optim_options + ) + + + def _compute_X_distance_matrix(self, **kwargs): + graphs = ( + [ + g.copy() for g in self._graphs + ] if self.copy_graphs else self._graphs + ) + + start_time = time.time() + + # if self.parallel == 'imap_unordered': + # dis_matrix = self._compute_X_dm_imap_unordered(graphs, **kwargs) + if self.parallel in [ + 'imap_unordered', 'joblib', 'concurrent', 'multiprocessing' + ]: + dis_matrix = self._compute_X_dm_parallel(graphs, **kwargs) + elif self.parallel is None: + dis_matrix = self._compute_X_dm_series(graphs, **kwargs) + else: + raise Exception('Parallel mode is not set correctly.') + + self._run_time = time.time() - start_time + + if self.verbose: + print( + 'Distance matrix of size %d built in %s seconds.' + % (len(self._graphs), self._run_time) + ) + + return dis_matrix + + + def _compute_X_dm_series(self, graphs, **kwargs): + n = len(graphs) + dis_matrix = np.zeros((n, n)) + + iterator = combinations(range(n), 2) + len_itr = int(n * (n - 1) / 2) + if self.verbose: + print('Graphs in total: %d.' % len(graphs)) + print('The total # of pairs is %d.' % len_itr) + for i, j in get_iters( + iterator, desc='Computing distance matrix', + file=sys.stdout, verbose=(self.verbose >= 2), length=len_itr + ): + g1, g2 = graphs[i], graphs[j] + dis_matrix[i, j], _ = self.compute_ged(g1, g2, **kwargs) + dis_matrix[j, i] = dis_matrix[i, j] + return dis_matrix + + + # todo: this is not refactored yet. + def _compute_X_dm_parallel(self, graphs, **kwargs): + """ + Highly optimized parallelized version of distance matrix computation between graphs. + + Parameters: + ----------- + graphs : list + List of graph objects to compute pairwise distances + n_jobs : int, default=-1 + Number of parallel jobs. -1 means using all available cores. + chunk_size : int, default=None + Number of tasks per chunk. If None, will be auto-calculated. + memory_limit : str or int, default='auto' + Memory limit per worker in MB or 'auto' to determine automatically. + method : str, default='joblib' + Parallelization backend: 'joblib', 'concurrent', or 'multiprocessing' + + Returns: + -------- + np.ndarray + Distance matrix of shape (n, n) + """ + n = len(graphs) + + # Get all pairs of indices + pairs = list(combinations(range(n), 2)) + len_itr = len(pairs) + + n_jobs = self.n_jobs + chunksize = self.chunksize + method = self.parallel + memory_limit = kwargs.get('memory_limit', 'auto') + + if self.verbose: + print('Graphs in total: %d.' % len(graphs)) + print('The total # of pairs is %d.' % len_itr) + + # Determine number of processes + if n_jobs == -1: + n_jobs = os.cpu_count() - 1 + n_jobs = min(n_jobs, os.cpu_count(), len_itr) + + # Auto-calculate optimal chunk size if not provided + if chunksize is None: + # # this seems to be slightly faster when using `test_ged_model.py` + # # with 100 graphs (0.0012 s vs 0.0016 s per pair). Yet gets slower with + # # larger number of graphs (e.g., 1000) (~ 31 mins vs ~ 40 mins in total). + # if len_itr < 100 * n_jobs: + # chunksize = int(len_itr / n_jobs) + 1 + # else: + # chunksize = 100 + + # Balancing chunk size: larger chunks reduce overhead but limit load balancing + # A good heuristic is sqrt(len_itr / n_jobs) * 4 + chunksize = max(1, int(np.sqrt(len_itr / n_jobs) * 4)) + + if self.verbose >= 2: + print( + f"Running with {n_jobs} parallel processes and chunk size of {chunksize}" + ) + + # For networkx graphs, we need to use a Manager to share them between processes + with Manager() as manager: + # Create a managed shared list for the graphs + shared_graphs = manager.list(graphs) + + # Get a function reference to compute_ged that can be pickled + # Using a Python trick to make the instance method picklable + compute_ged_func = self.compute_ged + + # Create a shared memory array for results + with numpy_shared_memory((n, n), dtype=np.float64) as ( + dis_matrix, shm_name + ): + + # # Define worker function that updates the shared matrix directly + # def process_pair(pair): + # i, j = pair + # g1, g2 = graphs[i], graphs[j] + # + # try: + # # Access the shared memory + # existing_shm = shared_memory.SharedMemory(name=shm_name) + # shared_matrix = np.ndarray( + # (n, n), dtype=np.float64, buffer=existing_shm.buf + # ) + # + # # Compute distance - use graph indices to avoid serializing graphs + # distance = self.compute_ged(g1, g2, **kwargs) + # + # # Update the matrix with thread/process-safe approach + # # We're only writing to unique cells so no locking needed + # shared_matrix[i, j] = distance + # shared_matrix[j, i] = distance + # + # finally: + # # Clean up local shared memory reference + # if 'existing_shm' in locals(): + # existing_shm.close() + # + # return i, j, distance # Return for progress tracking + + # Create a partial function with fixed arguments - must use module-level function + worker = partial( + self._process_pair_worker, + graphs_manager=shared_graphs, + shm_name=shm_name, + matrix_shape=(n, n), + compute_ged_func=compute_ged_func, + **kwargs + ) + + try: + # Force garbage collection before starting parallel processing + gc.collect() + + # Three different parallelization options for different scenarios + if method == 'joblib': + if memory_limit == 'auto': + # Set max_nbytes according to the size of the shared memory: + # Get the size of the shared memory in bytes: + shm = shared_memory.SharedMemory(name=shm_name) + memory_limit = shm.size + shm.close() + if self.verbose >= 2: + print( + f"Setting memory limit to {memory_limit} bytes per process." + ) + + # Option 1: joblib - great for large datasets, memory control, possible caching + with joblib.parallel_backend( + 'loky', n_jobs=n_jobs, inner_max_num_threads=1, + mmap_mode='r', temp_folder='/tmp' + ): + results = joblib.Parallel( + verbose=self.verbose >= 2, + prefer="processes", + batch_size=chunksize, + pre_dispatch='2*n_jobs', + max_nbytes=memory_limit + )( + joblib.delayed(worker)(pair) for pair in pairs + ) + + elif method == 'concurrent': + # Option 2: ProcessPoolExecutor - cleaner API, slightly faster for CPU-bound tasks + with ProcessPoolExecutor( + max_workers=n_jobs + ) as executor: + futures = [executor.submit(worker, pair) for pair + in pairs] + + # Track progress if verbose + if self.verbose >= 2: + results = [] + for f in tqdm( + futures, total=len(futures), + desc='Computing distance matrix' + ): + results.append(f.result()) + else: + results = [f.result() for f in futures] + + elif method in ['imap_unordered' or 'multiprocessing']: + # Option 3: multiprocessing.Pool with imap_unordered - more control, classic approach + with multiprocessing.Pool(processes=n_jobs) as pool: + if self.verbose >= 2: + results = list( + tqdm( + pool.imap_unordered( + worker, pairs, + chunksize=chunksize + ), + total=len_itr, + desc='Computing distance matrix', + file=sys.stdout + ) + ) + else: + results = list( + pool.imap_unordered( + worker, pairs, chunksize=chunksize + ) + ) + + else: + raise ValueError( + f"Unsupported parallelization method: {method}." + ) + + # Copy the result from shared memory to a regular numpy array + result = dis_matrix.copy() + + except Exception as e: + # Make sure we log any errors that occur during parallel execution + if self.verbose: + print(f"Error during parallel execution: {e}.") + raise + + # At this point, the Manager will automatically clean up shared resources + + return result + + + @staticmethod + def _process_pair_worker( + pair, graphs_manager, shm_name, matrix_shape, + compute_ged_func, **kwargs + ): + """Worker function that processes a pair of graphs and updates the shared matrix. + Must be defined at module level to be picklable.""" + i, j = pair + + # Access the shared graphs from the manager + g1 = graphs_manager[i] + g2 = graphs_manager[j] + + try: + # Access the shared memory + existing_shm = shared_memory.SharedMemory(name=shm_name) + shared_matrix = np.ndarray( + matrix_shape, dtype=np.float64, buffer=existing_shm.buf + ) + + # Compute distance using the function reference + distance, _ = compute_ged_func(g1, g2, **kwargs) + + # Update the matrix + shared_matrix[i, j] = distance + shared_matrix[j, i] = distance + + finally: + # Clean up local shared memory reference + if 'existing_shm' in locals(): + existing_shm.close() + + return i, j, distance # Return for progress tracking + + + def _compute_cross_distance_matrix_series(self, graphs1, graphs2, **kwargs): + """Compute the GED distance matrix between two sets of graphs (X and Y) + without parallelization. + + Parameters + ---------- + X, Y : list of graphs + The input graphs. + + Returns + ------- + dis_matrix : numpy array, shape = [n_X, n_Y] + The computed distance matrix. + + """ + n1 = len(graphs1) + n2 = len(graphs2) + + # Initialize distance matrix with zeros + dis_matrix = np.zeros((n1, n2)) + + # Cross set case: compute all pairs between the two sets + iterator = product(range(n1), range(n2)) + len_itr = n1 * n2 + + if self.verbose: + print(f'Computing distances between {n1} and {n2} graphs.') + print(f'The total # of pairs is {len_itr}.') + + for i, j in get_iters( + iterator, desc='Computing distance matrix', + file=sys.stdout, verbose=(self.verbose >= 2), length=len_itr + ): + g1, g2 = graphs1[i], graphs2[j] + dis_matrix[i, j], _ = self.compute_ged(g1, g2, **kwargs) + + return dis_matrix + + + def _compute_cross_distance_matrix_parallel( + self, graphs1, graphs2, **kwargs + ): + """Compute the GED distance matrix between two sets of graphs (X and Y) + with parallelization. + + Parameters + ---------- + X, Y : list of graphs + The input graphs. + + Returns + ------- + dis_matrix : numpy array, shape = [n_X, n_Y] + The computed distance matrix. + + References + ---------- + This method is written with the help of the Claude 3.7 Sonnet AI, + accessed on 2025.05.15. + + + todo: this can be merged with the _compute_X_dm_parallel method. + """ + # Handle the case where graphs2 is not provided + is_same_set = graphs2 is None + if is_same_set: + graphs2 = graphs1 + + n1 = len(graphs1) + n2 = len(graphs2) + + # Get all pairs of indices to compute + if is_same_set: + # Only compute upper triangular portion for efficiency when comparing within same set + pairs = list(combinations(range(n1), 2)) + else: + # Compute all pairs when comparing between different sets + pairs = list(product(range(n1), range(n2))) + + len_itr = len(pairs) + + n_jobs = self.n_jobs + chunksize = self.chunksize + method = self.parallel + memory_limit = kwargs.get('memory_limit', 'auto') + + if self.verbose: + if is_same_set: + print(f'Graphs in total: {n1}.') + else: + print(f'Computing distances between {n1} and {n2} graphs.') + print(f'The total # of pairs is {len_itr}.') + + # Determine number of processes + if n_jobs == -1: + n_jobs = os.cpu_count() - 1 + n_jobs = min(n_jobs, os.cpu_count(), len_itr) + + # Auto-calculate optimal chunk size if not provided + if chunksize is None: + # # this seems to be slightly faster when using `test_ged_model.py` + # # with 100 graphs (0.0012 s vs 0.0016 s per pair). Yet gets slower with + # # larger number of graphs (e.g., 1000) (~ 31 mins vs ~ 40 mins in total). + # if len_itr < 100 * n_jobs: + # chunksize = int(len_itr / n_jobs) + 1 + # else: + # chunksize = 100 + + # Balancing chunk size: larger chunks reduce overhead but limit load balancing + # A good heuristic is sqrt(len_itr / n_jobs) * 4 + chunksize = max(1, int(np.sqrt(len_itr / n_jobs) * 4)) + + if self.verbose >= 2: + print( + f"Running with {n_jobs} parallel processes and chunk size of {chunksize}" + ) + + # For networkx graphs, we need to use a Manager to share them between processes + with Manager() as manager: + # Create managed shared lists for both graph sets + shared_graphs1 = manager.list(graphs1) + shared_graphs2 = manager.list(graphs2) + + # Get a function reference to compute_ged that can be pickled + # Using a Python trick to make the instance method picklable + compute_ged_func = self.compute_ged + + # Create a shared memory array for results + with numpy_shared_memory((n1, n2), dtype=np.float64) as ( + dis_matrix, shm_name + ): + # Create a partial function with fixed arguments - MUST NOT use + # inline function here, as it won't be picklable: + worker = partial( + self._process_pair_worker_cross, + graphs1_manager=shared_graphs1, + graphs2_manager=shared_graphs2, + shm_name=shm_name, + matrix_shape=(n1, n2), + compute_ged_func=compute_ged_func, + is_same_set=is_same_set, + **kwargs + ) + + try: + # Force garbage collection before starting parallel processing + gc.collect() + + # Three different parallelization options for different scenarios + if method == 'joblib': + if memory_limit == 'auto': + # Set max_nbytes according to the size of the shared memory: + # Get the size of the shared memory in bytes: + shm = shared_memory.SharedMemory(name=shm_name) + memory_limit = shm.size + shm.close() + if self.verbose >= 2: + print( + f"Setting memory limit to {memory_limit} bytes per process." + ) + + # Option 1: joblib - great for large datasets, memory control, possible caching + with joblib.parallel_backend( + 'loky', n_jobs=n_jobs, inner_max_num_threads=1, + mmap_mode='r', temp_folder='/tmp' + ): + results = joblib.Parallel( + verbose=self.verbose >= 2, + prefer="processes", + batch_size=chunksize, + pre_dispatch='2*n_jobs', + max_nbytes=memory_limit + )( + joblib.delayed(worker)(pair) for pair in pairs + ) + + elif method == 'concurrent': + # Option 2: ProcessPoolExecutor - cleaner API, slightly faster for CPU-bound tasks + with ProcessPoolExecutor( + max_workers=n_jobs + ) as executor: + futures = [executor.submit(worker, pair) for pair + in pairs] + + # Track progress if verbose + if self.verbose >= 2: + results = [] + for f in tqdm( + futures, total=len(futures), + desc='Computing distance matrix' + ): + results.append(f.result()) + else: + results = [f.result() for f in futures] + + elif method in ['imap_unordered' or 'multiprocessing']: + # Option 3: multiprocessing.Pool with imap_unordered - more control, classic approach + with multiprocessing.Pool(processes=n_jobs) as pool: + if self.verbose >= 2: + results = list( + tqdm( + pool.imap_unordered( + worker, pairs, + chunksize=chunksize + ), + total=len_itr, + desc='Computing distance matrix', + file=sys.stdout + ) + ) + else: + results = list( + pool.imap_unordered( + worker, pairs, chunksize=chunksize + ) + ) + + else: + raise ValueError( + f"Unsupported parallelization method: {method}." + ) + + # Copy the result from shared memory to a regular numpy array + result = dis_matrix.copy() + + except Exception as e: + # Make sure we log any errors that occur during parallel execution + if self.verbose: + print(f"Error during parallel execution: {e}.") + raise + + # At this point, the Manager will automatically clean up shared resources + + return result + + + @staticmethod + def _process_pair_worker_cross( + pair, graphs1_manager, graphs2_manager, shm_name, matrix_shape, + compute_ged_func, is_same_set=False, **kwargs + ): + """Worker function that processes a pair of graphs and updates the shared matrix. + Must be defined at module level to be picklable.""" + i, j = pair + + # Access the shared graphs from the manager + g1 = graphs1_manager[i] + g2 = graphs2_manager[j] + + try: + # Access the shared memory + existing_shm = shared_memory.SharedMemory(name=shm_name) + shared_matrix = np.ndarray( + matrix_shape, dtype=np.float64, buffer=existing_shm.buf + ) + + # Compute distance using the function reference + distance, _ = compute_ged_func(g1, g2, **kwargs) + + # Update the matrix + shared_matrix[i, j] = distance + + # If computing within the same set, update symmetric position: + if is_same_set and i != j: + shared_matrix[j, i] = distance + + finally: + # Clean up local shared memory reference + if 'existing_shm' in locals(): + existing_shm.close() + + return i, j, distance # Return for progress tracking + + + def _wrapper_compute_ged(self, itr): + i = itr[0] + j = itr[1] + # @TODO: repeats are not considered here. + dis, _ = self.compute_ged(G_gn[i], G_gn[j]) + return i, j, dis + + + def compute_ged(self, Gi, Gj, **kwargs): + """ + Compute GED between two graphs according to edit_cost. + """ + env_type = self.get_env_type(graph=Gi) + ged_options = { + 'env_type': env_type, + 'edit_cost': self.edit_cost_fun, + 'method': self.ed_method, + 'edit_cost_constants': self._edit_cost_constants, + 'edit_cost_config': self.edit_cost_config, + } + repeats = kwargs.get('repeats', 1) + dis, pi_forward, pi_backward = pairwise_ged( + Gi, Gj, ged_options, repeats=repeats + ) + # @TODO: Better to have a if here. + # if self.compute_n_eo: + # n_eo_tmp = get_nb_edit_operations( + # Gi, Gj, pi_forward, pi_backward, + # edit_cost=self.edit_cost_fun, + # node_labels=self.node_labels, edge_labels=self.edge_labels + # ) + # else: + # n_eo_tmp = None + # return dis, n_eo_tmp + return dis, None + + + def get_env_type(self, graph: nx.Graph | None = None): + """ + Check the environment type of the graph. + If `env_type` is set on initialization, return it. + Otherwise, check the given graph's node and edge labels to determine the type. + + Only one node and one edge are checked to determine the type. + This function expects that all nodes have the same type of labels, so as all + edges. + """ + if self.env_type is not None: + return self.env_type + if graph is None: + raise ValueError( + 'Graph is not provided while `env_type` not set on initialization. ' + 'Cannot determine environment type.' + ) + # Use 'gxl' env type only if all nodes and edge labes are strings, and at least one + # node or edge label is present: + one_n_labels = graph.nodes[list(graph.nodes)[0]] + for k, v in one_n_labels.items(): + if not isinstance(v, str): + return 'attr' + if nx.number_of_edges(graph) != 0: + one_e_labels = graph.edges[list(graph.edges)[0]] + for k, v in one_e_labels.items(): + if not isinstance(v, str): + return 'attr' + if len(one_n_labels) > 0 or ( + nx.number_of_edges(graph) != 0 and len(one_e_labels) > 0 + ): + return 'gxl' + return 'attr' + + + + # def _compute_kernel_list(self, g1, g_list): + # start_time = time.time() + + # if self.parallel == 'imap_unordered': + # kernel_list = self._compute_kernel_list_imap_unordered(g1, g_list) + # elif self.parallel is None: + # kernel_list = self._compute_kernel_list_series(g1, g_list) + # else: + # raise Exception('Parallel mode is not set correctly.') + + # self._run_time = time.time() - start_time + # if self.verbose: + # print('Graph kernel bewteen a graph and a list of %d graphs built in %s seconds.' + # % (len(g_list), self._run_time)) + + # return kernel_list + + # def _compute_kernel_list_series(self, g1, g_list): + # pass + + # def _compute_kernel_list_imap_unordered(self, g1, g_list): + # pass + + # def _compute_single_kernel(self, g1, g2): + # start_time = time.time() + + # kernel = self._compute_single_kernel_series(g1, g2) + + # self._run_time = time.time() - start_time + # if self.verbose: + # print('Graph kernel bewteen two graphs built in %s seconds.' % (self._run_time)) + + # return kernel + + # def _compute_single_kernel_series(self, g1, g2): + # pass + + def is_graph(self, graph): + if isinstance(graph, nx.Graph): + return True + if isinstance(graph, nx.DiGraph): + return True + if isinstance(graph, nx.MultiGraph): + return True + if isinstance(graph, nx.MultiDiGraph): + return True + return False + + + def __repr__(self): + return ( + f"{self.__class__.__name__}(" + f"optim_method={self.optim_method}, " + f"ed_method={self.ed_method}, " + f"edit_cost_fun={self.edit_cost_fun}, " + f"node_labels={self.node_labels}, " + f"edge_labels={self.edge_labels}, " + f"optim_options={self.optim_options}, " + f"init_edit_cost_constants={self.init_edit_cost_constants}, " + f"copy_graphs={self.copy_graphs}, " + f"parallel={self.parallel}, " + f"n_jobs={self.n_jobs}, " + f"verbose={self.verbose}, " + f"normalize={self.normalize}, " if hasattr( + self, 'normalize' + ) else "" + f"run_time={self.run_time}" + f")" + ) + + + @property + def graphs(self): + return self._graphs + + + # @property + # def parallel(self): + # return self.parallel + + # @property + # def n_jobs(self): + # return self.n_jobs + + # @property + # def verbose(self): + # return self.verbose + + # @property + # def normalize(self): + # return self.normalize + + @property + def run_time(self): + return self._run_time + + + @property + def test_run_time(self): + return self._test_run_time + + + @property + def dis_matrix(self): + return self._dm_train + + + @dis_matrix.setter + def dis_matrix(self, value): + self._dm_train = value + + + @property + def metric_matrix(self): + return self._dm_train + + + @metric_matrix.setter + def metric_matrix(self, value): + self._dm_train = value + + + @property + def edit_cost_constants(self): + return self._edit_cost_constants + + + # @property + # def gram_matrix_unnorm(self): + # return self._gram_matrix_unnorm + + # @gram_matrix_unnorm.setter + # def gram_matrix_unnorm(self, value): + # self._gram_matrix_unnorm = value + + @property + def n_pairs(self): + """ + The number of pairs of graphs between which the GEDs are computed. + """ + try: + check_is_fitted(self, '_dm_train') + return len(self._dm_train) * (len(self._dm_train) - 1) / 2 + except NotFittedError: + return None + + +def _init_worker_ged_mat(gn_toshare): + global G_gn + G_gn = gn_toshare + + +# Context manager for shared memory with automatic cleanup +@contextmanager +def numpy_shared_memory(shape, dtype=np.float64): + """Create a numpy array in shared memory that automatically cleans up.""" + size = int(np.prod(shape)) * np.dtype(dtype).itemsize + shm = shared_memory.SharedMemory(create=True, size=size) + try: + array = np.ndarray(shape, dtype=dtype, buffer=shm.buf) + array.fill(0) # Initialize with zeros + yield array, shm.name + finally: + shm.close() + shm.unlink() + + diff --git a/gklearn/experiments/ged/ged_model/graph_generator.py b/gklearn/experiments/ged/ged_model/graph_generator.py new file mode 100644 index 0000000000..2fb2eac59c --- /dev/null +++ b/gklearn/experiments/ged/ged_model/graph_generator.py @@ -0,0 +1,209 @@ +""" +graph_generator + +@Author: jajupmochi +@Date: May 22 2025 +""" +import string + + +class GraphGenerator: + """ + A class to generate random graphs for the Graph Edit Distance (GED) model with given + constraints. + + Attributes: + - num_graphs: Number of graphs to generate. + - max_num_nodes: Maximum number of nodes in each graph. + - min_num_nodes: Minimum number of nodes in each graph. + - max_num_edges: Maximum number of edges in each graph. + - min_num_edges: Minimum number of edges in each graph. + - with_discrete_n_features: Whether to include discrete node features. + - with_discrete_e_features: Whether to include discrete edge features. + - with_continuous_n_features: Whether to include continuous node features. + - with_continuous_e_features: Whether to include continuous edge features. + - node_features: List of node feature names. Optional. + - edge_features: List of edge feature names. Optional. + - node_feature_values: Dictionary mapping node feature names to their possible values. Optional. + - edge_feature_values: Dictionary mapping edge feature names to their possible values. Optional. + - seed: Random seed for reproducibility. Default is None. + """ + + + def __init__( + self, + num_graphs: int, + max_num_nodes: int, + min_num_nodes: int, + max_num_edges: int, + min_num_edges: int, + node_feat_type: str | None = 'str', + edge_feat_type: str | None = 'str', + with_discrete_n_features: bool = False, + with_discrete_e_features: bool = False, + with_continuous_n_features: bool = False, + with_continuous_e_features: bool = False, + continuous_n_feature_key: str | None = 'feature', + continuous_e_feature_key: str | None = 'feature', + continuous_n_feature_dim: int | None = 10, + continuous_e_feature_dim: int | None = 10, + node_features: list = None, + edge_features: list = None, + node_feature_values: dict = None, + edge_feature_values: dict = None, + seed: int = None + ): + self.num_graphs = num_graphs + self.max_num_nodes = max_num_nodes + self.min_num_nodes = min_num_nodes + self.max_num_edges = max_num_edges + self.min_num_edges = min_num_edges + self.node_feat_type = node_feat_type + self.edge_feat_type = edge_feat_type + self.with_discrete_n_features = with_discrete_n_features + self.with_discrete_e_features = with_discrete_e_features + self.with_continuous_n_features = with_continuous_n_features + self.with_continuous_e_features = with_continuous_e_features + self.continuous_n_feature_key = continuous_n_feature_key + self.continuous_e_feature_key = continuous_e_feature_key + self.continuous_n_feature_dim = continuous_n_feature_dim + self.continuous_e_feature_dim = continuous_e_feature_dim + self.node_features = node_features if node_features else [] + self.edge_features = edge_features if edge_features else [] + self.node_feature_values = node_feature_values if node_feature_values else {} + self.edge_feature_values = edge_feature_values if edge_feature_values else {} + self.seed = seed + if with_discrete_n_features and node_features is None: + self.discrete_n_features = self.generate_discrete_feats() + if with_discrete_e_features and edge_features is None: + self.discrete_e_features = self.generate_discrete_feats() + + + def generate_discrete_feats(self): + + if self.node_feat_type == 'str': + discrete_features = self.generate_symbolic_feats() + elif self.node_feat_type == 'int': + discrete_features = [i for i in range(1, 100)] + elif self.node_feat_type == 'float': + discrete_features = None + else: + raise ValueError( + "node_feat_type must be 'str' or 'int'." + ) + return discrete_features + + + def generate_symbolic_feats(self): + flist = list(string.ascii_lowercase) + # Combine some letters to make them more realistic: + count = 0 + for i in string.ascii_lowercase: + for j in string.ascii_lowercase: + if i != j: + flist.append(i + j) + count += 1 + if count >= 26: + break + return flist + + + def generate_graphs(self): + """ + Generates a list of random graphs based on the specified constraints. + + Returns: + List of generated graphs. + """ + import numpy as np + import networkx as nx + + rng = np.random.default_rng(self.seed) + + graphs = [] + + for _ in range(self.num_graphs): + num_nodes = rng.integers(self.min_num_nodes, self.max_num_nodes + 1) + num_edges = rng.integers(self.min_num_edges, self.max_num_edges + 1) + + G = nx.Graph() + G.add_nodes_from(range(num_nodes)) + + if num_edges > 0: + edge_pairs = num_nodes * (num_nodes - 1) // 2 + if num_edges > edge_pairs: + num_edges = edge_pairs + # Generate random edges: + edges = rng.choice( + edge_pairs, num_edges, replace=False + ) + for edge in edges: + u = edge // (num_nodes - 1) + v = edge % (num_nodes - 1) + if v >= u: + v += 1 + G.add_edge(u, v) + + # Add discrete node features: + if self.with_discrete_n_features: + if self.node_feature_values == {}: + for node in G.nodes(): + G.nodes[node]['feature'] = rng.choice( + self.discrete_n_features + ).item() + + else: + pass + # for node in G.nodes(): + # for feature in self.node_features: + # G.nodes[node][feature] = random.choice( + # self.node_feature_values.get(feature, [0]) + # ) + + if self.with_discrete_e_features: + if self.edge_feature_values == {}: + for edge in G.edges(): + G.edges[edge]['feature'] = rng.choice( + self.discrete_e_features + ).item() + else: + pass + # for edge in G.edges(): + # for feature in self.edge_features: + # G.edges[edge][feature] = random.choice( + # self.edge_feature_values.get(feature, [0]) + # ) + + if self.with_continuous_n_features: + if self.node_feature_values == {}: + for node in G.nodes(): + feature = rng.random(self.continuous_n_feature_dim) + G.nodes[node][self.continuous_n_feature_key] = feature + + else: + pass + # for node in G.nodes(): + # for feature in self.node_features: + # G.nodes[node][feature] = random.uniform( + # self.node_feature_values.get(feature, (0, 1))[0], + # self.node_feature_values.get(feature, (0, 1))[1] + # ) + + if self.with_continuous_e_features: + if self.edge_feature_values == {}: + for edge in G.edges(): + feature = rng.random(self.continuous_e_feature_dim) + G.edges[edge][self.continuous_e_feature_key] = feature + + else: + pass + # for edge in G.edges(): + # for feature in self.edge_features: + # G.edges[edge][feature] = random.uniform( + # self.edge_feature_values.get(feature, (0, 1))[0], + # self.edge_feature_values.get(feature, (0, 1))[1] + # ) + + graphs.append(G) + + return graphs diff --git a/gklearn/ged/util/util.py b/gklearn/ged/util/util.py index 3abc2a38ba..44c6a32835 100644 --- a/gklearn/ged/util/util.py +++ b/gklearn/ged/util/util.py @@ -5,14 +5,16 @@ @author: ljia """ -import numpy as np -from itertools import combinations import multiprocessing -from multiprocessing import Pool -from functools import partial import sys +from functools import partial +from itertools import combinations +from multiprocessing import Pool + # from tqdm import tqdm import networkx as nx +import numpy as np + from gklearn.ged.env import GEDEnv from gklearn.utils import get_iters @@ -71,25 +73,30 @@ def pairwise_ged( - For methods such as BIPARTITE, the repeats may result same results. - # of edit operations are not computed in this method. """ - from gklearn.gedlib import librariesImport, gedlibpy + from gklearn.gedlib import gedlibpy - ged_env = gedlibpy.GEDEnv() + ged_env = gedlibpy.GEDEnv(env_type=options.get('env_type', 'attr'), verbose=False) ged_env.set_edit_cost( options['edit_cost'], - edit_cost_constant=options['edit_cost_constants'] + edit_cost_constant=options['edit_cost_constants'], + **options.get('edit_cost_config') and { + 'edit_cost_config': options['edit_cost_config'] + } or {} ) ged_env.add_nx_graph(g1, '') ged_env.add_nx_graph(g2, '') - listID = ged_env.get_all_graph_ids() + list_id = ged_env.get_all_graph_ids() ged_env.init( - init_option=(options[ - 'init_option'] if 'init_option' in options else 'EAGER_WITHOUT_SHUFFLED_COPIES') + init_option=( + options[ + 'init_option'] if 'init_option' in options else 'EAGER_WITHOUT_SHUFFLED_COPIES' + ) ) ged_env.set_method(options['method'], ged_options_to_string(options)) ged_env.init_method() - g = listID[0] - h = listID[1] + g = list_id[0] + h = list_id[1] dis_min = np.inf # print('------------------------------------------') for i in range(0, repeats): diff --git a/gklearn/gedlib/gedlibpy.py b/gklearn/gedlib/gedlibpy.py new file mode 100644 index 0000000000..37c0f9c9c6 --- /dev/null +++ b/gklearn/gedlib/gedlibpy.py @@ -0,0 +1,46 @@ +""" +gedlibpy.py + +@Author: jajupmochi +@Date: Jun 02 2025 +""" +# from gklearn.gedlib import libraries_import + + +def GEDEnv(env_type: str = 'attr', verbose: bool = False): + """ + Return the GED environment with the specified type and name. + + Parameters + ---------- + env_type: str + The type of the GED environment. Default is 'attr'. The available types are: + - 'attr': Attribute-based environment (with complex node and edge labels). + - 'gxl' or 'str': GXLLabel environment (with string labels). + """ + if env_type == 'attr': + if verbose: + print('Using Attribute-based GED environment.') + from gklearn.gedlib import libraries_import, gedlibpy_attr + return gedlibpy_attr.GEDEnvAttr() + elif env_type in ['gxl', 'str']: + if verbose: + print('Using GXLLabel GED environment.') + from gklearn.gedlib import libraries_import, gedlibpy_gxl + return gedlibpy_gxl.GEDEnvGXL() + else: + raise ValueError( + f'Unknown GED environment type: {env_type}. ' + f'Available types are: "attr", "gxl", "str".' + ) + + +if __name__ == '__main__': + # Example usage + ged_env = GEDEnv('gxl') + ged_env.set_edit_cost('NON_SYMBOLIC', [3, 3, 1, 3, 3, 1]) + print('Edit costs set successfully for GEDEnvGXL.') + + ged_env_attr = GEDEnv('attr') + ged_env_attr.set_edit_cost('GEOMETRIC', [3, 3, 1, 3, 3, 1]) + print('Edit costs set successfully for GEDEnvAttr.') diff --git a/gklearn/gedlib/gedlibpy.pyx b/gklearn/gedlib/gedlibpy.pyx old mode 100755 new mode 100644 index 5dc33b7e36..c575cb1ad2 --- a/gklearn/gedlib/gedlibpy.pyx +++ b/gklearn/gedlib/gedlibpy.pyx @@ -1,15 +1,15 @@ # distutils: language = c++ """ - Python GedLib module + Python GedLib module for the GXLLabel version (string labels) ====================== - - This module allow to use a C++ library for edit distance between graphs (GedLib) with Python. - + This module allows to use a C++ library for edit distance between graphs (GedLib) with Python. + + Authors ------------------- - + David Blumenthal Natacha Lambert Linlin Jia @@ -18,7 +18,7 @@ Classes & Functions ------------------- - + """ ################################# @@ -32,40 +32,47 @@ from libcpp.string cimport string from libcpp.map cimport map from libcpp cimport bool from libcpp.pair cimport pair -from libcpp.list cimport list #Long unsigned int equivalent cimport numpy as cnp + ctypedef cnp.npy_uint32 UINT32_t -from cpython cimport array - + cdef extern from "src/GedLibBind.hpp" namespace "pyged": - cdef vector[string] getEditCostStringOptions() except + cdef vector[string] getMethodStringOptions() except + cdef vector[string] getInitStringOptions() except + cdef size_t getDummyNode() except + - + cdef cppclass PyGEDEnv: PyGEDEnv() except + bool isInitialized() except + void restartEnv() except + - void loadGXLGraph(string pathFolder, string pathXML, bool node_type, bool edge_type) except + - pair[size_t,size_t] getGraphIds() except + + void loadGXLGraph( + string pathFolder, string pathXML, bool node_type, bool edge_type + ) except + + pair[size_t, size_t] getGraphIds() except + vector[size_t] getAllGraphIds() except + string getGraphClass(size_t id) except + string getGraphName(size_t id) except + size_t addGraph(string name, string classe) except + - void addNode(size_t graphId, string nodeId, map[string, string] nodeLabel) except + - void addEdge(size_t graphId, string tail, string head, map[string, string] edgeLabel, bool ignoreDuplicates) except + + void addNode( + size_t graphId, string nodeId, map[string, string] nodeLabel + ) except + + void addEdge( + size_t graphId, string tail, string head, map[string, string] edgeLabel, + bool ignoreDuplicates + ) except + void clearGraph(size_t graphId) except + size_t getGraphInternalId(size_t graphId) except + size_t getGraphNumNodes(size_t graphId) except + size_t getGraphNumEdges(size_t graphId) except + vector[string] getGraphOriginalNodeIds(size_t graphId) except + vector[map[string, string]] getGraphNodeLabels(size_t graphId) except + - map[pair[size_t, size_t], map[string, string]] getGraphEdges(size_t graphId) except + + map[pair[size_t, size_t], map[string, string]] getGraphEdges( + size_t graphId + ) except + vector[vector[size_t]] getGraphAdjacenceMatrix(size_t graphId) except + void setEditCost(string editCost, vector[double] editCostConstant) except + void setPersonalEditCost(vector[double] editCostConstant) except + @@ -81,33 +88,45 @@ cdef extern from "src/GedLibBind.hpp" namespace "pyged": size_t getNodeImage(size_t g, size_t h, size_t nodeId) except + size_t getNodePreImage(size_t g, size_t h, size_t nodeId) except + double getInducedCost(size_t g, size_t h) except + - vector[pair[size_t,size_t]] getNodeMap(size_t g, size_t h) except + + vector[pair[size_t, size_t]] getNodeMap(size_t g, size_t h) except + vector[vector[int]] getAssignmentMatrix(size_t g, size_t h) except + vector[vector[cnp.npy_uint64]] getAllMap(size_t g, size_t h) except + double getRuntime(size_t g, size_t h) except + bool quasimetricCosts() except + vector[vector[size_t]] hungarianLSAP(vector[vector[size_t]] matrixCost) except + - vector[vector[double]] hungarianLSAPE(vector[vector[double]] matrixCost) except + + vector[vector[double]] hungarianLSAPE( + vector[vector[double]] matrixCost + ) except + # added by Linlin Jia. size_t getNumNodeLabels() except + map[string, string] getNodeLabel(size_t label_id) except + size_t getNumEdgeLabels() except + map[string, string] getEdgeLabel(size_t label_id) except + -# size_t getNumNodes(size_t graph_id) except + + # size_t getNumNodes(size_t graph_id) except + double getAvgNumNodes() except + - double getNodeRelCost(map[string, string] & node_label_1, map[string, string] & node_label_2) except + + double getNodeRelCost( + map[string, string] & node_label_1, map[string, string] & node_label_2 + ) except + double getNodeDelCost(map[string, string] & node_label) except + double getNodeInsCost(map[string, string] & node_label) except + - map[string, string] getMedianNodeLabel(vector[map[string, string]] & node_labels) except + - double getEdgeRelCost(map[string, string] & edge_label_1, map[string, string] & edge_label_2) except + + map[string, string] getMedianNodeLabel( + vector[map[string, string]] & node_labels + ) except + + double getEdgeRelCost( + map[string, string] & edge_label_1, map[string, string] & edge_label_2 + ) except + double getEdgeDelCost(map[string, string] & edge_label) except + double getEdgeInsCost(map[string, string] & edge_label) except + - map[string, string] getMedianEdgeLabel(vector[map[string, string]] & edge_labels) except + + map[string, string] getMedianEdgeLabel( + vector[map[string, string]] & edge_labels + ) except + string getInitType() except + -# double getNodeCost(size_t label1, size_t label2) except + - double computeInducedCost(size_t g_id, size_t h_id, vector[pair[size_t,size_t]]) except + - - + # double getNodeCost(size_t label1, size_t label2) except + + double computeInducedCost( + size_t g_id, size_t h_id, vector[pair[size_t, size_t]] + ) except + + + ############################# ##CYTHON WRAPPER INTERFACES## ############################# @@ -120,112 +139,110 @@ from gklearn.ged.env import NodeMap # import librariesImport from ctypes import * import os -lib1 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/fann.2.2.0/libdoublefann.so') -lib2 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/libsvm.3.22/libsvm.so') -lib3 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libnomad.so') -lib4 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libsgtelib.so') - -def get_edit_cost_options() : +lib1 = cdll.LoadLibrary( + os.path.dirname(os.path.realpath(__file__)) + '/lib/fann.2.2.0/libdoublefann.so' + ) +lib2 = cdll.LoadLibrary( + os.path.dirname(os.path.realpath(__file__)) + '/lib/libsvm.3.22/libsvm.so' + ) +lib3 = cdll.LoadLibrary( + os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libnomad.so' + ) +lib4 = cdll.LoadLibrary( + os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libsgtelib.so' + ) + +def get_edit_cost_options(): """ Searchs the differents edit cost functions and returns the result. - + :return: The list of edit cost functions :rtype: list[string] - - .. warning:: This function is useless for an external use. Please use directly list_of_edit_cost_options. + + .. warning:: This function is useless for an external use. Please use directly list_of_edit_cost_options. .. note:: Prefer the list_of_edit_cost_options attribute of this module. """ - - return [option.decode('utf-8') for option in getEditCostStringOptions()] + return [option.decode('utf-8') for option in getEditCostStringOptions()] -def get_method_options() : +def get_method_options(): """ Searchs the differents method for edit distance computation between graphs and returns the result. - + :return: The list of method to compute the edit distance between graphs :rtype: list[string] - + .. warning:: This function is useless for an external use. Please use directly list_of_method_options. .. note:: Prefer the list_of_method_options attribute of this module. """ return [option.decode('utf-8') for option in getMethodStringOptions()] - -def get_init_options() : +def get_init_options(): """ Searchs the differents initialization parameters for the environment computation for graphs and returns the result. - + :return: The list of options to initialize the computation environment :rtype: list[string] - + .. warning:: This function is useless for an external use. Please use directly list_of_init_options. .. note:: Prefer the list_of_init_options attribute of this module. """ return [option.decode('utf-8') for option in getInitStringOptions()] - -def get_dummy_node() : +def get_dummy_node(): """ Returns the ID of a dummy node. :return: The ID of the dummy node (18446744073709551614 for my computer, the hugest number possible) :rtype: size_t - - .. note:: A dummy node is used when a node isn't associated to an other node. + + .. note:: A dummy node is used when a node isn't associated to an other node. """ return getDummyNode() - # @cython.auto_pickle(True) cdef class GEDEnv: """Cython wrapper class for C++ class PyGEDEnv """ -# cdef PyGEDEnv c_env # Hold a C++ instance which we're wrapping - cdef PyGEDEnv* c_env # hold a pointer to the C++ instance which we're wrapping - + # cdef PyGEDEnv c_env # Hold a C++ instance which we're wrapping + cdef PyGEDEnv * c_env # hold a pointer to the C++ instance which we're wrapping def __cinit__(self): -# self.c_env = PyGEDEnv() + # self.c_env = PyGEDEnv() self.c_env = new PyGEDEnv() - - + def __dealloc__(self): del self.c_env + # def __reduce__(self): + # # return GEDEnv, (self.c_env,) + # return GEDEnv, tuple() -# def __reduce__(self): -# # return GEDEnv, (self.c_env,) -# return GEDEnv, tuple() - - - def is_initialized(self) : + def is_initialized(self): """ Checks and returns if the computation environment is initialized or not. - + :return: True if it's initialized, False otherwise :rtype: bool - - .. note:: This function exists for internals verifications but you can use it for your code. + + .. note:: This function exists for internals verifications but you can use it for your code. """ return self.c_env.isInitialized() - - - def restart_env(self) : + + def restart_env(self): """ - Restarts the environment variable. All data related to it will be delete. - - .. warning:: This function deletes all graphs, computations and more so make sure you don't need anymore your environment. - .. note:: You can now delete and add somes graphs after initialization so you can avoid this function. + Restarts the environment variable. All data related to it will be delete. + + .. warning:: This function deletes all graphs, computations and more so make sure you don't need anymore your environment. + .. note:: You can now delete and add somes graphs after initialization so you can avoid this function. """ self.c_env.restartEnv() - - def load_GXL_graphs(self, path_folder, path_XML, node_type, edge_type) : + def load_GXL_graphs(self, path_folder, path_XML, node_type, edge_type): """ - Loads some GXL graphes on the environment which is in a same folder, and present in the XMLfile. - + Loads some GXL graphes on the environment which is in a same folder, and present in the XMLfile. + :param path_folder: The folder's path which contains GXL graphs :param path_XML: The XML's path which indicates which graphes you want to load :param node_type: Select if nodes are labeled or unlabeled @@ -235,104 +252,101 @@ cdef class GEDEnv: :type node_type: bool :type edge_type: bool - - .. note:: You can call this function multiple times if you want, but not after an init call. + + .. note:: You can call this function multiple times if you want, but not after an init call. """ - self.c_env.loadGXLGraph(path_folder.encode('utf-8'), path_XML.encode('utf-8'), node_type, edge_type) + self.c_env.loadGXLGraph( + path_folder.encode('utf-8'), path_XML.encode('utf-8'), node_type, edge_type + ) - - def graph_ids(self) : + def graph_ids(self): """ - Searchs the first and last IDs of the loaded graphs in the environment. - + Searchs the first and last IDs of the loaded graphs in the environment. + :return: The pair of the first and the last graphs Ids :rtype: tuple(size_t, size_t) - - .. note:: Prefer this function if you have huges structures with lots of graphs. + + .. note:: Prefer this function if you have huges structures with lots of graphs. """ return self.c_env.getGraphIds() - - def get_all_graph_ids(self) : + def get_all_graph_ids(self): """ - Searchs all the IDs of the loaded graphs in the environment. - - :return: The list of all graphs's Ids + Searchs all the IDs of the loaded graphs in the environment. + + :return: The list of all graphs's Ids :rtype: list[size_t] - - .. note:: The last ID is equal to (number of graphs - 1). The order correspond to the loading order. + + .. note:: The last ID is equal to (number of graphs - 1). The order correspond to the loading order. """ return self.c_env.getAllGraphIds() - - def get_graph_class(self, id) : + def get_graph_class(self, id): """ Returns the class of a graph with its ID. - + :param id: The ID of the wanted graph :type id: size_t :return: The class of the graph which correpond to the ID :rtype: string - + .. seealso:: get_graph_class() - .. note:: An empty string can be a class. + .. note:: An empty string can be a class. """ return self.c_env.getGraphClass(id) - - def get_graph_name(self, id) : + def get_graph_name(self, id): """ - Returns the name of a graph with its ID. - + Returns the name of a graph with its ID. + :param id: The ID of the wanted graph :type id: size_t :return: The name of the graph which correpond to the ID :rtype: string - + .. seealso:: get_graph_class() - .. note:: An empty string can be a name. + .. note:: An empty string can be a name. """ return self.c_env.getGraphName(id).decode('utf-8') - - def add_graph(self, name="", classe="") : + def add_graph(self, name="", classe=""): """ - Adds a empty graph on the environment, with its name and its class. Nodes and edges will be add in a second time. - + Adds a empty graph on the environment, with its name and its class. Nodes and edges will be add in a second time. + :param name: The name of the new graph, an empty string by default :param classe: The class of the new graph, an empty string by default :type name: string :type classe: string :return: The ID of the newly graphe :rtype: size_t - + .. seealso::add_node(), add_edge() , add_symmetrical_edge() - .. note:: You can call this function without parameters. You can also use this function after initialization, call init() after you're finished your modifications. + .. note:: You can call this function without parameters. You can also use this function after initialization, call init() after you're finished your modifications. """ return self.c_env.addGraph(name.encode('utf-8'), classe.encode('utf-8')) - def add_node(self, graph_id, node_id, node_label): """ - Adds a node on a graph selected by its ID. A ID and a label for the node is required. - + Adds a node on a graph selected by its ID. A ID and a label for the node is required. + :param graph_id: The ID of the wanted graph :param node_id: The ID of the new node :param node_label: The label of the new node :type graph_id: size_t :type node_id: string :type node_label: dict{string : string} - + .. seealso:: add_graph(), add_edge(), add_symmetrical_edge() - .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. """ - self.c_env.addNode(graph_id, node_id.encode('utf-8'), encode_your_map(node_label)) + self.c_env.addNode( + graph_id, node_id.encode('utf-8'), encode_your_map(node_label) + ) - - def add_edge(self, graph_id, tail, head, edge_label, ignore_duplicates=True) : + def add_edge(self, graph_id, tail, head, edge_label, ignore_duplicates=True): """ - Adds an edge on a graph selected by its ID. - + Adds an edge on a graph selected by its ID. + :param graph_id: The ID of the wanted graph :param tail: The ID of the tail node for the new edge :param head: The ID of the head node for the new edge @@ -343,17 +357,19 @@ cdef class GEDEnv: :type head: string :type edge_label: dict{string : string} :type ignore_duplicates: bool - + .. seealso:: add_graph(), add_node(), add_symmetrical_edge() - .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. """ - self.c_env.addEdge(graph_id, tail.encode('utf-8'), head.encode('utf-8'), encode_your_map(edge_label), ignore_duplicates) + self.c_env.addEdge( + graph_id, tail.encode('utf-8'), head.encode('utf-8'), + encode_your_map(edge_label), ignore_duplicates + ) - - def add_symmetrical_edge(self, graph_id, tail, head, edge_label) : + def add_symmetrical_edge(self, graph_id, tail, head, edge_label): """ - Adds a symmetrical edge on a graph selected by its ID. - + Adds a symmetrical edge on a graph selected by its ID. + :param graph_id: The ID of the wanted graph :param tail: The ID of the tail node for the new edge :param head: The ID of the head node for the new edge @@ -362,9 +378,9 @@ cdef class GEDEnv: :type tail: string :type head: string :type edge_label: dict{string : string} - + .. seealso:: add_graph(), add_node(), add_edge() - .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. """ tailB = tail.encode('utf-8') headB = head.encode('utf-8') @@ -372,305 +388,293 @@ cdef class GEDEnv: self.c_env.addEdge(graph_id, tailB, headB, edgeLabelB, True) self.c_env.addEdge(graph_id, headB, tailB, edgeLabelB, True) - - def clear_graph(self, graph_id) : + def clear_graph(self, graph_id): """ Deletes a graph, selected by its ID, to the environment. - + :param graph_id: The ID of the wanted graph :type graph_id: size_t - - .. note:: Call init() after you're finished your modifications. + + .. note:: Call init() after you're finished your modifications. """ self.c_env.clearGraph(graph_id) - - def get_graph_internal_id(self, graph_id) : + def get_graph_internal_id(self, graph_id): """ - Searchs and returns the internal Id of a graph, selected by its ID. - + Searchs and returns the internal Id of a graph, selected by its ID. + :param graph_id: The ID of the wanted graph :type graph_id: size_t :return: The internal ID of the selected graph :rtype: size_t - + .. seealso:: get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() .. note:: These functions allow to collect all the graph's informations. """ return self.c_env.getGraphInternalId(graph_id) - - def get_graph_num_nodes(self, graph_id) : + def get_graph_num_nodes(self, graph_id): """ - Searchs and returns the number of nodes on a graph, selected by its ID. - + Searchs and returns the number of nodes on a graph, selected by its ID. + :param graph_id: The ID of the wanted graph :type graph_id: size_t :return: The number of nodes on the selected graph :rtype: size_t - + .. seealso:: get_graph_internal_id(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() .. note:: These functions allow to collect all the graph's informations. """ return self.c_env.getGraphNumNodes(graph_id) - - def get_graph_num_edges(self, graph_id) : + def get_graph_num_edges(self, graph_id): """ - Searchs and returns the number of edges on a graph, selected by its ID. - + Searchs and returns the number of edges on a graph, selected by its ID. + :param graph_id: The ID of the wanted graph :type graph_id: size_t :return: The number of edges on the selected graph :rtype: size_t - + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() .. note:: These functions allow to collect all the graph's informations. """ return self.c_env.getGraphNumEdges(graph_id) - - def get_original_node_ids(self, graph_id) : + def get_original_node_ids(self, graph_id): """ - Searchs and returns all th Ids of nodes on a graph, selected by its ID. - + Searchs and returns all th Ids of nodes on a graph, selected by its ID. + :param graph_id: The ID of the wanted graph :type graph_id: size_t :return: The list of IDs's nodes on the selected graph :rtype: list[string] - + .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() .. note:: These functions allow to collect all the graph's informations. """ - return [gid.decode('utf-8') for gid in self.c_env.getGraphOriginalNodeIds(graph_id)] + return [gid.decode('utf-8') for gid in + self.c_env.getGraphOriginalNodeIds(graph_id)] - - def get_graph_node_labels(self, graph_id) : + def get_graph_node_labels(self, graph_id): """ - Searchs and returns all the labels of nodes on a graph, selected by its ID. - + Searchs and returns all the labels of nodes on a graph, selected by its ID. + :param graph_id: The ID of the wanted graph :type graph_id: size_t :return: The list of nodes' labels on the selected graph :rtype: list[dict{string : string}] - + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_edges(), get_graph_adjacence_matrix() .. note:: These functions allow to collect all the graph's informations. """ - return [decode_your_map(node_label) for node_label in self.c_env.getGraphNodeLabels(graph_id)] + return [decode_your_map(node_label) for node_label in + self.c_env.getGraphNodeLabels(graph_id)] - - def get_graph_edges(self, graph_id) : + def get_graph_edges(self, graph_id): """ - Searchs and returns all the edges on a graph, selected by its ID. - + Searchs and returns all the edges on a graph, selected by its ID. + :param graph_id: The ID of the wanted graph :type graph_id: size_t :return: The list of edges on the selected graph :rtype: dict{tuple(size_t, size_t) : dict{string : string}} - + .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_adjacence_matrix() .. note:: These functions allow to collect all the graph's informations. """ return decode_graph_edges(self.c_env.getGraphEdges(graph_id)) - - def get_graph_adjacence_matrix(self, graph_id) : + def get_graph_adjacence_matrix(self, graph_id): """ - Searchs and returns the adjacence list of a graph, selected by its ID. - + Searchs and returns the adjacence list of a graph, selected by its ID. + :param graph_id: The ID of the wanted graph :type graph_id: size_t :return: The adjacence list of the selected graph :rtype: list[list[size_t]] - + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges() .. note:: These functions allow to collect all the graph's informations. """ return self.c_env.getGraphAdjacenceMatrix(graph_id) - - def set_edit_cost(self, edit_cost, edit_cost_constant = []) : + def set_edit_cost(self, edit_cost, edit_cost_constant = []): """ - Sets an edit cost function to the environment, if it exists. - + Sets an edit cost function to the environment, if it exists. + :param edit_cost: The name of the edit cost function :type edit_cost: string :param edi_cost_constant: The parameters you will add to the editCost, empty by default :type edit_cost_constant: list - + .. seealso:: list_of_edit_cost_options - .. note:: Try to make sure the edit cost function exists with list_of_edit_cost_options, raise an error otherwise. + .. note:: Try to make sure the edit cost function exists with list_of_edit_cost_options, raise an error otherwise. """ if edit_cost in list_of_edit_cost_options: edit_cost_b = edit_cost.encode('utf-8') self.c_env.setEditCost(edit_cost_b, edit_cost_constant) else: - raise EditCostError("This edit cost function doesn't exist, please see list_of_edit_cost_options for selecting a edit cost function") + raise EditCostError( + "This edit cost function doesn't exist, please see list_of_edit_cost_options for selecting a edit cost function" + ) - - def set_personal_edit_cost(self, edit_cost_constant = []) : + def set_personal_edit_cost(self, edit_cost_constant = []): """ Sets an personal edit cost function to the environment. - + :param edit_cost_constant: The parameters you will add to the editCost, empty by default :type edit_cost_constant: list - + .. seealso:: list_of_edit_cost_options, set_edit_cost() - .. note::You have to modify the C++ function to use it. Please see the documentation to add your Edit Cost function. + .. note::You have to modify the C++ function to use it. Please see the documentation to add your Edit Cost function. """ self.c_env.setPersonalEditCost(edit_cost_constant) - - def init(self, init_option='EAGER_WITHOUT_SHUFFLED_COPIES', print_to_stdout=False) : + def init(self, init_option='EAGER_WITHOUT_SHUFFLED_COPIES', print_to_stdout=False): """ Initializes the environment with the chosen edit cost function and graphs. - + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default :type init_option: string - + .. seealso:: list_of_init_options - .. warning:: No modification were allowed after initialization. Try to make sure your choices is correct. You can though clear or add a graph, but recall init() after that. + .. warning:: No modification were allowed after initialization. Try to make sure your choices is correct. You can though clear or add a graph, but recall init() after that. .. note:: Try to make sure the option exists with list_of_init_options or choose no options, raise an error otherwise. """ - if init_option in list_of_init_options: + if init_option in list_of_init_options: init_option_b = init_option.encode('utf-8') - self.c_env.initEnv(init_option_b, print_to_stdout) + self.c_env.initEnv(init_option_b, print_to_stdout) else: - raise InitError("This init option doesn't exist, please see list_of_init_options for selecting an option. You can choose any options.") + raise InitError( + "This init option doesn't exist, please see list_of_init_options for selecting an option. You can choose any options." + ) - - def set_method(self, method, options="") : + def set_method(self, method, options=""): """ - Sets a computation method to the environment, if its exists. - + Sets a computation method to the environment, if its exists. + :param method: The name of the computation method :param options: The options of the method (like bash options), an empty string by default :type method: string :type options: string - + .. seealso:: init_method(), list_of_method_options - .. note:: Try to make sure the edit cost function exists with list_of_method_options, raise an error otherwise. Call init_method() after your set. + .. note:: Try to make sure the edit cost function exists with list_of_method_options, raise an error otherwise. Call init_method() after your set. """ if method in list_of_method_options: method_b = method.encode('utf-8') self.c_env.setMethod(method_b, options.encode('utf-8')) else: - raise MethodError("This method doesn't exist, please see list_of_method_options for selecting a method") + raise MethodError( + "This method doesn't exist, please see list_of_method_options for selecting a method" + ) - - def init_method(self) : + def init_method(self): """ Inits the environment with the set method. - + .. seealso:: set_method(), list_of_method_options - .. note:: Call this function after set the method. You can't launch computation or change the method after that. + .. note:: Call this function after set the method. You can't launch computation or change the method after that. """ self.c_env.initMethod() - - def get_init_time(self) : + def get_init_time(self): """ Returns the initialization time. - + :return: The initialization time :rtype: double """ return self.c_env.getInitime() - - - def run_method(self, g, h) : + + def run_method(self, g, h): """ - Computes the edit distance between two graphs g and h, with the edit cost function and method computation selected. - + Computes the edit distance between two graphs g and h, with the edit cost function and method computation selected. + :param g: The Id of the first graph to compare :param h: The Id of the second graph to compare :type g: size_t :type h: size_t - + .. seealso:: get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() - .. note:: This function only compute the distance between two graphs, without returning a result. Use the differents function to see the result between the two graphs. + .. note:: This function only compute the distance between two graphs, without returning a result. Use the differents function to see the result between the two graphs. """ self.c_env.runMethod(g, h) - - - def get_upper_bound(self, g, h) : + + def get_upper_bound(self, g, h): """ - Returns the upper bound of the edit distance cost between two graphs g and h. - - :param g: The Id of the first compared graph + Returns the upper bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t :return: The upper bound of the edit distance cost :rtype: double - + .. seealso:: run_method(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() - .. warning:: run_method() between the same two graph must be called before this function. + .. warning:: run_method() between the same two graph must be called before this function. .. note:: The upper bound is equivalent to the result of the pessimist edit distance cost. Methods are heuristics so the library can't compute the real perfect result because it's NP-Hard problem. """ return self.c_env.getUpperBound(g, h) - - - def get_lower_bound(self, g, h) : + + def get_lower_bound(self, g, h): """ - Returns the lower bound of the edit distance cost between two graphs g and h. - - :param g: The Id of the first compared graph + Returns the lower bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t :return: The lower bound of the edit distance cost :rtype: double - + .. seealso:: run_method(), get_upper_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: This function can be ignored, because lower bound doesn't have a crucial utility. + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function can be ignored, because lower bound doesn't have a crucial utility. """ return self.c_env.getLowerBound(g, h) - - - def get_forward_map(self, g, h) : + + def get_forward_map(self, g, h): """ - Returns the forward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. - - :param g: The Id of the first compared graph + Returns the forward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t :return: The forward map to the adjacence matrix between nodes of the two graphs :rtype: list[npy_uint32] - + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_backward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! """ return self.c_env.getForwardMap(g, h) - - - def get_backward_map(self, g, h) : + + def get_backward_map(self, g, h): """ - Returns the backward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. - - :param g: The Id of the first compared graph + Returns the backward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t :return: The backward map to the adjacence matrix between nodes of the two graphs :rtype: list[npy_uint32] - + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! """ return self.c_env.getBackwardMap(g, h) - - - def get_node_image(self, g, h, node_id) : + + def get_node_image(self, g, h, node_id): """ - Returns the node's image in the adjacence matrix, if it exists. - - :param g: The Id of the first compared graph + Returns the node's image in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :param node_id: The ID of the node which you want to see the image :type g: size_t @@ -678,20 +682,19 @@ cdef class GEDEnv: :type node_id: size_t :return: The ID of the image node :rtype: size_t - + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_pre_image(), get_node_map(), get_assignment_matrix() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: Use BackwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). - + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use BackwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + """ return self.c_env.getNodeImage(g, h, node_id) - - - def get_node_pre_image(self, g, h, node_id) : + + def get_node_pre_image(self, g, h, node_id): """ - Returns the node's preimage in the adjacence matrix, if it exists. - - :param g: The Id of the first compared graph + Returns the node's preimage in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :param node_id: The ID of the node which you want to see the preimage :type g: size_t @@ -699,172 +702,170 @@ cdef class GEDEnv: :type node_id: size_t :return: The ID of the preimage node :rtype: size_t - + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). - + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + """ return self.c_env.getNodePreImage(g, h, node_id) - - def get_induced_cost(self, g, h) : + def get_induced_cost(self, g, h): """ - Returns the induced cost between the two indicated graphs. + Returns the induced cost between the two indicated graphs. - :param g: The Id of the first compared graph + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t :return: The induced cost between the two indicated graphs :rtype: double - + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). - - """ - return self.c_env.getInducedCost(g, h) - - - def get_node_map(self, g, h) : - """ - Returns the Node Map, like C++ NodeMap. - - :param g: The Id of the first compared graph + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getInducedCost(g, h) + + def get_node_map(self, g, h): + """ + Returns the Node Map, like C++ NodeMap. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t - :return: The Node Map between the two selected graph. + :return: The Node Map between the two selected graph. :rtype: gklearn.ged.env.NodeMap. - + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_assignment_matrix() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: This function creates datas so use it if necessary, however you can understand how assignement works with this example. + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary, however you can understand how assignement works with this example. """ map_as_relation = self.c_env.getNodeMap(g, h) - induced_cost = self.c_env.getInducedCost(g, h) # @todo: the C++ implementation for this function in GedLibBind.ipp re-call get_node_map() once more, this is not neccessary. - source_map = [item.first if item.first < len(map_as_relation) else np.inf for item in map_as_relation] # item.first < len(map_as_relation) is not exactly correct. -# print(source_map) - target_map = [item.second if item.second < len(map_as_relation) else np.inf for item in map_as_relation] -# print(target_map) + induced_cost = self.c_env.getInducedCost( + g, h + ) # @todo: the C++ implementation for this function in GedLibBind.ipp re-call get_node_map() once more, this is not neccessary. + source_map = [item.first if item.first < len(map_as_relation) else np.inf for + item in + map_as_relation] # item.first < len(map_as_relation) is not exactly correct. + # print(source_map) + target_map = [item.second if item.second < len(map_as_relation) else np.inf for + item in map_as_relation] + # print(target_map) num_node_source = len([item for item in source_map if item != np.inf]) -# print(num_node_source) + # print(num_node_source) num_node_target = len([item for item in target_map if item != np.inf]) -# print(num_node_target) - + # print(num_node_target) + node_map = NodeMap(num_node_source, num_node_target) -# print(node_map.get_forward_map(), node_map.get_backward_map()) + # print(node_map.get_forward_map(), node_map.get_backward_map()) for i in range(len(source_map)): node_map.add_assignment(source_map[i], target_map[i]) node_map.set_induced_cost(induced_cost) - + return node_map - - - def get_assignment_matrix(self, g, h) : + + def get_assignment_matrix(self, g, h): """ - Returns the Assignment Matrix between two selected graphs g and h. - - :param g: The Id of the first compared graph + Returns the Assignment Matrix between two selected graphs g and h. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t - :return: The Assignment Matrix between the two selected graph. + :return: The Assignment Matrix between the two selected graph. :rtype: list[list[int]] - + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_node_map() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: This function creates datas so use it if necessary. + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary. """ return self.c_env.getAssignmentMatrix(g, h) - - - def get_all_map(self, g, h) : + + def get_all_map(self, g, h): """ - Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. - - :param g: The Id of the first compared graph + Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t :return: The forward and backward maps to the adjacence matrix between nodes of the two graphs :rtype: list[list[npy_uint32]] - + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: This function duplicates data so please don't use it. I also don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function duplicates data so please don't use it. I also don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! """ return self.c_env.getAllMap(g, h) - - - def get_runtime(self, g, h) : + + def get_runtime(self, g, h): """ - Returns the runtime to compute the edit distance cost between two graphs g and h - - :param g: The Id of the first compared graph + Returns the runtime to compute the edit distance cost between two graphs g and h + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t :return: The runtime of the computation of edit distance cost between the two selected graphs :rtype: double - + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), quasimetric_cost() - .. warning:: run_method() between the same two graph must be called before this function. - .. note:: Python is a bit longer than C++ due to the functions's encapsulate. + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Python is a bit longer than C++ due to the functions's encapsulate. """ - return self.c_env.getRuntime(g,h) - - - def quasimetric_cost(self) : + return self.c_env.getRuntime(g, h) + + def quasimetric_cost(self): """ - Checks and returns if the edit costs are quasimetric. - - :param g: The Id of the first compared graph + Checks and returns if the edit costs are quasimetric. + + :param g: The Id of the first compared graph :param h: The Id of the second compared graph :type g: size_t :type h: size_t :return: True if it's verified, False otherwise :rtype: bool - + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime() - .. warning:: run_method() between the same two graph must be called before this function. + .. warning:: run_method() between the same two graph must be called before this function. """ return self.c_env.quasimetricCosts() - - - def hungarian_LSAP(self, matrix_cost) : + + def hungarian_LSAP(self, matrix_cost): """ - Applies the hungarian algorithm (LSAP) on a matrix Cost. - - :param matrix_cost: The matrix Cost + Applies the hungarian algorithm (LSAP) on a matrix Cost. + + :param matrix_cost: The matrix Cost :type matrix_cost: vector[vector[size_t]] :return: The values of rho, varrho, u and v, in this order :rtype: vector[vector[size_t]] - - .. seealso:: hungarian_LSAPE() + + .. seealso:: hungarian_LSAPE() """ return self.c_env.hungarianLSAP(matrix_cost) - - - def hungarian_LSAPE(self, matrix_cost) : + + def hungarian_LSAPE(self, matrix_cost): """ - Applies the hungarian algorithm (LSAPE) on a matrix Cost. - - :param matrix_cost: The matrix Cost + Applies the hungarian algorithm (LSAPE) on a matrix Cost. + + :param matrix_cost: The matrix Cost :type matrix_cost: vector[vector[double]] :return: The values of rho, varrho, u and v, in this order :rtype: vector[vector[double]] - - .. seealso:: hungarian_LSAP() + + .. seealso:: hungarian_LSAP() """ return self.c_env.hungarianLSAPE(matrix_cost) - - - def add_random_graph(self, name, classe, list_of_nodes, list_of_edges, ignore_duplicates=True) : + + def add_random_graph( + self, name, classe, list_of_nodes, list_of_edges, ignore_duplicates=True + ): """ - Add a Graph (not GXL) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. - + Add a Graph (not GXL) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + :param name: The name of the graph to add, can be an empty string :param classe: The classe of the graph to add, can be an empty string :param list_of_nodes: The list of nodes to add @@ -877,9 +878,9 @@ cdef class GEDEnv: :type ignore_duplicates: bool :return: The ID of the newly added graphe :rtype: size_t - - .. note:: The graph must respect the GXL structure. Please see how a GXL graph is construct. - + + .. note:: The graph must respect the GXL structure. Please see how a GXL graph is construct. + """ id = self.add_graph(name, classe) for node in list_of_nodes: @@ -887,34 +888,38 @@ cdef class GEDEnv: for edge in list_of_edges: self.add_edge(id, edge[0], edge[1], edge[2], ignore_duplicates) return id - - - def add_nx_graph(self, g, classe, ignore_duplicates=True) : + + def add_nx_graph(self, g, classe, ignore_duplicates=True): """ - Add a Graph (made by networkx) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. - + Add a Graph (made by networkx) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + :param g: The graph to add (networkx graph) :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default :type g: networkx.graph :type ignore_duplicates: bool :return: The ID of the newly added graphe :rtype: size_t - - .. note:: The NX graph must respect the GXL structure. Please see how a GXL graph is construct. - + + .. note:: The NX graph must respect the GXL structure. Please see how a GXL graph is construct. + """ id = self.add_graph(g.name, classe) for node in g.nodes: self.add_node(id, str(node), g.nodes[node]) for edge in g.edges: - self.add_edge(id, str(edge[0]), str(edge[1]), g.get_edge_data(edge[0], edge[1]), ignore_duplicates) + self.add_edge( + id, str(edge[0]), str(edge[1]), g.get_edge_data(edge[0], edge[1]), + ignore_duplicates + ) return id - - - def compute_ged_on_two_graphs(self, g1, g2, edit_cost, method, options, init_option="EAGER_WITHOUT_SHUFFLED_COPIES") : + + def compute_ged_on_two_graphs( + self, g1, g2, edit_cost, method, options, + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): """ - Computes the edit distance between two NX graphs. - + Computes the edit distance between two NX graphs. + :param g1: The first graph to add and compute :param g2: The second graph to add and compute :param edit_cost: The name of the edit cost function @@ -927,39 +932,41 @@ cdef class GEDEnv: :type method: string :type options: string :type init_option: string - :return: The edit distance between the two graphs and the nodeMap between them. + :return: The edit distance between the two graphs and the nodeMap between them. :rtype: double, list[tuple(size_t, size_t)] - - .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options - .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. - + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + """ - if self.is_initialized() : + if self.is_initialized(): self.restart_env() - + g = self.add_nx_graph(g1, "") h = self.add_nx_graph(g2, "") - + self.set_edit_cost(edit_cost) self.init(init_option) - + self.set_method(method, options) self.init_method() - + resDistance = 0 resMapping = [] self.run_method(g, h) resDistance = self.get_upper_bound(g, h) resMapping = self.get_node_map(g, h) - + return resDistance, resMapping - - - def compute_edit_distance_on_nx_graphs(self, dataset, classes, edit_cost, method, options, init_option="EAGER_WITHOUT_SHUFFLED_COPIES") : + + def compute_edit_distance_on_nx_graphs( + self, dataset, classes, edit_cost, method, options, + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): """ - - Computes all the edit distance between each NX graphs on the dataset. - + + Computes all the edit distance between each NX graphs on the dataset. + :param dataset: The list of graphs to add and compute :param classes: The classe of all the graph, can be an empty string :param edit_cost: The name of the edit cost function @@ -974,47 +981,52 @@ cdef class GEDEnv: :type init_option: string :return: Two matrix, the first with edit distances between graphs and the second the nodeMap between graphs. The result between g and h is one the [g][h] coordinates. :rtype: list[list[double]], list[list[list[tuple(size_t, size_t)]]] - + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options - .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. - + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + """ - if self.is_initialized() : + if self.is_initialized(): self.restart_env() - + print("Loading graphs in progress...") - for graph in dataset : + for graph in dataset: self.add_nx_graph(graph, classes) listID = self.graph_ids() print("Graphs loaded ! ") print("Number of graphs = " + str(listID[1])) - + self.set_edit_cost(edit_cost) print("Initialization in progress...") self.init(init_option) print("Initialization terminated !") - + self.set_method(method, options) self.init_method() - + resDistance = [[]] resMapping = [[]] - for g in range(listID[0], listID[1]) : - print("Computation between graph " + str(g) + " with all the others including himself.") - for h in range(listID[0], listID[1]) : + for g in range(listID[0], listID[1]): + print("Computation between graph " + str( + g + ) + " with all the others including himself.") + for h in range(listID[0], listID[1]): #print("Computation between graph " + str(g) + " and graph " + str(h)) self.run_method(g, h) resDistance[g][h] = self.get_upper_bound(g, h) resMapping[g][h] = self.get_node_map(g, h) - - print("Finish ! The return contains edit distances and NodeMap but you can check the result with graphs'ID until you restart the environment") + + print( + "Finish ! The return contains edit distances and NodeMap but you can check the result with graphs'ID until you restart the environment") return resDistance, resMapping - - - def compute_edit_distance_on_GXl_graphs(self, path_folder, path_XML, edit_cost, method, options="", init_option="EAGER_WITHOUT_SHUFFLED_COPIES") : + + def compute_edit_distance_on_GXl_graphs( + self, path_folder, path_XML, edit_cost, method, options="", + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): """ - Computes all the edit distance between each GXL graphs on the folder and the XMl file. - + Computes all the edit distance between each GXL graphs on the folder and the XMl file. + :param path_folder: The folder's path which contains GXL graphs :param path_XML: The XML's path which indicates which graphes you want to load :param edit_cost: The name of the edit cost function @@ -1029,60 +1041,62 @@ cdef class GEDEnv: :type init_option: string :return: The list of the first and last-1 ID of graphs :rtype: tuple(size_t, size_t) - + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options - .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. - + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. + """ - - if self.is_initialized() : + + if self.is_initialized(): self.restart_env() - + print("Loading graphs in progress...") self.load_GXL_graphs(path_folder, path_XML) listID = self.graph_ids() print("Graphs loaded ! ") print("Number of graphs = " + str(listID[1])) - + self.set_edit_cost(edit_cost) print("Initialization in progress...") self.init(init_option) print("Initialization terminated !") - + self.set_method(method, options) self.init_method() - + #res = [] - for g in range(listID[0], listID[1]) : - print("Computation between graph " + str(g) + " with all the others including himself.") - for h in range(listID[0], listID[1]) : + for g in range(listID[0], listID[1]): + print("Computation between graph " + str( + g + ) + " with all the others including himself.") + for h in range(listID[0], listID[1]): #print("Computation between graph " + str(g) + " and graph " + str(h)) - self.run_method(g,h) - #res.append((get_upper_bound(g,h), get_node_map(g,h), get_runtime(g,h))) - + self.run_method(g, h) + #res.append((get_upper_bound(g,h), get_node_map(g,h), get_runtime(g,h))) + #return res - - print ("Finish ! You can check the result with each ID of graphs ! There are in the return") - print ("Please don't restart the environment or recall this function, you will lose your results !") + + print ( + "Finish ! You can check the result with each ID of graphs ! There are in the return") + print ( + "Please don't restart the environment or recall this function, you will lose your results !") return listID - - + def get_num_node_labels(self): """ Returns the number of node labels. - + :return: Number of pairwise different node labels contained in the environment. :rtype: size_t - + .. note:: If 1 is returned, the nodes are unlabeled. """ return self.c_env.getNumNodeLabels() - def get_node_label(self, label_id): """ Returns node label. - + :param label_id: ID of node label that should be returned. Must be between 1 and get_num_node_labels(). :type label_id: size_t :return: Node label for selected label ID. @@ -1090,55 +1104,52 @@ cdef class GEDEnv: """ return decode_your_map(self.c_env.getNodeLabel(label_id)) - def get_num_edge_labels(self): """ Returns the number of edge labels. - + :return: Number of pairwise different edge labels contained in the environment. :rtype: size_t - + .. note:: If 1 is returned, the edges are unlabeled. """ return self.c_env.getNumEdgeLabels() - def get_edge_label(self, label_id): """ Returns edge label. - + :param label_id: ID of edge label that should be returned. Must be between 1 and get_num_edge_labels(). :type label_id: size_t :return: Edge label for selected label ID. :rtype: dict{string : string} """ return decode_your_map(self.c_env.getEdgeLabel(label_id)) - - -# def get_num_nodes(self, graph_id): -# """ -# Returns the number of nodes. -# -# :param graph_id: ID of an input graph that has been added to the environment. -# :type graph_id: size_t -# :return: Number of nodes in the graph. -# :rtype: size_t -# """ -# return self.c_env.getNumNodes(graph_id) + + # def get_num_nodes(self, graph_id): + # """ + # Returns the number of nodes. + # + # :param graph_id: ID of an input graph that has been added to the environment. + # :type graph_id: size_t + # :return: Number of nodes in the graph. + # :rtype: size_t + # """ + # return self.c_env.getNumNodes(graph_id) def get_avg_num_nodes(self): """ Returns average number of nodes. - + :return: Average number of nodes of the graphs contained in the environment. :rtype: double - """ + """ return self.c_env.getAvgNumNodes() def get_node_rel_cost(self, node_label_1, node_label_2): """ Returns node relabeling cost. - + :param node_label_1: First node label. :param node_label_2: Second node label. :type node_label_1: dict{string : string} @@ -1146,13 +1157,14 @@ cdef class GEDEnv: :return: Node relabeling cost for the given node labels. :rtype: double """ - return self.c_env.getNodeRelCost(encode_your_map(node_label_1), encode_your_map(node_label_2)) - + return self.c_env.getNodeRelCost( + encode_your_map(node_label_1), encode_your_map(node_label_2) + ) def get_node_del_cost(self, node_label): """ Returns node deletion cost. - + :param node_label: Node label. :type node_label: dict{string : string} :return: Cost of deleting node with given label. @@ -1160,11 +1172,10 @@ cdef class GEDEnv: """ return self.c_env.getNodeDelCost(encode_your_map(node_label)) - def get_node_ins_cost(self, node_label): """ Returns node insertion cost. - + :param node_label: Node label. :type node_label: dict{string : string} :return: Cost of inserting node with given label. @@ -1172,11 +1183,10 @@ cdef class GEDEnv: """ return self.c_env.getNodeInsCost(encode_your_map(node_label)) - def get_median_node_label(self, node_labels): """ Computes median node label. - + :param node_labels: The node labels whose median should be computed. :type node_labels: list[dict{string : string}] :return: Median of the given node labels. @@ -1185,11 +1195,10 @@ cdef class GEDEnv: node_labels_b = [encode_your_map(node_label) for node_label in node_labels] return decode_your_map(self.c_env.getMedianNodeLabel(node_labels_b)) - def get_edge_rel_cost(self, edge_label_1, edge_label_2): """ Returns edge relabeling cost. - + :param edge_label_1: First edge label. :param edge_label_2: Second edge label. :type edge_label_1: dict{string : string} @@ -1197,13 +1206,14 @@ cdef class GEDEnv: :return: Edge relabeling cost for the given edge labels. :rtype: double """ - return self.c_env.getEdgeRelCost(encode_your_map(edge_label_1), encode_your_map(edge_label_2)) - + return self.c_env.getEdgeRelCost( + encode_your_map(edge_label_1), encode_your_map(edge_label_2) + ) def get_edge_del_cost(self, edge_label): """ Returns edge deletion cost. - + :param edge_label: Edge label. :type edge_label: dict{string : string} :return: Cost of deleting edge with given label. @@ -1211,11 +1221,10 @@ cdef class GEDEnv: """ return self.c_env.getEdgeDelCost(encode_your_map(edge_label)) - def get_edge_ins_cost(self, edge_label): """ Returns edge insertion cost. - + :param edge_label: Edge label. :type edge_label: dict{string : string} :return: Cost of inserting edge with given label. @@ -1223,21 +1232,21 @@ cdef class GEDEnv: """ return self.c_env.getEdgeInsCost(encode_your_map(edge_label)) - def get_median_edge_label(self, edge_labels): """ Computes median edge label. - + :param edge_labels: The edge labels whose median should be computed. :type edge_labels: list[dict{string : string}] :return: Median of the given edge labels. :rtype: dict{string : string} """ - edge_labels_b = [encode_your_map(edge_label) for edge_label in edge_labels] + edge_labels_b = [encode_your_map(edge_label) for edge_label in edge_labels] return decode_your_map(self.c_env.getMedianEdgeLabel(edge_label_b)) - - - def get_nx_graph(self, graph_id, adj_matrix=True, adj_lists=False, edge_list=False): # @todo + + def get_nx_graph( + self, graph_id, adj_matrix=True, adj_lists=False, edge_list=False + ): # @todo """ Get graph with id `graph_id` in the form of the NetworkX Graph. @@ -1245,13 +1254,13 @@ cdef class GEDEnv: ---------- graph_id : int ID of the selected graph. - + adj_matrix : bool Set to `True` to construct an adjacency matrix `adj_matrix` and a hash-map `edge_labels`, which has a key for each pair `(i,j)` such that `adj_matrix[i][j]` equals 1. No effect for now. - + adj_lists : bool No effect for now. - + edge_list : bool No effect for now. @@ -1266,22 +1275,21 @@ cdef class GEDEnv: nb_nodes = self.get_graph_num_nodes(graph_id) original_node_ids = self.get_original_node_ids(graph_id) node_labels = self.get_graph_node_labels(graph_id) -# print(original_node_ids) -# print(node_labels) + # print(original_node_ids) + # print(node_labels) graph.graph['original_node_ids'] = original_node_ids - + for node_id in range(0, nb_nodes): graph.add_node(node_id, **node_labels[node_id]) -# graph.nodes[node_id]['original_node_id'] = original_node_ids[node_id] - + # graph.nodes[node_id]['original_node_id'] = original_node_ids[node_id] + edges = self.get_graph_edges(graph_id) for (head, tail), labels in edges.items(): graph.add_edge(head, tail, **labels) -# print(edges) - + # print(edges) + return graph - - + def get_init_type(self): """ Returns the initialization type of the last initialization in string. @@ -1292,27 +1300,25 @@ cdef class GEDEnv: Initialization type in string. """ return self.c_env.getInitType().decode('utf-8') - - -# def get_node_cost(self, label1, label2): -# """ -# Returns node relabeling, insertion, or deletion cost. - -# Parameters -# ---------- -# label1 : int -# First node label. -# -# label2 : int -# Second node label. -# -# Returns -# ------- -# Node relabeling cost if `label1` and `label2` are both different from `ged::dummy_label()`, node insertion cost if `label1` equals `ged::dummy_label` and `label2` does not, node deletion cost if `label1` does not equal `ged::dummy_label` and `label2` does, and 0 otherwise. -# """ -# return self.c_env.getNodeCost(label1, label2) - - + + # def get_node_cost(self, label1, label2): + # """ + # Returns node relabeling, insertion, or deletion cost. + + # Parameters + # ---------- + # label1 : int + # First node label. + # + # label2 : int + # Second node label. + # + # Returns + # ------- + # Node relabeling cost if `label1` and `label2` are both different from `ged::dummy_label()`, node insertion cost if `label1` equals `ged::dummy_label` and `label2` does not, node deletion cost if `label1` does not equal `ged::dummy_label` and `label2` does, and 0 otherwise. + # """ + # return self.c_env.getNodeCost(label1, label2) + def load_nx_graph(self, nx_graph, graph_id, graph_name='', graph_class=''): """ Loads NetworkX Graph into the GED environment. @@ -1321,13 +1327,13 @@ cdef class GEDEnv: ---------- nx_graph : NetworkX Graph object The graph that should be loaded. - + graph_id : int or None The ID of a graph contained the environment (overwrite existing graph) or add new graph if `None`. - + graph_name : string, optional The name of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. - + graph_class : string, optional The class of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. @@ -1343,10 +1349,12 @@ cdef class GEDEnv: for node in nx_graph.nodes: self.add_node(graph_id, str(node), nx_graph.nodes[node]) for edge in nx_graph.edges: - self.add_edge(graph_id, str(edge[0]), str(edge[1]), nx_graph.get_edge_data(edge[0], edge[1])) + self.add_edge( + graph_id, str(edge[0]), str(edge[1]), + nx_graph.get_edge_data(edge[0], edge[1]) + ) return graph_id - - + def compute_induced_cost(self, g_id, h_id, node_map): """ Computes the edit cost between two graphs induced by a node map. @@ -1362,22 +1370,21 @@ cdef class GEDEnv: Returns ------- - None. + None. """ relation = [] node_map.as_relation(relation) -# print(relation) + # print(relation) dummy_node = get_dummy_node() -# print(dummy_node) + # print(dummy_node) for i, val in enumerate(relation): val1 = dummy_node if val[0] == np.inf else val[0] val2 = dummy_node if val[1] == np.inf else val[1] relation[i] = tuple((val1, val2)) -# print(relation) + # print(relation) induced_cost = self.c_env.computeInducedCost(g_id, h_id, relation) node_map.set_induced_cost(induced_cost) - ##################################################################### ##LISTS OF EDIT COST FUNCTIONS, METHOD COMPUTATION AND INIT OPTIONS## ##################################################################### @@ -1393,12 +1400,12 @@ list_of_init_options = get_init_options() class Error(Exception): """ - Class for error's management. This one is general. + Class for error's management. This one is general. """ pass -class EditCostError(Error) : +class EditCostError(Error): """ Class for Edit Cost Error. Raise an error if an edit cost function doesn't exist in the library (not in list_of_edit_cost_options). @@ -1407,15 +1414,15 @@ class EditCostError(Error) : """ def __init__(self, message): """ - Inits the error with its message. + Inits the error with its message. :param message: The message to print when the error is detected :type message: string """ self.message = message - - -class MethodError(Error) : + + +class MethodError(Error): """ Class for Method Error. Raise an error if a computation method doesn't exist in the library (not in list_of_method_options). @@ -1424,7 +1431,7 @@ class MethodError(Error) : """ def __init__(self, message): """ - Inits the error with its message. + Inits the error with its message. :param message: The message to print when the error is detected :type message: string @@ -1432,7 +1439,7 @@ class MethodError(Error) : self.message = message -class InitError(Error) : +class InitError(Error): """ Class for Init Error. Raise an error if an init option doesn't exist in the library (not in list_of_init_options). @@ -1441,7 +1448,7 @@ class InitError(Error) : """ def __init__(self, message): """ - Inits the error with its message. + Inits the error with its message. :param message: The message to print when the error is detected :type message: string @@ -1462,36 +1469,34 @@ def encode_your_map(map_u): :return: The encoded map :rtype: dict{'b'string : 'b'string} - .. note:: This function is used for type connection. - + .. note:: This function is used for type connection. + """ res = {} for key, value in map_u.items(): res[key.encode('utf-8')] = value.encode('utf-8') return res - def decode_your_map(map_b): """ - Decodes utf-8 byte strings in `map` from C++ functions to Python unicode strings. + Decodes utf-8 byte strings in `map` from C++ functions to Python unicode strings. :param map_b: The map to decode :type map_b: dict{'b'string : 'b'string} :return: The decoded map :rtype: dict{string : string} - .. note:: This function is used for type connection. - + .. note:: This function is used for type connection. + """ res = {} for key, value in map_b.items(): res[key.decode('utf-8')] = value.decode('utf-8') return res - -def decode_graph_edges(map_edge_b): +def decode_graph_edges(map_edge_b): """ - Decode utf-8 byte strings in graph edges `map` from C++ functions to Python unicode strings. + Decode utf-8 byte strings in graph edges `map` from C++ functions to Python unicode strings. Parameters ---------- @@ -1502,22 +1507,16 @@ def decode_graph_edges(map_edge_b): ------- dict{tuple(size_t, size_t) : dict{string : string}} The decoded map. - + Notes ----- This is a helper function for function `GEDEnv.get_graph_edges()`. """ map_edges = {} for key, value in map_edge_b.items(): - map_edges[key] = decode_your_map(value) + map_edges[key] = decode_your_map(value) return map_edges - - - - - - # cdef extern from "src/GedLibBind.h" namespace "shapes": # cdef cppclass Rectangle: # Rectangle() except + @@ -1526,7 +1525,7 @@ def decode_graph_edges(map_edge_b): # int getArea() # void getSize(int* width, int* height) # void move(int, int) - + # # Create a Cython extension type which holds a C++ instance # # as an attribute and create a bunch of forwarding methods @@ -1578,4 +1577,4 @@ def decode_graph_edges(map_edge_b): # return self.c_rect.y1 # @y1.setter # def y1(self, y1): -# self.c_rect.y1 = y1 +# self.c_rect.y1 = y1 \ No newline at end of file diff --git a/gklearn/gedlib/gedlibpy_attr.pyx b/gklearn/gedlib/gedlibpy_attr.pyx new file mode 100644 index 0000000000..f2fbf4fe58 --- /dev/null +++ b/gklearn/gedlib/gedlibpy_attr.pyx @@ -0,0 +1,2139 @@ +# distutils: language = c++ + +""" + Python GedLib module for the AttrLabel type + ====================== + + This module allows using a C++ library for edit distance between graphs (GedLib) with Python. + + + Authors + ------------------- + + Linlin Jia + David Blumenthal + Natacha Lambert + + Copyright (C) 2019-2025 by all the authors + + Classes & Functions + ------------------- + +""" + +################################# +##DECLARATION OF C++ INTERFACES## +################################# + + +#Types imports for C++ compatibility +from libcpp.vector cimport vector +from libcpp.string cimport string +from libcpp.unordered_map cimport unordered_map +from libcpp cimport bool +from libcpp.pair cimport pair +from libcpp.list cimport list + +#Long unsigned int equivalent +cimport numpy as cnp +ctypedef cnp.npy_uint32 UINT32_t +from cpython cimport array + + +cdef extern from "src/gedlib_bind_attr.hpp" namespace "pyged": + + cdef vector[string] getEditCostStringOptions() except + + cdef vector[string] getMethodStringOptions() except + + cdef vector[string] getInitStringOptions() except + + cdef size_t getDummyNode() except + + + cdef cppclass PyGEDEnvAttr: + PyGEDEnvAttr() except + + bool isInitialized() except + + void restartEnv() except + + # void loadGXLGraph( # todo: need conversion in ipp file + # string pathFolder, string pathXML, bool node_type, bool edge_type + # ) except + + pair[size_t, size_t] getGraphIds() except + + vector[size_t] getAllGraphIds() except + + string getGraphClass(size_t id) except + + string getGraphName(size_t id) except + + size_t addGraph(string name, string classe) except + + # void addNode( # todo: need conversion in ipp file + # size_t graphId, string nodeId, map[string, string] nodeLabel + # ) except + + void addNode( + size_t graphId, + string nodeId, + unordered_map[string, string] str_map, + unordered_map[string, int] int_map, + unordered_map[string, double] float_map, + unordered_map[string, vector[string]] list_str_map, + unordered_map[string, vector[int]] list_int_map, + unordered_map[string, vector[double]] list_float_map + ) except + + # void addEdge( + # size_t graphId, string tail, string head, map[string, string] edgeLabel, + # bool ignoreDuplicates + # ) except + + void addEdge( + size_t graphId, + string tail, + string head, + unordered_map[string, string] str_map, + unordered_map[string, int] int_map, + unordered_map[string, double] float_map, + unordered_map[string, vector[string]] list_str_map, + unordered_map[string, vector[int]] list_int_map, + unordered_map[string, vector[double]] list_float_map, + bool ignoreDuplicates + ) except + + void clearGraph(size_t graphId) except + + size_t getGraphInternalId(size_t graphId) except + + size_t getGraphNumNodes(size_t graphId) except + + size_t getGraphNumEdges(size_t graphId) except + + vector[string] getGraphOriginalNodeIds(size_t graphId) except + + # vector[map[string, string]] getGraphNodeLabels(size_t graphId) except + + # map[pair[size_t, size_t], map[string, string]] getGraphEdges( + # size_t graphId + # ) except + + vector[vector[size_t]] getGraphAdjacenceMatrix(size_t graphId) except + + void setEditCost( + string editCost, vector[double] editCostConstant, + unordered_map[string, string] str_config, + unordered_map[string, bool] bool_config, + ) except + + void setPersonalEditCost(vector[double] editCostConstant) except + + void initEnv(string initOption, bool print_to_stdout) except + + void setMethod(string method, string options) except + + void initMethod() except + + double getInitime() except + + void runMethod(size_t g, size_t h) except + + double getUpperBound(size_t g, size_t h) except + + double getLowerBound(size_t g, size_t h) except + + vector[cnp.npy_uint64] getForwardMap(size_t g, size_t h) except + + vector[cnp.npy_uint64] getBackwardMap(size_t g, size_t h) except + + size_t getNodeImage(size_t g, size_t h, size_t nodeId) except + + size_t getNodePreImage(size_t g, size_t h, size_t nodeId) except + + double getInducedCost(size_t g, size_t h) except + + vector[pair[size_t, size_t]] getNodeMap(size_t g, size_t h) except + + vector[vector[int]] getAssignmentMatrix(size_t g, size_t h) except + + vector[vector[cnp.npy_uint64]] getAllMap(size_t g, size_t h) except + + double getRuntime(size_t g, size_t h) except + + bool quasimetricCosts() except + + vector[vector[size_t]] hungarianLSAP(vector[vector[size_t]] matrixCost) except + + vector[vector[double]] hungarianLSAPE( + vector[vector[double]] matrixCost + ) except + + # added by Linlin Jia. + size_t getNumGraphs() except + + size_t getNumNodeLabels() except + + # map[string, string] getNodeLabel(size_t label_id) except + + size_t getNumEdgeLabels() except + + # map[string, string] getEdgeLabel(size_t label_id) except + + # size_t getNumNodes(size_t graph_id) except + + double getAvgNumNodes() except + + # double getNodeRelCost( + # map[string, string] & node_label_1, map[string, string] & node_label_2 + # ) except + + # double getNodeDelCost(map[string, string] & node_label) except + + # double getNodeInsCost(map[string, string] & node_label) except + + # map[string, string] getMedianNodeLabel( + # vector[map[string, string]] & node_labels + # ) except + + # double getEdgeRelCost( + # map[string, string] & edge_label_1, map[string, string] & edge_label_2 + # ) except + + # double getEdgeDelCost(map[string, string] & edge_label) except + + # double getEdgeInsCost(map[string, string] & edge_label) except + + # map[string, string] getMedianEdgeLabel( + # vector[map[string, string]] & edge_labels + # ) except + + string getInitType() except + + # double getNodeCost(size_t label1, size_t label2) except + + double computeInducedCost( + size_t g_id, size_t h_id, vector[pair[size_t, size_t]] + ) except + + + +############################# +##External Libs Import ## +############################# + +# from libraries_import import lib1, lib2, lib3, lib4 + +############################# +##CYTHON WRAPPER INTERFACES## +############################# + +# import cython +import numpy as np +import networkx as nx +from gklearn.ged.env import NodeMap +from builtins import list as py_list +from builtins import bool as py_bool + + +# import librariesImport +from ctypes import * +import os +lib1 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/fann.2.2.0/libdoublefann.so') +lib2 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/libsvm.3.22/libsvm.so') +lib3 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libnomad.so') +lib4 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libsgtelib.so') + + +def get_edit_cost_options() : + """ + Searchs the differents edit cost functions and returns the result. + + :return: The list of edit cost functions + :rtype: list[string] + + .. warning:: This function is useless for an external use. Please use directly list_of_edit_cost_options. + .. note:: Prefer the list_of_edit_cost_options attribute of this module. + """ + # # test only: + # print(f'[gedlibpy_attr.pyx] Available edit cost options: {getEditCostStringOptions()}.') + return [option.decode('utf-8') for option in getEditCostStringOptions()] + + +def get_method_options() : + """ + Searchs the differents method for edit distance computation between graphs and returns the result. + + :return: The list of method to compute the edit distance between graphs + :rtype: list[string] + + .. warning:: This function is useless for an external use. Please use directly list_of_method_options. + .. note:: Prefer the list_of_method_options attribute of this module. + """ + return [option.decode('utf-8') for option in getMethodStringOptions()] + + +def get_init_options() : + """ + Searchs the differents initialization parameters for the environment computation for graphs and returns the result. + + :return: The list of options to initialize the computation environment + :rtype: list[string] + + .. warning:: This function is useless for an external use. Please use directly list_of_init_options. + .. note:: Prefer the list_of_init_options attribute of this module. + """ + return [option.decode('utf-8') for option in getInitStringOptions()] + + +def get_dummy_node() : + """ + Returns the ID of a dummy node. + + :return: The ID of the dummy node (18446744073709551614 for my computer, the hugest number possible) + :rtype: size_t + + .. note:: A dummy node is used when a node isn't associated to an other node. + """ + return getDummyNode() + + +# @cython.auto_pickle(True) +cdef class GEDEnvAttr: + """Cython wrapper class for C++ class PyGEDEnv + """ + cdef PyGEDEnvAttr * c_env # hold a pointer to the C++ instance which we're wrapping + + + def __cinit__(self): + """ + Initializes the C++ environment for graph edit distance computations. + + Parameters + ---------- + label_type : str, optional + The type of labels used in the graphs, can be the following: + - 'gxl' or 'str': for GXLLabel (string labels) + - 'attr': for AttrLabel (complex attribute labels) + """ + self.c_env = new PyGEDEnvAttr() + + + def __dealloc__(self): + if self.c_env != NULL: + del self.c_env + self.c_env = NULL + +# def __reduce__(self): +# # return GEDEnv, (self.c_env,) +# return GEDEnv, tuple() + + + def is_initialized(self) : + """ + Checks and returns if the computation environment is initialized or not. + + :return: True if it's initialized, False otherwise + :rtype: bool + + .. note:: This function exists for internals verifications but you can use it for your code. + """ + return self.c_env.isInitialized() + + + def restart_env(self) : + """ + Restarts the environment variable. All data related to it will be delete. + + .. warning:: This function deletes all graphs, computations and more so make sure you don't need anymore your environment. + .. note:: You can now delete and add somes graphs after initialization so you can avoid this function. + """ + self.c_env.restartEnv() + + + # def load_GXL_graphs(self, path_folder, path_XML, node_type, edge_type) : + # """ + # Loads some GXL graphes on the environment which is in a same folder, and present in the XMLfile. + # + # :param path_folder: The folder's path which contains GXL graphs + # :param path_XML: The XML's path which indicates which graphes you want to load + # :param node_type: Select if nodes are labeled or unlabeled + # :param edge_type: Select if edges are labeled or unlabeled + # :type path_folder: string + # :type path_XML: string + # :type node_type: bool + # :type edge_type: bool + # + # + # .. note:: You can call this function multiple times if you want, but not after an init call. + # """ + # self.c_env.loadGXLGraph( + # path_folder.encode('utf-8'), path_XML.encode('utf-8'), node_type, edge_type + # ) + + + def graph_ids(self) : + """ + Searchs the first and last IDs of the loaded graphs in the environment. + + :return: The pair of the first and the last graphs Ids + :rtype: tuple(size_t, size_t) + + .. note:: Prefer this function if you have huges structures with lots of graphs. + """ + return self.c_env.getGraphIds() + + + def get_all_graph_ids(self) : + """ + Searchs all the IDs of the loaded graphs in the environment. + + :return: The list of all graphs's Ids + :rtype: list[size_t] + + .. note:: The last ID is equal to (number of graphs - 1). The order correspond to the loading order. + """ + return self.c_env.getAllGraphIds() + + + def get_graph_class(self, id) : + """ + Returns the class of a graph with its ID. + + :param id: The ID of the wanted graph + :type id: size_t + :return: The class of the graph which correpond to the ID + :rtype: string + + .. seealso:: get_graph_class() + .. note:: An empty string can be a class. + """ + return self.c_env.getGraphClass(id) + + + def get_graph_name(self, id) : + """ + Returns the name of a graph with its ID. + + :param id: The ID of the wanted graph + :type id: size_t + :return: The name of the graph which correpond to the ID + :rtype: string + + .. seealso:: get_graph_class() + .. note:: An empty string can be a name. + """ + return self.c_env.getGraphName(id).decode('utf-8') + + + def add_graph(self, name="", classe="") : + """ + Adds a empty graph on the environment, with its name and its class. Nodes and edges will be add in a second time. + + :param name: The name of the new graph, an empty string by default + :param classe: The class of the new graph, an empty string by default + :type name: string + :type classe: string + :return: The ID of the newly graphe + :rtype: size_t + + .. seealso::add_node(), add_edge() , add_symmetrical_edge() + .. note:: You can call this function without parameters. You can also use this function after initialization, call init() after you're finished your modifications. + """ + return self.c_env.addGraph(name.encode('utf-8'), classe.encode('utf-8')) + + + def add_node(self, graph_id, node_id, dict node_label): + """ + Adds a node on a graph selected by its ID. A ID and a (set of) labels for the + node is required. + + Parameters + ---------- + graph_id : size_t + The ID of the wanted graph. + + node_id : string + The ID of the new node. + + node_label : dict + The label of the new node. `node_label` supports mixed types: + str, int, float, + list[int] or np.ndarray[int], + list[float] or np.ndarray[float], + list[str] + """ + # # debug test only: + # print(f'The node label passed from Python is {node_label}.') + + ( + str_map, int_map, float_map, list_str_map, list_int_map, list_float_map + ) = encode_label_map(node_label) + + # print('The node labels passed to the C++ function in .pyx are:') + # print(f'str_map: {str_map}.') + # print(f'int_map: {int_map}.') + # print(f'float_map: {float_map}.') + # print(f'list_str_map: {list_str_map}.') + # print(f'list_int_map: {list_int_map}.') + # print(f'list_float_map: {list_float_map}.') + + self.c_env.addNode( + graph_id, node_id.encode('utf-8'), + str_map, int_map, float_map, list_str_map, list_int_map, list_float_map + ) + + + def add_edge( + self, size_t graph_id, tail, head, dict edge_label, + ignore_duplicates=True + ): + """ + Adds an edge on a graph selected by its ID. + + :param graph_id: The ID of the wanted graph + :param tail: The ID of the tail node for the new edge + :param head: The ID of the head node for the new edge + :param edge_label: The label of the new edge + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type graph_id: size_t + :type tail: string + :type head: string + :type edge_label: supports mixed types: + str, int, float, + list[int] or np.ndarray[int], + list[float] or np.ndarray[float], + list[str] + :type ignore_duplicates: bool + + .. seealso:: add_graph(), add_node(), add_symmetrical_edge() + """ + # # debug test only: + # print(f'The edge label passed from Python is {edge_label}.') + + ( + str_map, int_map, float_map, list_str_map, list_int_map, list_float_map + ) = encode_label_map(edge_label) + self.c_env.addEdge( + graph_id, tail.encode('utf-8'), head.encode('utf-8'), + str_map, int_map, float_map, list_str_map, list_int_map, list_float_map, + ignore_duplicates + ) + + # print('The edge labels passed to the C++ function in .pyx are:') + # print(f'str_map: {str_map}.') + # print(f'int_map: {int_map}.') + # print(f'float_map: {float_map}.') + # print(f'list_str_map: {list_str_map}.') + # print(f'list_int_map: {list_int_map}.') + # print(f'list_float_map: {list_float_map}.') + + + # def add_node_str(self, graph_id, node_id, node_label: dict[str, str]): + # """ + # Adds a node on a graph selected by its ID. A ID and a label for the node is required. + # + # :param graph_id: The ID of the wanted graph + # :param node_id: The ID of the new node + # :param node_label: The label of the new node + # :type graph_id: size_t + # :type node_id: string + # :type node_label: dict{string : string} + # + # .. seealso:: add_graph(), add_edge(), add_symmetrical_edge() + # + # Notes + # ----- + # - You can also use this function after initialization, but only on a newly + # added graph. Call init() after you're finished your modifications. + # - this function only supports string labels and should be replaced + # by the new `add_node` function. It is kept for backward compatibility and + # comparison. + # """ + # self.c_env.addNode( + # graph_id, node_id.encode('utf-8'), encode_your_map(node_label) + # ) + + + # def add_edge_str( + # self, graph_id, tail, head, edge_label: dict[str, str], ignore_duplicates=True + # ): + # """ + # Adds an edge on a graph selected by its ID. + # + # :param graph_id: The ID of the wanted graph + # :param tail: The ID of the tail node for the new edge + # :param head: The ID of the head node for the new edge + # :param edge_label: The label of the new edge + # :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + # :type graph_id: size_t + # :type tail: string + # :type head: string + # :type edge_label: dict{string : string} + # :type ignore_duplicates: bool + # + # .. seealso:: add_graph(), add_node(), add_symmetrical_edge() + # + # Notes + # ----- + # - You can also use this function after initialization, but only on a newly added + # graph. Call init() after you're finished your modifications. + # - this function only supports string labels and should be replaced + # by the new `add_edge` function. It is kept for backward compatibility and + # comparison. + # """ + # self.c_env.addEdge( + # graph_id, tail.encode('utf-8'), head.encode('utf-8'), + # encode_your_map(edge_label), ignore_duplicates + # ) + + # todo: fix for attr label + # def add_symmetrical_edge(self, graph_id, tail, head, edge_label): + # """ + # Adds a symmetrical edge on a graph selected by its ID. + # + # :param graph_id: The ID of the wanted graph + # :param tail: The ID of the tail node for the new edge + # :param head: The ID of the head node for the new edge + # :param edge_label: The label of the new edge + # :type graph_id: size_t + # :type tail: string + # :type head: string + # :type edge_label: dict{string : string} + # + # .. seealso:: add_graph(), add_node(), add_edge() + # .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + # """ + # tailB = tail.encode('utf-8') + # headB = head.encode('utf-8') + # edgeLabelB = encode_your_map(edge_label) + # self.c_env.addEdge(graph_id, tailB, headB, edgeLabelB, True) + # self.c_env.addEdge(graph_id, headB, tailB, edgeLabelB, True) + + def clear_graph(self, graph_id): + """ + Deletes a graph, selected by its ID, to the environment. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + + .. note:: Call init() after you're finished your modifications. + """ + self.c_env.clearGraph(graph_id) + + + def get_graph_internal_id(self, graph_id) : + """ + Searchs and returns the internal Id of a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The internal ID of the selected graph + :rtype: size_t + + .. seealso:: get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphInternalId(graph_id) + + + def get_graph_num_nodes(self, graph_id) : + """ + Searchs and returns the number of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The number of nodes on the selected graph + :rtype: size_t + + .. seealso:: get_graph_internal_id(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphNumNodes(graph_id) + + + def get_graph_num_edges(self, graph_id) : + """ + Searchs and returns the number of edges on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The number of edges on the selected graph + :rtype: size_t + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphNumEdges(graph_id) + + + def get_original_node_ids(self, graph_id) : + """ + Searchs and returns all th Ids of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The list of IDs's nodes on the selected graph + :rtype: list[string] + + .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return [ + gid.decode('utf-8') for gid in + self.c_env.getGraphOriginalNodeIds(graph_id) + ] + + # todo: fix for attr label + # def get_graph_node_labels(self, graph_id): + # """ + # Searchs and returns all the labels of nodes on a graph, selected by its ID. + # + # :param graph_id: The ID of the wanted graph + # :type graph_id: size_t + # :return: The list of nodes' labels on the selected graph + # :rtype: list[dict{string : string}] + # + # .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_edges(), get_graph_adjacence_matrix() + # .. note:: These functions allow to collect all the graph's informations. + # """ + # return [decode_your_map(node_label) for node_label in + # self.c_env.getGraphNodeLabels(graph_id)] + + # todo: fix for attr label + # def get_graph_edges(self, graph_id): + # """ + # Searchs and returns all the edges on a graph, selected by its ID. + # + # :param graph_id: The ID of the wanted graph + # :type graph_id: size_t + # :return: The list of edges on the selected graph + # :rtype: dict{tuple(size_t, size_t) : dict{string : string}} + # + # .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_adjacence_matrix() + # .. note:: These functions allow to collect all the graph's informations. + # """ + # return decode_graph_edges(self.c_env.getGraphEdges(graph_id)) + + + def get_graph_adjacence_matrix(self, graph_id): + """ + Searchs and returns the adjacence list of a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The adjacence list of the selected graph + :rtype: list[list[size_t]] + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphAdjacenceMatrix(graph_id) + + + def set_edit_cost(self, edit_cost, edit_cost_constant = [], edit_cost_config: dict = {}) : + """ + Sets an edit cost function to the environment, if it exists. + + Parameters + ---------- + edit_cost : str + The name of the edit cost function, must be one of the available options + in `list_of_edit_cost_options`. + + edit_cost_constant : list, optional + The edit cost values you will add to the edit cost, empty by default. + + edit_cost_config : dict, optional + The configuration parameters for the edit cost function, empty by default. + It can contain string or boolean values. + + See Also + -------- + .. function:: list_of_edit_cost_options + + Notes + ----- + Try to make sure the edit cost function exists with list_of_edit_cost_options, raise an error otherwise. + """ + if edit_cost in list_of_edit_cost_options: + edit_cost_b = edit_cost.encode('utf-8') + + # Convert the edit_cost_config to unordered_maps: + config_str, config_bool = {}, {} + for key, value in edit_cost_config.items(): + if isinstance(value, str): + config_str[key.encode('utf-8')] = value.encode('utf-8') + elif isinstance(value, py_bool): + config_bool[key.encode('utf-8')] = value + else: + raise EditCostError( + "Edit cost configuration values must be either string or boolean." + ) + + # # debug test only: + # print(f'[gedlibpy_attr.pyx] Edit cost config passed to C++ wrapper is {edit_cost_config}.') + + self.c_env.setEditCost( + edit_cost_b, edit_cost_constant, config_str, config_bool + ) + else: + raise EditCostError( + "This edit cost function doesn't exist, please see list_of_edit_cost_options for selecting a edit cost function" + ) + + + def set_personal_edit_cost(self, edit_cost_constant = []) : + """ + Sets an personal edit cost function to the environment. + + :param edit_cost_constant: The parameters you will add to the editCost, empty by default + :type edit_cost_constant: list + + .. seealso:: list_of_edit_cost_options, set_edit_cost() + .. note::You have to modify the C++ function to use it. Please see the documentation to add your Edit Cost function. + """ + self.c_env.setPersonalEditCost(edit_cost_constant) + + + def init(self, init_option='EAGER_WITHOUT_SHUFFLED_COPIES', print_to_stdout=False) : + """ + Initializes the environment with the chosen edit cost function and graphs. + + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type init_option: string + + .. seealso:: list_of_init_options + .. warning:: No modification were allowed after initialization. Try to make sure your choices is correct. You can though clear or add a graph, but recall init() after that. + .. note:: Try to make sure the option exists with list_of_init_options or choose no options, raise an error otherwise. + """ + if init_option in list_of_init_options: + init_option_b = init_option.encode('utf-8') + self.c_env.initEnv(init_option_b, print_to_stdout) + else: + raise InitError( + "This init option doesn't exist, please see list_of_init_options for selecting an option. You can choose any options." + ) + + + def set_method(self, method, options="") : + """ + Sets a computation method to the environment, if its exists. + + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :type method: string + :type options: string + + .. seealso:: init_method(), list_of_method_options + .. note:: Try to make sure the edit cost function exists with list_of_method_options, raise an error otherwise. Call init_method() after your set. + """ + if method in list_of_method_options: + method_b = method.encode('utf-8') + self.c_env.setMethod(method_b, options.encode('utf-8')) + else: + raise MethodError( + "This method doesn't exist, please see list_of_method_options for selecting a method" + ) + + + def init_method(self) : + """ + Inits the environment with the set method. + + .. seealso:: set_method(), list_of_method_options + .. note:: Call this function after set the method. You can't launch computation or change the method after that. + """ + self.c_env.initMethod() + + + def get_init_time(self) : + """ + Returns the initialization time. + + :return: The initialization time + :rtype: double + """ + return self.c_env.getInitime() + + + def run_method(self, g, h) : + """ + Computes the edit distance between two graphs g and h, with the edit cost function and method computation selected. + + :param g: The Id of the first graph to compare + :param h: The Id of the second graph to compare + :type g: size_t + :type h: size_t + + .. seealso:: get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. note:: This function only compute the distance between two graphs, without returning a result. Use the differents function to see the result between the two graphs. + """ + self.c_env.runMethod(g, h) + + + def get_upper_bound(self, g, h) : + """ + Returns the upper bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The upper bound of the edit distance cost + :rtype: double + + .. seealso:: run_method(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: The upper bound is equivalent to the result of the pessimist edit distance cost. Methods are heuristics so the library can't compute the real perfect result because it's NP-Hard problem. + """ + return self.c_env.getUpperBound(g, h) + + + def get_lower_bound(self, g, h) : + """ + Returns the lower bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The lower bound of the edit distance cost + :rtype: double + + .. seealso:: run_method(), get_upper_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function can be ignored, because lower bound doesn't have a crucial utility. + """ + return self.c_env.getLowerBound(g, h) + + + def get_forward_map(self, g, h) : + """ + Returns the forward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The forward map to the adjacence matrix between nodes of the two graphs + :rtype: list[npy_uint32] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_backward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getForwardMap(g, h) + + + def get_backward_map(self, g, h) : + """ + Returns the backward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The backward map to the adjacence matrix between nodes of the two graphs + :rtype: list[npy_uint32] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getBackwardMap(g, h) + + + def get_node_image(self, g, h, node_id) : + """ + Returns the node's image in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :param node_id: The ID of the node which you want to see the image + :type g: size_t + :type h: size_t + :type node_id: size_t + :return: The ID of the image node + :rtype: size_t + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_pre_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use BackwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getNodeImage(g, h, node_id) + + + def get_node_pre_image(self, g, h, node_id) : + """ + Returns the node's preimage in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :param node_id: The ID of the node which you want to see the preimage + :type g: size_t + :type h: size_t + :type node_id: size_t + :return: The ID of the preimage node + :rtype: size_t + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getNodePreImage(g, h, node_id) + + + def get_induced_cost(self, g, h) : + """ + Returns the induced cost between the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The induced cost between the two indicated graphs + :rtype: double + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getInducedCost(g, h) + + + def get_node_map(self, g, h) : + """ + Returns the Node Map, like C++ NodeMap. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The Node Map between the two selected graph. + :rtype: gklearn.ged.env.NodeMap. + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary, however you can understand how assignement works with this example. + """ + map_as_relation = self.c_env.getNodeMap(g, h) + induced_cost = self.c_env.getInducedCost( + g, h + ) # @todo: the C++ implementation for this function in GedLibBind.ipp re-call get_node_map() once more, this is not neccessary. + source_map = [ + item.first if item.first < len(map_as_relation) else np.inf for + item in map_as_relation + ] # item.first < len(map_as_relation) is not exactly correct. + # print(source_map) + target_map = [ + item.second if item.second < len(map_as_relation) else np.inf for + item in map_as_relation + ] + # print(target_map) + num_node_source = len([item for item in source_map if item != np.inf]) + # print(num_node_source) + num_node_target = len([item for item in target_map if item != np.inf]) + # print(num_node_target) + + node_map = NodeMap(num_node_source, num_node_target) + # print(node_map.get_forward_map(), node_map.get_backward_map()) + for i in range(len(source_map)): + node_map.add_assignment(source_map[i], target_map[i]) + node_map.set_induced_cost(induced_cost) + + return node_map + + + def get_assignment_matrix(self, g, h) : + """ + Returns the Assignment Matrix between two selected graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The Assignment Matrix between the two selected graph. + :rtype: list[list[int]] + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_node_map() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary. + """ + return self.c_env.getAssignmentMatrix(g, h) + + + def get_all_map(self, g, h) : + """ + Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The forward and backward maps to the adjacence matrix between nodes of the two graphs + :rtype: list[list[npy_uint32]] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function duplicates data so please don't use it. I also don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getAllMap(g, h) + + + def get_runtime(self, g, h) : + """ + Returns the runtime to compute the edit distance cost between two graphs g and h + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The runtime of the computation of edit distance cost between the two selected graphs + :rtype: double + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Python is a bit longer than C++ due to the functions's encapsulate. + """ + return self.c_env.getRuntime(g,h) + + + def quasimetric_cost(self) : + """ + Checks and returns if the edit costs are quasimetric. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: True if it's verified, False otherwise + :rtype: bool + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime() + .. warning:: run_method() between the same two graph must be called before this function. + """ + return self.c_env.quasimetricCosts() + + + def hungarian_LSAP(self, matrix_cost) : + """ + Applies the hungarian algorithm (LSAP) on a matrix Cost. + + :param matrix_cost: The matrix Cost + :type matrix_cost: vector[vector[size_t]] + :return: The values of rho, varrho, u and v, in this order + :rtype: vector[vector[size_t]] + + .. seealso:: hungarian_LSAPE() + """ + return self.c_env.hungarianLSAP(matrix_cost) + + + def hungarian_LSAPE(self, matrix_cost): + """ + Applies the hungarian algorithm (LSAPE) on a matrix Cost. + + :param matrix_cost: The matrix Cost + :type matrix_cost: vector[vector[double]] + :return: The values of rho, varrho, u and v, in this order + :rtype: vector[vector[double]] + + .. seealso:: hungarian_LSAP() + """ + return self.c_env.hungarianLSAPE(matrix_cost) + + + def add_random_graph_str( + self, name, classe, list_of_nodes, list_of_edges, ignore_duplicates=True + ): # todo: this function only support string labels for now. + """ + Add a Graph (not GXL) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param name: The name of the graph to add, can be an empty string + :param classe: The classe of the graph to add, can be an empty string + :param list_of_nodes: The list of nodes to add + :param list_of_edges: The list of edges to add + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type name: string + :type classe: string + :type list_of_nodes: list[tuple(size_t, dict{string : string})] + :type list_of_edges: list[tuple(tuple(size_t,size_t), dict{string : string})] + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + .. note:: The graph must respect the GXL structure. Please see how a GXL graph is construct. + + """ + id = self.add_graph(name, classe) + for node in list_of_nodes: + self.add_node(id, node[0], node[1]) + for edge in list_of_edges: + self.add_edge(id, edge[0], edge[1], edge[2], ignore_duplicates) + return id + + + def add_random_graph( + self, name, classe, list_of_nodes, list_of_edges, ignore_duplicates=True + ): + """ + # todo: no idea if this is correct... `add_random_graph_str` is never used anyway. + Add a Graph (not GXL) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param name: The name of the graph to add, can be an empty string + :param classe: The classe of the graph to add, can be an empty string + :param list_of_nodes: The list of nodes to add + :param list_of_edges: The list of edges to add + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type name: string + :type classe: string + :type list_of_nodes: list[tuple(size_t, dict{string : mixed})] + :type list_of_edges: list[tuple(tuple(size_t,size_t), dict{string : mixed})] + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + .. note:: The graph must respect the GXL structure. Please see how a GXL graph is construct. + + """ + id = self.add_graph(name, classe) + for node in list_of_nodes: + self.add_node(id, node[0], *encode_label_map(node[1])) + for edge in list_of_edges: + self.add_edge(id, edge[0], edge[1], *encode_label_map(edge[2]), ignore_duplicates) + return id + + + def add_nx_graph_str(self, g, classe, ignore_duplicates=True): + """ + Add a Graph (made by networkx) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param g: The graph to add (networkx graph) + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type g: networkx.graph + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + Notes + ----- + - The NX graph must respect the GXL structure. Please see how a GXL graph is construct. + - This function only supports string labels. Replace it by add_nx_graph() if you want to + use other types of labels. + """ + id = self.add_graph(g.name, classe) + for node in g.nodes: + self.add_node(id, str(node), g.nodes[node]) + for edge in g.edges: + self.add_edge( + id, str(edge[0]), str(edge[1]), g.get_edge_data(edge[0], edge[1]), + ignore_duplicates + ) + return id + + + def add_nx_graph( + self, g: nx.Graph, classe: str = '', ignore_duplicates: bool = True + ): + """ + Add a Graph (made by networkx) on the environment. Be careful to respect the same + format as GXL graphs for labeling nodes and edges. + + Parameters + ---------- + g : networkx.Graph + The graph to add (networkx graph) + + classe : str + The class of the graph to add. Default is empty string. + + ignore_duplicates : bool + If True, duplicate edges are ignored, otherwise it's raise an error if an + existing edge is added. True by default. + + Returns + ------- + size_t + The ID of the newly added graph. + + Notes + ----- + - The NX graph must respect the GXL structure. Please see how a GXL graph is constructed. + - This function supports mixed-type node and edge labels (int, float, str, list of + int/float/str, np.array of int and float). + + """ + id = self.add_graph(g.name, classe) + for node in g.nodes: + node_label = g.nodes[node] + # encoded_label = encode_label_map(node_label) + self.add_node(id, str(node), node_label) + for edge in g.edges: + edge_label = g.get_edge_data(edge[0], edge[1]) + # encoded_label = encode_label_map(edge_label) + self.add_edge( + id, str(edge[0]), str(edge[1]), edge_label, + ignore_duplicates + ) + return id + + + def compute_ged_on_two_graphs( + self, g1, g2, edit_cost, method, options, + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + Computes the edit distance between two NX graphs. + + :param g1: The first graph to add and compute + :param g2: The second graph to add and compute + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type g1: networksx.graph + :type g2: networksx.graph + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: The edit distance between the two graphs and the nodeMap between them. + :rtype: double, list[tuple(size_t, size_t)] + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + + """ + if self.is_initialized(): + self.restart_env() + + g = self.add_nx_graph(g1, "") + h = self.add_nx_graph(g2, "") + + self.set_edit_cost(edit_cost) + self.init(init_option) + + self.set_method(method, options) + self.init_method() + + resDistance = 0 + resMapping = [] + self.run_method(g, h) + resDistance = self.get_upper_bound(g, h) + resMapping = self.get_node_map(g, h) + + return resDistance, resMapping + + + def compute_edit_distance_on_nx_graphs( + self, dataset, classes, edit_cost, method, options, + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + + Computes all the edit distance between each NX graphs on the dataset. + + :param dataset: The list of graphs to add and compute + :param classes: The classe of all the graph, can be an empty string + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type dataset: list[networksx.graph] + :type classes: string + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: Two matrix, the first with edit distances between graphs and the second the nodeMap between graphs. The result between g and h is one the [g][h] coordinates. + :rtype: list[list[double]], list[list[list[tuple(size_t, size_t)]]] + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + + """ + if self.is_initialized(): + self.restart_env() + + print("Loading graphs in progress...") + for graph in dataset: + self.add_nx_graph(graph, classes) + listID = self.graph_ids() + print("Graphs loaded ! ") + print("Number of graphs = " + str(listID[1])) + + self.set_edit_cost(edit_cost) + print("Initialization in progress...") + self.init(init_option) + print("Initialization terminated !") + + self.set_method(method, options) + self.init_method() + + resDistance = [[]] + resMapping = [[]] + for g in range(listID[0], listID[1]): + print("Computation between graph " + str( + g + ) + " with all the others including himself.") + for h in range(listID[0], listID[1]): + #print("Computation between graph " + str(g) + " and graph " + str(h)) + self.run_method(g, h) + resDistance[g][h] = self.get_upper_bound(g, h) + resMapping[g][h] = self.get_node_map(g, h) + + print( + "Finish ! The return contains edit distances and NodeMap but you can check the result with graphs'ID until you restart the environment") + return resDistance, resMapping + + + def compute_edit_distance_on_GXl_graphs( + self, path_folder, path_XML, edit_cost, method, options="", + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + Computes all the edit distance between each GXL graphs on the folder and the XMl file. + + :param path_folder: The folder's path which contains GXL graphs + :param path_XML: The XML's path which indicates which graphes you want to load + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type path_folder: string + :type path_XML: string + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: The list of the first and last-1 ID of graphs + :rtype: tuple(size_t, size_t) + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. + + """ + + if self.is_initialized(): + self.restart_env() + + print("Loading graphs in progress...") + self.load_GXL_graphs(path_folder, path_XML) + listID = self.graph_ids() + print("Graphs loaded ! ") + print("Number of graphs = " + str(listID[1])) + + self.set_edit_cost(edit_cost) + print("Initialization in progress...") + self.init(init_option) + print("Initialization terminated !") + + self.set_method(method, options) + self.init_method() + + #res = [] + for g in range(listID[0], listID[1]): + print("Computation between graph " + str( + g + ) + " with all the others including himself.") + for h in range(listID[0], listID[1]): + #print("Computation between graph " + str(g) + " and graph " + str(h)) + self.run_method(g, h) + #res.append((get_upper_bound(g,h), get_node_map(g,h), get_runtime(g,h))) + + #return res + + print ( + "Finish ! You can check the result with each ID of graphs ! There are in the return") + print ( + "Please don't restart the environment or recall this function, you will lose your results !") + return listID + + + def get_num_graphs(self) -> int: + """ + Returns the number of graphs in the environment. + + Returns + ------- + size_t + Number of graphs in the environment. + """ + return self.c_env.getNumGraphs() + + + def get_num_node_labels(self): + """ + Returns the number of node labels. + + :return: Number of pairwise different node labels contained in the environment. + :rtype: size_t + + .. note:: If 1 is returned, the nodes are unlabeled. + """ + return self.c_env.getNumNodeLabels() + + # todo: fix this with AttrLabel + # def get_node_label(self, label_id): + # """ + # Returns node label. + # + # :param label_id: ID of node label that should be returned. Must be between 1 and get_num_node_labels(). + # :type label_id: size_t + # :return: Node label for selected label ID. + # :rtype: dict{string : string} + # """ + # return decode_your_map(self.c_env.getNodeLabel(label_id)) + + + def get_num_edge_labels(self): + """ + Returns the number of edge labels. + + :return: Number of pairwise different edge labels contained in the environment. + :rtype: size_t + + .. note:: If 1 is returned, the edges are unlabeled. + """ + return self.c_env.getNumEdgeLabels() + + # todo: fix this with AttrLabel + # def get_edge_label(self, label_id): + # """ + # Returns edge label. + # + # :param label_id: ID of edge label that should be returned. Must be between 1 and get_num_edge_labels(). + # :type label_id: size_t + # :return: Edge label for selected label ID. + # :rtype: dict{string : string} + # """ + # return decode_your_map(self.c_env.getEdgeLabel(label_id)) + + # def get_num_nodes(self, graph_id): + # """ + # Returns the number of nodes. + # + # :param graph_id: ID of an input graph that has been added to the environment. + # :type graph_id: size_t + # :return: Number of nodes in the graph. + # :rtype: size_t + # """ + # return self.c_env.getNumNodes(graph_id) + + def get_avg_num_nodes(self): + """ + Returns average number of nodes. + + :return: Average number of nodes of the graphs contained in the environment. + :rtype: double + """ + return self.c_env.getAvgNumNodes() + + # todo: fix this with AttrLabel + # def get_node_rel_cost(self, node_label_1, node_label_2): + # """ + # Returns node relabeling cost. + # + # :param node_label_1: First node label. + # :param node_label_2: Second node label. + # :type node_label_1: dict{string : string} + # :type node_label_2: dict{string : string} + # :return: Node relabeling cost for the given node labels. + # :rtype: double + # """ + # return self.c_env.getNodeRelCost( + # encode_your_map(node_label_1), encode_your_map(node_label_2) + # ) + + + # todo: fix this with AttrLabel + # def get_node_del_cost(self, node_label): + # """ + # Returns node deletion cost. + # + # :param node_label: Node label. + # :type node_label: dict{string : string} + # :return: Cost of deleting node with given label. + # :rtype: double + # """ + # return self.c_env.getNodeDelCost(encode_your_map(node_label)) + + # todo: fix this with AttrLabel + # def get_node_ins_cost(self, node_label): + # """ + # Returns node insertion cost. + # + # :param node_label: Node label. + # :type node_label: dict{string : string} + # :return: Cost of inserting node with given label. + # :rtype: double + # """ + # return self.c_env.getNodeInsCost(encode_your_map(node_label)) + + # todo: fix this with AttrLabel + # def get_median_node_label(self, node_labels): + # """ + # Computes median node label. + # + # :param node_labels: The node labels whose median should be computed. + # :type node_labels: list[dict{string : string}] + # :return: Median of the given node labels. + # :rtype: dict{string : string} + # """ + # node_labels_b = [encode_your_map(node_label) for node_label in node_labels] + # return decode_your_map(self.c_env.getMedianNodeLabel(node_labels_b)) + + # todo: fix this with AttrLabel + # def get_edge_rel_cost(self, edge_label_1, edge_label_2): + # """ + # Returns edge relabeling cost. + # + # :param edge_label_1: First edge label. + # :param edge_label_2: Second edge label. + # :type edge_label_1: dict{string : string} + # :type edge_label_2: dict{string : string} + # :return: Edge relabeling cost for the given edge labels. + # :rtype: double + # """ + # return self.c_env.getEdgeRelCost( + # encode_your_map(edge_label_1), encode_your_map(edge_label_2) + # ) + + # todo: fix this with AttrLabel + # def get_edge_del_cost(self, edge_label): + # """ + # Returns edge deletion cost. + # + # :param edge_label: Edge label. + # :type edge_label: dict{string : string} + # :return: Cost of deleting edge with given label. + # :rtype: double + # """ + # return self.c_env.getEdgeDelCost(encode_your_map(edge_label)) + + # todo: fix this with AttrLabel + # def get_edge_ins_cost(self, edge_label): + # """ + # Returns edge insertion cost. + # + # :param edge_label: Edge label. + # :type edge_label: dict{string : string} + # :return: Cost of inserting edge with given label. + # :rtype: double + # """ + # return self.c_env.getEdgeInsCost(encode_your_map(edge_label)) + + # todo: fix this with AttrLabel + # def get_median_edge_label(self, edge_labels): + # """ + # Computes median edge label. + # + # :param edge_labels: The edge labels whose median should be computed. + # :type edge_labels: list[dict{string : string}] + # :return: Median of the given edge labels. + # :rtype: dict{string : string} + # """ + # edge_labels_b = [encode_your_map(edge_label) for edge_label in edge_labels] + # return decode_your_map(self.c_env.getMedianEdgeLabel(edge_label_b)) + + def get_nx_graph( + self, graph_id, adj_matrix=True, adj_lists=False, edge_list=False + ): # @todo + """ + Get graph with id `graph_id` in the form of the NetworkX Graph. + + Parameters + ---------- + graph_id : int + ID of the selected graph. + + adj_matrix : bool + Set to `True` to construct an adjacency matrix `adj_matrix` and a hash-map `edge_labels`, which has a key for each pair `(i,j)` such that `adj_matrix[i][j]` equals 1. No effect for now. + + adj_lists : bool + No effect for now. + + edge_list : bool + No effect for now. + + Returns + ------- + NetworkX Graph object + The obtained graph. + """ + graph = nx.Graph() + graph.graph['id'] = graph_id + + nb_nodes = self.get_graph_num_nodes(graph_id) + original_node_ids = self.get_original_node_ids(graph_id) + node_labels = self.get_graph_node_labels(graph_id) +# print(original_node_ids) +# print(node_labels) + graph.graph['original_node_ids'] = original_node_ids + + for node_id in range(0, nb_nodes): + graph.add_node(node_id, **node_labels[node_id]) + # graph.nodes[node_id]['original_node_id'] = original_node_ids[node_id] + + edges = self.get_graph_edges(graph_id) + for (head, tail), labels in edges.items(): + graph.add_edge(head, tail, **labels) + # print(edges) + + return graph + + + def get_init_type(self): + """ + Returns the initialization type of the last initialization in string. + + Returns + ------- + string + Initialization type in string. + """ + return self.c_env.getInitType().decode('utf-8') + + + # def get_node_cost(self, label1, label2): + # """ + # Returns node relabeling, insertion, or deletion cost. + + # Parameters + # ---------- + # label1 : int + # First node label. + # + # label2 : int + # Second node label. + # + # Returns + # ------- + # Node relabeling cost if `label1` and `label2` are both different from `ged::dummy_label()`, node insertion cost if `label1` equals `ged::dummy_label` and `label2` does not, node deletion cost if `label1` does not equal `ged::dummy_label` and `label2` does, and 0 otherwise. + # """ + # return self.c_env.getNodeCost(label1, label2) + + def load_nx_graph(self, nx_graph, graph_id, graph_name='', graph_class=''): + """ + Loads NetworkX Graph into the GED environment. + + Parameters + ---------- + nx_graph : NetworkX Graph object + The graph that should be loaded. + + graph_id : int or None + The ID of a graph contained the environment (overwrite existing graph) or add new graph if `None`. + + graph_name : string, optional + The name of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. + + graph_class : string, optional + The class of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. + + Returns + ------- + int + The ID of the newly loaded graph. + """ + if graph_id is None: + graph_id = self.add_graph(graph_name, graph_class) + else: + self.clear_graph(graph_id) + for node in nx_graph.nodes: + self.add_node(graph_id, str(node), nx_graph.nodes[node]) + for edge in nx_graph.edges: + self.add_edge( + graph_id, str(edge[0]), str(edge[1]), + nx_graph.get_edge_data(edge[0], edge[1]) + ) + return graph_id + + + def compute_induced_cost(self, g_id, h_id, node_map): + """ + Computes the edit cost between two graphs induced by a node map. + + Parameters + ---------- + g_id : int + ID of input graph. + h_id : int + ID of input graph. + node_map: gklearn.ged.env.NodeMap. + The NodeMap instance whose reduced cost will be computed and re-assigned. + + Returns + ------- + None. + """ + relation = [] + node_map.as_relation(relation) +# print(relation) + dummy_node = get_dummy_node() +# print(dummy_node) + for i, val in enumerate(relation): + val1 = dummy_node if val[0] == np.inf else val[0] + val2 = dummy_node if val[1] == np.inf else val[1] + relation[i] = tuple((val1, val2)) +# print(relation) + induced_cost = self.c_env.computeInducedCost(g_id, h_id, relation) + node_map.set_induced_cost(induced_cost) + + + def __repr__(self): + return f"PyGEDEnvAttr(num_graphs={self.get_num_graphs()})" + + + def info(self): + """Print environment information""" + print(f"GED Environment Information:") + print(f" Number of graphs: {self.get_num_graphs()}.") + + +##################################################################### +##LISTS OF EDIT COST FUNCTIONS, METHOD COMPUTATION AND INIT OPTIONS## +##################################################################### + +list_of_edit_cost_options = get_edit_cost_options() +list_of_method_options = get_method_options() +list_of_init_options = get_init_options() + + +##################### +##ERRORS MANAGEMENT## +##################### + +class Error(Exception): + """ + Class for error's management. This one is general. + """ + pass + + +class EditCostError(Error) : + """ + Class for Edit Cost Error. Raise an error if an edit cost function doesn't exist in the library (not in list_of_edit_cost_options). + + :attribute message: The message to print when an error is detected. + :type message: string + """ + def __init__(self, message): + """ + Inits the error with its message. + + :param message: The message to print when the error is detected + :type message: string + """ + self.message = message + + +class MethodError(Error) : + """ + Class for Method Error. Raise an error if a computation method doesn't exist in the library (not in list_of_method_options). + + :attribute message: The message to print when an error is detected. + :type message: string + """ + def __init__(self, message): + """ + Inits the error with its message. + + :param message: The message to print when the error is detected + :type message: string + """ + self.message = message + + +class InitError(Error) : + """ + Class for Init Error. Raise an error if an init option doesn't exist in the library (not in list_of_init_options). + + :attribute message: The message to print when an error is detected. + :type message: string + """ + def __init__(self, message): + """ + Inits the error with its message. + + :param message: The message to print when the error is detected + :type message: string + """ + self.message = message + + +############################################### +##Help functions for label mapping conversion## +############################################### + + +# Converts string -> string +cdef unordered_map[string, string] convert_str_map(dict d): + cdef unordered_map[string, string] out + for k, v in d.items(): + out[k.encode('utf-8')] = v.encode('utf-8') + return out + + +cdef unordered_map[string, int] convert_int_map(dict d): + cdef unordered_map[string, int] out + for k, v in d.items(): + out[k.encode('utf-8')] = v + return out + + +cdef unordered_map[string, double] convert_float_map(dict d): + cdef unordered_map[string, double] out + for k, v in d.items(): + out[k.encode('utf-8')] = v + return out + + +cdef unordered_map[string, vector[string]] convert_list_str_map(dict d): + cdef unordered_map[string, vector[string]] out + for k, v in d.items(): + out[k.encode('utf-8')] = to_vector_str(v) + return out + + +cdef unordered_map[string, vector[int]] convert_list_and_array_int_map(dict d_list, dict d_array): + cdef unordered_map[string, vector[int]] out + for k, v in d_list.items(): + out[k.encode('utf-8')] = list_to_vector_int(v) + for k, v in d_array.items(): + out[k.encode('utf-8')] = array_to_vector_int(v) + return out + + +# cdef unordered_map[string, vector[int]] convert_list_int_map(dict d): +# cdef unordered_map[string, vector[int]] out +# for k, v in d.items(): +# out[string(k.encode('utf-8'))] = to_vector_int(v) +# return out + + +cdef unordered_map[string, vector[double]] convert_list_and_array_float_map(dict d_list, dict d_array): + cdef unordered_map[string, vector[double]] out + for k, v in d_list.items(): + out[k.encode('utf-8')] = list_to_vector_float(v) + for k, v in d_array.items(): + out[k.encode('utf-8')] = array_to_vector_float(v) + return out + + +# cdef unordered_map[string, vector[double]] convert_list_float_map(dict d): +# cdef unordered_map[string, vector[double]] out +# for k, v in d.items(): +# out[string(k.encode('utf-8'))] = to_vector_float(v) +# return out + + +# For string lists only (list of str) +cdef vector[string] to_vector_str(object obj): + # Attention: type check (if it is str) must be done before calling this function!! + + cdef vector[string] vec + cdef str x + + for x in obj: + vec.push_back(x.encode('utf-8')) + return vec + + +# General conversion for list of int +cdef vector[int] list_to_vector_int(obj): + # Attention: type check (if it is int) must be done before calling this function!! + # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? + + cdef vector[int] vec + + for x in obj: + check_int32_range(x) + for x in obj: + vec.push_back(x) + + return vec + + +# General conversion for np.ndarray[int] +cdef vector[int] array_to_vector_int(obj): + # Attention: type check (if it is int) must be done before calling this function!! + # Check the dimension of the array before calling this function!! + # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? + + cdef vector[int] vec + + if obj.dtype != np.int32: + raise TypeError( + f'Expected np.ndarray[int32]. Got {obj.dtype} instead. ' + f'Convert your array to int32 before passing it, or help us enhance this function. ;)' + ) + for i in range(obj.shape[0]): + vec.push_back((obj[i])) + + return vec + +# # General conversion for list or np.ndarray[int] +# cdef vector[int] to_vector_int(obj): +# # Attention: type check (if it is int) must be done before calling this function!! +# # Check the dimension of the array before calling this function!! +# # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? +# +# cdef vector[int] vec +# # cdef cnp.ndarray[cnp.int64_t, ndim=1] arr # Cannot declare cdef inside an if statement +# +# if isinstance(obj, list): +# for x in obj: +# check_int32_range(x) +# for x in obj: +# vec.push_back(x) +# elif isinstance(obj, np.ndarray): +# if obj.dtype != np.int32: +# raise TypeError( +# f'Expected np.ndarray[int32]. Got {obj.dtype} instead. ' +# f'Convert your array to int32 before passing it, or help us enhance this function. ;)' +# ) +# for i in range(obj.shape[0]): +# vec.push_back((obj[i])) +# else: +# raise TypeError("Expected list[int] or np.ndarray[int]") +# +# return vec + + +# General conversion for list of float +cdef vector[double] list_to_vector_float(obj): + # Attention: type check (if it is float) must be done before calling this function!! + # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? + + cdef vector[double] vec + + for x in obj: + vec.push_back(x) + return vec + + +# General conversion for np.ndarray[float] +cdef vector[double] array_to_vector_float(obj): + # Attention: type check (if it is float) must be done before calling this function!! + # Check the dimension of the array before calling this function!! + # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? + + cdef vector[double] vec + + for i in range(obj.shape[0]): + vec.push_back((obj[i])) + return vec + + +# # For float arrays +# cdef vector[double] to_vector_float(obj): +# cdef vector[double] vec +# if isinstance(obj, list): +# for x in obj: +# vec.push_back(x) +# elif isinstance(obj, np.ndarray): +# cdef np.ndarray[np.float64_t, ndim=1] arr = obj +# for i in range(arr.shape[0]): +# vec.push_back(arr[i]) +# else: +# raise TypeError("Expected list[float] or np.ndarray[float]") +# return vec + +######################################### +##PYTHON FUNCTIONS FOR SOME COMPUTATION## +######################################### + +def encode_your_map(map_u): + """ + Encodes Python unicode strings in dictionnary `map` to utf-8 byte strings for C++ functions. + + :param map_b: The map to encode + :type map_b: dict{string : string} + :return: The encoded map + :rtype: dict{'b'string : 'b'string} + + .. note:: This function is used for type connection. + + """ + # # debug test only: + # print(f'The labels passed from python are: {map_u}.') + + res = {} + for key, value in map_u.items(): + res[key.encode('utf-8')] = value.encode('utf-8') + + # print(f'The labels encoded for C++ are: {res}.') + + return res + + +def decode_your_map(map_b): + """ + Decodes utf-8 byte strings in `map` from C++ functions to Python unicode strings. + + :param map_b: The map to decode + :type map_b: dict{'b'string : 'b'string} + :return: The decoded map + :rtype: dict{string : string} + + .. note:: This function is used for type connection. + + """ + res = {} + for key, value in map_b.items(): + res[key.decode('utf-8')] = value.decode('utf-8') + return res + + +def decode_graph_edges(map_edge_b): + """ + Decode utf-8 byte strings in graph edges `map` from C++ functions to Python unicode strings. + + Parameters + ---------- + map_edge_b : dict{tuple(size_t, size_t) : dict{'b'string : 'b'string}} + The map to decode. + + Returns + ------- + dict{tuple(size_t, size_t) : dict{string : string}} + The decoded map. + + Notes + ----- + This is a helper function for function `GEDEnv.get_graph_edges()`. + """ + map_edges = {} + for key, value in map_edge_b.items(): + map_edges[key] = decode_your_map(value) + return map_edges + + +def encode_label_map(label_map: dict[str, Any]): + """ + Encode a mixed-type label dict into separate dicts for each type. + + Parameters + ---------- + label_map : dict[str, Any] + A dict containing string keys and mixed-type values (int, float, str, list of + int/float/str, np.array of int and float). + + Returns + ------- + str_map : dict[str, str] + A dict containing string keys and string values. + """ + str_map = {} + int_map = {} + float_map = {} + list_str_map = {} + list_int_map = {} + np_array_int_map = {} + list_float_map = {} + np_array_float_map = {} + + for k, v in label_map.items(): + if isinstance(v, str): + str_map[k] = v + elif isinstance(v, int): + int_map[k] = v + elif isinstance(v, float): + float_map[k] = v + elif isinstance(v, np.ndarray): + if v.ndim != 1: + raise ValueError( + f'Expected 1D array for list of int, got {v.ndim}D array instead.' + ) + if v.dtype == np.int32 or v.dtype == np.int64: + np_array_int_map[k] = v + elif v.dtype == np.float64 or v.dtype == np.float32: + np_array_float_map[k] = v + else: + raise TypeError( + f'Expected np.ndarray[int32/64] or np.ndarray[float32/64], got {v.dtype} instead.' + ) + elif isinstance(v, py_list) and len(v) > 0: + # todo: here we only check the type of the first element. + if isinstance(v[0], str): + list_str_map[k] = v + if isinstance(v[0], int): + list_int_map[k] = v + elif isinstance(v[0], float): + list_float_map[k] = v + else: + raise TypeError( + f'Expected list of int or float, got invalid types in list for key {k}.' + ) + else: + raise TypeError(f'Unsupported label type: {k}: {type(v)}.') + + return ( + convert_str_map(str_map), + convert_int_map(int_map), + convert_float_map(float_map), + convert_list_str_map(list_str_map), + convert_list_and_array_int_map(list_int_map, np_array_int_map), + convert_list_and_array_float_map(list_float_map, np_array_float_map) + ) + + +def check_int32_range(value: int): + """ + Check if the value is in the range of int32. + + Parameters + ---------- + value : int + The value to check. + + Raises + ------ + ValueError + If the value is not in the range of int32. + """ + if not (-2147483648 <= value <= 2147483647): + raise ValueError(f'Value {value} is out of int32 range.') \ No newline at end of file diff --git a/gklearn/gedlib/gedlibpy_gxl.pyx b/gklearn/gedlib/gedlibpy_gxl.pyx new file mode 100644 index 0000000000..3308a2a27d --- /dev/null +++ b/gklearn/gedlib/gedlibpy_gxl.pyx @@ -0,0 +1,1582 @@ +# distutils: language = c++ + +""" + Python GedLib module for the GXLLabel type + ====================== + + This module allows using C++ library for edit distance between graphs (GedLib) with Python. + + + Authors + ------------------- + + David Blumenthal + Natacha Lambert + Linlin Jia + + Copyright (C) 2019-2025 by all the authors + + Classes & Functions + ------------------- + +""" + +################################# +##DECLARATION OF C++ INTERFACES## +################################# + + +#Types imports for C++ compatibility +from libcpp.vector cimport vector +from libcpp.string cimport string +from libcpp.map cimport map +from libcpp cimport bool +from libcpp.pair cimport pair +from libcpp.list cimport list + +#Long unsigned int equivalent +cimport numpy as cnp +ctypedef cnp.npy_uint32 UINT32_t +from cpython cimport array + + +cdef extern from "src/gedlib_bind_gxl.hpp" namespace "pyged": + + cdef vector[string] getEditCostStringOptions() except + + cdef vector[string] getMethodStringOptions() except + + cdef vector[string] getInitStringOptions() except + + cdef size_t getDummyNode() except + + + cdef cppclass PyGEDEnvGXL: + PyGEDEnvGXL() except + + bool isInitialized() except + + void restartEnv() except + + void loadGXLGraph(string pathFolder, string pathXML, bool node_type, bool edge_type) except + + pair[size_t,size_t] getGraphIds() except + + vector[size_t] getAllGraphIds() except + + string getGraphClass(size_t id) except + + string getGraphName(size_t id) except + + size_t addGraph(string name, string classe) except + + void addNode(size_t graphId, string nodeId, map[string, string] nodeLabel) except + + void addEdge(size_t graphId, string tail, string head, map[string, string] edgeLabel, bool ignoreDuplicates) except + + void clearGraph(size_t graphId) except + + size_t getGraphInternalId(size_t graphId) except + + size_t getGraphNumNodes(size_t graphId) except + + size_t getGraphNumEdges(size_t graphId) except + + vector[string] getGraphOriginalNodeIds(size_t graphId) except + + vector[map[string, string]] getGraphNodeLabels(size_t graphId) except + + map[pair[size_t, size_t], map[string, string]] getGraphEdges(size_t graphId) except + + vector[vector[size_t]] getGraphAdjacenceMatrix(size_t graphId) except + + void setEditCost(string editCost, vector[double] editCostConstant) except + + void setPersonalEditCost(vector[double] editCostConstant) except + + void initEnv(string initOption, bool print_to_stdout) except + + void setMethod(string method, string options) except + + void initMethod() except + + double getInitime() except + + void runMethod(size_t g, size_t h) except + + double getUpperBound(size_t g, size_t h) except + + double getLowerBound(size_t g, size_t h) except + + vector[cnp.npy_uint64] getForwardMap(size_t g, size_t h) except + + vector[cnp.npy_uint64] getBackwardMap(size_t g, size_t h) except + + size_t getNodeImage(size_t g, size_t h, size_t nodeId) except + + size_t getNodePreImage(size_t g, size_t h, size_t nodeId) except + + double getInducedCost(size_t g, size_t h) except + + vector[pair[size_t,size_t]] getNodeMap(size_t g, size_t h) except + + vector[vector[int]] getAssignmentMatrix(size_t g, size_t h) except + + vector[vector[cnp.npy_uint64]] getAllMap(size_t g, size_t h) except + + double getRuntime(size_t g, size_t h) except + + bool quasimetricCosts() except + + vector[vector[size_t]] hungarianLSAP(vector[vector[size_t]] matrixCost) except + + vector[vector[double]] hungarianLSAPE(vector[vector[double]] matrixCost) except + + # added by Linlin Jia. + size_t getNumNodeLabels() except + + map[string, string] getNodeLabel(size_t label_id) except + + size_t getNumEdgeLabels() except + + map[string, string] getEdgeLabel(size_t label_id) except + +# size_t getNumNodes(size_t graph_id) except + + double getAvgNumNodes() except + + double getNodeRelCost(map[string, string] & node_label_1, map[string, string] & node_label_2) except + + double getNodeDelCost(map[string, string] & node_label) except + + double getNodeInsCost(map[string, string] & node_label) except + + map[string, string] getMedianNodeLabel(vector[map[string, string]] & node_labels) except + + double getEdgeRelCost(map[string, string] & edge_label_1, map[string, string] & edge_label_2) except + + double getEdgeDelCost(map[string, string] & edge_label) except + + double getEdgeInsCost(map[string, string] & edge_label) except + + map[string, string] getMedianEdgeLabel(vector[map[string, string]] & edge_labels) except + + string getInitType() except + +# double getNodeCost(size_t label1, size_t label2) except + + double computeInducedCost(size_t g_id, size_t h_id, vector[pair[size_t,size_t]]) except + + + +############################# +##CYTHON WRAPPER INTERFACES## +############################# + +# import cython +import numpy as np +import networkx as nx +from gklearn.ged.env import NodeMap + +# import librariesImport +from ctypes import * +import os +lib1 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/fann.2.2.0/libdoublefann.so') +lib2 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/libsvm.3.22/libsvm.so') +lib3 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libnomad.so') +lib4 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libsgtelib.so') + + +def get_edit_cost_options() : + """ + Searchs the differents edit cost functions and returns the result. + + :return: The list of edit cost functions + :rtype: list[string] + + .. warning:: This function is useless for an external use. Please use directly list_of_edit_cost_options. + .. note:: Prefer the list_of_edit_cost_options attribute of this module. + """ + + return [option.decode('utf-8') for option in getEditCostStringOptions()] + + +def get_method_options() : + """ + Searchs the differents method for edit distance computation between graphs and returns the result. + + :return: The list of method to compute the edit distance between graphs + :rtype: list[string] + + .. warning:: This function is useless for an external use. Please use directly list_of_method_options. + .. note:: Prefer the list_of_method_options attribute of this module. + """ + return [option.decode('utf-8') for option in getMethodStringOptions()] + + +def get_init_options() : + """ + Searchs the differents initialization parameters for the environment computation for graphs and returns the result. + + :return: The list of options to initialize the computation environment + :rtype: list[string] + + .. warning:: This function is useless for an external use. Please use directly list_of_init_options. + .. note:: Prefer the list_of_init_options attribute of this module. + """ + return [option.decode('utf-8') for option in getInitStringOptions()] + + +def get_dummy_node() : + """ + Returns the ID of a dummy node. + + :return: The ID of the dummy node (18446744073709551614 for my computer, the hugest number possible) + :rtype: size_t + + .. note:: A dummy node is used when a node isn't associated to an other node. + """ + return getDummyNode() + + +# @cython.auto_pickle(True) +cdef class GEDEnvGXL: + """Cython wrapper class for C++ class PyGEDEnv + """ + cdef PyGEDEnvGXL* c_env # hold a pointer to the C++ instance which we're wrapping + + + def __cinit__(self): +# self.c_env = PyGEDEnv() + self.c_env = new PyGEDEnvGXL() + + + def __dealloc__(self): + if self.c_env != NULL: + del self.c_env + self.c_env = NULL + + +# def __reduce__(self): +# # return GEDEnv, (self.c_env,) +# return GEDEnv, tuple() + + + def is_initialized(self) : + """ + Checks and returns if the computation environment is initialized or not. + + :return: True if it's initialized, False otherwise + :rtype: bool + + .. note:: This function exists for internals verifications but you can use it for your code. + """ + return self.c_env.isInitialized() + + + def restart_env(self) : + """ + Restarts the environment variable. All data related to it will be delete. + + .. warning:: This function deletes all graphs, computations and more so make sure you don't need anymore your environment. + .. note:: You can now delete and add somes graphs after initialization so you can avoid this function. + """ + self.c_env.restartEnv() + + + def load_GXL_graphs(self, path_folder, path_XML, node_type, edge_type) : + """ + Loads some GXL graphes on the environment which is in a same folder, and present in the XMLfile. + + :param path_folder: The folder's path which contains GXL graphs + :param path_XML: The XML's path which indicates which graphes you want to load + :param node_type: Select if nodes are labeled or unlabeled + :param edge_type: Select if edges are labeled or unlabeled + :type path_folder: string + :type path_XML: string + :type node_type: bool + :type edge_type: bool + + + .. note:: You can call this function multiple times if you want, but not after an init call. + """ + self.c_env.loadGXLGraph(path_folder.encode('utf-8'), path_XML.encode('utf-8'), node_type, edge_type) + + + def graph_ids(self) : + """ + Searchs the first and last IDs of the loaded graphs in the environment. + + :return: The pair of the first and the last graphs Ids + :rtype: tuple(size_t, size_t) + + .. note:: Prefer this function if you have huges structures with lots of graphs. + """ + return self.c_env.getGraphIds() + + + def get_all_graph_ids(self) : + """ + Searchs all the IDs of the loaded graphs in the environment. + + :return: The list of all graphs's Ids + :rtype: list[size_t] + + .. note:: The last ID is equal to (number of graphs - 1). The order correspond to the loading order. + """ + return self.c_env.getAllGraphIds() + + + def get_graph_class(self, id) : + """ + Returns the class of a graph with its ID. + + :param id: The ID of the wanted graph + :type id: size_t + :return: The class of the graph which correpond to the ID + :rtype: string + + .. seealso:: get_graph_class() + .. note:: An empty string can be a class. + """ + return self.c_env.getGraphClass(id) + + + def get_graph_name(self, id) : + """ + Returns the name of a graph with its ID. + + :param id: The ID of the wanted graph + :type id: size_t + :return: The name of the graph which correpond to the ID + :rtype: string + + .. seealso:: get_graph_class() + .. note:: An empty string can be a name. + """ + return self.c_env.getGraphName(id).decode('utf-8') + + + def add_graph(self, name="", classe="") : + """ + Adds a empty graph on the environment, with its name and its class. Nodes and edges will be add in a second time. + + :param name: The name of the new graph, an empty string by default + :param classe: The class of the new graph, an empty string by default + :type name: string + :type classe: string + :return: The ID of the newly graphe + :rtype: size_t + + .. seealso::add_node(), add_edge() , add_symmetrical_edge() + .. note:: You can call this function without parameters. You can also use this function after initialization, call init() after you're finished your modifications. + """ + return self.c_env.addGraph(name.encode('utf-8'), classe.encode('utf-8')) + + + def add_node(self, graph_id, node_id, node_label): + """ + Adds a node on a graph selected by its ID. A ID and a label for the node is required. + + :param graph_id: The ID of the wanted graph + :param node_id: The ID of the new node + :param node_label: The label of the new node + :type graph_id: size_t + :type node_id: string + :type node_label: dict{string : string} + + .. seealso:: add_graph(), add_edge(), add_symmetrical_edge() + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + """ + self.c_env.addNode(graph_id, node_id.encode('utf-8'), encode_your_map(node_label)) + + + def add_edge(self, graph_id, tail, head, edge_label, ignore_duplicates=True) : + """ + Adds an edge on a graph selected by its ID. + + :param graph_id: The ID of the wanted graph + :param tail: The ID of the tail node for the new edge + :param head: The ID of the head node for the new edge + :param edge_label: The label of the new edge + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type graph_id: size_t + :type tail: string + :type head: string + :type edge_label: dict{string : string} + :type ignore_duplicates: bool + + .. seealso:: add_graph(), add_node(), add_symmetrical_edge() + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + """ + self.c_env.addEdge(graph_id, tail.encode('utf-8'), head.encode('utf-8'), encode_your_map(edge_label), ignore_duplicates) + + + def add_symmetrical_edge(self, graph_id, tail, head, edge_label) : + """ + Adds a symmetrical edge on a graph selected by its ID. + + :param graph_id: The ID of the wanted graph + :param tail: The ID of the tail node for the new edge + :param head: The ID of the head node for the new edge + :param edge_label: The label of the new edge + :type graph_id: size_t + :type tail: string + :type head: string + :type edge_label: dict{string : string} + + .. seealso:: add_graph(), add_node(), add_edge() + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + """ + tailB = tail.encode('utf-8') + headB = head.encode('utf-8') + edgeLabelB = encode_your_map(edge_label) + self.c_env.addEdge(graph_id, tailB, headB, edgeLabelB, True) + self.c_env.addEdge(graph_id, headB, tailB, edgeLabelB, True) + + + def clear_graph(self, graph_id) : + """ + Deletes a graph, selected by its ID, to the environment. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + + .. note:: Call init() after you're finished your modifications. + """ + self.c_env.clearGraph(graph_id) + + + def get_graph_internal_id(self, graph_id) : + """ + Searchs and returns the internal Id of a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The internal ID of the selected graph + :rtype: size_t + + .. seealso:: get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphInternalId(graph_id) + + + def get_graph_num_nodes(self, graph_id) : + """ + Searchs and returns the number of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The number of nodes on the selected graph + :rtype: size_t + + .. seealso:: get_graph_internal_id(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphNumNodes(graph_id) + + + def get_graph_num_edges(self, graph_id) : + """ + Searchs and returns the number of edges on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The number of edges on the selected graph + :rtype: size_t + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphNumEdges(graph_id) + + + def get_original_node_ids(self, graph_id) : + """ + Searchs and returns all th Ids of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The list of IDs's nodes on the selected graph + :rtype: list[string] + + .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return [gid.decode('utf-8') for gid in self.c_env.getGraphOriginalNodeIds(graph_id)] + + + def get_graph_node_labels(self, graph_id) : + """ + Searchs and returns all the labels of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The list of nodes' labels on the selected graph + :rtype: list[dict{string : string}] + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return [decode_your_map(node_label) for node_label in self.c_env.getGraphNodeLabels(graph_id)] + + + def get_graph_edges(self, graph_id) : + """ + Searchs and returns all the edges on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The list of edges on the selected graph + :rtype: dict{tuple(size_t, size_t) : dict{string : string}} + + .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return decode_graph_edges(self.c_env.getGraphEdges(graph_id)) + + + def get_graph_adjacence_matrix(self, graph_id) : + """ + Searchs and returns the adjacence list of a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The adjacence list of the selected graph + :rtype: list[list[size_t]] + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphAdjacenceMatrix(graph_id) + + + def set_edit_cost(self, edit_cost, edit_cost_constant = []) : + """ + Sets an edit cost function to the environment, if it exists. + + :param edit_cost: The name of the edit cost function + :type edit_cost: string + :param edi_cost_constant: The parameters you will add to the editCost, empty by default + :type edit_cost_constant: list + + .. seealso:: list_of_edit_cost_options + .. note:: Try to make sure the edit cost function exists with list_of_edit_cost_options, raise an error otherwise. + """ + if edit_cost in list_of_edit_cost_options: + edit_cost_b = edit_cost.encode('utf-8') + self.c_env.setEditCost(edit_cost_b, edit_cost_constant) + else: + raise EditCostError("This edit cost function doesn't exist, please see list_of_edit_cost_options for selecting a edit cost function") + + + def set_personal_edit_cost(self, edit_cost_constant = []) : + """ + Sets an personal edit cost function to the environment. + + :param edit_cost_constant: The parameters you will add to the editCost, empty by default + :type edit_cost_constant: list + + .. seealso:: list_of_edit_cost_options, set_edit_cost() + .. note::You have to modify the C++ function to use it. Please see the documentation to add your Edit Cost function. + """ + self.c_env.setPersonalEditCost(edit_cost_constant) + + + def init(self, init_option='EAGER_WITHOUT_SHUFFLED_COPIES', print_to_stdout=False) : + """ + Initializes the environment with the chosen edit cost function and graphs. + + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type init_option: string + + .. seealso:: list_of_init_options + .. warning:: No modification were allowed after initialization. Try to make sure your choices is correct. You can though clear or add a graph, but recall init() after that. + .. note:: Try to make sure the option exists with list_of_init_options or choose no options, raise an error otherwise. + """ + if init_option in list_of_init_options: + init_option_b = init_option.encode('utf-8') + self.c_env.initEnv(init_option_b, print_to_stdout) + else: + raise InitError("This init option doesn't exist, please see list_of_init_options for selecting an option. You can choose any options.") + + + def set_method(self, method, options="") : + """ + Sets a computation method to the environment, if its exists. + + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :type method: string + :type options: string + + .. seealso:: init_method(), list_of_method_options + .. note:: Try to make sure the edit cost function exists with list_of_method_options, raise an error otherwise. Call init_method() after your set. + """ + if method in list_of_method_options: + method_b = method.encode('utf-8') + self.c_env.setMethod(method_b, options.encode('utf-8')) + else: + raise MethodError("This method doesn't exist, please see list_of_method_options for selecting a method") + + + def init_method(self) : + """ + Inits the environment with the set method. + + .. seealso:: set_method(), list_of_method_options + .. note:: Call this function after set the method. You can't launch computation or change the method after that. + """ + self.c_env.initMethod() + + + def get_init_time(self) : + """ + Returns the initialization time. + + :return: The initialization time + :rtype: double + """ + return self.c_env.getInitime() + + + def run_method(self, g, h) : + """ + Computes the edit distance between two graphs g and h, with the edit cost function and method computation selected. + + :param g: The Id of the first graph to compare + :param h: The Id of the second graph to compare + :type g: size_t + :type h: size_t + + .. seealso:: get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. note:: This function only compute the distance between two graphs, without returning a result. Use the differents function to see the result between the two graphs. + """ + self.c_env.runMethod(g, h) + + + def get_upper_bound(self, g, h) : + """ + Returns the upper bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The upper bound of the edit distance cost + :rtype: double + + .. seealso:: run_method(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: The upper bound is equivalent to the result of the pessimist edit distance cost. Methods are heuristics so the library can't compute the real perfect result because it's NP-Hard problem. + """ + return self.c_env.getUpperBound(g, h) + + + def get_lower_bound(self, g, h) : + """ + Returns the lower bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The lower bound of the edit distance cost + :rtype: double + + .. seealso:: run_method(), get_upper_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function can be ignored, because lower bound doesn't have a crucial utility. + """ + return self.c_env.getLowerBound(g, h) + + + def get_forward_map(self, g, h) : + """ + Returns the forward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The forward map to the adjacence matrix between nodes of the two graphs + :rtype: list[npy_uint32] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_backward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getForwardMap(g, h) + + + def get_backward_map(self, g, h) : + """ + Returns the backward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The backward map to the adjacence matrix between nodes of the two graphs + :rtype: list[npy_uint32] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getBackwardMap(g, h) + + + def get_node_image(self, g, h, node_id) : + """ + Returns the node's image in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :param node_id: The ID of the node which you want to see the image + :type g: size_t + :type h: size_t + :type node_id: size_t + :return: The ID of the image node + :rtype: size_t + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_pre_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use BackwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getNodeImage(g, h, node_id) + + + def get_node_pre_image(self, g, h, node_id) : + """ + Returns the node's preimage in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :param node_id: The ID of the node which you want to see the preimage + :type g: size_t + :type h: size_t + :type node_id: size_t + :return: The ID of the preimage node + :rtype: size_t + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getNodePreImage(g, h, node_id) + + + def get_induced_cost(self, g, h) : + """ + Returns the induced cost between the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The induced cost between the two indicated graphs + :rtype: double + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getInducedCost(g, h) + + + def get_node_map(self, g, h) : + """ + Returns the Node Map, like C++ NodeMap. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The Node Map between the two selected graph. + :rtype: gklearn.ged.env.NodeMap. + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary, however you can understand how assignement works with this example. + """ + map_as_relation = self.c_env.getNodeMap(g, h) + induced_cost = self.c_env.getInducedCost(g, h) # @todo: the C++ implementation for this function in GedLibBind.ipp re-call get_node_map() once more, this is not neccessary. + source_map = [item.first if item.first < len(map_as_relation) else np.inf for item in map_as_relation] # item.first < len(map_as_relation) is not exactly correct. +# print(source_map) + target_map = [item.second if item.second < len(map_as_relation) else np.inf for item in map_as_relation] +# print(target_map) + num_node_source = len([item for item in source_map if item != np.inf]) +# print(num_node_source) + num_node_target = len([item for item in target_map if item != np.inf]) +# print(num_node_target) + + node_map = NodeMap(num_node_source, num_node_target) +# print(node_map.get_forward_map(), node_map.get_backward_map()) + for i in range(len(source_map)): + node_map.add_assignment(source_map[i], target_map[i]) + node_map.set_induced_cost(induced_cost) + + return node_map + + + def get_assignment_matrix(self, g, h) : + """ + Returns the Assignment Matrix between two selected graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The Assignment Matrix between the two selected graph. + :rtype: list[list[int]] + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_node_map() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary. + """ + return self.c_env.getAssignmentMatrix(g, h) + + + def get_all_map(self, g, h) : + """ + Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The forward and backward maps to the adjacence matrix between nodes of the two graphs + :rtype: list[list[npy_uint32]] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function duplicates data so please don't use it. I also don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getAllMap(g, h) + + + def get_runtime(self, g, h) : + """ + Returns the runtime to compute the edit distance cost between two graphs g and h + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The runtime of the computation of edit distance cost between the two selected graphs + :rtype: double + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Python is a bit longer than C++ due to the functions's encapsulate. + """ + return self.c_env.getRuntime(g,h) + + + def quasimetric_cost(self) : + """ + Checks and returns if the edit costs are quasimetric. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: True if it's verified, False otherwise + :rtype: bool + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime() + .. warning:: run_method() between the same two graph must be called before this function. + """ + return self.c_env.quasimetricCosts() + + + def hungarian_LSAP(self, matrix_cost) : + """ + Applies the hungarian algorithm (LSAP) on a matrix Cost. + + :param matrix_cost: The matrix Cost + :type matrix_cost: vector[vector[size_t]] + :return: The values of rho, varrho, u and v, in this order + :rtype: vector[vector[size_t]] + + .. seealso:: hungarian_LSAPE() + """ + return self.c_env.hungarianLSAP(matrix_cost) + + + def hungarian_LSAPE(self, matrix_cost) : + """ + Applies the hungarian algorithm (LSAPE) on a matrix Cost. + + :param matrix_cost: The matrix Cost + :type matrix_cost: vector[vector[double]] + :return: The values of rho, varrho, u and v, in this order + :rtype: vector[vector[double]] + + .. seealso:: hungarian_LSAP() + """ + return self.c_env.hungarianLSAPE(matrix_cost) + + + def add_random_graph(self, name, classe, list_of_nodes, list_of_edges, ignore_duplicates=True) : + """ + Add a Graph (not GXL) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param name: The name of the graph to add, can be an empty string + :param classe: The classe of the graph to add, can be an empty string + :param list_of_nodes: The list of nodes to add + :param list_of_edges: The list of edges to add + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type name: string + :type classe: string + :type list_of_nodes: list[tuple(size_t, dict{string : string})] + :type list_of_edges: list[tuple(tuple(size_t,size_t), dict{string : string})] + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + .. note:: The graph must respect the GXL structure. Please see how a GXL graph is construct. + + """ + id = self.add_graph(name, classe) + for node in list_of_nodes: + self.add_node(id, node[0], node[1]) + for edge in list_of_edges: + self.add_edge(id, edge[0], edge[1], edge[2], ignore_duplicates) + return id + + + def add_nx_graph(self, g, classe, ignore_duplicates=True) : + """ + Add a Graph (made by networkx) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param g: The graph to add (networkx graph) + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type g: networkx.graph + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + .. note:: The NX graph must respect the GXL structure. Please see how a GXL graph is construct. + + """ + id = self.add_graph(g.name, classe) + for node in g.nodes: + self.add_node(id, str(node), g.nodes[node]) + for edge in g.edges: + self.add_edge(id, str(edge[0]), str(edge[1]), g.get_edge_data(edge[0], edge[1]), ignore_duplicates) + return id + + + def compute_ged_on_two_graphs(self, g1, g2, edit_cost, method, options, init_option="EAGER_WITHOUT_SHUFFLED_COPIES") : + """ + Computes the edit distance between two NX graphs. + + :param g1: The first graph to add and compute + :param g2: The second graph to add and compute + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type g1: networksx.graph + :type g2: networksx.graph + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: The edit distance between the two graphs and the nodeMap between them. + :rtype: double, list[tuple(size_t, size_t)] + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + + """ + if self.is_initialized() : + self.restart_env() + + g = self.add_nx_graph(g1, "") + h = self.add_nx_graph(g2, "") + + self.set_edit_cost(edit_cost) + self.init(init_option) + + self.set_method(method, options) + self.init_method() + + resDistance = 0 + resMapping = [] + self.run_method(g, h) + resDistance = self.get_upper_bound(g, h) + resMapping = self.get_node_map(g, h) + + return resDistance, resMapping + + + def compute_edit_distance_on_nx_graphs(self, dataset, classes, edit_cost, method, options, init_option="EAGER_WITHOUT_SHUFFLED_COPIES") : + """ + + Computes all the edit distance between each NX graphs on the dataset. + + :param dataset: The list of graphs to add and compute + :param classes: The classe of all the graph, can be an empty string + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type dataset: list[networksx.graph] + :type classes: string + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: Two matrix, the first with edit distances between graphs and the second the nodeMap between graphs. The result between g and h is one the [g][h] coordinates. + :rtype: list[list[double]], list[list[list[tuple(size_t, size_t)]]] + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + + """ + if self.is_initialized() : + self.restart_env() + + print("Loading graphs in progress...") + for graph in dataset : + self.add_nx_graph(graph, classes) + listID = self.graph_ids() + print("Graphs loaded ! ") + print("Number of graphs = " + str(listID[1])) + + self.set_edit_cost(edit_cost) + print("Initialization in progress...") + self.init(init_option) + print("Initialization terminated !") + + self.set_method(method, options) + self.init_method() + + resDistance = [[]] + resMapping = [[]] + for g in range(listID[0], listID[1]) : + print("Computation between graph " + str(g) + " with all the others including himself.") + for h in range(listID[0], listID[1]) : + #print("Computation between graph " + str(g) + " and graph " + str(h)) + self.run_method(g, h) + resDistance[g][h] = self.get_upper_bound(g, h) + resMapping[g][h] = self.get_node_map(g, h) + + print("Finish ! The return contains edit distances and NodeMap but you can check the result with graphs'ID until you restart the environment") + return resDistance, resMapping + + + def compute_edit_distance_on_GXl_graphs(self, path_folder, path_XML, edit_cost, method, options="", init_option="EAGER_WITHOUT_SHUFFLED_COPIES") : + """ + Computes all the edit distance between each GXL graphs on the folder and the XMl file. + + :param path_folder: The folder's path which contains GXL graphs + :param path_XML: The XML's path which indicates which graphes you want to load + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type path_folder: string + :type path_XML: string + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: The list of the first and last-1 ID of graphs + :rtype: tuple(size_t, size_t) + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. + + """ + + if self.is_initialized() : + self.restart_env() + + print("Loading graphs in progress...") + self.load_GXL_graphs(path_folder, path_XML) + listID = self.graph_ids() + print("Graphs loaded ! ") + print("Number of graphs = " + str(listID[1])) + + self.set_edit_cost(edit_cost) + print("Initialization in progress...") + self.init(init_option) + print("Initialization terminated !") + + self.set_method(method, options) + self.init_method() + + #res = [] + for g in range(listID[0], listID[1]) : + print("Computation between graph " + str(g) + " with all the others including himself.") + for h in range(listID[0], listID[1]) : + #print("Computation between graph " + str(g) + " and graph " + str(h)) + self.run_method(g,h) + #res.append((get_upper_bound(g,h), get_node_map(g,h), get_runtime(g,h))) + + #return res + + print ("Finish ! You can check the result with each ID of graphs ! There are in the return") + print ("Please don't restart the environment or recall this function, you will lose your results !") + return listID + + + def get_num_node_labels(self): + """ + Returns the number of node labels. + + :return: Number of pairwise different node labels contained in the environment. + :rtype: size_t + + .. note:: If 1 is returned, the nodes are unlabeled. + """ + return self.c_env.getNumNodeLabels() + + + def get_node_label(self, label_id): + """ + Returns node label. + + :param label_id: ID of node label that should be returned. Must be between 1 and get_num_node_labels(). + :type label_id: size_t + :return: Node label for selected label ID. + :rtype: dict{string : string} + """ + return decode_your_map(self.c_env.getNodeLabel(label_id)) + + + def get_num_edge_labels(self): + """ + Returns the number of edge labels. + + :return: Number of pairwise different edge labels contained in the environment. + :rtype: size_t + + .. note:: If 1 is returned, the edges are unlabeled. + """ + return self.c_env.getNumEdgeLabels() + + + def get_edge_label(self, label_id): + """ + Returns edge label. + + :param label_id: ID of edge label that should be returned. Must be between 1 and get_num_edge_labels(). + :type label_id: size_t + :return: Edge label for selected label ID. + :rtype: dict{string : string} + """ + return decode_your_map(self.c_env.getEdgeLabel(label_id)) + + +# def get_num_nodes(self, graph_id): +# """ +# Returns the number of nodes. +# +# :param graph_id: ID of an input graph that has been added to the environment. +# :type graph_id: size_t +# :return: Number of nodes in the graph. +# :rtype: size_t +# """ +# return self.c_env.getNumNodes(graph_id) + + def get_avg_num_nodes(self): + """ + Returns average number of nodes. + + :return: Average number of nodes of the graphs contained in the environment. + :rtype: double + """ + return self.c_env.getAvgNumNodes() + + def get_node_rel_cost(self, node_label_1, node_label_2): + """ + Returns node relabeling cost. + + :param node_label_1: First node label. + :param node_label_2: Second node label. + :type node_label_1: dict{string : string} + :type node_label_2: dict{string : string} + :return: Node relabeling cost for the given node labels. + :rtype: double + """ + return self.c_env.getNodeRelCost(encode_your_map(node_label_1), encode_your_map(node_label_2)) + + + def get_node_del_cost(self, node_label): + """ + Returns node deletion cost. + + :param node_label: Node label. + :type node_label: dict{string : string} + :return: Cost of deleting node with given label. + :rtype: double + """ + return self.c_env.getNodeDelCost(encode_your_map(node_label)) + + + def get_node_ins_cost(self, node_label): + """ + Returns node insertion cost. + + :param node_label: Node label. + :type node_label: dict{string : string} + :return: Cost of inserting node with given label. + :rtype: double + """ + return self.c_env.getNodeInsCost(encode_your_map(node_label)) + + + def get_median_node_label(self, node_labels): + """ + Computes median node label. + + :param node_labels: The node labels whose median should be computed. + :type node_labels: list[dict{string : string}] + :return: Median of the given node labels. + :rtype: dict{string : string} + """ + node_labels_b = [encode_your_map(node_label) for node_label in node_labels] + return decode_your_map(self.c_env.getMedianNodeLabel(node_labels_b)) + + + def get_edge_rel_cost(self, edge_label_1, edge_label_2): + """ + Returns edge relabeling cost. + + :param edge_label_1: First edge label. + :param edge_label_2: Second edge label. + :type edge_label_1: dict{string : string} + :type edge_label_2: dict{string : string} + :return: Edge relabeling cost for the given edge labels. + :rtype: double + """ + return self.c_env.getEdgeRelCost(encode_your_map(edge_label_1), encode_your_map(edge_label_2)) + + + def get_edge_del_cost(self, edge_label): + """ + Returns edge deletion cost. + + :param edge_label: Edge label. + :type edge_label: dict{string : string} + :return: Cost of deleting edge with given label. + :rtype: double + """ + return self.c_env.getEdgeDelCost(encode_your_map(edge_label)) + + + def get_edge_ins_cost(self, edge_label): + """ + Returns edge insertion cost. + + :param edge_label: Edge label. + :type edge_label: dict{string : string} + :return: Cost of inserting edge with given label. + :rtype: double + """ + return self.c_env.getEdgeInsCost(encode_your_map(edge_label)) + + + def get_median_edge_label(self, edge_labels): + """ + Computes median edge label. + + :param edge_labels: The edge labels whose median should be computed. + :type edge_labels: list[dict{string : string}] + :return: Median of the given edge labels. + :rtype: dict{string : string} + """ + edge_labels_b = [encode_your_map(edge_label) for edge_label in edge_labels] + return decode_your_map(self.c_env.getMedianEdgeLabel(edge_label_b)) + + + def get_nx_graph(self, graph_id, adj_matrix=True, adj_lists=False, edge_list=False): # @todo + """ + Get graph with id `graph_id` in the form of the NetworkX Graph. + + Parameters + ---------- + graph_id : int + ID of the selected graph. + + adj_matrix : bool + Set to `True` to construct an adjacency matrix `adj_matrix` and a hash-map `edge_labels`, which has a key for each pair `(i,j)` such that `adj_matrix[i][j]` equals 1. No effect for now. + + adj_lists : bool + No effect for now. + + edge_list : bool + No effect for now. + + Returns + ------- + NetworkX Graph object + The obtained graph. + """ + graph = nx.Graph() + graph.graph['id'] = graph_id + + nb_nodes = self.get_graph_num_nodes(graph_id) + original_node_ids = self.get_original_node_ids(graph_id) + node_labels = self.get_graph_node_labels(graph_id) +# print(original_node_ids) +# print(node_labels) + graph.graph['original_node_ids'] = original_node_ids + + for node_id in range(0, nb_nodes): + graph.add_node(node_id, **node_labels[node_id]) +# graph.nodes[node_id]['original_node_id'] = original_node_ids[node_id] + + edges = self.get_graph_edges(graph_id) + for (head, tail), labels in edges.items(): + graph.add_edge(head, tail, **labels) +# print(edges) + + return graph + + + def get_init_type(self): + """ + Returns the initialization type of the last initialization in string. + + Returns + ------- + string + Initialization type in string. + """ + return self.c_env.getInitType().decode('utf-8') + + +# def get_node_cost(self, label1, label2): +# """ +# Returns node relabeling, insertion, or deletion cost. + +# Parameters +# ---------- +# label1 : int +# First node label. +# +# label2 : int +# Second node label. +# +# Returns +# ------- +# Node relabeling cost if `label1` and `label2` are both different from `ged::dummy_label()`, node insertion cost if `label1` equals `ged::dummy_label` and `label2` does not, node deletion cost if `label1` does not equal `ged::dummy_label` and `label2` does, and 0 otherwise. +# """ +# return self.c_env.getNodeCost(label1, label2) + + + def load_nx_graph(self, nx_graph, graph_id, graph_name='', graph_class=''): + """ + Loads NetworkX Graph into the GED environment. + + Parameters + ---------- + nx_graph : NetworkX Graph object + The graph that should be loaded. + + graph_id : int or None + The ID of a graph contained the environment (overwrite existing graph) or add new graph if `None`. + + graph_name : string, optional + The name of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. + + graph_class : string, optional + The class of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. + + Returns + ------- + int + The ID of the newly loaded graph. + """ + if graph_id is None: + graph_id = self.add_graph(graph_name, graph_class) + else: + self.clear_graph(graph_id) + for node in nx_graph.nodes: + self.add_node(graph_id, str(node), nx_graph.nodes[node]) + for edge in nx_graph.edges: + self.add_edge(graph_id, str(edge[0]), str(edge[1]), nx_graph.get_edge_data(edge[0], edge[1])) + return graph_id + + + def compute_induced_cost(self, g_id, h_id, node_map): + """ + Computes the edit cost between two graphs induced by a node map. + + Parameters + ---------- + g_id : int + ID of input graph. + h_id : int + ID of input graph. + node_map: gklearn.ged.env.NodeMap. + The NodeMap instance whose reduced cost will be computed and re-assigned. + + Returns + ------- + None. + """ + relation = [] + node_map.as_relation(relation) +# print(relation) + dummy_node = get_dummy_node() +# print(dummy_node) + for i, val in enumerate(relation): + val1 = dummy_node if val[0] == np.inf else val[0] + val2 = dummy_node if val[1] == np.inf else val[1] + relation[i] = tuple((val1, val2)) +# print(relation) + induced_cost = self.c_env.computeInducedCost(g_id, h_id, relation) + node_map.set_induced_cost(induced_cost) + + +##################################################################### +##LISTS OF EDIT COST FUNCTIONS, METHOD COMPUTATION AND INIT OPTIONS## +##################################################################### + +list_of_edit_cost_options = get_edit_cost_options() +list_of_method_options = get_method_options() +list_of_init_options = get_init_options() + + +##################### +##ERRORS MANAGEMENT## +##################### + +class Error(Exception): + """ + Class for error's management. This one is general. + """ + pass + + +class EditCostError(Error) : + """ + Class for Edit Cost Error. Raise an error if an edit cost function doesn't exist in the library (not in list_of_edit_cost_options). + + :attribute message: The message to print when an error is detected. + :type message: string + """ + def __init__(self, message): + """ + Inits the error with its message. + + :param message: The message to print when the error is detected + :type message: string + """ + self.message = message + + +class MethodError(Error) : + """ + Class for Method Error. Raise an error if a computation method doesn't exist in the library (not in list_of_method_options). + + :attribute message: The message to print when an error is detected. + :type message: string + """ + def __init__(self, message): + """ + Inits the error with its message. + + :param message: The message to print when the error is detected + :type message: string + """ + self.message = message + + +class InitError(Error) : + """ + Class for Init Error. Raise an error if an init option doesn't exist in the library (not in list_of_init_options). + + :attribute message: The message to print when an error is detected. + :type message: string + """ + def __init__(self, message): + """ + Inits the error with its message. + + :param message: The message to print when the error is detected + :type message: string + """ + self.message = message + + +######################################### +##PYTHON FUNCTIONS FOR SOME COMPUTATION## +######################################### + +def encode_your_map(map_u): + """ + Encodes Python unicode strings in dictionnary `map` to utf-8 byte strings for C++ functions. + + :param map_b: The map to encode + :type map_b: dict{string : string} + :return: The encoded map + :rtype: dict{'b'string : 'b'string} + + .. note:: This function is used for type connection. + + """ + res = {} + for key, value in map_u.items(): + res[key.encode('utf-8')] = value.encode('utf-8') + return res + + +def decode_your_map(map_b): + """ + Decodes utf-8 byte strings in `map` from C++ functions to Python unicode strings. + + :param map_b: The map to decode + :type map_b: dict{'b'string : 'b'string} + :return: The decoded map + :rtype: dict{string : string} + + .. note:: This function is used for type connection. + + """ + res = {} + for key, value in map_b.items(): + res[key.decode('utf-8')] = value.decode('utf-8') + return res + + +def decode_graph_edges(map_edge_b): + """ + Decode utf-8 byte strings in graph edges `map` from C++ functions to Python unicode strings. + + Parameters + ---------- + map_edge_b : dict{tuple(size_t, size_t) : dict{'b'string : 'b'string}} + The map to decode. + + Returns + ------- + dict{tuple(size_t, size_t) : dict{string : string}} + The decoded map. + + Notes + ----- + This is a helper function for function `GEDEnv.get_graph_edges()`. + """ + map_edges = {} + for key, value in map_edge_b.items(): + map_edges[key] = decode_your_map(value) + return map_edges + + + + + + + +# cdef extern from "src/GedLibBind.h" namespace "shapes": +# cdef cppclass Rectangle: +# Rectangle() except + +# Rectangle(int, int, int, int) except + +# int x0, y0, x1, y1 +# int getArea() +# void getSize(int* width, int* height) +# void move(int, int) + + +# # Create a Cython extension type which holds a C++ instance +# # as an attribute and create a bunch of forwarding methods +# # Python extension type. +# cdef class PyRectangle: +# cdef Rectangle c_rect # Hold a C++ instance which we're wrapping + +# def __cinit__(self, int x0, int y0, int x1, int y1): +# self.c_rect = Rectangle(x0, y0, x1, y1) + +# def get_area(self): +# return self.c_rect.getArea() + +# def get_size(self): +# cdef int width, height +# self.c_rect.getSize(&width, &height) +# return width, height + +# def move(self, dx, dy): +# self.c_rect.move(dx, dy) + +# # Attribute access +# @property +# def x0(self): +# return self.c_rect.x0 +# @x0.setter +# def x0(self, x0): +# self.c_rect.x0 = x0 + +# # Attribute access +# @property +# def x1(self): +# return self.c_rect.x1 +# @x1.setter +# def x1(self, x1): +# self.c_rect.x1 = x1 + +# # Attribute access +# @property +# def y0(self): +# return self.c_rect.y0 +# @y0.setter +# def y0(self, y0): +# self.c_rect.y0 = y0 + +# # Attribute access +# @property +# def y1(self): +# return self.c_rect.y1 +# @y1.setter +# def y1(self, y1): +# self.c_rect.y1 = y1 diff --git a/gklearn/gedlib/librariesImport.py b/gklearn/gedlib/librariesImport.py deleted file mode 100755 index df25902ece..0000000000 --- a/gklearn/gedlib/librariesImport.py +++ /dev/null @@ -1,6 +0,0 @@ -from ctypes import * -import os -lib1 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/fann.2.2.0/libdoublefann.so') -lib2 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/libsvm.3.22/libsvm.so') -lib3 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libnomad.so') -lib4 = cdll.LoadLibrary(os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libsgtelib.so') diff --git a/gklearn/gedlib/libraries_import.py b/gklearn/gedlib/libraries_import.py new file mode 100644 index 0000000000..914a80d973 --- /dev/null +++ b/gklearn/gedlib/libraries_import.py @@ -0,0 +1,15 @@ +import os +from ctypes import * + +lib1 = cdll.LoadLibrary( + os.path.dirname(os.path.realpath(__file__)) + '/lib/fann.2.2.0/libdoublefann.so' + ) +lib2 = cdll.LoadLibrary( + os.path.dirname(os.path.realpath(__file__)) + '/lib/libsvm.3.22/libsvm.so' + ) +lib3 = cdll.LoadLibrary( + os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libnomad.so' + ) +lib4 = cdll.LoadLibrary( + os.path.dirname(os.path.realpath(__file__)) + '/lib/nomad.3.8.1/libsgtelib.so' + ) diff --git a/gklearn/gedlib/refactor_log.md b/gklearn/gedlib/refactor_log.md new file mode 100644 index 0000000000..7a730dfe7d --- /dev/null +++ b/gklearn/gedlib/refactor_log.md @@ -0,0 +1,64 @@ +## (2025.06.02) Trial 005: resume ifdef wrappers and add control in setup: + +Comparing to trial 004, resume the `#ifdef GXL_GEDLIB_SHARED` and `#ifndef SRC_ENV_GED_ENV_GXL_CPP_` +wrappers in `gedlib/ged_env.hpp`, and add `"-DGXL_GEDLIB_SHARED"` in `gedlibpy/setup_core.py` -> `get_extensions()->Extension.extra_compile_args`. + +**This does not solve the multiple definition error.** + +## (2025.06.02) Trial 004: remove `"include/gedlib-master/src/env/ged_env.gxl.cpp"`: + +Comparing to trial 003, remove `"include/gedlib-master/src/env/ged_env.gxl.cpp"` from +`gedlibpy/setup_core.py` -> `get_extensions()->Extension.sources`. + +**This does not solve the multiple definition error.** + +## (2025.06.02) Trial 003: remove ifdef wrappers: + +Comparing to trial 002, remove the `#ifdef GXL_GEDLIB_SHARED` and `#ifndef SRC_ENV_GED_ENV_GXL_CPP_` +wrappers in `gedlib/ged_env.hpp`. + +**This does not solve the multiple definition error.** + +## (2025.06.02) Trial 002: Add `#define SRC_ENV_GED_ENV_GXL_CPP_`: + +Comparing to trial 001, add `#define SRC_ENV_GED_ENV_GXL_CPP_` in `gedlibpy/src/gedlib_header.hpp`. + +**This does not solve the multiple definition error.** + +## (2025.06.02) Trial 001: Add extern declaration and instantiation for `AttrLabel` + +In `gedlib/ged_env.hpp`, add `extern template class GEDEnv;` as +follows: + +```cpp +#ifdef GXL_GEDLIB_SHARED +#ifndef SRC_ENV_GED_ENV_GXL_CPP_ +extern template class GEDEnv; +extern template class GEDEnv; +#endif /* SRC_ENV_GED_ENV_GXL_CPP_ */ +#endif /* GXL_GEDLIB_SHARED */ +``` + +Add `template class GEDEnv;` in `ged_env.gxl.cpp`. + +Add `"include/gedlib-master/src/env/ged_env.gxl.cpp"` in +`gedlibpy/setup_core.py` as + +```python +def get_extensions(): + exts = [ + Extension( + "gedlibpy", + # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], + sources=[ + "common_bind.pyx", "gedlibpy_gxl.pyx", "gedlibpy_attr.pyx", + "src/gedlib_bind_gxl.cpp", "src/gedlib_bind_attr.cpp", "src/gedlib_bind_util.cpp", + "include/gedlib-master/src/env/ged_env.gxl.cpp" + ], +..... +``` + +**This does not solve the multiple definition error.** E.g., +```bash +graphkit-learn/gklearn/gedlib/src/../include/gedlib-master/src/env/ged_env.ipp:255: multiple definition of `ged::GEDEnv, std::allocator >, std::map, std::allocator >, std::__cxx11::basic_string, std::allocator >, std::less, std::allocator > >, std::allocator, std::allocator > const, std::__cxx11::basic_string, std::allocator > > > >, std::map, std::allocator >, std::__cxx11::basic_string, std::allocator >, std::less, std::allocator > >, std::allocator, std::allocator > const, std::__cxx11::basic_string, std::allocator > > > > >::load_gxl_graphs(std::__cxx11::basic_string, std::allocator > const&, std::__cxx11::basic_string, std::allocator > const&, ged::Options::GXLNodeEdgeType, ged::Options::GXLNodeEdgeType, std::unordered_set, std::allocator >, std::hash, std::allocator > >, std::equal_to, std::allocator > >, std::allocator, std::allocator > > > const&, std::unordered_set, std::allocator >, std::hash, std::allocator > >, std::equal_to, std::allocator > >, std::allocator, std::allocator > > > const&)'; build/temp.linux-x86_64-cpython-310/common_bind.o:/mnt/F0CA2D75CA2D38EC/research-repo/codes/graphkit-learn/gklearn/gedlib/./src/../include/gedlib-master/src/env/ged_env.ipp:255: first defined here +``` diff --git a/gklearn/gedlib/setup.py b/gklearn/gedlib/setup.py index 1b07c939c2..640d672845 100755 --- a/gklearn/gedlib/setup.py +++ b/gklearn/gedlib/setup.py @@ -301,17 +301,53 @@ def clean_previous_build(): # def get_extensions(include_glibc): def get_extensions(): exts = [ + # Extension( + # "gedlibpy", + # # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], + # sources=[ + # "common_bind.pyx", "gedlibpy_gxl.pyx", "gedlibpy_attr.pyx", + # "src/gedlib_bind_gxl.cpp", "src/gedlib_bind_attr.cpp", "src/gedlib_bind_util.cpp", + # # "include/gedlib-master/src/env/ged_env.gxl.cpp" + # ], + # include_dirs=[ + # "src", + # "include", + # "include/lsape.5/include", + # "include/eigen.3.3.4/Eigen", + # "include/nomad.3.8.1/src", + # "include/nomad.3.8.1/ext/sgtelib/src", + # "include/libsvm.3.22", + # "include/fann.2.2.0/include", + # "include/boost.1.69.0" + # ], + # library_dirs=[ + # "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", + # "lib/nomad.3.8.1" + # ], + # libraries=["doublefann", "sgtelib", "svm", "nomad"], + # language="c++", + # extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + # extra_link_args=["-std=c++17"] + # ) Extension( - "gedlibpy", + "gedlibpy_gxl", # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], - sources=["gedlibpy.pyx"], + sources=[ + # "include/gedlib-master/src/env/ged_env.gxl.cpp", + "gedlibpy_gxl.pyx", + # "src/gedlib_bind_gxl.cpp", + # "src/gedlib_bind_util.cpp", + ], include_dirs=[ - "src", "include", "include/lsape.5/include", + "src", + "include", + "include/lsape.5/include", "include/eigen.3.3.4/Eigen", "include/nomad.3.8.1/src", "include/nomad.3.8.1/ext/sgtelib/src", "include/libsvm.3.22", - "include/fann.2.2.0/include", "include/boost.1.69.0" + "include/fann.2.2.0/include", + "include/boost.1.69.0" ], library_dirs=[ "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", @@ -319,9 +355,38 @@ def get_extensions(): ], libraries=["doublefann", "sgtelib", "svm", "nomad"], language="c++", - extra_compile_args=["-std=c++11"], - extra_link_args=["-std=c++11"] - ) + extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + extra_link_args=["-std=c++17"] + ), + Extension( + "gedlibpy_attr", + # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], + sources=[ + # "include/gedlib-master/src/env/ged_env.gxl.cpp", + "gedlibpy_attr.pyx", + # "src/gedlib_bind_gxl.cpp", + # "src/gedlib_bind_util.cpp", + ], + include_dirs=[ + "src", + "include", + "include/lsape.5/include", + "include/eigen.3.3.4/Eigen", + "include/nomad.3.8.1/src", + "include/nomad.3.8.1/ext/sgtelib/src", + "include/libsvm.3.22", + "include/fann.2.2.0/include", + "include/boost.1.69.0" + ], + library_dirs=[ + "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", + "lib/nomad.3.8.1" + ], + libraries=["doublefann", "sgtelib", "svm", "nomad"], + language="c++", + extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + extra_link_args=["-std=c++17"] + ), ] return exts @@ -366,11 +431,19 @@ def remove_includes(): setup( ext_modules=cythonize( extensions, - compiler_directives={'language_level': '3'} + compiler_directives={'language_level': '3'}, + # Generate .html files for Cython annotations, should be set to False for production + # (i.e., when the package is installed, not when it is developed): + annotate=True, # todo + # Only recompile if the .pyx files are modified: + force=True, # fixme: check if it still works if c++ wrappers are modified + # Use N threads for compilation multiple .pyx files to .cpp files (works + # only if there are multiple extensions): + nthreads=4, # todo: change as needed ), name="gedlibpy", author="Lambert Natacha and Linlin Jia", - author_email="linlin.jia@unibe.ch", + author_email="jajupmochi@gmail.com", description="A Python wrapper library for C++ library GEDLIB of graph edit distances", long_description=long_description, long_description_content_type="text/markdown", @@ -380,14 +453,16 @@ def remove_includes(): 'Tracker': 'https://github.com/jajupmochi/graphkit-learn/issues', }, url="https://github.com/jajupmochi/graphkit-learn/tree/master/gklearn/gedlib", + license="GPL-3.0-or-later", # SPDX license identifier classifiers=[ "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + # "License :: OSI Approved", "Operating System :: OS Independent", 'Intended Audience :: Science/Research', 'Intended Audience :: Developers', ], - include_dirs=[numpy.get_include()] + include_dirs=[numpy.get_include()], + zip_safe=False, # Do not zip the package, so that .so files can be loaded correctly ) # List generated files: @@ -406,4 +481,13 @@ def remove_includes(): # Remove GEDLIB include files: remove_includes() -# Commande Bash : python setup.py build_ext --inplace +# Bash Command: +# python3 setup.py build_ext --inplace --parallel 8 +# -- Explain: +# -- --parallel N: parallel the construction of .cpp files using c++ compiler (g++ or +# clang++) with N threads (on c++ layer, from .cpp to .so). +# It is different from the N threads in cythonize(), which is on cython layer (from +# .pyx to .cpp). +# if error: command 'clang++' failed: No such file or directory: +# Add "CXX=g++". e.g., +# CXX=g++ python setup_core.py build_ext --inplace diff --git a/gklearn/gedlib/setup_core.py b/gklearn/gedlib/setup_core.py new file mode 100644 index 0000000000..ee4094040b --- /dev/null +++ b/gklearn/gedlib/setup_core.py @@ -0,0 +1,239 @@ +import importlib +import os +import shutil +import sys +from distutils.core import setup +from distutils.extension import Extension + +import numpy +from Cython.Build import cythonize + + +def install(package): + try: + importlib.import_module(package) + except ImportError: + import subprocess + cur_python = sys.executable + subprocess.call([cur_python, '-m', 'pip', 'install', package]) + + +# def install_gedlib(): +# """ +# Notes +# ----- +# The followings are done during this installation: +# - Build files in `ext/fann.2.2.0/lib/`; +# - Clean obj files in `ext/nomad.3.8.1/ext/sgtelib/src/`; +# - Build files in `ext/nomad.3.8.1/ext/sgtelib/lib/`; +# - Generate executable files in `ext/nomad.3.8.1/ext/sgtelib/bin/`; +# - Build files in `ext/nomad.3.8.1/lib/`; +# - Generate `ext/libsvm.3.22/libsvm.so`; +# """ +# print() +# print('Installing GEDLIB...') +# import subprocess +# cur_python = sys.executable +# subprocess.call([cur_python, '--version']) +# subprocess.call(['which', cur_python]) +# gedlib_dir = 'include/gedlib-master/' +# subprocess.call( +# [cur_python, 'install.py'], cwd=gedlib_dir +# ) +# print() + + +# def clean_previous_build(): +# # clean previous build +# print() +# print('Cleaning previous build...') +# for name in os.listdir(): +# if (name.startswith('gedlibpy') and not ( +# name.endswith('.pyx') or name.endswith('.pxd'))): +# os.remove(name) +# if name == 'build': +# shutil.rmtree(name) +# print('Done!') +# print() + + +# def get_extensions(include_glibc): +def get_extensions(): + exts = [ + # Extension( + # "gedlibpy", + # # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], + # sources=[ + # "common_bind.pyx", "gedlibpy_gxl.pyx", "gedlibpy_attr.pyx", + # "src/gedlib_bind_gxl.cpp", "src/gedlib_bind_attr.cpp", "src/gedlib_bind_util.cpp", + # # "include/gedlib-master/src/env/ged_env.gxl.cpp" + # ], + # include_dirs=[ + # "src", + # "include", + # "include/lsape.5/include", + # "include/eigen.3.3.4/Eigen", + # "include/nomad.3.8.1/src", + # "include/nomad.3.8.1/ext/sgtelib/src", + # "include/libsvm.3.22", + # "include/fann.2.2.0/include", + # "include/boost.1.69.0" + # ], + # library_dirs=[ + # "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", + # "lib/nomad.3.8.1" + # ], + # libraries=["doublefann", "sgtelib", "svm", "nomad"], + # language="c++", + # extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + # extra_link_args=["-std=c++17"] + # ) + Extension( + "gedlibpy_gxl", + # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], + sources=[ + # "include/gedlib-master/src/env/ged_env.gxl.cpp", + "gedlibpy_gxl.pyx", + # "src/gedlib_bind_gxl.cpp", + # "src/gedlib_bind_util.cpp", + ], + include_dirs=[ + "src", + "include", + "include/lsape.5/include", + "include/eigen.3.3.4/Eigen", + "include/nomad.3.8.1/src", + "include/nomad.3.8.1/ext/sgtelib/src", + "include/libsvm.3.22", + "include/fann.2.2.0/include", + "include/boost.1.69.0" + ], + library_dirs=[ + "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", + "lib/nomad.3.8.1" + ], + libraries=["doublefann", "sgtelib", "svm", "nomad"], + language="c++", + extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + extra_link_args=["-std=c++17"] + ), + Extension( + "gedlibpy_attr", + # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], + sources=[ + # "include/gedlib-master/src/env/ged_env.gxl.cpp", + "gedlibpy_attr.pyx", + # "src/gedlib_bind_gxl.cpp", + # "src/gedlib_bind_util.cpp", + ], + include_dirs=[ + "src", + "include", + "include/lsape.5/include", + "include/eigen.3.3.4/Eigen", + "include/nomad.3.8.1/src", + "include/nomad.3.8.1/ext/sgtelib/src", + "include/libsvm.3.22", + "include/fann.2.2.0/include", + "include/boost.1.69.0" + ], + library_dirs=[ + "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", + "lib/nomad.3.8.1" + ], + libraries=["doublefann", "sgtelib", "svm", "nomad"], + language="c++", + extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + extra_link_args=["-std=c++17"] + ), + ] + return exts + + +def remove_includes(): + print() + print('Deleting includes...') + name = os.path.join(os.getcwd(), 'include/') + shutil.rmtree(name) + print('Done!') + print() + + +if __name__ == '__main__': + # if args.build_gedlib == 'true': + # # Install GEDLIB: + # install_gedlib() + + # # clean previous build: + # clean_previous_build() + + print() + print('Start building...') + # Build gedlibpy: + # extensions = get_extensions(include_glibc) + extensions = get_extensions() + with open("README.rst", "r") as fh: + long_description = fh.read() + + # Attention: setup function can not be put inside a function! + setup( + ext_modules=cythonize( + extensions, + compiler_directives={'language_level': '3'}, + # Generate .html files for Cython annotations, should be set to False for production + # (i.e., when the package is installed, not when it is developed): + annotate=True, # todo + # Only recompile if the .pyx files are modified: + force=True, # fixme: check if it still works if c++ wrappers are modified + # Use N threads for compilation multiple .pyx files to .cpp files (works + # only if there are multiple extensions): + nthreads=4, # todo: change as needed + ), + name="gedlibpy", + author="Lambert Natacha and Linlin Jia", + author_email="jajupmochi@gmail.com", + description="A Python wrapper library for C++ library GEDLIB of graph edit distances", + long_description=long_description, + long_description_content_type="text/markdown", + project_urls={ + # 'Documentation': 'https://graphkit-learn.readthedocs.io', + 'Source': 'https://github.com/jajupmochi/graphkit-learn/tree/master/gklearn/gedlib', + 'Tracker': 'https://github.com/jajupmochi/graphkit-learn/issues', + }, + url="https://github.com/jajupmochi/graphkit-learn/tree/master/gklearn/gedlib", + license="GPL-3.0-or-later", # SPDX license identifier + classifiers=[ + "Programming Language :: Python :: 3", + # "License :: OSI Approved", + "Operating System :: OS Independent", + 'Intended Audience :: Science/Research', + 'Intended Audience :: Developers', + ], + include_dirs=[numpy.get_include()], + zip_safe=False, # Do not zip the package, so that .so files can be loaded correctly + ) + + # List generated files: + print() + print('The following files are generated:') + for name in os.listdir(): + if (name.startswith('gedlibpy') and not ( + name.endswith('.pyx') or name.endswith('.pxd'))): + print(name) + + print() + print('Build completed!') + print() + +# Bash Command: +# python3 setup_core.py build_ext --inplace --parallel 8 +# -- Explain: +# -- --parallel N: parallel the construction of .cpp files using c++ compiler (g++ or +# clang++) with N threads (on c++ layer, from .cpp to .so). +# It is different from the N threads in cythonize(), which is on cython layer (from +# .pyx to .cpp). +# if error: command 'clang++' failed: No such file or directory: +# Add "CXX=g++". e.g., +# CXX=g++ python setup_core.py build_ext --inplace + + diff --git a/gklearn/gedlib/setup_simple.py b/gklearn/gedlib/setup_simple.py index 8fcd1c73c6..f540bdd06e 100755 --- a/gklearn/gedlib/setup_simple.py +++ b/gklearn/gedlib/setup_simple.py @@ -300,17 +300,53 @@ def clean_previous_build(): def get_extensions(include_glibc): exts = [ + # Extension( + # "gedlibpy", + # # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], + # sources=[ + # "common_bind.pyx", "gedlibpy_gxl.pyx", "gedlibpy_attr.pyx", + # "src/gedlib_bind_gxl.cpp", "src/gedlib_bind_attr.cpp", "src/gedlib_bind_util.cpp", + # # "include/gedlib-master/src/env/ged_env.gxl.cpp" + # ], + # include_dirs=[ + # "src", + # "include", + # "include/lsape.5/include", + # "include/eigen.3.3.4/Eigen", + # "include/nomad.3.8.1/src", + # "include/nomad.3.8.1/ext/sgtelib/src", + # "include/libsvm.3.22", + # "include/fann.2.2.0/include", + # "include/boost.1.69.0" + # ], + # library_dirs=[ + # "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", + # "lib/nomad.3.8.1" + # ], + # libraries=["doublefann", "sgtelib", "svm", "nomad"], + # language="c++", + # extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + # extra_link_args=["-std=c++17"] + # ) Extension( - "gedlibpy", + "gedlibpy_gxl", # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], - sources=["gedlibpy.pyx"], + sources=[ + # "include/gedlib-master/src/env/ged_env.gxl.cpp", + "gedlibpy_gxl.pyx", + # "src/gedlib_bind_gxl.cpp", + # "src/gedlib_bind_util.cpp", + ], include_dirs=[ - "src", "include", "include/lsape.5/include", + "src", + "include", + "include/lsape.5/include", "include/eigen.3.3.4/Eigen", "include/nomad.3.8.1/src", "include/nomad.3.8.1/ext/sgtelib/src", "include/libsvm.3.22", - "include/fann.2.2.0/include", "include/boost.1.69.0" + "include/fann.2.2.0/include", + "include/boost.1.69.0" ], library_dirs=[ "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", @@ -318,9 +354,38 @@ def get_extensions(include_glibc): ], libraries=["doublefann", "sgtelib", "svm", "nomad"], language="c++", - extra_compile_args=["-std=c++11"], - extra_link_args=["-std=c++11"] - ) + extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + extra_link_args=["-std=c++17"] + ), + Extension( + "gedlibpy_attr", + # sources=["gedlibpy.pyx", "src/GedLibBind.cpp"], + sources=[ + # "include/gedlib-master/src/env/ged_env.gxl.cpp", + "gedlibpy_attr.pyx", + # "src/gedlib_bind_gxl.cpp", + # "src/gedlib_bind_util.cpp", + ], + include_dirs=[ + "src", + "include", + "include/lsape.5/include", + "include/eigen.3.3.4/Eigen", + "include/nomad.3.8.1/src", + "include/nomad.3.8.1/ext/sgtelib/src", + "include/libsvm.3.22", + "include/fann.2.2.0/include", + "include/boost.1.69.0" + ], + library_dirs=[ + "lib/fann.2.2.0", "lib/libsvm.3.22", # "lib/gedlib", + "lib/nomad.3.8.1" + ], + libraries=["doublefann", "sgtelib", "svm", "nomad"], + language="c++", + extra_compile_args=["-std=c++17"], # , "-DGXL_GEDLIB_SHARED"], + extra_link_args=["-std=c++17"] + ), ] return exts @@ -364,11 +429,19 @@ def remove_includes(): setup( ext_modules=cythonize( extensions, - compiler_directives={'language_level': '3'} + compiler_directives={'language_level': '3'}, + # Generate .html files for Cython annotations, should be set to False for production + # (i.e., when the package is installed, not when it is developed): + annotate=True, # todo + # Only recompile if the .pyx files are modified: + force=True, # fixme: check if it still works if c++ wrappers are modified + # Use N threads for compilation multiple .pyx files to .cpp files (works + # only if there are multiple extensions): + nthreads=4, # todo: change as needed ), name="gedlibpy", author="Lambert Natacha and Linlin Jia", - author_email="linlin.jia@unibe.ch", + author_email="jajupmochi@gmail.com", description="A Python wrapper library for C++ library GEDLIB of graph edit distances", long_description=long_description, long_description_content_type="text/markdown", @@ -378,14 +451,16 @@ def remove_includes(): 'Tracker': 'https://github.com/jajupmochi/graphkit-learn/issues', }, url="https://github.com/jajupmochi/graphkit-learn/tree/master/gklearn/gedlib", + license="GPL-3.0-or-later", # SPDX license identifier classifiers=[ "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + # "License :: OSI Approved", "Operating System :: OS Independent", 'Intended Audience :: Science/Research', 'Intended Audience :: Developers', ], - include_dirs=[numpy.get_include()] + include_dirs=[numpy.get_include()], + zip_safe=False, # Do not zip the package, so that .so files can be loaded correctly ) # List generated files: @@ -404,4 +479,13 @@ def remove_includes(): # Remove GEDLIB include files: remove_includes() -# Commande Bash : python setup.py build_ext --inplace +# Bash Command: +# python3 setup_simple.py build_ext --inplace --parallel 8 +# -- Explain: +# -- --parallel N: parallel the construction of .cpp files using c++ compiler (g++ or +# clang++) with N threads (on c++ layer, from .cpp to .so). +# It is different from the N threads in cythonize(), which is on cython layer (from +# .pyx to .cpp). +# if error: command 'clang++' failed: No such file or directory: +# Add "CXX=g++". e.g., +# CXX=g++ python setup_core.py build_ext --inplace diff --git a/gklearn/gedlib/single_dlib_version_bk/gedlibpy_attr.pxd b/gklearn/gedlib/single_dlib_version_bk/gedlibpy_attr.pxd new file mode 100644 index 0000000000..1ea2c8598b --- /dev/null +++ b/gklearn/gedlib/single_dlib_version_bk/gedlibpy_attr.pxd @@ -0,0 +1,165 @@ +# distutils: language = c++ + +""" + Python GedLib module for the AttrLabel type + ====================== + + This module allows using a C++ library for edit distance between graphs (GedLib) with Python. + + + Authors + ------------------- + + Linlin Jia + David Blumenthal + Natacha Lambert + + Copyright (C) 2019-2025 by all the authors + + Classes & Functions + ------------------- + +""" + +################################# +##DECLARATION OF C++ INTERFACES## +################################# + + +#Types imports for C++ compatibility +from libcpp.vector cimport vector +from libcpp.string cimport string +from libcpp.unordered_map cimport unordered_map +from libcpp cimport bool +from libcpp.pair cimport pair + +#Long unsigned int equivalent +cimport numpy as cnp + +ctypedef cnp.npy_uint32 UINT32_t + + +cdef extern from "src/gedlib_bind.hpp" namespace "pyged": + + cdef cppclass PyGEDEnvAttr: + # PyGEDEnvAttr() except + + PyGEDEnvAttr() except + + bool isInitialized() except + + void restartEnv() except + + void loadGXLGraph( + string pathFolder, string pathXML, bool node_type, bool edge_type + ) except + + pair[size_t, size_t] getGraphIds() except + + vector[size_t] getAllGraphIds() except + + string getGraphClass(size_t id) except + + string getGraphName(size_t id) except + + size_t addGraph(string name, string classe) except + + # void addNode( # todo: need conversion in ipp file + # size_t graphId, string nodeId, map[string, string] nodeLabel + # ) except + + void addNode( + size_t graphId, + string nodeId, + unordered_map[string, string] str_map, + unordered_map[string, int] int_map, + unordered_map[string, double] float_map, + unordered_map[string, vector[string]] list_str_map, + unordered_map[string, vector[int]] list_int_map, + unordered_map[string, vector[double]] list_float_map + ) except + + # void addEdge( + # size_t graphId, string tail, string head, map[string, string] edgeLabel, + # bool ignoreDuplicates + # ) except + + void addEdge( + size_t graphId, + string tail, + string head, + unordered_map[string, string] str_map, + unordered_map[string, int] int_map, + unordered_map[string, double] float_map, + unordered_map[string, vector[string]] list_str_map, + unordered_map[string, vector[int]] list_int_map, + unordered_map[string, vector[double]] list_float_map, + bool ignoreDuplicates + ) except + + void clearGraph(size_t graphId) except + + size_t getGraphInternalId(size_t graphId) except + + size_t getGraphNumNodes(size_t graphId) except + + size_t getGraphNumEdges(size_t graphId) except + + vector[string] getGraphOriginalNodeIds(size_t graphId) except + + # vector[map[string, string]] getGraphNodeLabels(size_t graphId) except + + # map[pair[size_t, size_t], map[string, string]] getGraphEdges( + # size_t graphId + # ) except + + vector[vector[size_t]] getGraphAdjacenceMatrix(size_t graphId) except + + void setEditCost(string editCost, vector[double] editCostConstant) except + + void setPersonalEditCost(vector[double] editCostConstant) except + + void initEnv(string initOption, bool print_to_stdout) except + + void setMethod(string method, string options) except + + void initMethod() except + + double getInitime() except + + void runMethod(size_t g, size_t h) except + + double getUpperBound(size_t g, size_t h) except + + double getLowerBound(size_t g, size_t h) except + + vector[cnp.npy_uint64] getForwardMap(size_t g, size_t h) except + + vector[cnp.npy_uint64] getBackwardMap(size_t g, size_t h) except + + size_t getNodeImage(size_t g, size_t h, size_t nodeId) except + + size_t getNodePreImage(size_t g, size_t h, size_t nodeId) except + + double getInducedCost(size_t g, size_t h) except + + vector[pair[size_t, size_t]] getNodeMap(size_t g, size_t h) except + + vector[vector[int]] getAssignmentMatrix(size_t g, size_t h) except + + vector[vector[cnp.npy_uint64]] getAllMap(size_t g, size_t h) except + + double getRuntime(size_t g, size_t h) except + + bool quasimetricCosts() except + + vector[vector[size_t]] hungarianLSAP(vector[vector[size_t]] matrixCost) except + + vector[vector[double]] hungarianLSAPE( + vector[vector[double]] matrixCost + ) except + + # added by Linlin Jia. + size_t getNumGraphs() except + + size_t getNumNodeLabels() except + + # map[string, string] getNodeLabel(size_t label_id) except + + size_t getNumEdgeLabels() except + + # map[string, string] getEdgeLabel(size_t label_id) except + + # size_t getNumNodes(size_t graph_id) except + + double getAvgNumNodes() except + + # double getNodeRelCost( + # map[string, string] & node_label_1, map[string, string] & node_label_2 + # ) except + + # double getNodeDelCost(map[string, string] & node_label) except + + # double getNodeInsCost(map[string, string] & node_label) except + + # map[string, string] getMedianNodeLabel( + # vector[map[string, string]] & node_labels + # ) except + + # double getEdgeRelCost( + # map[string, string] & edge_label_1, map[string, string] & edge_label_2 + # ) except + + # double getEdgeDelCost(map[string, string] & edge_label) except + + # double getEdgeInsCost(map[string, string] & edge_label) except + + # map[string, string] getMedianEdgeLabel( + # vector[map[string, string]] & edge_labels + # ) except + + string getInitType() except + + # double getNodeCost(size_t label1, size_t label2) except + + double computeInducedCost( + size_t g_id, size_t h_id, vector[pair[size_t, size_t]] + ) except + + + +############################# +##External Libs Import ## +############################# + +from libraries_import import lib1, lib2, lib3, lib4 + +############################# +##CYTHON WRAPPER INTERFACES## +############################# + +# @cython.auto_pickle(True) +cdef class GEDEnvAttr: + """Cython wrapper class for C++ class PyGEDEnvAttr + """ + # cdef PyGEDEnvAttr c_env # Hold a C++ instance which we're wrapping + cdef PyGEDEnvAttr * c_env # hold a pointer to the C++ instance which we're wrapping diff --git a/gklearn/gedlib/single_dlib_version_bk/gedlibpy_attr.pyx b/gklearn/gedlib/single_dlib_version_bk/gedlibpy_attr.pyx new file mode 100644 index 0000000000..b5f737fd21 --- /dev/null +++ b/gklearn/gedlib/single_dlib_version_bk/gedlibpy_attr.pyx @@ -0,0 +1,1815 @@ +# distutils: language = c++ + +""" + Python GedLib module for the AttrLabel type + ====================== + + This module allows using a C++ library for edit distance between graphs (GedLib) with Python. + + + Authors + ------------------- + + Linlin Jia + David Blumenthal + Natacha Lambert + + Copyright (C) 2019-2025 by all the authors + + Classes & Functions + ------------------- + +""" + +################################# +##DECLARATION OF C++ INTERFACES## +################################# + + +#Types imports for C++ compatibility +from libcpp.vector cimport vector +from libcpp.string cimport string +from libcpp.unordered_map cimport unordered_map +from libcpp cimport bool + +#Long unsigned int equivalent + +# ctypedef cnp.npy_uint32 UINT32_t + +from gklearn.gedlib.lib.common_bind cimport * + + +############################# +##CYTHON WRAPPER INTERFACES## +############################# + +# import cython +import numpy as np +import networkx as nx +from gklearn.ged.env import NodeMap +from builtins import list as py_list + +# import librariesImport + +# @cython.auto_pickle(True) +cdef class GEDEnvAttr: + """Cython wrapper class for C++ class PyGEDEnvAttr + """ + # # cdef PyGEDEnvAttr c_env # Hold a C++ instance which we're wrapping + # cdef PyGEDEnvAttr * c_env # hold a pointer to the C++ instance which we're wrapping + + def __cinit__(self, label_type: str = 'attr'): + """ + Initializes the C++ environment for graph edit distance computations. + + Parameters + ---------- + label_type : str, optional + The type of labels used in the graphs, can be the following: + - 'gxl' or 'str': for GXLLabel (string labels) + - 'attr': for AttrLabel (complex attribute labels) + """ + self.c_env = new PyGEDEnvAttr() + # self.c_env = new PyGEDEnv() + + + def __dealloc__(self): + if self.c_env != NULL: + del self.c_env + self.c_env = NULL + + # def __reduce__(self): + # # return GEDEnv, (self.c_env,) + # return GEDEnv, tuple() + + + def is_initialized(self): + """ + Checks and returns if the computation environment is initialized or not. + + :return: True if it's initialized, False otherwise + :rtype: bool + + .. note:: This function exists for internals verifications but you can use it for your code. + """ + return self.c_env.isInitialized() + + def restart_env(self): + """ + Restarts the environment variable. All data related to it will be delete. + + .. warning:: This function deletes all graphs, computations and more so make sure you don't need anymore your environment. + .. note:: You can now delete and add somes graphs after initialization so you can avoid this function. + """ + self.c_env.restartEnv() + + def load_GXL_graphs(self, path_folder, path_XML, node_type, edge_type): + """ + Loads some GXL graphes on the environment which is in a same folder, and present in the XMLfile. + + :param path_folder: The folder's path which contains GXL graphs + :param path_XML: The XML's path which indicates which graphes you want to load + :param node_type: Select if nodes are labeled or unlabeled + :param edge_type: Select if edges are labeled or unlabeled + :type path_folder: string + :type path_XML: string + :type node_type: bool + :type edge_type: bool + + + .. note:: You can call this function multiple times if you want, but not after an init call. + """ + self.c_env.loadGXLGraph( + path_folder.encode('utf-8'), path_XML.encode('utf-8'), node_type, edge_type + ) + + def graph_ids(self): + """ + Searchs the first and last IDs of the loaded graphs in the environment. + + :return: The pair of the first and the last graphs Ids + :rtype: tuple(size_t, size_t) + + .. note:: Prefer this function if you have huges structures with lots of graphs. + """ + return self.c_env.getGraphIds() + + def get_all_graph_ids(self): + """ + Searchs all the IDs of the loaded graphs in the environment. + + :return: The list of all graphs's Ids + :rtype: list[size_t] + + .. note:: The last ID is equal to (number of graphs - 1). The order correspond to the loading order. + """ + return self.c_env.getAllGraphIds() + + def get_graph_class(self, id): + """ + Returns the class of a graph with its ID. + + :param id: The ID of the wanted graph + :type id: size_t + :return: The class of the graph which correpond to the ID + :rtype: string + + .. seealso:: get_graph_class() + .. note:: An empty string can be a class. + """ + return self.c_env.getGraphClass(id) + + def get_graph_name(self, id): + """ + Returns the name of a graph with its ID. + + :param id: The ID of the wanted graph + :type id: size_t + :return: The name of the graph which correpond to the ID + :rtype: string + + .. seealso:: get_graph_class() + .. note:: An empty string can be a name. + """ + return self.c_env.getGraphName(id).decode('utf-8') + + def add_graph(self, name="", classe=""): + """ + Adds a empty graph on the environment, with its name and its class. Nodes and edges will be add in a second time. + + :param name: The name of the new graph, an empty string by default + :param classe: The class of the new graph, an empty string by default + :type name: string + :type classe: string + :return: The ID of the newly graphe + :rtype: size_t + + .. seealso::add_node(), add_edge() , add_symmetrical_edge() + .. note:: You can call this function without parameters. You can also use this function after initialization, call init() after you're finished your modifications. + """ + return self.c_env.addGraph(name.encode('utf-8'), classe.encode('utf-8')) + + + def add_node(self, size_t graph_id, node_id, dict node_label): + """ + Adds a node on a graph selected by its ID. A ID and a (set of) labels for the + node is required. + + Parameters + ---------- + graph_id : size_t + The ID of the wanted graph. + + node_id : string + The ID of the new node. + + node_label : dict + The label of the new node. `node_label` supports mixed types: + str, int, float, + list[int] or np.ndarray[int], + list[float] or np.ndarray[float], + list[str] + """ + # fixme: debug test only: + print(f'The node label passed from Python is {node_label}.') + + ( + str_map, int_map, float_map, list_str_map, list_int_map, list_float_map + ) = encode_label_map(node_label) + + print('The node labels passed to the C++ function in .pyx are:') + print(f'str_map: {str_map}.') + print(f'int_map: {int_map}.') + print(f'float_map: {float_map}.') + print(f'list_str_map: {list_str_map}.') + print(f'list_int_map: {list_int_map}.') + print(f'list_float_map: {list_float_map}.') + + self.c_env.addNode( + graph_id, node_id.encode('utf-8'), + str_map, int_map, float_map, list_str_map, list_int_map, list_float_map + ) + + + def add_edge( + self, size_t graph_id, tail, head, dict edge_label, + ignore_duplicates=True + ): + """ + Adds an edge on a graph selected by its ID. + + :param graph_id: The ID of the wanted graph + :param tail: The ID of the tail node for the new edge + :param head: The ID of the head node for the new edge + :param edge_label: The label of the new edge + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type graph_id: size_t + :type tail: string + :type head: string + :type edge_label: supports mixed types: + str, int, float, + list[int] or np.ndarray[int], + list[float] or np.ndarray[float], + list[str] + :type ignore_duplicates: bool + + .. seealso:: add_graph(), add_node(), add_symmetrical_edge() + """ + # fixme: debug test only: + print(f'The edge label passed from Python is {edge_label}.') + + ( + str_map, int_map, float_map, list_str_map, list_int_map, list_float_map + ) = encode_label_map(edge_label) + self.c_env.addEdge( + graph_id, tail.encode('utf-8'), head.encode('utf-8'), + str_map, int_map, float_map, list_str_map, list_int_map, list_float_map, + ignore_duplicates + ) + + print('The edge labels passed to the C++ function in .pyx are:') + print(f'str_map: {str_map}.') + print(f'int_map: {int_map}.') + print(f'float_map: {float_map}.') + print(f'list_str_map: {list_str_map}.') + print(f'list_int_map: {list_int_map}.') + print(f'list_float_map: {list_float_map}.') + + + # def add_node_str(self, graph_id, node_id, node_label: dict[str, str]): + # """ + # Adds a node on a graph selected by its ID. A ID and a label for the node is required. + # + # :param graph_id: The ID of the wanted graph + # :param node_id: The ID of the new node + # :param node_label: The label of the new node + # :type graph_id: size_t + # :type node_id: string + # :type node_label: dict{string : string} + # + # .. seealso:: add_graph(), add_edge(), add_symmetrical_edge() + # + # Notes + # ----- + # - You can also use this function after initialization, but only on a newly + # added graph. Call init() after you're finished your modifications. + # - this function only supports string labels and should be replaced + # by the new `add_node` function. It is kept for backward compatibility and + # comparison. + # """ + # self.c_env.addNode( + # graph_id, node_id.encode('utf-8'), encode_your_map(node_label) + # ) + + + # def add_edge_str( + # self, graph_id, tail, head, edge_label: dict[str, str], ignore_duplicates=True + # ): + # """ + # Adds an edge on a graph selected by its ID. + # + # :param graph_id: The ID of the wanted graph + # :param tail: The ID of the tail node for the new edge + # :param head: The ID of the head node for the new edge + # :param edge_label: The label of the new edge + # :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + # :type graph_id: size_t + # :type tail: string + # :type head: string + # :type edge_label: dict{string : string} + # :type ignore_duplicates: bool + # + # .. seealso:: add_graph(), add_node(), add_symmetrical_edge() + # + # Notes + # ----- + # - You can also use this function after initialization, but only on a newly added + # graph. Call init() after you're finished your modifications. + # - this function only supports string labels and should be replaced + # by the new `add_edge` function. It is kept for backward compatibility and + # comparison. + # """ + # self.c_env.addEdge( + # graph_id, tail.encode('utf-8'), head.encode('utf-8'), + # encode_your_map(edge_label), ignore_duplicates + # ) + + # todo: fix for attr label + # def add_symmetrical_edge(self, graph_id, tail, head, edge_label): + # """ + # Adds a symmetrical edge on a graph selected by its ID. + # + # :param graph_id: The ID of the wanted graph + # :param tail: The ID of the tail node for the new edge + # :param head: The ID of the head node for the new edge + # :param edge_label: The label of the new edge + # :type graph_id: size_t + # :type tail: string + # :type head: string + # :type edge_label: dict{string : string} + # + # .. seealso:: add_graph(), add_node(), add_edge() + # .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + # """ + # tailB = tail.encode('utf-8') + # headB = head.encode('utf-8') + # edgeLabelB = encode_your_map(edge_label) + # self.c_env.addEdge(graph_id, tailB, headB, edgeLabelB, True) + # self.c_env.addEdge(graph_id, headB, tailB, edgeLabelB, True) + + def clear_graph(self, graph_id): + """ + Deletes a graph, selected by its ID, to the environment. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + + .. note:: Call init() after you're finished your modifications. + """ + self.c_env.clearGraph(graph_id) + + def get_graph_internal_id(self, graph_id): + """ + Searchs and returns the internal Id of a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The internal ID of the selected graph + :rtype: size_t + + .. seealso:: get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphInternalId(graph_id) + + def get_graph_num_nodes(self, graph_id): + """ + Searchs and returns the number of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The number of nodes on the selected graph + :rtype: size_t + + .. seealso:: get_graph_internal_id(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphNumNodes(graph_id) + + def get_graph_num_edges(self, graph_id): + """ + Searchs and returns the number of edges on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The number of edges on the selected graph + :rtype: size_t + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphNumEdges(graph_id) + + def get_original_node_ids(self, graph_id): + """ + Searchs and returns all th Ids of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The list of IDs's nodes on the selected graph + :rtype: list[string] + + .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return [gid.decode('utf-8') for gid in + self.c_env.getGraphOriginalNodeIds(graph_id)] + + # todo: fix for attr label + # def get_graph_node_labels(self, graph_id): + # """ + # Searchs and returns all the labels of nodes on a graph, selected by its ID. + # + # :param graph_id: The ID of the wanted graph + # :type graph_id: size_t + # :return: The list of nodes' labels on the selected graph + # :rtype: list[dict{string : string}] + # + # .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_edges(), get_graph_adjacence_matrix() + # .. note:: These functions allow to collect all the graph's informations. + # """ + # return [decode_your_map(node_label) for node_label in + # self.c_env.getGraphNodeLabels(graph_id)] + + # todo: fix for attr label + # def get_graph_edges(self, graph_id): + # """ + # Searchs and returns all the edges on a graph, selected by its ID. + # + # :param graph_id: The ID of the wanted graph + # :type graph_id: size_t + # :return: The list of edges on the selected graph + # :rtype: dict{tuple(size_t, size_t) : dict{string : string}} + # + # .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_adjacence_matrix() + # .. note:: These functions allow to collect all the graph's informations. + # """ + # return decode_graph_edges(self.c_env.getGraphEdges(graph_id)) + + def get_graph_adjacence_matrix(self, graph_id): + """ + Searchs and returns the adjacence list of a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The adjacence list of the selected graph + :rtype: list[list[size_t]] + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphAdjacenceMatrix(graph_id) + + def set_edit_cost(self, edit_cost, edit_cost_constant = []): + """ + Sets an edit cost function to the environment, if it exists. + + :param edit_cost: The name of the edit cost function + :type edit_cost: string + :param edi_cost_constant: The parameters you will add to the editCost, empty by default + :type edit_cost_constant: list + + .. seealso:: list_of_edit_cost_options + .. note:: Try to make sure the edit cost function exists with list_of_edit_cost_options, raise an error otherwise. + """ + if edit_cost in list_of_edit_cost_options: + edit_cost_b = edit_cost.encode('utf-8') + self.c_env.setEditCost(edit_cost_b, edit_cost_constant) + else: + raise EditCostError( + "This edit cost function doesn't exist, please see list_of_edit_cost_options for selecting a edit cost function" + ) + + def set_personal_edit_cost(self, edit_cost_constant = []): + """ + Sets an personal edit cost function to the environment. + + :param edit_cost_constant: The parameters you will add to the editCost, empty by default + :type edit_cost_constant: list + + .. seealso:: list_of_edit_cost_options, set_edit_cost() + .. note::You have to modify the C++ function to use it. Please see the documentation to add your Edit Cost function. + """ + self.c_env.setPersonalEditCost(edit_cost_constant) + + def init(self, init_option='EAGER_WITHOUT_SHUFFLED_COPIES', print_to_stdout=False): + """ + Initializes the environment with the chosen edit cost function and graphs. + + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type init_option: string + + .. seealso:: list_of_init_options + .. warning:: No modification were allowed after initialization. Try to make sure your choices is correct. You can though clear or add a graph, but recall init() after that. + .. note:: Try to make sure the option exists with list_of_init_options or choose no options, raise an error otherwise. + """ + if init_option in list_of_init_options: + init_option_b = init_option.encode('utf-8') + self.c_env.initEnv(init_option_b, print_to_stdout) + else: + raise InitError( + "This init option doesn't exist, please see list_of_init_options for selecting an option. You can choose any options." + ) + + def set_method(self, method, options=""): + """ + Sets a computation method to the environment, if its exists. + + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :type method: string + :type options: string + + .. seealso:: init_method(), list_of_method_options + .. note:: Try to make sure the edit cost function exists with list_of_method_options, raise an error otherwise. Call init_method() after your set. + """ + if method in list_of_method_options: + method_b = method.encode('utf-8') + self.c_env.setMethod(method_b, options.encode('utf-8')) + else: + raise MethodError( + "This method doesn't exist, please see list_of_method_options for selecting a method" + ) + + def init_method(self): + """ + Inits the environment with the set method. + + .. seealso:: set_method(), list_of_method_options + .. note:: Call this function after set the method. You can't launch computation or change the method after that. + """ + self.c_env.initMethod() + + def get_init_time(self): + """ + Returns the initialization time. + + :return: The initialization time + :rtype: double + """ + return self.c_env.getInitime() + + def run_method(self, g, h): + """ + Computes the edit distance between two graphs g and h, with the edit cost function and method computation selected. + + :param g: The Id of the first graph to compare + :param h: The Id of the second graph to compare + :type g: size_t + :type h: size_t + + .. seealso:: get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. note:: This function only compute the distance between two graphs, without returning a result. Use the differents function to see the result between the two graphs. + """ + self.c_env.runMethod(g, h) + + def get_upper_bound(self, g, h): + """ + Returns the upper bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The upper bound of the edit distance cost + :rtype: double + + .. seealso:: run_method(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: The upper bound is equivalent to the result of the pessimist edit distance cost. Methods are heuristics so the library can't compute the real perfect result because it's NP-Hard problem. + """ + return self.c_env.getUpperBound(g, h) + + def get_lower_bound(self, g, h): + """ + Returns the lower bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The lower bound of the edit distance cost + :rtype: double + + .. seealso:: run_method(), get_upper_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function can be ignored, because lower bound doesn't have a crucial utility. + """ + return self.c_env.getLowerBound(g, h) + + def get_forward_map(self, g, h): + """ + Returns the forward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The forward map to the adjacence matrix between nodes of the two graphs + :rtype: list[npy_uint32] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_backward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getForwardMap(g, h) + + def get_backward_map(self, g, h): + """ + Returns the backward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The backward map to the adjacence matrix between nodes of the two graphs + :rtype: list[npy_uint32] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getBackwardMap(g, h) + + def get_node_image(self, g, h, node_id): + """ + Returns the node's image in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :param node_id: The ID of the node which you want to see the image + :type g: size_t + :type h: size_t + :type node_id: size_t + :return: The ID of the image node + :rtype: size_t + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_pre_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use BackwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getNodeImage(g, h, node_id) + + def get_node_pre_image(self, g, h, node_id): + """ + Returns the node's preimage in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :param node_id: The ID of the node which you want to see the preimage + :type g: size_t + :type h: size_t + :type node_id: size_t + :return: The ID of the preimage node + :rtype: size_t + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getNodePreImage(g, h, node_id) + + def get_induced_cost(self, g, h): + """ + Returns the induced cost between the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The induced cost between the two indicated graphs + :rtype: double + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getInducedCost(g, h) + + def get_node_map(self, g, h): + """ + Returns the Node Map, like C++ NodeMap. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The Node Map between the two selected graph. + :rtype: gklearn.ged.env.NodeMap. + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary, however you can understand how assignement works with this example. + """ + map_as_relation = self.c_env.getNodeMap(g, h) + induced_cost = self.c_env.getInducedCost( + g, h + ) # @todo: the C++ implementation for this function in GedLibBind.ipp re-call get_node_map() once more, this is not neccessary. + source_map = [ + item.first if item.first < len(map_as_relation) else np.inf for + item in map_as_relation + ] # item.first < len(map_as_relation) is not exactly correct. + # print(source_map) + target_map = [ + item.second if item.second < len(map_as_relation) else np.inf for + item in map_as_relation + ] + # print(target_map) + num_node_source = len([item for item in source_map if item != np.inf]) + # print(num_node_source) + num_node_target = len([item for item in target_map if item != np.inf]) + # print(num_node_target) + + node_map = NodeMap(num_node_source, num_node_target) + # print(node_map.get_forward_map(), node_map.get_backward_map()) + for i in range(len(source_map)): + node_map.add_assignment(source_map[i], target_map[i]) + node_map.set_induced_cost(induced_cost) + + return node_map + + def get_assignment_matrix(self, g, h): + """ + Returns the Assignment Matrix between two selected graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The Assignment Matrix between the two selected graph. + :rtype: list[list[int]] + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_node_map() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary. + """ + return self.c_env.getAssignmentMatrix(g, h) + + def get_all_map(self, g, h): + """ + Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The forward and backward maps to the adjacence matrix between nodes of the two graphs + :rtype: list[list[npy_uint32]] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function duplicates data so please don't use it. I also don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getAllMap(g, h) + + def get_runtime(self, g, h): + """ + Returns the runtime to compute the edit distance cost between two graphs g and h + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The runtime of the computation of edit distance cost between the two selected graphs + :rtype: double + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Python is a bit longer than C++ due to the functions's encapsulate. + """ + return self.c_env.getRuntime(g, h) + + def quasimetric_cost(self): + """ + Checks and returns if the edit costs are quasimetric. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: True if it's verified, False otherwise + :rtype: bool + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime() + .. warning:: run_method() between the same two graph must be called before this function. + """ + return self.c_env.quasimetricCosts() + + def hungarian_LSAP(self, matrix_cost): + """ + Applies the hungarian algorithm (LSAP) on a matrix Cost. + + :param matrix_cost: The matrix Cost + :type matrix_cost: vector[vector[size_t]] + :return: The values of rho, varrho, u and v, in this order + :rtype: vector[vector[size_t]] + + .. seealso:: hungarian_LSAPE() + """ + return self.c_env.hungarianLSAP(matrix_cost) + + def hungarian_LSAPE(self, matrix_cost): + """ + Applies the hungarian algorithm (LSAPE) on a matrix Cost. + + :param matrix_cost: The matrix Cost + :type matrix_cost: vector[vector[double]] + :return: The values of rho, varrho, u and v, in this order + :rtype: vector[vector[double]] + + .. seealso:: hungarian_LSAP() + """ + return self.c_env.hungarianLSAPE(matrix_cost) + + + def add_random_graph_str( + self, name, classe, list_of_nodes, list_of_edges, ignore_duplicates=True + ): # todo: this function only support string labels for now. + """ + Add a Graph (not GXL) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param name: The name of the graph to add, can be an empty string + :param classe: The classe of the graph to add, can be an empty string + :param list_of_nodes: The list of nodes to add + :param list_of_edges: The list of edges to add + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type name: string + :type classe: string + :type list_of_nodes: list[tuple(size_t, dict{string : string})] + :type list_of_edges: list[tuple(tuple(size_t,size_t), dict{string : string})] + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + .. note:: The graph must respect the GXL structure. Please see how a GXL graph is construct. + + """ + id = self.add_graph(name, classe) + for node in list_of_nodes: + self.add_node(id, node[0], node[1]) + for edge in list_of_edges: + self.add_edge(id, edge[0], edge[1], edge[2], ignore_duplicates) + return id + + + def add_random_graph( + self, name, classe, list_of_nodes, list_of_edges, ignore_duplicates=True + ): + """ + # todo: no idea if this is correct... `add_random_graph_str` is never used anyway. + Add a Graph (not GXL) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param name: The name of the graph to add, can be an empty string + :param classe: The classe of the graph to add, can be an empty string + :param list_of_nodes: The list of nodes to add + :param list_of_edges: The list of edges to add + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type name: string + :type classe: string + :type list_of_nodes: list[tuple(size_t, dict{string : mixed})] + :type list_of_edges: list[tuple(tuple(size_t,size_t), dict{string : mixed})] + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + .. note:: The graph must respect the GXL structure. Please see how a GXL graph is construct. + + """ + id = self.add_graph(name, classe) + for node in list_of_nodes: + self.add_node(id, node[0], *encode_label_map(node[1])) + for edge in list_of_edges: + self.add_edge(id, edge[0], edge[1], *encode_label_map(edge[2]), ignore_duplicates) + return id + + + def add_nx_graph_str(self, g, classe, ignore_duplicates=True): + """ + Add a Graph (made by networkx) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param g: The graph to add (networkx graph) + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type g: networkx.graph + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + Notes + ----- + - The NX graph must respect the GXL structure. Please see how a GXL graph is construct. + - This function only supports string labels. Replace it by add_nx_graph() if you want to + use other types of labels. + """ + id = self.add_graph(g.name, classe) + for node in g.nodes: + self.add_node(id, str(node), g.nodes[node]) + for edge in g.edges: + self.add_edge( + id, str(edge[0]), str(edge[1]), g.get_edge_data(edge[0], edge[1]), + ignore_duplicates + ) + return id + + + def add_nx_graph( + self, g: nx.Graph, classe: str = '', ignore_duplicates: bool = True + ): + """ + Add a Graph (made by networkx) on the environment. Be careful to respect the same + format as GXL graphs for labeling nodes and edges. + + Parameters + ---------- + g : networkx.Graph + The graph to add (networkx graph) + + classe : str + The class of the graph to add. Default is empty string. + + ignore_duplicates : bool + If True, duplicate edges are ignored, otherwise it's raise an error if an + existing edge is added. True by default. + + Returns + ------- + size_t + The ID of the newly added graph. + + Notes + ----- + - The NX graph must respect the GXL structure. Please see how a GXL graph is constructed. + - This function supports mixed-type node and edge labels (int, float, str, list of + int/float/str, np.array of int and float). + + """ + id = self.add_graph(g.name, classe) + for node in g.nodes: + node_label = g.nodes[node] + encoded_label = encode_label_map(node_label) + self.add_node(id, str(node), **encoded_label) + for edge in g.edges: + edge_label = g.get_edge_data(edge[0], edge[1]) + encoded_label = encode_label_map(edge_label) + self.add_edge( + id, str(edge[0]), str(edge[1]), *encoded_label, + ignore_duplicates + ) + return id + + + def compute_ged_on_two_graphs( + self, g1, g2, edit_cost, method, options, + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + Computes the edit distance between two NX graphs. + + :param g1: The first graph to add and compute + :param g2: The second graph to add and compute + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type g1: networksx.graph + :type g2: networksx.graph + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: The edit distance between the two graphs and the nodeMap between them. + :rtype: double, list[tuple(size_t, size_t)] + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + + """ + if self.is_initialized(): + self.restart_env() + + g = self.add_nx_graph(g1, "") + h = self.add_nx_graph(g2, "") + + self.set_edit_cost(edit_cost) + self.init(init_option) + + self.set_method(method, options) + self.init_method() + + resDistance = 0 + resMapping = [] + self.run_method(g, h) + resDistance = self.get_upper_bound(g, h) + resMapping = self.get_node_map(g, h) + + return resDistance, resMapping + + def compute_edit_distance_on_nx_graphs( + self, dataset, classes, edit_cost, method, options, + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + + Computes all the edit distance between each NX graphs on the dataset. + + :param dataset: The list of graphs to add and compute + :param classes: The classe of all the graph, can be an empty string + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type dataset: list[networksx.graph] + :type classes: string + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: Two matrix, the first with edit distances between graphs and the second the nodeMap between graphs. The result between g and h is one the [g][h] coordinates. + :rtype: list[list[double]], list[list[list[tuple(size_t, size_t)]]] + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + + """ + if self.is_initialized(): + self.restart_env() + + print("Loading graphs in progress...") + for graph in dataset: + self.add_nx_graph(graph, classes) + listID = self.graph_ids() + print("Graphs loaded ! ") + print("Number of graphs = " + str(listID[1])) + + self.set_edit_cost(edit_cost) + print("Initialization in progress...") + self.init(init_option) + print("Initialization terminated !") + + self.set_method(method, options) + self.init_method() + + resDistance = [[]] + resMapping = [[]] + for g in range(listID[0], listID[1]): + print("Computation between graph " + str( + g + ) + " with all the others including himself.") + for h in range(listID[0], listID[1]): + #print("Computation between graph " + str(g) + " and graph " + str(h)) + self.run_method(g, h) + resDistance[g][h] = self.get_upper_bound(g, h) + resMapping[g][h] = self.get_node_map(g, h) + + print( + "Finish ! The return contains edit distances and NodeMap but you can check the result with graphs'ID until you restart the environment") + return resDistance, resMapping + + def compute_edit_distance_on_GXl_graphs( + self, path_folder, path_XML, edit_cost, method, options="", + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + Computes all the edit distance between each GXL graphs on the folder and the XMl file. + + :param path_folder: The folder's path which contains GXL graphs + :param path_XML: The XML's path which indicates which graphes you want to load + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type path_folder: string + :type path_XML: string + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: The list of the first and last-1 ID of graphs + :rtype: tuple(size_t, size_t) + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. + + """ + + if self.is_initialized(): + self.restart_env() + + print("Loading graphs in progress...") + self.load_GXL_graphs(path_folder, path_XML) + listID = self.graph_ids() + print("Graphs loaded ! ") + print("Number of graphs = " + str(listID[1])) + + self.set_edit_cost(edit_cost) + print("Initialization in progress...") + self.init(init_option) + print("Initialization terminated !") + + self.set_method(method, options) + self.init_method() + + #res = [] + for g in range(listID[0], listID[1]): + print("Computation between graph " + str( + g + ) + " with all the others including himself.") + for h in range(listID[0], listID[1]): + #print("Computation between graph " + str(g) + " and graph " + str(h)) + self.run_method(g, h) + #res.append((get_upper_bound(g,h), get_node_map(g,h), get_runtime(g,h))) + + #return res + + print ( + "Finish ! You can check the result with each ID of graphs ! There are in the return") + print ( + "Please don't restart the environment or recall this function, you will lose your results !") + return listID + + + def get_num_graphs(self) -> int: + """ + Returns the number of graphs in the environment. + + Returns + ------- + size_t + Number of graphs in the environment. + """ + return self.c_env.getNumGraphs() + + + def get_num_node_labels(self): + """ + Returns the number of node labels. + + :return: Number of pairwise different node labels contained in the environment. + :rtype: size_t + + .. note:: If 1 is returned, the nodes are unlabeled. + """ + return self.c_env.getNumNodeLabels() + + # todo: fix this with AttrLabel + # def get_node_label(self, label_id): + # """ + # Returns node label. + # + # :param label_id: ID of node label that should be returned. Must be between 1 and get_num_node_labels(). + # :type label_id: size_t + # :return: Node label for selected label ID. + # :rtype: dict{string : string} + # """ + # return decode_your_map(self.c_env.getNodeLabel(label_id)) + + def get_num_edge_labels(self): + """ + Returns the number of edge labels. + + :return: Number of pairwise different edge labels contained in the environment. + :rtype: size_t + + .. note:: If 1 is returned, the edges are unlabeled. + """ + return self.c_env.getNumEdgeLabels() + + # todo: fix this with AttrLabel + # def get_edge_label(self, label_id): + # """ + # Returns edge label. + # + # :param label_id: ID of edge label that should be returned. Must be between 1 and get_num_edge_labels(). + # :type label_id: size_t + # :return: Edge label for selected label ID. + # :rtype: dict{string : string} + # """ + # return decode_your_map(self.c_env.getEdgeLabel(label_id)) + + # def get_num_nodes(self, graph_id): + # """ + # Returns the number of nodes. + # + # :param graph_id: ID of an input graph that has been added to the environment. + # :type graph_id: size_t + # :return: Number of nodes in the graph. + # :rtype: size_t + # """ + # return self.c_env.getNumNodes(graph_id) + + def get_avg_num_nodes(self): + """ + Returns average number of nodes. + + :return: Average number of nodes of the graphs contained in the environment. + :rtype: double + """ + return self.c_env.getAvgNumNodes() + + # todo: fix this with AttrLabel + # def get_node_rel_cost(self, node_label_1, node_label_2): + # """ + # Returns node relabeling cost. + # + # :param node_label_1: First node label. + # :param node_label_2: Second node label. + # :type node_label_1: dict{string : string} + # :type node_label_2: dict{string : string} + # :return: Node relabeling cost for the given node labels. + # :rtype: double + # """ + # return self.c_env.getNodeRelCost( + # encode_your_map(node_label_1), encode_your_map(node_label_2) + # ) + + + # todo: fix this with AttrLabel + # def get_node_del_cost(self, node_label): + # """ + # Returns node deletion cost. + # + # :param node_label: Node label. + # :type node_label: dict{string : string} + # :return: Cost of deleting node with given label. + # :rtype: double + # """ + # return self.c_env.getNodeDelCost(encode_your_map(node_label)) + + # todo: fix this with AttrLabel + # def get_node_ins_cost(self, node_label): + # """ + # Returns node insertion cost. + # + # :param node_label: Node label. + # :type node_label: dict{string : string} + # :return: Cost of inserting node with given label. + # :rtype: double + # """ + # return self.c_env.getNodeInsCost(encode_your_map(node_label)) + + # todo: fix this with AttrLabel + # def get_median_node_label(self, node_labels): + # """ + # Computes median node label. + # + # :param node_labels: The node labels whose median should be computed. + # :type node_labels: list[dict{string : string}] + # :return: Median of the given node labels. + # :rtype: dict{string : string} + # """ + # node_labels_b = [encode_your_map(node_label) for node_label in node_labels] + # return decode_your_map(self.c_env.getMedianNodeLabel(node_labels_b)) + + # todo: fix this with AttrLabel + # def get_edge_rel_cost(self, edge_label_1, edge_label_2): + # """ + # Returns edge relabeling cost. + # + # :param edge_label_1: First edge label. + # :param edge_label_2: Second edge label. + # :type edge_label_1: dict{string : string} + # :type edge_label_2: dict{string : string} + # :return: Edge relabeling cost for the given edge labels. + # :rtype: double + # """ + # return self.c_env.getEdgeRelCost( + # encode_your_map(edge_label_1), encode_your_map(edge_label_2) + # ) + + # todo: fix this with AttrLabel + # def get_edge_del_cost(self, edge_label): + # """ + # Returns edge deletion cost. + # + # :param edge_label: Edge label. + # :type edge_label: dict{string : string} + # :return: Cost of deleting edge with given label. + # :rtype: double + # """ + # return self.c_env.getEdgeDelCost(encode_your_map(edge_label)) + + # todo: fix this with AttrLabel + # def get_edge_ins_cost(self, edge_label): + # """ + # Returns edge insertion cost. + # + # :param edge_label: Edge label. + # :type edge_label: dict{string : string} + # :return: Cost of inserting edge with given label. + # :rtype: double + # """ + # return self.c_env.getEdgeInsCost(encode_your_map(edge_label)) + + # todo: fix this with AttrLabel + # def get_median_edge_label(self, edge_labels): + # """ + # Computes median edge label. + # + # :param edge_labels: The edge labels whose median should be computed. + # :type edge_labels: list[dict{string : string}] + # :return: Median of the given edge labels. + # :rtype: dict{string : string} + # """ + # edge_labels_b = [encode_your_map(edge_label) for edge_label in edge_labels] + # return decode_your_map(self.c_env.getMedianEdgeLabel(edge_label_b)) + + def get_nx_graph( + self, graph_id, adj_matrix=True, adj_lists=False, edge_list=False + ): # @todo + """ + Get graph with id `graph_id` in the form of the NetworkX Graph. + + Parameters + ---------- + graph_id : int + ID of the selected graph. + + adj_matrix : bool + Set to `True` to construct an adjacency matrix `adj_matrix` and a hash-map `edge_labels`, which has a key for each pair `(i,j)` such that `adj_matrix[i][j]` equals 1. No effect for now. + + adj_lists : bool + No effect for now. + + edge_list : bool + No effect for now. + + Returns + ------- + NetworkX Graph object + The obtained graph. + """ + graph = nx.Graph() + graph.graph['id'] = graph_id + + nb_nodes = self.get_graph_num_nodes(graph_id) + original_node_ids = self.get_original_node_ids(graph_id) + node_labels = self.get_graph_node_labels(graph_id) + # print(original_node_ids) + # print(node_labels) + graph.graph['original_node_ids'] = original_node_ids + + for node_id in range(0, nb_nodes): + graph.add_node(node_id, **node_labels[node_id]) + # graph.nodes[node_id]['original_node_id'] = original_node_ids[node_id] + + edges = self.get_graph_edges(graph_id) + for (head, tail), labels in edges.items(): + graph.add_edge(head, tail, **labels) + # print(edges) + + return graph + + def get_init_type(self): + """ + Returns the initialization type of the last initialization in string. + + Returns + ------- + string + Initialization type in string. + """ + return self.c_env.getInitType().decode('utf-8') + + # def get_node_cost(self, label1, label2): + # """ + # Returns node relabeling, insertion, or deletion cost. + + # Parameters + # ---------- + # label1 : int + # First node label. + # + # label2 : int + # Second node label. + # + # Returns + # ------- + # Node relabeling cost if `label1` and `label2` are both different from `ged::dummy_label()`, node insertion cost if `label1` equals `ged::dummy_label` and `label2` does not, node deletion cost if `label1` does not equal `ged::dummy_label` and `label2` does, and 0 otherwise. + # """ + # return self.c_env.getNodeCost(label1, label2) + + def load_nx_graph(self, nx_graph, graph_id, graph_name='', graph_class=''): + """ + Loads NetworkX Graph into the GED environment. + + Parameters + ---------- + nx_graph : NetworkX Graph object + The graph that should be loaded. + + graph_id : int or None + The ID of a graph contained the environment (overwrite existing graph) or add new graph if `None`. + + graph_name : string, optional + The name of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. + + graph_class : string, optional + The class of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. + + Returns + ------- + int + The ID of the newly loaded graph. + """ + if graph_id is None: + graph_id = self.add_graph(graph_name, graph_class) + else: + self.clear_graph(graph_id) + for node in nx_graph.nodes: + self.add_node(graph_id, str(node), nx_graph.nodes[node]) + for edge in nx_graph.edges: + self.add_edge( + graph_id, str(edge[0]), str(edge[1]), + nx_graph.get_edge_data(edge[0], edge[1]) + ) + return graph_id + + def compute_induced_cost(self, g_id, h_id, node_map): + """ + Computes the edit cost between two graphs induced by a node map. + + Parameters + ---------- + g_id : int + ID of input graph. + h_id : int + ID of input graph. + node_map: gklearn.ged.env.NodeMap. + The NodeMap instance whose reduced cost will be computed and re-assigned. + + Returns + ------- + None. + """ + relation = [] + node_map.as_relation(relation) + # print(relation) + dummy_node = get_dummy_node() + # print(dummy_node) + for i, val in enumerate(relation): + val1 = dummy_node if val[0] == np.inf else val[0] + val2 = dummy_node if val[1] == np.inf else val[1] + relation[i] = tuple((val1, val2)) + # print(relation) + induced_cost = self.c_env.computeInducedCost(g_id, h_id, relation) + node_map.set_induced_cost(induced_cost) + + + def __repr__(self): + return f"PyGEDEnvAttr(num_graphs={self.get_num_graphs()})" + + + def info(self): + """Print environment information""" + print(f"GED Environment Information:") + print(f" Number of graphs: {self.get_num_graphs()}.") + + +############################################### +##Help functions for label mapping conversion## +############################################### + + +# Converts string -> string +cdef unordered_map[string, string] convert_str_map(dict d): + cdef unordered_map[string, string] out + for k, v in d.items(): + out[k.encode('utf-8')] = v.encode('utf-8') + return out + + +cdef unordered_map[string, int] convert_int_map(dict d): + cdef unordered_map[string, int] out + for k, v in d.items(): + out[k.encode('utf-8')] = v + return out + + +cdef unordered_map[string, double] convert_float_map(dict d): + cdef unordered_map[string, double] out + for k, v in d.items(): + out[k.encode('utf-8')] = v + return out + + +cdef unordered_map[string, vector[string]] convert_list_str_map(dict d): + cdef unordered_map[string, vector[string]] out + for k, v in d.items(): + out[k.encode('utf-8')] = to_vector_str(v) + return out + + +cdef unordered_map[string, vector[int]] convert_list_and_array_int_map(dict d_list, dict d_array): + cdef unordered_map[string, vector[int]] out + for k, v in d_list.items(): + out[k.encode('utf-8')] = list_to_vector_int(v) + for k, v in d_array.items(): + out[k.encode('utf-8')] = array_to_vector_int(v) + return out + + +# cdef unordered_map[string, vector[int]] convert_list_int_map(dict d): +# cdef unordered_map[string, vector[int]] out +# for k, v in d.items(): +# out[string(k.encode('utf-8'))] = to_vector_int(v) +# return out + + +cdef unordered_map[string, vector[double]] convert_list_and_array_float_map(dict d_list, dict d_array): + cdef unordered_map[string, vector[double]] out + for k, v in d_list.items(): + out[k.encode('utf-8')] = list_to_vector_float(v) + for k, v in d_array.items(): + out[k.encode('utf-8')] = array_to_vector_float(v) + return out + + +# cdef unordered_map[string, vector[double]] convert_list_float_map(dict d): +# cdef unordered_map[string, vector[double]] out +# for k, v in d.items(): +# out[string(k.encode('utf-8'))] = to_vector_float(v) +# return out + + +# For string lists only (list of str) +cdef vector[string] to_vector_str(object obj): + # Attention: type check (if it is str) must be done before calling this function!! + + cdef vector[string] vec + cdef str x + + for x in obj: + vec.push_back(x.encode('utf-8')) + return vec + + +# General conversion for list of int +cdef vector[int] list_to_vector_int(obj): + # Attention: type check (if it is int) must be done before calling this function!! + # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? + + cdef vector[int] vec + + for x in obj: + check_int32_range(x) + for x in obj: + vec.push_back(x) + + return vec + + +# General conversion for np.ndarray[int] +cdef vector[int] array_to_vector_int(obj): + # Attention: type check (if it is int) must be done before calling this function!! + # Check the dimension of the array before calling this function!! + # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? + + cdef vector[int] vec + + if obj.dtype != np.int32: + raise TypeError( + f'Expected np.ndarray[int32]. Got {obj.dtype} instead. ' + f'Convert your array to int32 before passing it, or help us enhance this function. ;)' + ) + for i in range(obj.shape[0]): + vec.push_back((obj[i])) + + return vec + + +# # General conversion for list or np.ndarray[int] +# cdef vector[int] to_vector_int(obj): +# # Attention: type check (if it is int) must be done before calling this function!! +# # Check the dimension of the array before calling this function!! +# # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? +# +# cdef vector[int] vec +# # cdef cnp.ndarray[cnp.int64_t, ndim=1] arr # Cannot declare cdef inside an if statement +# +# if isinstance(obj, list): +# for x in obj: +# check_int32_range(x) +# for x in obj: +# vec.push_back(x) +# elif isinstance(obj, np.ndarray): +# if obj.dtype != np.int32: +# raise TypeError( +# f'Expected np.ndarray[int32]. Got {obj.dtype} instead. ' +# f'Convert your array to int32 before passing it, or help us enhance this function. ;)' +# ) +# for i in range(obj.shape[0]): +# vec.push_back((obj[i])) +# else: +# raise TypeError("Expected list[int] or np.ndarray[int]") +# +# return vec + + +# General conversion for list of float +cdef vector[double] list_to_vector_float(obj): + # Attention: type check (if it is float) must be done before calling this function!! + # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? + + cdef vector[double] vec + + for x in obj: + vec.push_back(x) + return vec + + +# General conversion for np.ndarray[float] +cdef vector[double] array_to_vector_float(obj): + # Attention: type check (if it is float) must be done before calling this function!! + # Check the dimension of the array before calling this function!! + # todo: the 64 and 32 bit versions should be handled better. Maybe pybind can do it automatically? + + cdef vector[double] vec + + for i in range(obj.shape[0]): + vec.push_back((obj[i])) + return vec + + +# # For float arrays +# cdef vector[double] to_vector_float(obj): +# cdef vector[double] vec +# if isinstance(obj, list): +# for x in obj: +# vec.push_back(x) +# elif isinstance(obj, np.ndarray): +# cdef np.ndarray[np.float64_t, ndim=1] arr = obj +# for i in range(arr.shape[0]): +# vec.push_back(arr[i]) +# else: +# raise TypeError("Expected list[float] or np.ndarray[float]") +# return vec + + +######################################### +##PYTHON FUNCTIONS FOR SOME COMPUTATION## +######################################### + + +def encode_your_map(map_u): + """ + Encodes Python unicode strings in dictionnary `map` to utf-8 byte strings for C++ functions. + + :param map_b: The map to encode + :type map_b: dict{string : string} + :return: The encoded map + :rtype: dict{'b'string : 'b'string} + + .. note:: This function is used for type connection. + + """ + # fixme: debug test only: + print(f'The labels passed from python are: {map_u}.') + + res = {} + for key, value in map_u.items(): + res[key.encode('utf-8')] = value.encode('utf-8') + + print(f'The labels encoded for C++ are: {res}.') + + return res + + +def decode_your_map(map_b): + """ + Decodes utf-8 byte strings in `map` from C++ functions to Python unicode strings. + + :param map_b: The map to decode + :type map_b: dict{'b'string : 'b'string} + :return: The decoded map + :rtype: dict{string : string} + + .. note:: This function is used for type connection. + + """ + res = {} + for key, value in map_b.items(): + res[key.decode('utf-8')] = value.decode('utf-8') + return res + + +def decode_graph_edges(map_edge_b): + """ + Decode utf-8 byte strings in graph edges `map` from C++ functions to Python unicode strings. + + Parameters + ---------- + map_edge_b : dict{tuple(size_t, size_t) : dict{'b'string : 'b'string}} + The map to decode. + + Returns + ------- + dict{tuple(size_t, size_t) : dict{string : string}} + The decoded map. + + Notes + ----- + This is a helper function for function `GEDEnv.get_graph_edges()`. + """ + map_edges = {} + for key, value in map_edge_b.items(): + map_edges[key] = decode_your_map(value) + return map_edges + + +def encode_label_map(label_map: dict[str, Any]): + """ + Encode a mixed-type label dict into separate dicts for each type. + + Parameters + ---------- + label_map : dict[str, Any] + A dict containing string keys and mixed-type values (int, float, str, list of + int/float/str, np.array of int and float). + + Returns + ------- + str_map : dict[str, str] + A dict containing string keys and string values. + """ + str_map = {} + int_map = {} + float_map = {} + list_str_map = {} + list_int_map = {} + np_array_int_map = {} + list_float_map = {} + np_array_float_map = {} + + for k, v in label_map.items(): + if isinstance(v, str): + str_map[k] = v + elif isinstance(v, int): + int_map[k] = v + elif isinstance(v, float): + float_map[k] = v + elif isinstance(v, np.ndarray): + if v.ndim != 1: + raise ValueError( + f'Expected 1D array for list of int, got {v.ndim}D array instead.' + ) + if v.dtype == np.int32 or v.dtype == np.int64: + np_array_int_map[k] = v + elif v.dtype == np.float64 or v.dtype == np.float32: + np_array_float_map[k] = v + else: + raise TypeError( + f'Expected np.ndarray[int32/64] or np.ndarray[float32/64], got {v.dtype} instead.' + ) + elif isinstance(v, py_list) and len(v) > 0: + # todo: here we only check the type of the first element. + if isinstance(v[0], str): + list_str_map[k] = v + if isinstance(v[0], int): + list_int_map[k] = v + elif isinstance(v[0], float): + list_float_map[k] = v + else: + raise TypeError( + f'Expected list of int or float, got invalid types in list for key {k}.' + ) + else: + raise TypeError(f'Unsupported label type: {k}: {type(v)}.') + + return ( + convert_str_map(str_map), + convert_int_map(int_map), + convert_float_map(float_map), + convert_list_str_map(list_str_map), + convert_list_and_array_int_map(list_int_map, np_array_int_map), + convert_list_and_array_float_map(list_float_map, np_array_float_map) + ) + + +def check_int32_range(value: int): + """ + Check if the value is in the range of int32. + + Parameters + ---------- + value : int + The value to check. + + Raises + ------ + ValueError + If the value is not in the range of int32. + """ + if not (-2147483648 <= value <= 2147483647): + raise ValueError(f'Value {value} is out of int32 range.') diff --git a/gklearn/gedlib/single_dlib_version_bk/gedlibpy_gxl.pxd b/gklearn/gedlib/single_dlib_version_bk/gedlibpy_gxl.pxd new file mode 100644 index 0000000000..c18489f5c9 --- /dev/null +++ b/gklearn/gedlib/single_dlib_version_bk/gedlibpy_gxl.pxd @@ -0,0 +1,203 @@ +# distutils: language = c++ + +""" + Python GedLib module for the GXLLabel version (string labels) + ====================== + + This module allows using a C++ library for edit distance between graphs (GedLib) with Python. + + + Authors + ------------------- + + Linlin Jia + David Blumenthal + Natacha Lambert + + Copyright (C) 2019-2025 by all the authors + + Classes & Functions + ------------------- + +""" + +################################# +##DECLARATION OF C++ INTERFACES## +################################# + + +#Types imports for C++ compatibility +from libcpp.vector cimport vector +from libcpp.string cimport string +from libcpp.map cimport map +from libcpp cimport bool +from libcpp.pair cimport pair + +#Long unsigned int equivalent +cimport numpy as cnp + +ctypedef cnp.npy_uint32 UINT32_t + + +cdef extern from "src/gedlib_bind.hpp" namespace "pyged": + + + cdef cppclass PyGEDEnvGXL: + PyGEDEnvGXL() except + + bool isInitialized() except + + void restartEnv() except + + void loadGXLGraph( + string pathFolder, string pathXML, bool node_type, bool edge_type + ) except + + pair[size_t, size_t] getGraphIds() except + + vector[size_t] getAllGraphIds() except + + string getGraphClass(size_t id) except + + string getGraphName(size_t id) except + + size_t addGraph(string name, string classe) except + + void addNode( + size_t graphId, string nodeId, map[string, string] nodeLabel + ) except + + void addEdge( + size_t graphId, string tail, string head, map[string, string] edgeLabel, + bool ignoreDuplicates + ) except + + void clearGraph(size_t graphId) except + + size_t getGraphInternalId(size_t graphId) except + + size_t getGraphNumNodes(size_t graphId) except + + size_t getGraphNumEdges(size_t graphId) except + + vector[string] getGraphOriginalNodeIds(size_t graphId) except + + vector[map[string, string]] getGraphNodeLabels(size_t graphId) except + + map[pair[size_t, size_t], map[string, string]] getGraphEdges( + size_t graphId + ) except + + vector[vector[size_t]] getGraphAdjacenceMatrix(size_t graphId) except + + void setEditCost(string editCost, vector[double] editCostConstant) except + + void setPersonalEditCost(vector[double] editCostConstant) except + + void initEnv(string initOption, bool print_to_stdout) except + + void setMethod(string method, string options) except + + void initMethod() except + + double getInitime() except + + void runMethod(size_t g, size_t h) except + + double getUpperBound(size_t g, size_t h) except + + double getLowerBound(size_t g, size_t h) except + + vector[cnp.npy_uint64] getForwardMap(size_t g, size_t h) except + + vector[cnp.npy_uint64] getBackwardMap(size_t g, size_t h) except + + size_t getNodeImage(size_t g, size_t h, size_t nodeId) except + + size_t getNodePreImage(size_t g, size_t h, size_t nodeId) except + + double getInducedCost(size_t g, size_t h) except + + vector[pair[size_t, size_t]] getNodeMap(size_t g, size_t h) except + + vector[vector[int]] getAssignmentMatrix(size_t g, size_t h) except + + vector[vector[cnp.npy_uint64]] getAllMap(size_t g, size_t h) except + + double getRuntime(size_t g, size_t h) except + + bool quasimetricCosts() except + + vector[vector[size_t]] hungarianLSAP(vector[vector[size_t]] matrixCost) except + + vector[vector[double]] hungarianLSAPE( + vector[vector[double]] matrixCost + ) except + + # added by Linlin Jia. + size_t getNumNodeLabels() except + + map[string, string] getNodeLabel(size_t label_id) except + + size_t getNumEdgeLabels() except + + map[string, string] getEdgeLabel(size_t label_id) except + + # size_t getNumNodes(size_t graph_id) except + + double getAvgNumNodes() except + + double getNodeRelCost( + map[string, string] & node_label_1, map[string, string] & node_label_2 + ) except + + double getNodeDelCost(map[string, string] & node_label) except + + double getNodeInsCost(map[string, string] & node_label) except + + map[string, string] getMedianNodeLabel( + vector[map[string, string]] & node_labels + ) except + + double getEdgeRelCost( + map[string, string] & edge_label_1, map[string, string] & edge_label_2 + ) except + + double getEdgeDelCost(map[string, string] & edge_label) except + + double getEdgeInsCost(map[string, string] & edge_label) except + + map[string, string] getMedianEdgeLabel( + vector[map[string, string]] & edge_labels + ) except + + string getInitType() except + + # double getNodeCost(size_t label1, size_t label2) except + + double computeInducedCost( + size_t g_id, size_t h_id, vector[pair[size_t, size_t]] + ) except + + + +############################# +##External Libs Import ## +############################# + +############################# +##CYTHON WRAPPER INTERFACES## +############################# + +# @cython.auto_pickle(True) +cdef class GEDEnvGXL: + """Cython wrapper class for C++ class PyGEDEnvGXL + """ + # cdef PyGEDEnv c_env # Hold a C++ instance which we're wrapping + cdef PyGEDEnvGXL * c_env # hold a pointer to the C++ instance which we're wrapping + + +# cdef extern from "src/GedLibBind.h" namespace "shapes": +# cdef cppclass Rectangle: +# Rectangle() except + +# Rectangle(int, int, int, int) except + +# int x0, y0, x1, y1 +# int getArea() +# void getSize(int* width, int* height) +# void move(int, int) + + +# # Create a Cython extension type which holds a C++ instance +# # as an attribute and create a bunch of forwarding methods +# # Python extension type. +# cdef class PyRectangle: +# cdef Rectangle c_rect # Hold a C++ instance which we're wrapping + +# def __cinit__(self, int x0, int y0, int x1, int y1): +# self.c_rect = Rectangle(x0, y0, x1, y1) + +# def get_area(self): +# return self.c_rect.getArea() + +# def get_size(self): +# cdef int width, height +# self.c_rect.getSize(&width, &height) +# return width, height + +# def move(self, dx, dy): +# self.c_rect.move(dx, dy) + +# # Attribute access +# @property +# def x0(self): +# return self.c_rect.x0 +# @x0.setter +# def x0(self, x0): +# self.c_rect.x0 = x0 + +# # Attribute access +# @property +# def x1(self): +# return self.c_rect.x1 +# @x1.setter +# def x1(self, x1): +# self.c_rect.x1 = x1 + +# # Attribute access +# @property +# def y0(self): +# return self.c_rect.y0 +# @y0.setter +# def y0(self, y0): +# self.c_rect.y0 = y0 + +# # Attribute access +# @property +# def y1(self): +# return self.c_rect.y1 +# @y1.setter +# def y1(self, y1): +# self.c_rect.y1 = y1 \ No newline at end of file diff --git a/gklearn/gedlib/single_dlib_version_bk/gedlibpy_gxl.pyx b/gklearn/gedlib/single_dlib_version_bk/gedlibpy_gxl.pyx new file mode 100644 index 0000000000..ca1390995f --- /dev/null +++ b/gklearn/gedlib/single_dlib_version_bk/gedlibpy_gxl.pyx @@ -0,0 +1,1299 @@ +# distutils: language = c++ + +""" + Python GedLib module for the GXLLabel version (string labels) + ====================== + + This module allows using a C++ library for edit distance between graphs (GedLib) with Python. + + + Authors + ------------------- + + David Blumenthal + Natacha Lambert + Linlin Jia + + Copyright (C) 2019-2025 by all the authors + + Classes & Functions + ------------------- + +""" + +################################# +##DECLARATION OF C++ INTERFACES## +################################# + + +#Types imports for C++ compatibility +from libcpp.vector cimport vector +# from libcpp.map cimport map +# from libcpp cimport bool +# from libcpp.pair cimport pair + +#Long unsigned int equivalent + +# ctypedef cnp.npy_uint32 UINT32_t + +from gklearn.gedlib.lib.common_bind cimport * + + +############################# +##CYTHON WRAPPER INTERFACES## +############################# + +# import cython +import numpy as np +import networkx as nx +from gklearn.ged.env import NodeMap + +# import librariesImport + +# @cython.auto_pickle(True) +cdef class GEDEnvGXL: + """Cython wrapper class for C++ class PyGEDEnvGXL + """ + # # cdef PyGEDEnv c_env # Hold a C++ instance which we're wrapping + # cdef PyGEDEnvGXL * c_env # hold a pointer to the C++ instance which we're wrapping + + + def __cinit__(self): + # self.c_env = PyGEDEnv() + self.c_env = new PyGEDEnvGXL() + + + def __dealloc__(self): + del self.c_env + + # def __reduce__(self): + # # return GEDEnv, (self.c_env,) + # return GEDEnv, tuple() + + def is_initialized(self): + """ + Checks and returns if the computation environment is initialized or not. + + :return: True if it's initialized, False otherwise + :rtype: bool + + .. note:: This function exists for internals verifications but you can use it for your code. + """ + return self.c_env.isInitialized() + + def restart_env(self): + """ + Restarts the environment variable. All data related to it will be delete. + + .. warning:: This function deletes all graphs, computations and more so make sure you don't need anymore your environment. + .. note:: You can now delete and add somes graphs after initialization so you can avoid this function. + """ + self.c_env.restartEnv() + + def load_GXL_graphs(self, path_folder, path_XML, node_type, edge_type): + """ + Loads some GXL graphes on the environment which is in a same folder, and present in the XMLfile. + + :param path_folder: The folder's path which contains GXL graphs + :param path_XML: The XML's path which indicates which graphes you want to load + :param node_type: Select if nodes are labeled or unlabeled + :param edge_type: Select if edges are labeled or unlabeled + :type path_folder: string + :type path_XML: string + :type node_type: bool + :type edge_type: bool + + + .. note:: You can call this function multiple times if you want, but not after an init call. + """ + self.c_env.loadGXLGraph( + path_folder.encode('utf-8'), path_XML.encode('utf-8'), node_type, edge_type + ) + + def graph_ids(self): + """ + Searchs the first and last IDs of the loaded graphs in the environment. + + :return: The pair of the first and the last graphs Ids + :rtype: tuple(size_t, size_t) + + .. note:: Prefer this function if you have huges structures with lots of graphs. + """ + return self.c_env.getGraphIds() + + def get_all_graph_ids(self): + """ + Searchs all the IDs of the loaded graphs in the environment. + + :return: The list of all graphs's Ids + :rtype: list[size_t] + + .. note:: The last ID is equal to (number of graphs - 1). The order correspond to the loading order. + """ + return self.c_env.getAllGraphIds() + + def get_graph_class(self, id): + """ + Returns the class of a graph with its ID. + + :param id: The ID of the wanted graph + :type id: size_t + :return: The class of the graph which correpond to the ID + :rtype: string + + .. seealso:: get_graph_class() + .. note:: An empty string can be a class. + """ + return self.c_env.getGraphClass(id) + + def get_graph_name(self, id): + """ + Returns the name of a graph with its ID. + + :param id: The ID of the wanted graph + :type id: size_t + :return: The name of the graph which correpond to the ID + :rtype: string + + .. seealso:: get_graph_class() + .. note:: An empty string can be a name. + """ + return self.c_env.getGraphName(id).decode('utf-8') + + def add_graph(self, name="", classe=""): + """ + Adds a empty graph on the environment, with its name and its class. Nodes and edges will be add in a second time. + + :param name: The name of the new graph, an empty string by default + :param classe: The class of the new graph, an empty string by default + :type name: string + :type classe: string + :return: The ID of the newly graphe + :rtype: size_t + + .. seealso::add_node(), add_edge() , add_symmetrical_edge() + .. note:: You can call this function without parameters. You can also use this function after initialization, call init() after you're finished your modifications. + """ + return self.c_env.addGraph(name.encode('utf-8'), classe.encode('utf-8')) + + def add_node(self, graph_id, node_id, node_label): + """ + Adds a node on a graph selected by its ID. A ID and a label for the node is required. + + :param graph_id: The ID of the wanted graph + :param node_id: The ID of the new node + :param node_label: The label of the new node + :type graph_id: size_t + :type node_id: string + :type node_label: dict{string : string} + + .. seealso:: add_graph(), add_edge(), add_symmetrical_edge() + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + """ + self.c_env.addNode( + graph_id, node_id.encode('utf-8'), encode_your_map(node_label) + ) + + def add_edge(self, graph_id, tail, head, edge_label, ignore_duplicates=True): + """ + Adds an edge on a graph selected by its ID. + + :param graph_id: The ID of the wanted graph + :param tail: The ID of the tail node for the new edge + :param head: The ID of the head node for the new edge + :param edge_label: The label of the new edge + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type graph_id: size_t + :type tail: string + :type head: string + :type edge_label: dict{string : string} + :type ignore_duplicates: bool + + .. seealso:: add_graph(), add_node(), add_symmetrical_edge() + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + """ + self.c_env.addEdge( + graph_id, tail.encode('utf-8'), head.encode('utf-8'), + encode_your_map(edge_label), ignore_duplicates + ) + + def add_symmetrical_edge(self, graph_id, tail, head, edge_label): + """ + Adds a symmetrical edge on a graph selected by its ID. + + :param graph_id: The ID of the wanted graph + :param tail: The ID of the tail node for the new edge + :param head: The ID of the head node for the new edge + :param edge_label: The label of the new edge + :type graph_id: size_t + :type tail: string + :type head: string + :type edge_label: dict{string : string} + + .. seealso:: add_graph(), add_node(), add_edge() + .. note:: You can also use this function after initialization, but only on a newly added graph. Call init() after you're finished your modifications. + """ + tailB = tail.encode('utf-8') + headB = head.encode('utf-8') + edgeLabelB = encode_your_map(edge_label) + self.c_env.addEdge(graph_id, tailB, headB, edgeLabelB, True) + self.c_env.addEdge(graph_id, headB, tailB, edgeLabelB, True) + + def clear_graph(self, graph_id): + """ + Deletes a graph, selected by its ID, to the environment. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + + .. note:: Call init() after you're finished your modifications. + """ + self.c_env.clearGraph(graph_id) + + def get_graph_internal_id(self, graph_id): + """ + Searchs and returns the internal Id of a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The internal ID of the selected graph + :rtype: size_t + + .. seealso:: get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphInternalId(graph_id) + + def get_graph_num_nodes(self, graph_id): + """ + Searchs and returns the number of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The number of nodes on the selected graph + :rtype: size_t + + .. seealso:: get_graph_internal_id(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphNumNodes(graph_id) + + def get_graph_num_edges(self, graph_id): + """ + Searchs and returns the number of edges on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The number of edges on the selected graph + :rtype: size_t + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphNumEdges(graph_id) + + def get_original_node_ids(self, graph_id): + """ + Searchs and returns all th Ids of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The list of IDs's nodes on the selected graph + :rtype: list[string] + + .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_graph_node_labels(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return [gid.decode('utf-8') for gid in + self.c_env.getGraphOriginalNodeIds(graph_id)] + + def get_graph_node_labels(self, graph_id): + """ + Searchs and returns all the labels of nodes on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The list of nodes' labels on the selected graph + :rtype: list[dict{string : string}] + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_edges(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return [decode_your_map(node_label) for node_label in + self.c_env.getGraphNodeLabels(graph_id)] + + def get_graph_edges(self, graph_id): + """ + Searchs and returns all the edges on a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The list of edges on the selected graph + :rtype: dict{tuple(size_t, size_t) : dict{string : string}} + + .. seealso::get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_adjacence_matrix() + .. note:: These functions allow to collect all the graph's informations. + """ + return decode_graph_edges(self.c_env.getGraphEdges(graph_id)) + + def get_graph_adjacence_matrix(self, graph_id): + """ + Searchs and returns the adjacence list of a graph, selected by its ID. + + :param graph_id: The ID of the wanted graph + :type graph_id: size_t + :return: The adjacence list of the selected graph + :rtype: list[list[size_t]] + + .. seealso:: get_graph_internal_id(), get_graph_num_nodes(), get_graph_num_edges(), get_original_node_ids(), get_graph_node_labels(), get_graph_edges() + .. note:: These functions allow to collect all the graph's informations. + """ + return self.c_env.getGraphAdjacenceMatrix(graph_id) + + def set_edit_cost(self, edit_cost, edit_cost_constant = []): + """ + Sets an edit cost function to the environment, if it exists. + + :param edit_cost: The name of the edit cost function + :type edit_cost: string + :param edi_cost_constant: The parameters you will add to the editCost, empty by default + :type edit_cost_constant: list + + .. seealso:: list_of_edit_cost_options + .. note:: Try to make sure the edit cost function exists with list_of_edit_cost_options, raise an error otherwise. + """ + if edit_cost in list_of_edit_cost_options: + edit_cost_b = edit_cost.encode('utf-8') + self.c_env.setEditCost(edit_cost_b, edit_cost_constant) + else: + raise EditCostError( + "This edit cost function doesn't exist, please see list_of_edit_cost_options for selecting a edit cost function" + ) + + def set_personal_edit_cost(self, edit_cost_constant = []): + """ + Sets an personal edit cost function to the environment. + + :param edit_cost_constant: The parameters you will add to the editCost, empty by default + :type edit_cost_constant: list + + .. seealso:: list_of_edit_cost_options, set_edit_cost() + .. note::You have to modify the C++ function to use it. Please see the documentation to add your Edit Cost function. + """ + self.c_env.setPersonalEditCost(edit_cost_constant) + + def init(self, init_option='EAGER_WITHOUT_SHUFFLED_COPIES', print_to_stdout=False): + """ + Initializes the environment with the chosen edit cost function and graphs. + + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type init_option: string + + .. seealso:: list_of_init_options + .. warning:: No modification were allowed after initialization. Try to make sure your choices is correct. You can though clear or add a graph, but recall init() after that. + .. note:: Try to make sure the option exists with list_of_init_options or choose no options, raise an error otherwise. + """ + if init_option in list_of_init_options: + init_option_b = init_option.encode('utf-8') + self.c_env.initEnv(init_option_b, print_to_stdout) + else: + raise InitError( + "This init option doesn't exist, please see list_of_init_options for selecting an option. You can choose any options." + ) + + def set_method(self, method, options=""): + """ + Sets a computation method to the environment, if its exists. + + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :type method: string + :type options: string + + .. seealso:: init_method(), list_of_method_options + .. note:: Try to make sure the edit cost function exists with list_of_method_options, raise an error otherwise. Call init_method() after your set. + """ + if method in list_of_method_options: + method_b = method.encode('utf-8') + self.c_env.setMethod(method_b, options.encode('utf-8')) + else: + raise MethodError( + "This method doesn't exist, please see list_of_method_options for selecting a method" + ) + + def init_method(self): + """ + Inits the environment with the set method. + + .. seealso:: set_method(), list_of_method_options + .. note:: Call this function after set the method. You can't launch computation or change the method after that. + """ + self.c_env.initMethod() + + def get_init_time(self): + """ + Returns the initialization time. + + :return: The initialization time + :rtype: double + """ + return self.c_env.getInitime() + + def run_method(self, g, h): + """ + Computes the edit distance between two graphs g and h, with the edit cost function and method computation selected. + + :param g: The Id of the first graph to compare + :param h: The Id of the second graph to compare + :type g: size_t + :type h: size_t + + .. seealso:: get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. note:: This function only compute the distance between two graphs, without returning a result. Use the differents function to see the result between the two graphs. + """ + self.c_env.runMethod(g, h) + + def get_upper_bound(self, g, h): + """ + Returns the upper bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The upper bound of the edit distance cost + :rtype: double + + .. seealso:: run_method(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: The upper bound is equivalent to the result of the pessimist edit distance cost. Methods are heuristics so the library can't compute the real perfect result because it's NP-Hard problem. + """ + return self.c_env.getUpperBound(g, h) + + def get_lower_bound(self, g, h): + """ + Returns the lower bound of the edit distance cost between two graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The lower bound of the edit distance cost + :rtype: double + + .. seealso:: run_method(), get_upper_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function can be ignored, because lower bound doesn't have a crucial utility. + """ + return self.c_env.getLowerBound(g, h) + + def get_forward_map(self, g, h): + """ + Returns the forward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The forward map to the adjacence matrix between nodes of the two graphs + :rtype: list[npy_uint32] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_backward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getForwardMap(g, h) + + def get_backward_map(self, g, h): + """ + Returns the backward map (or the half of the adjacence matrix) between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The backward map to the adjacence matrix between nodes of the two graphs + :rtype: list[npy_uint32] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_runtime(), quasimetric_cost(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: I don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getBackwardMap(g, h) + + def get_node_image(self, g, h, node_id): + """ + Returns the node's image in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :param node_id: The ID of the node which you want to see the image + :type g: size_t + :type h: size_t + :type node_id: size_t + :return: The ID of the image node + :rtype: size_t + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_pre_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use BackwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getNodeImage(g, h, node_id) + + def get_node_pre_image(self, g, h, node_id): + """ + Returns the node's preimage in the adjacence matrix, if it exists. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :param node_id: The ID of the node which you want to see the preimage + :type g: size_t + :type h: size_t + :type node_id: size_t + :return: The ID of the preimage node + :rtype: size_t + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getNodePreImage(g, h, node_id) + + def get_induced_cost(self, g, h): + """ + Returns the induced cost between the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The induced cost between the two indicated graphs + :rtype: double + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_map(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Use ForwardMap's Node to find its images ! You can also use get_forward_map() and get_backward_map(). + + """ + return self.c_env.getInducedCost(g, h) + + def get_node_map(self, g, h): + """ + Returns the Node Map, like C++ NodeMap. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The Node Map between the two selected graph. + :rtype: gklearn.ged.env.NodeMap. + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_assignment_matrix() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary, however you can understand how assignement works with this example. + """ + map_as_relation = self.c_env.getNodeMap(g, h) + induced_cost = self.c_env.getInducedCost( + g, h + ) # @todo: the C++ implementation for this function in GedLibBind.ipp re-call get_node_map() once more, this is not neccessary. + source_map = [item.first if item.first < len(map_as_relation) else np.inf for + item in + map_as_relation] # item.first < len(map_as_relation) is not exactly correct. + # print(source_map) + target_map = [item.second if item.second < len(map_as_relation) else np.inf for + item in map_as_relation] + # print(target_map) + num_node_source = len([item for item in source_map if item != np.inf]) + # print(num_node_source) + num_node_target = len([item for item in target_map if item != np.inf]) + # print(num_node_target) + + node_map = NodeMap(num_node_source, num_node_target) + # print(node_map.get_forward_map(), node_map.get_backward_map()) + for i in range(len(source_map)): + node_map.add_assignment(source_map[i], target_map[i]) + node_map.set_induced_cost(induced_cost) + + return node_map + + def get_assignment_matrix(self, g, h): + """ + Returns the Assignment Matrix between two selected graphs g and h. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The Assignment Matrix between the two selected graph. + :rtype: list[list[int]] + + .. seealso:: run_method(), get_forward_map(), get_backward_map(), get_node_image(), get_node_pre_image(), get_node_map() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function creates datas so use it if necessary. + """ + return self.c_env.getAssignmentMatrix(g, h) + + def get_all_map(self, g, h): + """ + Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The forward and backward maps to the adjacence matrix between nodes of the two graphs + :rtype: list[list[npy_uint32]] + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: This function duplicates data so please don't use it. I also don't know how to connect the two map to reconstruct the adjacence matrix. Please come back when I know how it's work ! + """ + return self.c_env.getAllMap(g, h) + + def get_runtime(self, g, h): + """ + Returns the runtime to compute the edit distance cost between two graphs g and h + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: The runtime of the computation of edit distance cost between the two selected graphs + :rtype: double + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), quasimetric_cost() + .. warning:: run_method() between the same two graph must be called before this function. + .. note:: Python is a bit longer than C++ due to the functions's encapsulate. + """ + return self.c_env.getRuntime(g, h) + + def quasimetric_cost(self): + """ + Checks and returns if the edit costs are quasimetric. + + :param g: The Id of the first compared graph + :param h: The Id of the second compared graph + :type g: size_t + :type h: size_t + :return: True if it's verified, False otherwise + :rtype: bool + + .. seealso:: run_method(), get_upper_bound(), get_lower_bound(), get_forward_map(), get_backward_map(), get_runtime() + .. warning:: run_method() between the same two graph must be called before this function. + """ + return self.c_env.quasimetricCosts() + + def hungarian_LSAP(self, matrix_cost): + """ + Applies the hungarian algorithm (LSAP) on a matrix Cost. + + :param matrix_cost: The matrix Cost + :type matrix_cost: vector[vector[size_t]] + :return: The values of rho, varrho, u and v, in this order + :rtype: vector[vector[size_t]] + + .. seealso:: hungarian_LSAPE() + """ + return self.c_env.hungarianLSAP(matrix_cost) + + def hungarian_LSAPE(self, matrix_cost): + """ + Applies the hungarian algorithm (LSAPE) on a matrix Cost. + + :param matrix_cost: The matrix Cost + :type matrix_cost: vector[vector[double]] + :return: The values of rho, varrho, u and v, in this order + :rtype: vector[vector[double]] + + .. seealso:: hungarian_LSAP() + """ + return self.c_env.hungarianLSAPE(matrix_cost) + + def add_random_graph( + self, name, classe, list_of_nodes, list_of_edges, ignore_duplicates=True + ): + """ + Add a Graph (not GXL) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param name: The name of the graph to add, can be an empty string + :param classe: The classe of the graph to add, can be an empty string + :param list_of_nodes: The list of nodes to add + :param list_of_edges: The list of edges to add + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type name: string + :type classe: string + :type list_of_nodes: list[tuple(size_t, dict{string : string})] + :type list_of_edges: list[tuple(tuple(size_t,size_t), dict{string : string})] + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + .. note:: The graph must respect the GXL structure. Please see how a GXL graph is construct. + + """ + id = self.add_graph(name, classe) + for node in list_of_nodes: + self.add_node(id, node[0], node[1]) + for edge in list_of_edges: + self.add_edge(id, edge[0], edge[1], edge[2], ignore_duplicates) + return id + + def add_nx_graph(self, g, classe, ignore_duplicates=True): + """ + Add a Graph (made by networkx) on the environment. Be careful to respect the same format as GXL graphs for labelling nodes and edges. + + :param g: The graph to add (networkx graph) + :param ignore_duplicates: If True, duplicate edges are ignored, otherwise it's raise an error if an existing edge is added. True by default + :type g: networkx.graph + :type ignore_duplicates: bool + :return: The ID of the newly added graphe + :rtype: size_t + + .. note:: The NX graph must respect the GXL structure. Please see how a GXL graph is construct. + + """ + id = self.add_graph(g.name, classe) + for node in g.nodes: + self.add_node(id, str(node), g.nodes[node]) + for edge in g.edges: + self.add_edge( + id, str(edge[0]), str(edge[1]), g.get_edge_data(edge[0], edge[1]), + ignore_duplicates + ) + return id + + def compute_ged_on_two_graphs( + self, g1, g2, edit_cost, method, options, + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + Computes the edit distance between two NX graphs. + + :param g1: The first graph to add and compute + :param g2: The second graph to add and compute + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type g1: networksx.graph + :type g2: networksx.graph + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: The edit distance between the two graphs and the nodeMap between them. + :rtype: double, list[tuple(size_t, size_t)] + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + + """ + if self.is_initialized(): + self.restart_env() + + g = self.add_nx_graph(g1, "") + h = self.add_nx_graph(g2, "") + + self.set_edit_cost(edit_cost) + self.init(init_option) + + self.set_method(method, options) + self.init_method() + + resDistance = 0 + resMapping = [] + self.run_method(g, h) + resDistance = self.get_upper_bound(g, h) + resMapping = self.get_node_map(g, h) + + return resDistance, resMapping + + def compute_edit_distance_on_nx_graphs( + self, dataset, classes, edit_cost, method, options, + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + + Computes all the edit distance between each NX graphs on the dataset. + + :param dataset: The list of graphs to add and compute + :param classes: The classe of all the graph, can be an empty string + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type dataset: list[networksx.graph] + :type classes: string + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: Two matrix, the first with edit distances between graphs and the second the nodeMap between graphs. The result between g and h is one the [g][h] coordinates. + :rtype: list[list[double]], list[list[list[tuple(size_t, size_t)]]] + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. The structure of graphs must be similar as GXL. + + """ + if self.is_initialized(): + self.restart_env() + + print("Loading graphs in progress...") + for graph in dataset: + self.add_nx_graph(graph, classes) + listID = self.graph_ids() + print("Graphs loaded ! ") + print("Number of graphs = " + str(listID[1])) + + self.set_edit_cost(edit_cost) + print("Initialization in progress...") + self.init(init_option) + print("Initialization terminated !") + + self.set_method(method, options) + self.init_method() + + resDistance = [[]] + resMapping = [[]] + for g in range(listID[0], listID[1]): + print("Computation between graph " + str( + g + ) + " with all the others including himself.") + for h in range(listID[0], listID[1]): + #print("Computation between graph " + str(g) + " and graph " + str(h)) + self.run_method(g, h) + resDistance[g][h] = self.get_upper_bound(g, h) + resMapping[g][h] = self.get_node_map(g, h) + + print( + "Finish ! The return contains edit distances and NodeMap but you can check the result with graphs'ID until you restart the environment") + return resDistance, resMapping + + def compute_edit_distance_on_GXl_graphs( + self, path_folder, path_XML, edit_cost, method, options="", + init_option="EAGER_WITHOUT_SHUFFLED_COPIES" + ): + """ + Computes all the edit distance between each GXL graphs on the folder and the XMl file. + + :param path_folder: The folder's path which contains GXL graphs + :param path_XML: The XML's path which indicates which graphes you want to load + :param edit_cost: The name of the edit cost function + :param method: The name of the computation method + :param options: The options of the method (like bash options), an empty string by default + :param init_option: The name of the init option, "EAGER_WITHOUT_SHUFFLED_COPIES" by default + :type path_folder: string + :type path_XML: string + :type edit_cost: string + :type method: string + :type options: string + :type init_option: string + :return: The list of the first and last-1 ID of graphs + :rtype: tuple(size_t, size_t) + + .. seealso:: list_of_edit_cost_options, list_of_method_options, list_of_init_options + .. note:: Make sure each parameter exists with your architecture and these lists : list_of_edit_cost_options, list_of_method_options, list_of_init_options. + + """ + + if self.is_initialized(): + self.restart_env() + + print("Loading graphs in progress...") + self.load_GXL_graphs(path_folder, path_XML) + listID = self.graph_ids() + print("Graphs loaded ! ") + print("Number of graphs = " + str(listID[1])) + + self.set_edit_cost(edit_cost) + print("Initialization in progress...") + self.init(init_option) + print("Initialization terminated !") + + self.set_method(method, options) + self.init_method() + + #res = [] + for g in range(listID[0], listID[1]): + print("Computation between graph " + str( + g + ) + " with all the others including himself.") + for h in range(listID[0], listID[1]): + #print("Computation between graph " + str(g) + " and graph " + str(h)) + self.run_method(g, h) + #res.append((get_upper_bound(g,h), get_node_map(g,h), get_runtime(g,h))) + + #return res + + print ( + "Finish ! You can check the result with each ID of graphs ! There are in the return") + print ( + "Please don't restart the environment or recall this function, you will lose your results !") + return listID + + def get_num_node_labels(self): + """ + Returns the number of node labels. + + :return: Number of pairwise different node labels contained in the environment. + :rtype: size_t + + .. note:: If 1 is returned, the nodes are unlabeled. + """ + return self.c_env.getNumNodeLabels() + + def get_node_label(self, label_id): + """ + Returns node label. + + :param label_id: ID of node label that should be returned. Must be between 1 and get_num_node_labels(). + :type label_id: size_t + :return: Node label for selected label ID. + :rtype: dict{string : string} + """ + return decode_your_map(self.c_env.getNodeLabel(label_id)) + + def get_num_edge_labels(self): + """ + Returns the number of edge labels. + + :return: Number of pairwise different edge labels contained in the environment. + :rtype: size_t + + .. note:: If 1 is returned, the edges are unlabeled. + """ + return self.c_env.getNumEdgeLabels() + + def get_edge_label(self, label_id): + """ + Returns edge label. + + :param label_id: ID of edge label that should be returned. Must be between 1 and get_num_edge_labels(). + :type label_id: size_t + :return: Edge label for selected label ID. + :rtype: dict{string : string} + """ + return decode_your_map(self.c_env.getEdgeLabel(label_id)) + + # def get_num_nodes(self, graph_id): + # """ + # Returns the number of nodes. + # + # :param graph_id: ID of an input graph that has been added to the environment. + # :type graph_id: size_t + # :return: Number of nodes in the graph. + # :rtype: size_t + # """ + # return self.c_env.getNumNodes(graph_id) + + def get_avg_num_nodes(self): + """ + Returns average number of nodes. + + :return: Average number of nodes of the graphs contained in the environment. + :rtype: double + """ + return self.c_env.getAvgNumNodes() + + def get_node_rel_cost(self, node_label_1, node_label_2): + """ + Returns node relabeling cost. + + :param node_label_1: First node label. + :param node_label_2: Second node label. + :type node_label_1: dict{string : string} + :type node_label_2: dict{string : string} + :return: Node relabeling cost for the given node labels. + :rtype: double + """ + return self.c_env.getNodeRelCost( + encode_your_map(node_label_1), encode_your_map(node_label_2) + ) + + def get_node_del_cost(self, node_label): + """ + Returns node deletion cost. + + :param node_label: Node label. + :type node_label: dict{string : string} + :return: Cost of deleting node with given label. + :rtype: double + """ + return self.c_env.getNodeDelCost(encode_your_map(node_label)) + + def get_node_ins_cost(self, node_label): + """ + Returns node insertion cost. + + :param node_label: Node label. + :type node_label: dict{string : string} + :return: Cost of inserting node with given label. + :rtype: double + """ + return self.c_env.getNodeInsCost(encode_your_map(node_label)) + + def get_median_node_label(self, node_labels): + """ + Computes median node label. + + :param node_labels: The node labels whose median should be computed. + :type node_labels: list[dict{string : string}] + :return: Median of the given node labels. + :rtype: dict{string : string} + """ + node_labels_b = [encode_your_map(node_label) for node_label in node_labels] + return decode_your_map(self.c_env.getMedianNodeLabel(node_labels_b)) + + def get_edge_rel_cost(self, edge_label_1, edge_label_2): + """ + Returns edge relabeling cost. + + :param edge_label_1: First edge label. + :param edge_label_2: Second edge label. + :type edge_label_1: dict{string : string} + :type edge_label_2: dict{string : string} + :return: Edge relabeling cost for the given edge labels. + :rtype: double + """ + return self.c_env.getEdgeRelCost( + encode_your_map(edge_label_1), encode_your_map(edge_label_2) + ) + + def get_edge_del_cost(self, edge_label): + """ + Returns edge deletion cost. + + :param edge_label: Edge label. + :type edge_label: dict{string : string} + :return: Cost of deleting edge with given label. + :rtype: double + """ + return self.c_env.getEdgeDelCost(encode_your_map(edge_label)) + + def get_edge_ins_cost(self, edge_label): + """ + Returns edge insertion cost. + + :param edge_label: Edge label. + :type edge_label: dict{string : string} + :return: Cost of inserting edge with given label. + :rtype: double + """ + return self.c_env.getEdgeInsCost(encode_your_map(edge_label)) + + def get_median_edge_label(self, edge_labels): + """ + Computes median edge label. + + :param edge_labels: The edge labels whose median should be computed. + :type edge_labels: list[dict{string : string}] + :return: Median of the given edge labels. + :rtype: dict{string : string} + """ + edge_labels_b = [encode_your_map(edge_label) for edge_label in edge_labels] + return decode_your_map(self.c_env.getMedianEdgeLabel(edge_label_b)) + + def get_nx_graph( + self, graph_id, adj_matrix=True, adj_lists=False, edge_list=False + ): # @todo + """ + Get graph with id `graph_id` in the form of the NetworkX Graph. + + Parameters + ---------- + graph_id : int + ID of the selected graph. + + adj_matrix : bool + Set to `True` to construct an adjacency matrix `adj_matrix` and a hash-map `edge_labels`, which has a key for each pair `(i,j)` such that `adj_matrix[i][j]` equals 1. No effect for now. + + adj_lists : bool + No effect for now. + + edge_list : bool + No effect for now. + + Returns + ------- + NetworkX Graph object + The obtained graph. + """ + graph = nx.Graph() + graph.graph['id'] = graph_id + + nb_nodes = self.get_graph_num_nodes(graph_id) + original_node_ids = self.get_original_node_ids(graph_id) + node_labels = self.get_graph_node_labels(graph_id) + # print(original_node_ids) + # print(node_labels) + graph.graph['original_node_ids'] = original_node_ids + + for node_id in range(0, nb_nodes): + graph.add_node(node_id, **node_labels[node_id]) + # graph.nodes[node_id]['original_node_id'] = original_node_ids[node_id] + + edges = self.get_graph_edges(graph_id) + for (head, tail), labels in edges.items(): + graph.add_edge(head, tail, **labels) + # print(edges) + + return graph + + def get_init_type(self): + """ + Returns the initialization type of the last initialization in string. + + Returns + ------- + string + Initialization type in string. + """ + return self.c_env.getInitType().decode('utf-8') + + # def get_node_cost(self, label1, label2): + # """ + # Returns node relabeling, insertion, or deletion cost. + + # Parameters + # ---------- + # label1 : int + # First node label. + # + # label2 : int + # Second node label. + # + # Returns + # ------- + # Node relabeling cost if `label1` and `label2` are both different from `ged::dummy_label()`, node insertion cost if `label1` equals `ged::dummy_label` and `label2` does not, node deletion cost if `label1` does not equal `ged::dummy_label` and `label2` does, and 0 otherwise. + # """ + # return self.c_env.getNodeCost(label1, label2) + + def load_nx_graph(self, nx_graph, graph_id, graph_name='', graph_class=''): + """ + Loads NetworkX Graph into the GED environment. + + Parameters + ---------- + nx_graph : NetworkX Graph object + The graph that should be loaded. + + graph_id : int or None + The ID of a graph contained the environment (overwrite existing graph) or add new graph if `None`. + + graph_name : string, optional + The name of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. + + graph_class : string, optional + The class of newly added graph. The default is ''. Has no effect unless `graph_id` equals `None`. + + Returns + ------- + int + The ID of the newly loaded graph. + """ + if graph_id is None: + graph_id = self.add_graph(graph_name, graph_class) + else: + self.clear_graph(graph_id) + for node in nx_graph.nodes: + self.add_node(graph_id, str(node), nx_graph.nodes[node]) + for edge in nx_graph.edges: + self.add_edge( + graph_id, str(edge[0]), str(edge[1]), + nx_graph.get_edge_data(edge[0], edge[1]) + ) + return graph_id + + def compute_induced_cost(self, g_id, h_id, node_map): + """ + Computes the edit cost between two graphs induced by a node map. + + Parameters + ---------- + g_id : int + ID of input graph. + h_id : int + ID of input graph. + node_map: gklearn.ged.env.NodeMap. + The NodeMap instance whose reduced cost will be computed and re-assigned. + + Returns + ------- + None. + """ + relation = [] + node_map.as_relation(relation) + # print(relation) + dummy_node = get_dummy_node() + # print(dummy_node) + for i, val in enumerate(relation): + val1 = dummy_node if val[0] == np.inf else val[0] + val2 = dummy_node if val[1] == np.inf else val[1] + relation[i] = tuple((val1, val2)) + # print(relation) + induced_cost = self.c_env.computeInducedCost(g_id, h_id, relation) + node_map.set_induced_cost(induced_cost) + + +######################################### +##PYTHON FUNCTIONS FOR SOME COMPUTATION## +######################################### + +def encode_your_map(map_u): + """ + Encodes Python unicode strings in dictionnary `map` to utf-8 byte strings for C++ functions. + + :param map_b: The map to encode + :type map_b: dict{string : string} + :return: The encoded map + :rtype: dict{'b'string : 'b'string} + + .. note:: This function is used for type connection. + + """ + res = {} + for key, value in map_u.items(): + res[key.encode('utf-8')] = value.encode('utf-8') + return res + +def decode_your_map(map_b): + """ + Decodes utf-8 byte strings in `map` from C++ functions to Python unicode strings. + + :param map_b: The map to decode + :type map_b: dict{'b'string : 'b'string} + :return: The decoded map + :rtype: dict{string : string} + + .. note:: This function is used for type connection. + + """ + res = {} + for key, value in map_b.items(): + res[key.decode('utf-8')] = value.decode('utf-8') + return res + +def decode_graph_edges(map_edge_b): + """ + Decode utf-8 byte strings in graph edges `map` from C++ functions to Python unicode strings. + + Parameters + ---------- + map_edge_b : dict{tuple(size_t, size_t) : dict{'b'string : 'b'string}} + The map to decode. + + Returns + ------- + dict{tuple(size_t, size_t) : dict{string : string}} + The decoded map. + + Notes + ----- + This is a helper function for function `GEDEnv.get_graph_edges()`. + """ + map_edges = {} + for key, value in map_edge_b.items(): + map_edges[key] = decode_your_map(value) + return map_edges diff --git a/gklearn/gedlib/src/GedLibBind_bk.hpp b/gklearn/gedlib/src/GedLibBind_bk.hpp new file mode 100644 index 0000000000..b1fe4c729e --- /dev/null +++ b/gklearn/gedlib/src/GedLibBind_bk.hpp @@ -0,0 +1,658 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2020 by Natacha Lambert, David B. Blumenthal and * + * Linlin Jia * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file GedLibBind.hpp + * @brief Classe and function declarations to call easly GebLib in Python without Gedlib's types + */ +#ifndef GEDLIBBIND_HPP +#define GEDLIBBIND_HPP + +//Include standard libraries. +#include +#include +#include +#include +#include +#include +#include +#include +#include "../include/gedlib-master/src/env/ged_env.hpp" +#include "../include/gedlib-master/src/env/node_map.hpp" +#include "../include/gedlib-master/src/env/common_types.hpp" + + +/*! + * @namespace pyged + * @brief Global namespace for gedlibpy. + */ +namespace pyged { + +/*! +* @brief Get list of available edit cost functions readable by Python. +*/ +std::vector getEditCostStringOptions(); + +/*! +* @brief Get list of available computation methods readable by Python. +*/ +std::vector getMethodStringOptions(); + +/*! +* @brief Get list of available initilaization options readable by Python. +*/ +std::vector getInitStringOptions(); + +/*! +* @brief Returns a dummy node. +* @return ID of dummy node. +*/ +static std::size_t getDummyNode(); + + +/*! +* @brief Provides the API of GEDLIB for Python. +*/ +class PyGEDEnv { + + +public: + + enum LabelType { GXLLabel, AttrLabel }; + + +private: + + // Define the environment variable as a variant type that can hold either GEDEnvGXL or GEDEnvAttr + + using GEDEnvGXL = ged::GEDEnv; + + using GEDEnvAttr = ged::GEDEnv; + + using EnvVariant = std::variant; + +// ged::GEDEnv * env_; // environment variable + + LabelType type_; // type of labels used in the environment, also indicates the env type + + EnvVariant env_; // environment variable, + // either GEDEnvGXL or GEDEnvAttr depending on the label type + + bool initialized; // initialization boolean (because env has one but not accessible) + + +public: + + /*! + * @brief Constructor. + */ + PyGEDEnv(LabelType type = AttrLabel); + + // PyGEDEnv(); + + /*! + * @brief Destructor. + */ + ~PyGEDEnv(); + + // ======== The Scope Guard API ======== + + /*! + * @brief Calls the function with the environment pointer. + * @param func The function to call with the environment pointer. + * @return The result of the function call. + * @details Since there are multiple environment types, this function uses std::visit + * to call the function on the correct environment type. Otherwise, the function would + * not know which environment type to call. + */ + template decltype(auto) withEnv(Func&& func); + + // ======== Environment Public APIs ======== + + /*! + * @brief Get the label type of the environment. + * @return The label type of the environment. + * @details The label type can be one of the following: + * - @p GXLLabel: for string labels. + * - @p AttrLabel: for complex attributed labels. + */ + LabelType getLabelType() const; + + /*! + * @brief Tests if the environment is initialized or not. + * @return Boolean @p true if the environment is initialized and @p false otherwise. + */ + bool isInitialized(); + + /*! + * @brief Restart the environment (recall a new empty environment). + */ + void restartEnv(); + + /*! + * @brief Loads graph given in the [GXL file format](http://www.gupro.de/GXL/). + * @param[in] pathFolder The path to the directory containing the graphs. + * @param[in] pathXML The path to a XML file thats lists the graphs contained in @p pathFolder that should be loaded. + * @param[in] node_type Select if nodes are labeled or unlabeled. + * @param[in] edge_type Select if edges are labeled or unlabeled. + */ + void loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type); + + /*! + * @brief Provides access to the IDs of the graphs contained in the environment. + * @return Pair (ID of first graphs, ID of last graph + 1) of graph IDs. + * If both entries equal 0, the environment does not contain any graphs. + */ + std::pair getGraphIds() const; + + /*! + * @brief Returns the list of graphs IDs which are loaded in the environment. + * @return A vector which contains all the graphs Ids. + */ + std::vector getAllGraphIds(); + + /*! + * @brief Returns the graph class. + * @param[in] id ID of an input graph that has been added to the environment. + * @return Class of the input graph. + */ + const std::string getGraphClass(std::size_t id) const; + + /*! + * @brief Returns the graph name. + * @param[in] id ID of an input graph that has been added to the environment. + * @return Name of the input graph. + */ + const std::string getGraphName(std::size_t id) const; + + /*! + * @brief Adds a new uninitialized graph to the environment. Call initEnv() after calling this method. + * @param[in] name The name of the added graph. Empty if not specified. + * @param[in] class The class of the added graph. Empty if not specified. + * @return The ID of the newly added graph. + */ + std::size_t addGraph(const std::string & graph_name, const std::string & graph_class); + + /*! + * @brief Adds a labeled node. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] nodeId The user-specific ID of the vertex that has to be added. + * @param[in] nodeLabel The label of the vertex that has to be added. Only supports string labels. + */ + void addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel); + + /*! + * @brief Adds a labeled node. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] nodeId The user-specific ID of the vertex that has to be added. + * @param[in] str_map The string attributes of the node. + * @param[in] int_map The integer attributes of the node. + * @param[in] float_map The float attributes of the node. + * @param[in] list_int_map The list of integer attributes of the node. + * @param[in] list_float_map The list of float attributes of the node. + * @param[in] list_str_map The list of string attributes of the node. + */ + void addNode( + std::size_t graphId, + const std::string& nodeId, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map + ); + + /*! + * @brief Adds a labeled edge. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] tail The user-specific ID of the tail of the edge that has to be added. + * @param[in] head The user-specific ID of the head of the edge that has to be added. + * @param[in] edgeLabel The label of the vertex that has to be added. Only supports string labels. + * @param[in] ignoreDuplicates If @p true, duplicate edges are ignores. Otherwise, an exception is thrown if an existing edge is added to the graph. + */ + void addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates = true); + + /*! + * @brief Adds a labeled edge. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] tail The user-specific ID of the tail of the edge that has to be added. + * @param[in] head The user-specific ID of the head of the edge that has to be added. + * @param[in] str_map The string attributes of the edge. + * @param[in] int_map The integer attributes of the edge. + * @param[in] float_map The float attributes of the edge. + * @param[in] list_int_map The list of integer attributes of the edge. + * @param[in] list_float_map The list of float attributes of the edge. + * @param[in] list_str_map The list of string attributes of the edge. + * @param[in] ignoreDuplicates If @p true, duplicate edges are ignores. Otherwise, an exception is thrown if an existing edge is added to the graph. + */ + void addEdge( + std::size_t graphId, + const std::string& tail, + const std::string& head, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map, + bool ignoreDuplicates + ); + + /*! + * @brief Clears and de-initializes a graph that has previously been added to the environment. Call initEnv() after calling this method. + * @param[in] graphId ID of graph that has to be cleared. + */ + void clearGraph(std::size_t graphId); + + /*! + * @brief Returns ged::ExchangeGraph representation. + * @param graphId ID of the selected graph. + * @return ged::ExchangeGraph representation of the selected graph. + */ + ged::ExchangeGraph getGraph(std::size_t graphId) const; + + /*! + * @brief Returns the internal Id of a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The internal ID of the selected graph + */ + std::size_t getGraphInternalId(std::size_t graphId); + + /*! + * @brief Returns all the number of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The number of nodes on the selected graph + */ + std::size_t getGraphNumNodes(std::size_t graphId); + + /*! + * @brief Returns all the number of edges on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The number of edges on the selected graph + */ + std::size_t getGraphNumEdges(std::size_t graphId); + + /*! + * @brief Returns all th Ids of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of IDs's nodes on the selected graph + */ + std::vector getGraphOriginalNodeIds(std::size_t graphId); + + /*! + * @brief Returns all the labels of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of labels's nodes on the selected graph + */ + std::vector> getGraphNodeLabels(std::size_t graphId); + + /*! + * @brief Returns all the edges on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of edges on the selected graph + */ + std::map, std::map> getGraphEdges(std::size_t graphId); + + /*! + * @brief Returns the adjacence list of a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The adjacence list of the selected graph + */ + std::vector> getGraphAdjacenceMatrix(std::size_t graphId); + + /*! + * @brief Sets the edit costs to one of the predefined edit costs. + * @param[in] editCost Select one of the predefined edit costs. + * @param[in] editCostConstants Parameters for the edit cost, empty by default. + */ + void setEditCost(std::string editCost, std::vector editCostConstants = {}); + + /*! + * @brief Sets the edit costs to a personal Edit Cost Class. + * @param[in] editCostConstants Parameters for the edit cost, empty by default. + * @note You have to add your class, which should inherit from EditCost class, in the function. After that, you can compile and use it in Python + */ + void setPersonalEditCost(std::vector editCostConstants = {}); + + /*! + * @brief Initializes the environment. + * @param[in] initOption Select initialization options. + * @param[in] print_to_stdout If set to @p true, the progress of the initialization is printed to std::out. + */ + void initEnv(std::string initOption = "EAGER_WITH_SHUFFLED_COPIES", bool print_to_stdout = false); + + /*! + * @brief Sets the GEDMethod to be used by run_method(). + * @param[in] method Select the method that is to be used. + * @param[in] options An options string of the form @"[--@ @] [...]@" passed to the selected method. + */ + void setMethod(std::string method, const std::string & options); + + /*! + * @brief Initializes the method specified by call to set_method(). + */ + void initMethod(); + + /*! + * @brief Returns initialization time. + * @return Runtime of the last call to init_method(). + */ + double getInitime() const; + + /*! + * @brief Runs the GED method specified by call to set_method() between the graphs with IDs @p g and @p h. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + */ + void runMethod(std::size_t g, std::size_t h); + + /*! + * @brief Returns upper bound for edit distance between the input graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Upper bound computed by the last call to run_method() with arguments @p g and @p h. + */ + double getUpperBound(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns lower bound for edit distance between the input graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Lower bound computed by the last call to run_method() with arguments @p g and @p h. + */ + double getLowerBound(std::size_t g,std::size_t h) const; + + /*! + * @brief Returns the forward map between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The forward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector getForwardMap(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns the backward map between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The backward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector getBackwardMap(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns image of a node. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @param[in] nodeId Node whose image is to be returned. + * @return Node to which node @p node is assigned. + */ + std::size_t getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const; + + /*! + * @brief Returns pre-image of a node. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @param[in] nodeId Node whose pre-image is to be returned. + * @return Node to which node @p node is assigned. + */ + std::size_t getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const; + + /*! + * @brief Returns the induced cost between the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The induced cost between the two indicated graphs. + */ + double getInducedCost(std::size_t g, std::size_t h) const; + + + /*! + * @brief Returns node map between the input graphs. This function duplicates datas. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Node map computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getNodeMap(std::size_t g, std::size_t h); + + /*! + * @brief Returns assignment matrix between the input graphs. This function duplicates datas. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Assignment matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getAssignmentMatrix(std::size_t g, std::size_t h); + + /*! + * @brief Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The forward and backward maps to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getAllMap(std::size_t g, std::size_t h); + + /*! + * @brief Returns runtime. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Runtime of last call to run_method() with arguments @p g and @p h. + */ + double getRuntime(std::size_t g, std::size_t h) const; + + /*! + * @brief Checks if the edit costs are quasimetric. + * @return Boolean @p true if the edit costs are quasimetric and @p false, otherwise. + */ + bool quasimetricCosts() const; + + /*! + * @brief Applies the hungarian algorithm (LSAP) to a matrix cost. + * @param[in] matrixCost The matrix cost. + * @return the values of rho, varrho, u and v, in this order. + */ + std::vector> hungarianLSAP(std::vector> matrixCost); + + /*! + * @brief Applies the hungarian algorithm (LSAPE) to a matrix cost. + * @param[in] matrixCost The matrix cost. + * @return the values of rho, varrho, u and v, in this order. + */ + std::vector> hungarianLSAPE(std::vector> matrixCost); + + + /*! + * @brief Returns the number of graphs. + * @return Number of graphs contained in the environment. + */ + std::size_t getNumGraphs() const; + + /*! + * @brief Returns the number of node labels. + * @return Number of pairwise different node labels contained in the environment. + * @note If @p 1 is returned, the nodes are unlabeled. + */ + std::size_t getNumNodeLabels() const; + + /*! + * @brief Returns node label. + * @param[in] label_id ID of node label that should be returned. Must be between 1 and num_node_labels(). + * @return Node label for selected label ID. + */ + std::map getNodeLabel(std::size_t label_id) const; + + /*! + * @brief Returns the number of edge labels. + * @return Number of pairwise different edge labels contained in the environment. + * @note If @p 1 is returned, the edges are unlabeled. + */ + std::size_t getNumEdgeLabels() const; + + /*! + * @brief Returns edge label. + * @param[in] label_id ID of edge label that should be returned. Must be between 1 and num_node_labels(). + * @return Edge label for selected label ID. + */ + std::map getEdgeLabel(std::size_t label_id) const; + + // /*! + // * @brief Returns the number of nodes. + // * @param[in] graph_id ID of an input graph that has been added to the environment. + // * @return Number of nodes in the graph. + // */ + // std::size_t getNumNodes(std::size_t graph_id) const; + + /*! + * @brief Returns average number of nodes. + * @return Average number of nodes of the graphs contained in the environment. + */ + double getAvgNumNodes() const; + + /*! + * @brief Returns node relabeling cost. + * @param[in] node_label_1 First node label. + * @param[in] node_label_2 Second node label. + * @return Node relabeling cost for the given node labels. + */ + double getNodeRelCost(const std::map & node_label_1, const std::map & node_label_2) const; + + /*! + * @brief Returns node deletion cost. + * @param[in] node_label Node label. + * @return Cost of deleting node with given label. + */ + double getNodeDelCost(const std::map & node_label) const; + + /*! + * @brief Returns node insertion cost. + * @param[in] node_label Node label. + * @return Cost of inserting node with given label. + */ + double getNodeInsCost(const std::map & node_label) const; + + /*! + * @brief Computes median node label. + * @param[in] node_labels The node labels whose median should be computed. + * @return Median of the given node labels. + */ + std::map getMedianNodeLabel(const std::vector> & node_labels) const; + + /*! + * @brief Returns edge relabeling cost. + * @param[in] edge_label_1 First edge label. + * @param[in] edge_label_2 Second edge label. + * @return Edge relabeling cost for the given edge labels. + */ + double getEdgeRelCost(const std::map & edge_label_1, const std::map & edge_label_2) const; + + /*! + * @brief Returns edge deletion cost. + * @param[in] edge_label Edge label. + * @return Cost of deleting edge with given label. + */ + double getEdgeDelCost(const std::map & edge_label) const; + + /*! + * @brief Returns edge insertion cost. + * @param[in] edge_label Edge label. + * @return Cost of inserting edge with given label. + */ + double getEdgeInsCost(const std::map & edge_label) const; + + /*! + * @brief Computes median edge label. + * @param[in] edge_labels The edge labels whose median should be computed. + * @return Median of the given edge labels. + */ + std::map getMedianEdgeLabel(const std::vector> & edge_labels) const; + + /*! + * @brief Returns the initialization type of the last initialization. + * @return Initialization type in string. + */ + std::string getInitType() const; + + /*! + * @brief Computes the edit cost between two graphs induced by a node map. + * @param[in] g_id ID of input graph. + * @param[in] h_id ID of input graph. + * @return Computed induced cost. + */ + double computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const; + + // /*! + // * @brief Returns node relabeling, insertion, or deletion cost. + // * @param[in] label1 First node label. + // * @param[in] label2 Second node label. + // * @return Node relabeling cost if @p label1 and @p label2 are both different from ged::dummy_label(), + // * node insertion cost if @p label1 equals ged::dummy_label and @p label2 does not, + // * node deletion cost if @p label1 does not equal ged::dummy_label and @p label2 does, + // * and 0 otherwise. + // */ + // double getNodeCost(std::size_t label1, std::size_t label2) const; + + + /*! + * @brief Constructs a ged::AttrLabel from the given attribute maps. + * @param str_map The string attributes of the node/edge. + * @param int_map The integer attributes of the node/edge. + * @param float_map The float attributes of the node/edge. + * @param list_str_map The list of string attributes of the node/edge. + * @param list_int_map The list of integer attributes of the node/edge. + * @param list_float_map The list of float attributes of the node/edge. + * @return A ged::AttrLabel constructed from the given maps. + */ + static ged::AttrLabel constructAttrLabelFromMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map + ); + + +private: + + // Helper methods for type casting + template + ged::GEDEnv* get_typed_env(); + + +}; + +} + +#include "GedLibBind.ipp" + +#endif /* SRC_GEDLIB_BIND_HPP */ + + + + + + + + + + + +// namespace shapes { +// class Rectangle { +// public: +// int x0, y0, x1, y1; +// Rectangle(); +// Rectangle(int x0, int y0, int x1, int y1); +// ~Rectangle(); +// int getArea(); +// void getSize(int* width, int* height); +// void move(int dx, int dy); +// }; +// } \ No newline at end of file diff --git a/gklearn/gedlib/src/GedLibBind_bk.ipp b/gklearn/gedlib/src/GedLibBind_bk.ipp new file mode 100644 index 0000000000..0e0d38564f --- /dev/null +++ b/gklearn/gedlib/src/GedLibBind_bk.ipp @@ -0,0 +1,919 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2020 by Natacha Lambert, David B. Blumenthal and * + * Linlin Jia * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file GedLibBind.ipp + * @brief Classe and function definitions to call easly GebLib in Python without Gedlib's types + */ +#ifndef GEDLIBBIND_IPP +#define GEDLIBBIND_IPP + +//Include standard libraries + GedLib library +// #include +// #include "GedLibBind.h" +// #include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/median/src/median_graph_estimator.hpp" + +using namespace std; + +//Definition of types and templates used in this code for my human's memory :). +//ged::GEDEnv env; +//template struct ExchangeGraph + +//typedef std::map GXLLabel; +//typedef std::string GXLNodeID; + + +namespace pyged { + +//!< List of available edit cost functions readable by Python. +std::vector editCostStringOptions = { + "CHEM_1", + "CHEM_2", + "CMU", + "GREC_1", + "GREC_2", + "LETTER", + "LETTER2", + "NON_SYMBOLIC", + "GEOMETRIC", + "FINGERPRINT", + "PROTEIN", + "CONSTANT" +}; + +//!< Map of available edit cost functions between enum type in C++ and string in Python +std::map editCostOptions = { + {"CHEM_1", ged::Options::EditCosts::CHEM_1}, + {"CHEM_2", ged::Options::EditCosts::CHEM_2}, + {"CMU", ged::Options::EditCosts::CMU}, + {"GREC_1", ged::Options::EditCosts::GREC_1}, + {"GREC_2", ged::Options::EditCosts::GREC_2}, + {"LETTER", ged::Options::EditCosts::LETTER}, + {"LETTER2", ged::Options::EditCosts::LETTER2}, + {"NON_SYMBOLIC", ged::Options::EditCosts::NON_SYMBOLIC}, + {"GEOMETRIC", ged::Options::EditCosts::GEOMETRIC}, + {"FINGERPRINT", ged::Options::EditCosts::FINGERPRINT}, + {"PROTEIN", ged::Options::EditCosts::PROTEIN}, + {"CONSTANT", ged::Options::EditCosts::CONSTANT} +}; + + //!< List of available computation methods readable by Python. +std::vector methodStringOptions = { + "BRANCH", + "BRANCH_FAST", + "BRANCH_TIGHT", + "BRANCH_UNIFORM", + "BRANCH_COMPACT", + "PARTITION", + "HYBRID", + "RING", + "ANCHOR_AWARE_GED", + "WALKS", + "IPFP", + "BIPARTITE", + "SUBGRAPH", + "NODE", + "RING_ML", + "BIPARTITE_ML", + "REFINE", + "BP_BEAM", + "SIMULATED_ANNEALING", + "HED", + "STAR" +}; + +//!< Map of available computation methods readables between enum type in C++ and string in Python +std::map methodOptions = { + {"BRANCH", ged::Options::GEDMethod::BRANCH}, + {"BRANCH_FAST", ged::Options::GEDMethod::BRANCH_FAST}, + {"BRANCH_TIGHT", ged::Options::GEDMethod::BRANCH_TIGHT}, + {"BRANCH_UNIFORM", ged::Options::GEDMethod::BRANCH_UNIFORM}, + {"BRANCH_COMPACT", ged::Options::GEDMethod::BRANCH_COMPACT}, + {"PARTITION", ged::Options::GEDMethod::PARTITION}, + {"HYBRID", ged::Options::GEDMethod::HYBRID}, + {"RING", ged::Options::GEDMethod::RING}, + {"ANCHOR_AWARE_GED", ged::Options::GEDMethod::ANCHOR_AWARE_GED}, + {"WALKS", ged::Options::GEDMethod::WALKS}, + {"IPFP", ged::Options::GEDMethod::IPFP}, + {"BIPARTITE", ged::Options::GEDMethod::BIPARTITE}, + {"SUBGRAPH", ged::Options::GEDMethod::SUBGRAPH}, + {"NODE", ged::Options::GEDMethod::NODE}, + {"RING_ML", ged::Options::GEDMethod::RING_ML}, + {"BIPARTITE_ML",ged::Options::GEDMethod::BIPARTITE_ML}, + {"REFINE",ged::Options::GEDMethod::REFINE}, + {"BP_BEAM", ged::Options::GEDMethod::BP_BEAM}, + {"SIMULATED_ANNEALING", ged::Options::GEDMethod::SIMULATED_ANNEALING}, + {"HED", ged::Options::GEDMethod::HED}, + {"STAR" , ged::Options::GEDMethod::STAR}, +}; + +//! initStringOptions = { + "LAZY_WITHOUT_SHUFFLED_COPIES", + "EAGER_WITHOUT_SHUFFLED_COPIES", + "LAZY_WITH_SHUFFLED_COPIES", + "EAGER_WITH_SHUFFLED_COPIES" +}; + +//!< Map of available initilaization options readables between enum type in C++ and string in Python +std::map initOptions = { + {"LAZY_WITHOUT_SHUFFLED_COPIES", ged::Options::InitType::LAZY_WITHOUT_SHUFFLED_COPIES}, + {"EAGER_WITHOUT_SHUFFLED_COPIES", ged::Options::InitType::EAGER_WITHOUT_SHUFFLED_COPIES}, + {"LAZY_WITH_SHUFFLED_COPIES", ged::Options::InitType::LAZY_WITH_SHUFFLED_COPIES}, + {"EAGER_WITH_SHUFFLED_COPIES", ged::Options::InitType::EAGER_WITH_SHUFFLED_COPIES} +}; + +std::vector getEditCostStringOptions() { + return editCostStringOptions; +} + +std::vector getMethodStringOptions() { + return methodStringOptions; +} + +std::vector getInitStringOptions() { + return initStringOptions; +} + +static std::size_t getDummyNode() { + return ged::GEDGraph::dummy_node(); +} + + +/*! + * @brief Returns the enum EditCost which correspond to the string parameter + * @param editCost Select one of the predefined edit costs in the list. + * @return The edit cost function which correspond in the edit cost functions map. + */ +ged::Options::EditCosts translateEditCost(std::string editCost) { + for (std::size_t i = 0; i != editCostStringOptions.size(); i++) { + if (editCostStringOptions[i] == editCost) { + return editCostOptions[editCostStringOptions[i]]; + } + } + return ged::Options::EditCosts::CONSTANT; +} + +/*! + * @brief Returns the enum IniType which correspond to the string parameter + * @param initOption Select initialization options. + * @return The init Type which correspond in the init options map. + */ +ged::Options::InitType translateInitOptions(std::string initOption) { + for (std::size_t i = 0; i != initStringOptions.size(); i++) { + if (initStringOptions[i] == initOption) { + return initOptions[initStringOptions[i]]; + } + } + return ged::Options::InitType::EAGER_WITHOUT_SHUFFLED_COPIES; +} + +/*! + * @brief Returns the string correspond to the enum IniType. + * @param initOption Select initialization options. + * @return The string which correspond to the enum IniType @p initOption. + */ + std::string initOptionsToString(ged::Options::InitType initOption) { + for (std::size_t i = 0; i != initOptions.size(); i++) { + if (initOptions[initStringOptions[i]] == initOption) { + return initStringOptions[i]; + } + } + return "EAGER_WITHOUT_SHUFFLED_COPIES"; +} + +/*! + * @brief Returns the enum Method which correspond to the string parameter + * @param method Select the method that is to be used. + * @return The computation method which correspond in the edit cost functions map. + */ +ged::Options::GEDMethod translateMethod(std::string method) { + for (std::size_t i = 0; i != methodStringOptions.size(); i++) { + if (methodStringOptions[i] == method) { + return methodOptions[methodStringOptions[i]]; + } + } + return ged::Options::GEDMethod::STAR; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(std::size_t* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back(pointer[i]); + } + return res; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The double pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(double* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back(pointer[i]); + } + return res; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values, with double type. + */ +std::vector translateAndConvertPointer(std::size_t* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back((double)pointer[i]); + } + return res; +} + +/*! + * @brief Returns the string which contains all element of a int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector) { + std::string res = ""; + + for (std::size_t i = 0; i != vector.size(); i++) + { + res += std::to_string(vector[i]) + " "; + } + + return res; +} + +/*! + * @brief Returns the string which contains all element of a unsigned long int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector) { + std::string res = ""; + + for (std::size_t i = 0; i != vector.size(); i++) + { + res += std::to_string(vector[i]) + " "; + } + + return res; +} + + +PyGEDEnv::PyGEDEnv (LabelType type) : type_(type), env_(nullptr), initialized(false) { + // Initialize the environment with the specified label type: + switch (type) { + case GXLLabel: + env_ = new GEDEnvGXL(); + break; + case AttrLabel: + env_ = new GEDEnvAttr(); + break; + default: + throw std::invalid_argument("Unsupported label type"); + } +// env_ = new ged::GEDEnv(); +// this->initialized = false; +} + +PyGEDEnv::~PyGEDEnv () { + if (env_ != nullptr) { + std::visit([](auto* env_ptr) { + delete env_ptr; + }, env_); + env_ = nullptr; + } +} + +// ======== The Scope Guard API ======== + +template +decltype(auto) +PyGEDEnv::withEnv(Func&& func) { + // Use std::visit to call the function on the correct environment type: + return std::visit([&func](auto* env_ptr) -> decltype(auto) { + // Get the reference to the environment pointer: + auto& env = *env_ptr; + // Call the function with the environment pointer and return its result: + return std::forward(func)(env); + }, env_); +} + +// ======== Environment Public APIs ======== + +PyGEDEnv::LabelType PyGEDEnv::getLabelType() const { + return type_; +} + +// bool initialized = false; //Initialization boolean (because Env has one but not accessible). + +bool PyGEDEnv::isInitialized() { + return initialized; +} + +void PyGEDEnv::restartEnv() { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } + switch (type_) { + case GXLLabel: + env_ = new ged::GEDEnvGXL(); + break; + case AttrLabel: + env_ = new ged::GEDEnvAttr(); + break; + default: + throw std::invalid_argument("Unsupported label type"); + } + initialized = false; +} + +void PyGEDEnv::loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type) { + return withEnv([&](auto& env_) { + std::vector tmp_graph_ids(env_.load_gxl_graph(pathFolder, pathXML, + (node_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), + (edge_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), + std::unordered_set(), std::unordered_set())); + } + +} + +std::pair PyGEDEnv::getGraphIds() const { + return env_->graph_ids(); +} + +std::vector PyGEDEnv::getAllGraphIds() { + std::vector listID; + for (std::size_t i = env_->graph_ids().first; i != env_->graph_ids().second; i++) { + listID.push_back(i); + } + return listID; +} + +const std::string PyGEDEnv::getGraphClass(std::size_t id) const { + return env_->get_graph_class(id); +} + +const std::string PyGEDEnv::getGraphName(std::size_t id) const { + return env_->get_graph_name(id); +} + +std::size_t PyGEDEnv::addGraph(const std::string & graph_name, const std::string & graph_class) { + ged::GEDGraph::GraphID newId = env_->add_graph(graph_name, graph_class); + initialized = false; + return std::stoi(std::to_string(newId)); +} + +void PyGEDEnv::addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel) { + env_->add_node(graphId, nodeId, nodeLabel); + initialized = false; +} + +void PyGEDEnv::addNode( + std::size_t graphId, + const std::string& nodeId, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { + // fixme: debug test only: + std::cout << "The node labels received by the c++ bindings are: " << std::endl; + printLabelMaps(str_map, int_map, float_map, list_str_map, list_int_map, list_float_map); + + // Merge the maps into AttrLabel: + ged::AttrLabel nodeLabel = PyGEDEnv::constructAttrLabelFromMaps( + str_map, + int_map, + float_map, + list_str_map, + list_int_map, + list_float_map + ); + + std::cout << "The node label passed to c++ env is: " << nodeLabel << std::endl; + + env_->add_node(graphId, nodeId, nodeLabel); + initialized = false; +} + +/*void addEdge(std::size_t graphId, ged::GXLNodeID tail, ged::GXLNodeID head, ged::GXLLabel edgeLabel) { + env_->add_edge(graphId, tail, head, edgeLabel); +}*/ + +void PyGEDEnv::addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates) { + env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); + initialized = false; +} + +void PyGEDEnv::addEdge( + std::size_t graphId, + const std::string& tail, + const std::string& head, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map, + bool ignoreDuplicates +) { + // fixme: debug test only: + std::cout << "The edge labels received by the c++ bindings are: " << std::endl; + printLabelMaps(str_map, int_map, float_map, list_str_map, list_int_map, list_float_map); + + // Merge the maps into AttrLabel: + ged::AttrLabel edgeLabel = PyGEDEnv::constructAttrLabelFromMaps( + str_map, + int_map, + float_map, + list_str_map, + list_int_map, + list_float_map + ); + + std::cout << "The edge label passed to c++ env is: " << edgeLabel << std::endl; + + env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); + initialized = false; +} + +void PyGEDEnv::clearGraph(std::size_t graphId) { + env_->clear_graph(graphId); + initialized = false; +} + +ged::ExchangeGraph PyGEDEnv::getGraph(std::size_t graphId) const { + return env_->get_graph(graphId); +} + +std::size_t PyGEDEnv::getGraphInternalId(std::size_t graphId) { + return getGraph(graphId).id; +} + +std::size_t PyGEDEnv::getGraphNumNodes(std::size_t graphId) { + return getGraph(graphId).num_nodes; +} + +std::size_t PyGEDEnv::getGraphNumEdges(std::size_t graphId) { + return getGraph(graphId).num_edges; +} + +std::vector PyGEDEnv::getGraphOriginalNodeIds(std::size_t graphId) { + return getGraph(graphId).original_node_ids; +} + +std::vector> PyGEDEnv::getGraphNodeLabels(std::size_t graphId) { + return getGraph(graphId).node_labels; +} + +std::map, std::map> PyGEDEnv::getGraphEdges(std::size_t graphId) { + return getGraph(graphId).edge_labels; +} + +std::vector> PyGEDEnv::getGraphAdjacenceMatrix(std::size_t graphId) { + return getGraph(graphId).adj_matrix; +} + +void PyGEDEnv::setEditCost(std::string editCost, std::vector editCostConstants) { + env_->set_edit_costs(translateEditCost(editCost), editCostConstants); +} + +void PyGEDEnv::setPersonalEditCost(std::vector editCostConstants) { + //env_->set_edit_costs(Your EditCost Class(editCostConstants)); +} + +// void PyGEDEnv::initEnv() { +// env_->init(); +// initialized = true; +// } + +void PyGEDEnv::initEnv(std::string initOption, bool print_to_stdout) { + env_->init(translateInitOptions(initOption), print_to_stdout); + initialized = true; +} + +void PyGEDEnv::setMethod(std::string method, const std::string & options) { + env_->set_method(translateMethod(method), options); +} + +void PyGEDEnv::initMethod() { + env_->init_method(); +} + +double PyGEDEnv::getInitime() const { + return env_->get_init_time(); +} + +void PyGEDEnv::runMethod(std::size_t g, std::size_t h) { + env_->run_method(g, h); +} + +double PyGEDEnv::getUpperBound(std::size_t g, std::size_t h) const { + return env_->get_upper_bound(g, h); +} + +double PyGEDEnv::getLowerBound(std::size_t g, std::size_t h) const { + return env_->get_lower_bound(g, h); +} + +std::vector PyGEDEnv::getForwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_forward_map(); +} + +std::vector PyGEDEnv::getBackwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_backward_map(); +} + +std::size_t PyGEDEnv::getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).image(nodeId); +} + +std::size_t PyGEDEnv::getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).pre_image(nodeId); +} + +double PyGEDEnv::getInducedCost(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).induced_cost(); +} + +std::vector> PyGEDEnv::getNodeMap(std::size_t g, std::size_t h) { + std::vector> res; + std::vector relation; + env_->get_node_map(g, h).as_relation(relation); + for (const auto & assignment : relation) { + res.push_back(std::make_pair(assignment.first, assignment.second)); + } + return res; +} + +std::vector> PyGEDEnv::getAssignmentMatrix(std::size_t g, std::size_t h) { + std::vector> res; + for(std::size_t i = 0; i != getForwardMap(g, h).size(); i++) { + std::vector newLine; + bool have1 = false; + for(std::size_t j = 0; j != getBackwardMap(g, h).size(); j++) { + if (getNodeImage(g, h, i) == j) { + newLine.push_back(1); + have1 = true; + } + else{ + newLine.push_back(0); + } + } + if(have1) { + newLine.push_back(0); + } + else{ + newLine.push_back(1); + } + res.push_back(newLine); + } + std::vector lastLine; + for (size_t k = 0; k != getBackwardMap(g,h).size(); k++) { + if (getBackwardMap(g,h)[k] == ged::GEDGraph::dummy_node()) { + lastLine.push_back(1); + } + else{ + lastLine.push_back(0); + } + } + res.push_back(lastLine); + return res; +} + +std::vector> PyGEDEnv::getAllMap(std::size_t g, std::size_t h) { + std::vector> res; + res.push_back(getForwardMap(g, h)); + res.push_back(getBackwardMap(g,h)); + return res; +} + +double PyGEDEnv::getRuntime(std::size_t g, std::size_t h) const { + return env_->get_runtime(g, h); +} + +bool PyGEDEnv::quasimetricCosts() const { + return env_->quasimetric_costs(); +} + +std::vector> PyGEDEnv::hungarianLSAP(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows], *varrho = new std::size_t[ncols]; + std::size_t *u = new std::size_t[nrows], *v = new std::size_t[ncols]; + std::size_t *C = new std::size_t[nrows*ncols]; + // std::size_t i = 0, j; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAP(C,nrows,ncols,rho,u,v,varrho); + std::vector> res; + res.push_back(translatePointer(rho, nrows)); + res.push_back(translatePointer(varrho, ncols)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::vector> PyGEDEnv::hungarianLSAPE(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows-1], *varrho = new std::size_t[ncols-1]; + double *u = new double[nrows], *v = new double[ncols]; + double *C = new double[nrows*ncols]; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAPE(C,nrows,ncols,rho,varrho,u,v); + std::vector> res; + res.push_back(translateAndConvertPointer(rho, nrows-1)); + res.push_back(translateAndConvertPointer(varrho, ncols-1)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::size_t PyGEDEnv::getNumGraphs() const { + return env_->num_graphs(); +} + +std::size_t PyGEDEnv::getNumNodeLabels() const { + return env_->num_node_labels(); +} + +std::map PyGEDEnv::getNodeLabel(std::size_t label_id) const { + return env_->get_node_label(label_id); +} + +std::size_t PyGEDEnv::getNumEdgeLabels() const { + return env_->num_edge_labels(); +} + +std::map PyGEDEnv::getEdgeLabel(std::size_t label_id) const { + return env_->get_edge_label(label_id); +} + +// std::size_t PyGEDEnv::getNumNodes(std::size_t graph_id) const { +// return env_->get_num_nodes(graph_id); +// } + +double PyGEDEnv::getAvgNumNodes() const { + return env_->get_avg_num_nodes(); +} + +double PyGEDEnv::getNodeRelCost(const std::map & node_label_1, const std::map & node_label_2) const { + return env_->node_rel_cost(node_label_1, node_label_2); +} + +double PyGEDEnv::getNodeDelCost(const std::map & node_label) const { + return env_->node_del_cost(node_label); +} + +double PyGEDEnv::getNodeInsCost(const std::map & node_label) const { + return env_->node_ins_cost(node_label); +} + +std::map PyGEDEnv::getMedianNodeLabel(const std::vector> & node_labels) const { + return env_->median_node_label(node_labels); +} + +double PyGEDEnv::getEdgeRelCost(const std::map & edge_label_1, const std::map & edge_label_2) const { + return env_->edge_rel_cost(edge_label_1, edge_label_2); +} + +double PyGEDEnv::getEdgeDelCost(const std::map & edge_label) const { + return env_->edge_del_cost(edge_label); +} + +double PyGEDEnv::getEdgeInsCost(const std::map & edge_label) const { + return env_->edge_ins_cost(edge_label); +} + +std::map PyGEDEnv::getMedianEdgeLabel(const std::vector> & edge_labels) const { + return env_->median_edge_label(edge_labels); +} + +std::string PyGEDEnv::getInitType() const { + return initOptionsToString(env_->get_init_type()); +} + +double PyGEDEnv::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const { + ged::NodeMap node_map = ged::NodeMap(env_->get_num_nodes(g_id), env_->get_num_nodes(h_id)); + for (const auto & assignment : relation) { + node_map.add_assignment(assignment.first, assignment.second); + // std::cout << assignment.first << assignment.second << endl; + } + const std::vector forward_map = node_map.get_forward_map(); + for (std::size_t i{0}; i < node_map.num_source_nodes(); i++) { + if (forward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(i, ged::GEDGraph::dummy_node()); + } + } + const std::vector backward_map = node_map.get_backward_map(); + for (std::size_t i{0}; i < node_map.num_target_nodes(); i++) { + if (backward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(ged::GEDGraph::dummy_node(), i); + } + } + // for (auto & map : node_map.get_forward_map()) { + // std::cout << map << ", "; + // } + // std::cout << endl; + // for (auto & map : node_map.get_backward_map()) { + // std::cout << map << ", "; + // } + env_->compute_induced_cost(g_id, h_id, node_map); + return node_map.induced_cost(); +} + + +ged::AttrLabel +PyGEDEnv::constructAttrLabelFromMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { + // using ged::AttrLabel = std::unordered_map, std::vector, std::vector>>; + ged::AttrLabel attr_label; + for (const auto& pair : str_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : int_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : float_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_str_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_int_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_float_map) { + attr_label[pair.first] = pair.second; + } + return attr_label; +} + + +static void printLabelMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { + // Print the label maps for debugging purposes + std::cout << "String map: "; + for (const auto& pair : str_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nInt map: "; + for (const auto& pair : int_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nFloat map: "; + for (const auto& pair : float_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nList of strings map: "; + for (const auto& pair : list_str_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + std::cout << "\nList of ints map: "; + for (const auto& pair : list_int_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + std::cout << "\nList of floats map: "; + for (const auto& pair : list_float_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + + std::cout << std::endl; + +} + + + +// double PyGEDEnv::getNodeCost(std::size_t label1, std::size_t label2) const { +// return env_->ged_data_node_cost(label1, label2); +// } + + +/*void medianLetter(pathFolder, pathXML, editCost, method, options="", initOption = "EAGER_WITHOUT_SHUFFLED_COPIES") { + + if(isInitialized()) { + restartEnv(); + } + setEditCost(editCost);*/ + + /*std::string letter_class("A"); + if (argc > 1) { + letter_class = std::string(argv[1]); + }*/ + //std::string seed("0"); + /*if (argc > 2) { + seed = std::string(argv[2]); + }*/ + + /*loadGXLGraph(pathFolder, pathXML); + std::vector graph_ids = getAllGraphIds(); + std::size_t median_id = env_->add_graph("median", ""); + + initEnv(initOption); + + setMethod(method); + + ged::MedianGraphEstimator median_estimator(&env, false); + median_estimator.set_options("--init-type RANDOM --randomness PSEUDO --seed " + seed); + median_estimator.run(graph_ids, median_id); + std::string gxl_file_name("../output/gen_median_Letter_HIGH_" + letter_class + ".gxl"); + env_->save_as_gxl_graph(median_id, gxl_file_name);*/ + + /*std::string tikz_file_name("../output/gen_median_Letter_HIGH_" + letter_class + ".tex"); + save_letter_graph_as_tikz_file(env_->get_graph(median_id), tikz_file_name);*/ +//} + +} + +#endif /* SRC_GEDLIB_BIND_IPP */ + +// namespace shapes { + +// // Default constructor +// Rectangle::Rectangle () {} + +// // Overloaded constructor +// Rectangle::Rectangle (int x0, int y0, int x1, int y1) { +// this->x0 = x0; +// this->y0 = y0; +// this->x1 = x1; +// this->y1 = y1; +// } + +// // Destructor +// Rectangle::~Rectangle () {} + +// // Return the area of the rectangle +// int Rectangle::getArea () { +// return (this->x1 - this->x0) * (this->y1 - this->y0); +// } + +// // Get the size of the rectangle. +// // Put the size in the pointer args +// void Rectangle::getSize (int *width, int *height) { +// (*width) = x1 - x0; +// (*height) = y1 - y0; +// } + +// // Move the rectangle by dx dy +// void Rectangle::move (int dx, int dy) { +// this->x0 += dx; +// this->y0 += dy; +// this->x1 += dx; +// this->y1 += dy; +// } +// } \ No newline at end of file diff --git a/gklearn/gedlib/src/gedlib_bind_attr.hpp b/gklearn/gedlib/src/gedlib_bind_attr.hpp new file mode 100644 index 0000000000..e849478523 --- /dev/null +++ b/gklearn/gedlib/src/gedlib_bind_attr.hpp @@ -0,0 +1,611 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_attr.hpp + * @brief Class and function declarations to call easily GebLib in Python without Gedlib's types + * with the AttrLabel (complex labels). + * @todo: it is better to refactor it along with GedLibBindGXL.hpp or use pybind11. + */ +#pragma once +//#ifndef SRC_GEDLIB_BIND_ATTR_HPP +//#define SRC_GEDLIB_BIND_ATTR_HPP + +//Include standard libraries. +#include +#include +#include +#include +#include +#include +#include "../include/gedlib-master/src/env/ged_env.hpp" +#include "../include/gedlib-master/src/env/node_map.hpp" +#include "../include/gedlib-master/src/env/common_types.hpp" + + +/*! + * @namespace pyged + * @brief Global namespace for gedlibpy. + */ +namespace pyged { + +/*! +* @brief Get list of available edit cost functions readable by Python. +*/ +std::vector getEditCostStringOptions(); + +/*! +* @brief Get list of available computation methods readable by Python. +*/ +std::vector getMethodStringOptions(); + +/*! +* @brief Get list of available initilaization options readable by Python. +*/ +std::vector getInitStringOptions(); + +/*! +* @brief Returns a dummy node. +* @return ID of dummy node. +*/ +static std::size_t getDummyNode(); + + +/*! +* @brief Provides the API of GEDLIB for Python for attr labels (complex labels). +*/ +class PyGEDEnvAttr { + +public: + + /*! + * @brief Constructor. + */ + PyGEDEnvAttr(); + + // PyGEDEnvAttr(); + + /*! + * @brief Destructor. + */ + ~PyGEDEnvAttr(); + + // ======== Environment Public APIs ======== + + /*! + * @brief Tests if the environment is initialized or not. + * @return Boolean @p true if the environment is initialized and @p false otherwise. + */ + bool isInitialized(); + + /*! + * @brief Restart the environment (recall a new empty environment). + */ + void restartEnv(); + +// /*! +// * @brief Loads graph given in the [GXL file format](http://www.gupro.de/GXL/). +// * @param[in] pathFolder The path to the directory containing the graphs. +// * @param[in] pathXML The path to a XML file thats lists the graphs contained in @p pathFolder that should be loaded. +// * @param[in] node_type Select if nodes are labeled or unlabeled. +// * @param[in] edge_type Select if edges are labeled or unlabeled. +// */ +// void loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type); + + /*! + * @brief Provides access to the IDs of the graphs contained in the environment. + * @return Pair (ID of first graphs, ID of last graph + 1) of graph IDs. + * If both entries equal 0, the environment does not contain any graphs. + */ + std::pair getGraphIds() const; + + /*! + * @brief Returns the list of graphs IDs which are loaded in the environment. + * @return A vector which contains all the graphs Ids. + */ + std::vector getAllGraphIds(); + + /*! + * @brief Returns the graph class. + * @param[in] id ID of an input graph that has been added to the environment. + * @return Class of the input graph. + */ + const std::string getGraphClass(std::size_t id) const; + + /*! + * @brief Returns the graph name. + * @param[in] id ID of an input graph that has been added to the environment. + * @return Name of the input graph. + */ + const std::string getGraphName(std::size_t id) const; + + /*! + * @brief Adds a new uninitialized graph to the environment. Call initEnv() after calling this method. + * @param[in] name The name of the added graph. Empty if not specified. + * @param[in] class The class of the added graph. Empty if not specified. + * @return The ID of the newly added graph. + */ + std::size_t addGraph(const std::string & graph_name, const std::string & graph_class); + +// /*! +// * @brief Adds a labeled node. +// * @param[in] graphId ID of graph that has been added to the environment. +// * @param[in] nodeId The user-specific ID of the vertex that has to be added. +// * @param[in] nodeLabel The label of the vertex that has to be added. Only supports string labels. +// */ +// void addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel); + + /*! + * @brief Adds a labeled node. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] nodeId The user-specific ID of the vertex that has to be added. + * @param[in] str_map The string attributes of the node. + * @param[in] int_map The integer attributes of the node. + * @param[in] float_map The float attributes of the node. + * @param[in] list_int_map The list of integer attributes of the node. + * @param[in] list_float_map The list of float attributes of the node. + * @param[in] list_str_map The list of string attributes of the node. + */ + void addNode( + std::size_t graphId, + const std::string& nodeId, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map + ); + +// /*! +// * @brief Adds a labeled edge. +// * @param[in] graphId ID of graph that has been added to the environment. +// * @param[in] tail The user-specific ID of the tail of the edge that has to be added. +// * @param[in] head The user-specific ID of the head of the edge that has to be added. +// * @param[in] edgeLabel The label of the vertex that has to be added. Only supports string labels. +// * @param[in] ignoreDuplicates If @p true, duplicate edges are ignores. Otherwise, an exception is thrown if an existing edge is added to the graph. +// */ +// void addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates = true); + + /*! + * @brief Adds a labeled edge. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] tail The user-specific ID of the tail of the edge that has to be added. + * @param[in] head The user-specific ID of the head of the edge that has to be added. + * @param[in] str_map The string attributes of the edge. + * @param[in] int_map The integer attributes of the edge. + * @param[in] float_map The float attributes of the edge. + * @param[in] list_int_map The list of integer attributes of the edge. + * @param[in] list_float_map The list of float attributes of the edge. + * @param[in] list_str_map The list of string attributes of the edge. + * @param[in] ignoreDuplicates If @p true, duplicate edges are ignores. Otherwise, an exception is thrown if an existing edge is added to the graph. + */ + void addEdge( + std::size_t graphId, + const std::string& tail, + const std::string& head, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map, + bool ignoreDuplicates + ); + + /*! + * @brief Clears and de-initializes a graph that has previously been added to the environment. Call initEnv() after calling this method. + * @param[in] graphId ID of graph that has to be cleared. + */ + void clearGraph(std::size_t graphId); + + /*! + * @brief Returns ged::ExchangeGraph representation. + * @param graphId ID of the selected graph. + * @return ged::ExchangeGraph representation of the selected graph. + */ + ged::ExchangeGraph getGraph(std::size_t graphId) const; + + /*! + * @brief Returns the internal Id of a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The internal ID of the selected graph + */ + std::size_t getGraphInternalId(std::size_t graphId); + + /*! + * @brief Returns all the number of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The number of nodes on the selected graph + */ + std::size_t getGraphNumNodes(std::size_t graphId); + + /*! + * @brief Returns all the number of edges on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The number of edges on the selected graph + */ + std::size_t getGraphNumEdges(std::size_t graphId); + + /*! + * @brief Returns all th Ids of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of IDs's nodes on the selected graph + */ + std::vector getGraphOriginalNodeIds(std::size_t graphId); + + /*! + * @brief Returns all the labels of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of labels's nodes on the selected graph + */ + std::vector getGraphNodeLabels(std::size_t graphId); + + /*! + * @brief Returns all the edges on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of edges on the selected graph + */ + std::map, ged::AttrLabel> getGraphEdges(std::size_t graphId); + + /*! + * @brief Returns the adjacence list of a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The adjacence list of the selected graph + */ + std::vector> getGraphAdjacenceMatrix(std::size_t graphId); + + /*! + * @brief Sets the edit costs to one of the predefined edit costs. + * @param[in] editCost Select one of the predefined edit costs. + * @param[in] editCostConstants Parameters for the edit cost, empty by default. + * @param[in] editCostConfig Configuration passed to the constructor of the edit cost class selected by @p editCost. If not specified, the default value is used. + * @note The edit cost configuration is a map of string keys and values that can be either strings or booleans. + * @todo: In GEDEnv this config can be a std::any, but in Python we use only std::string or bool to avoid issues with cython. Consider using pybind11 in the future. + */ + void setEditCost( + std::string editCost, std::vector editCostConstants = {}, + std::unordered_map editCostConfigStr = {}, + std::unordered_map editCostConfigBool = {} + ); + + /*! + * @brief Sets the edit costs to a personal Edit Cost Class. + * @param[in] editCostConstants Parameters for the edit cost, empty by default. + * @note You have to add your class, which should inherit from EditCost class, in the function. After that, you can compile and use it in Python + */ + void setPersonalEditCost(std::vector editCostConstants = {}); + + /*! + * @brief Initializes the environment. + * @param[in] initOption Select initialization options. + * @param[in] print_to_stdout If set to @p true, the progress of the initialization is printed to std::out. + */ + void initEnv(std::string initOption = "EAGER_WITH_SHUFFLED_COPIES", bool print_to_stdout = false); + + /*! + * @brief Sets the GEDMethod to be used by run_method(). + * @param[in] method Select the method that is to be used. + * @param[in] options An options string of the form @"[--@ @] [...]@" passed to the selected method. + */ + void setMethod(std::string method, const std::string & options); + + /*! + * @brief Initializes the method specified by call to set_method(). + */ + void initMethod(); + + /*! + * @brief Returns initialization time. + * @return Runtime of the last call to init_method(). + */ + double getInitime() const; + + /*! + * @brief Runs the GED method specified by call to set_method() between the graphs with IDs @p g and @p h. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + */ + void runMethod(std::size_t g, std::size_t h); + + /*! + * @brief Returns upper bound for edit distance between the input graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Upper bound computed by the last call to run_method() with arguments @p g and @p h. + */ + double getUpperBound(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns lower bound for edit distance between the input graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Lower bound computed by the last call to run_method() with arguments @p g and @p h. + */ + double getLowerBound(std::size_t g,std::size_t h) const; + + /*! + * @brief Returns the forward map between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The forward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector getForwardMap(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns the backward map between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The backward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector getBackwardMap(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns image of a node. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @param[in] nodeId Node whose image is to be returned. + * @return Node to which node @p node is assigned. + */ + std::size_t getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const; + + /*! + * @brief Returns pre-image of a node. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @param[in] nodeId Node whose pre-image is to be returned. + * @return Node to which node @p node is assigned. + */ + std::size_t getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const; + + /*! + * @brief Returns the induced cost between the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The induced cost between the two indicated graphs. + */ + double getInducedCost(std::size_t g, std::size_t h) const; + + + /*! + * @brief Returns node map between the input graphs. This function duplicates datas. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Node map computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getNodeMap(std::size_t g, std::size_t h); + + /*! + * @brief Returns assignment matrix between the input graphs. This function duplicates datas. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Assignment matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getAssignmentMatrix(std::size_t g, std::size_t h); + + /*! + * @brief Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The forward and backward maps to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getAllMap(std::size_t g, std::size_t h); + + /*! + * @brief Returns runtime. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Runtime of last call to run_method() with arguments @p g and @p h. + */ + double getRuntime(std::size_t g, std::size_t h) const; + + /*! + * @brief Checks if the edit costs are quasimetric. + * @return Boolean @p true if the edit costs are quasimetric and @p false, otherwise. + */ + bool quasimetricCosts() const; + + /*! + * @brief Applies the hungarian algorithm (LSAP) to a matrix cost. + * @param[in] matrixCost The matrix cost. + * @return the values of rho, varrho, u and v, in this order. + */ + std::vector> hungarianLSAP(std::vector> matrixCost); + + /*! + * @brief Applies the hungarian algorithm (LSAPE) to a matrix cost. + * @param[in] matrixCost The matrix cost. + * @return the values of rho, varrho, u and v, in this order. + */ + std::vector> hungarianLSAPE(std::vector> matrixCost); + + + /*! + * @brief Returns the number of graphs. + * @return Number of graphs contained in the environment. + */ + std::size_t getNumGraphs() const; + + /*! + * @brief Returns the number of node labels. + * @return Number of pairwise different node labels contained in the environment. + * @note If @p 1 is returned, the nodes are unlabeled. + */ + std::size_t getNumNodeLabels() const; + + /*! + * @brief Returns node label. + * @param[in] label_id ID of node label that should be returned. Must be between 1 and num_node_labels(). + * @return Node label for selected label ID. + */ + ged::AttrLabel getNodeLabel(std::size_t label_id) const; + + /*! + * @brief Returns the number of edge labels. + * @return Number of pairwise different edge labels contained in the environment. + * @note If @p 1 is returned, the edges are unlabeled. + */ + std::size_t getNumEdgeLabels() const; + + /*! + * @brief Returns edge label. + * @param[in] label_id ID of edge label that should be returned. Must be between 1 and num_node_labels(). + * @return Edge label for selected label ID. + */ + ged::AttrLabel getEdgeLabel(std::size_t label_id) const; + + // /*! + // * @brief Returns the number of nodes. + // * @param[in] graph_id ID of an input graph that has been added to the environment. + // * @return Number of nodes in the graph. + // */ + // std::size_t getNumNodes(std::size_t graph_id) const; + + /*! + * @brief Returns average number of nodes. + * @return Average number of nodes of the graphs contained in the environment. + */ + double getAvgNumNodes() const; + + /*! + * @brief Returns node relabeling cost. + * @param[in] node_label_1 First node label. + * @param[in] node_label_2 Second node label. + * @return Node relabeling cost for the given node labels. + */ + double getNodeRelCost(const ged::AttrLabel & node_label_1, const ged::AttrLabel & node_label_2) const; + + /*! + * @brief Returns node deletion cost. + * @param[in] node_label Node label. + * @return Cost of deleting node with given label. + */ + double getNodeDelCost(const ged::AttrLabel & node_label) const; + + /*! + * @brief Returns node insertion cost. + * @param[in] node_label Node label. + * @return Cost of inserting node with given label. + */ + double getNodeInsCost(const ged::AttrLabel & node_label) const; + + /*! + * @brief Computes median node label. + * @param[in] node_labels The node labels whose median should be computed. + * @return Median of the given node labels. + */ + ged::AttrLabel getMedianNodeLabel(const std::vector & node_labels) const; + + /*! + * @brief Returns edge relabeling cost. + * @param[in] edge_label_1 First edge label. + * @param[in] edge_label_2 Second edge label. + * @return Edge relabeling cost for the given edge labels. + */ + double getEdgeRelCost(const ged::AttrLabel & edge_label_1, const ged::AttrLabel & edge_label_2) const; + + /*! + * @brief Returns edge deletion cost. + * @param[in] edge_label Edge label. + * @return Cost of deleting edge with given label. + */ + double getEdgeDelCost(const ged::AttrLabel & edge_label) const; + + /*! + * @brief Returns edge insertion cost. + * @param[in] edge_label Edge label. + * @return Cost of inserting edge with given label. + */ + double getEdgeInsCost(const ged::AttrLabel & edge_label) const; + + /*! + * @brief Computes median edge label. + * @param[in] edge_labels The edge labels whose median should be computed. + * @return Median of the given edge labels. + */ + ged::AttrLabel getMedianEdgeLabel(const std::vector & edge_labels) const; + + /*! + * @brief Returns the initialization type of the last initialization. + * @return Initialization type in string. + */ + std::string getInitType() const; + + /*! + * @brief Computes the edit cost between two graphs induced by a node map. + * @param[in] g_id ID of input graph. + * @param[in] h_id ID of input graph. + * @return Computed induced cost. + */ + double computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const; + + // /*! + // * @brief Returns node relabeling, insertion, or deletion cost. + // * @param[in] label1 First node label. + // * @param[in] label2 Second node label. + // * @return Node relabeling cost if @p label1 and @p label2 are both different from ged::dummy_label(), + // * node insertion cost if @p label1 equals ged::dummy_label and @p label2 does not, + // * node deletion cost if @p label1 does not equal ged::dummy_label and @p label2 does, + // * and 0 otherwise. + // */ + // double getNodeCost(std::size_t label1, std::size_t label2) const; + + + /*! + * @brief Constructs a ged::AttrLabel from the given attribute maps. + * @param str_map The string attributes of the node/edge. + * @param int_map The integer attributes of the node/edge. + * @param float_map The float attributes of the node/edge. + * @param list_str_map The list of string attributes of the node/edge. + * @param list_int_map The list of integer attributes of the node/edge. + * @param list_float_map The list of float attributes of the node/edge. + * @return A ged::AttrLabel constructed from the given maps. + */ + static ged::AttrLabel constructAttrLabelFromMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map + ); + + +private: + + ged::GEDEnv * env_; // environment variable + + bool initialized; // initialization boolean (because env has one but not accessible) + +}; // class PyGEDEnvAttr + +static void printLabelMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +); + +static void printAttrLabel(const ged::AttrLabel & attr_label); + +} // namespace pyged + +// According to some Chat AI, if implementation is in the .ipp file, it will be +// recognized by the compiler as inline code, so I can include it here and does not +// cause the multiple definition error. +// I will not use .cpp files to implement, it is a PAIN In The A to compile and link +// while avoiding multiple definition errors... +#include "gedlib_bind_attr.ipp" + +//#endif /* SRC_GEDLIB_BIND_ATTR_HPP */ \ No newline at end of file diff --git a/gklearn/gedlib/src/gedlib_bind_attr.ipp b/gklearn/gedlib/src/gedlib_bind_attr.ipp new file mode 100644 index 0000000000..0809234964 --- /dev/null +++ b/gklearn/gedlib/src/gedlib_bind_attr.ipp @@ -0,0 +1,883 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_attr.ipp + * @brief implementations of classes and functions to call easily GebLib in Python without Gedlib's types + */ +#pragma once +//#ifndef SRC_GEDLIB_BIND_ATTR_IPP +//#define SRC_GEDLIB_BIND_ATTR_IPP + +//Include standard libraries + GedLib library +// #include +// #include "GedLibBind.h" +// #include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/median/src/median_graph_estimator.hpp" +// #include "gedlib_bind_gxl.hpp" + +using namespace std; + +//Definition of types and templates used in this code for my human's memory :). +//ged::GEDEnv env; +//template struct ExchangeGraph + +//typedef std::map GXLLabel; +//typedef std::string GXLNodeID; + + +namespace pyged { + +//extern template class ged::GEDEnv; + +//!< List of available edit cost functions readable by Python. +std::vector editCostStringOptions = { + "CHEM_1", + "CHEM_2", + "CMU", + "GREC_1", + "GREC_2", + "LETTER", + "LETTER2", + "NON_SYMBOLIC", + "GEOMETRIC", + "FINGERPRINT", + "PROTEIN", + "CONSTANT" +}; + +//!< Map of available edit cost functions between enum type in C++ and string in Python +std::map editCostOptions = { + {"CHEM_1", ged::Options::EditCosts::CHEM_1}, + {"CHEM_2", ged::Options::EditCosts::CHEM_2}, + {"CMU", ged::Options::EditCosts::CMU}, + {"GREC_1", ged::Options::EditCosts::GREC_1}, + {"GREC_2", ged::Options::EditCosts::GREC_2}, + {"LETTER", ged::Options::EditCosts::LETTER}, + {"LETTER2", ged::Options::EditCosts::LETTER2}, + {"NON_SYMBOLIC", ged::Options::EditCosts::NON_SYMBOLIC}, + {"GEOMETRIC", ged::Options::EditCosts::GEOMETRIC}, + {"FINGERPRINT", ged::Options::EditCosts::FINGERPRINT}, + {"PROTEIN", ged::Options::EditCosts::PROTEIN}, + {"CONSTANT", ged::Options::EditCosts::CONSTANT} +}; + + //!< List of available computation methods readable by Python. +std::vector methodStringOptions = { + "BRANCH", + "BRANCH_FAST", + "BRANCH_TIGHT", + "BRANCH_UNIFORM", + "BRANCH_COMPACT", + "PARTITION", + "HYBRID", + "RING", + "ANCHOR_AWARE_GED", + "WALKS", + "IPFP", + "BIPARTITE", + "SUBGRAPH", + "NODE", + "RING_ML", + "BIPARTITE_ML", + "REFINE", + "BP_BEAM", + "SIMULATED_ANNEALING", + "HED", + "STAR" +}; + +//!< Map of available computation methods readables between enum type in C++ and string in Python +std::map methodOptions = { + {"BRANCH", ged::Options::GEDMethod::BRANCH}, + {"BRANCH_FAST", ged::Options::GEDMethod::BRANCH_FAST}, + {"BRANCH_TIGHT", ged::Options::GEDMethod::BRANCH_TIGHT}, + {"BRANCH_UNIFORM", ged::Options::GEDMethod::BRANCH_UNIFORM}, + {"BRANCH_COMPACT", ged::Options::GEDMethod::BRANCH_COMPACT}, + {"PARTITION", ged::Options::GEDMethod::PARTITION}, + {"HYBRID", ged::Options::GEDMethod::HYBRID}, + {"RING", ged::Options::GEDMethod::RING}, + {"ANCHOR_AWARE_GED", ged::Options::GEDMethod::ANCHOR_AWARE_GED}, + {"WALKS", ged::Options::GEDMethod::WALKS}, + {"IPFP", ged::Options::GEDMethod::IPFP}, + {"BIPARTITE", ged::Options::GEDMethod::BIPARTITE}, + {"SUBGRAPH", ged::Options::GEDMethod::SUBGRAPH}, + {"NODE", ged::Options::GEDMethod::NODE}, + {"RING_ML", ged::Options::GEDMethod::RING_ML}, + {"BIPARTITE_ML",ged::Options::GEDMethod::BIPARTITE_ML}, + {"REFINE",ged::Options::GEDMethod::REFINE}, + {"BP_BEAM", ged::Options::GEDMethod::BP_BEAM}, + {"SIMULATED_ANNEALING", ged::Options::GEDMethod::SIMULATED_ANNEALING}, + {"HED", ged::Options::GEDMethod::HED}, + {"STAR" , ged::Options::GEDMethod::STAR}, +}; + +//! initStringOptions = { + "LAZY_WITHOUT_SHUFFLED_COPIES", + "EAGER_WITHOUT_SHUFFLED_COPIES", + "LAZY_WITH_SHUFFLED_COPIES", + "EAGER_WITH_SHUFFLED_COPIES" +}; + +//!< Map of available initilaization options readables between enum type in C++ and string in Python +std::map initOptions = { + {"LAZY_WITHOUT_SHUFFLED_COPIES", ged::Options::InitType::LAZY_WITHOUT_SHUFFLED_COPIES}, + {"EAGER_WITHOUT_SHUFFLED_COPIES", ged::Options::InitType::EAGER_WITHOUT_SHUFFLED_COPIES}, + {"LAZY_WITH_SHUFFLED_COPIES", ged::Options::InitType::LAZY_WITH_SHUFFLED_COPIES}, + {"EAGER_WITH_SHUFFLED_COPIES", ged::Options::InitType::EAGER_WITH_SHUFFLED_COPIES} +}; + +std::vector getEditCostStringOptions() { +// // test only: +// std::cout << "[gedlib_bind_attr.ipp] Available edit costs: "; +// for (const auto& cost : editCostStringOptions) { +// std::cout << cost << " "; +// } + + return editCostStringOptions; +} + +std::vector getMethodStringOptions() { + return methodStringOptions; +} + +std::vector getInitStringOptions() { + return initStringOptions; +} + +static std::size_t getDummyNode() { + return ged::GEDGraph::dummy_node(); +} + + +/*! + * @brief Returns the enum EditCost which correspond to the string parameter + * @param editCost Select one of the predefined edit costs in the list. + * @return The edit cost function which correspond in the edit cost functions map. + */ +ged::Options::EditCosts translateEditCost(std::string editCost) { + for (std::size_t i = 0; i != editCostStringOptions.size(); i++) { + if (editCostStringOptions[i] == editCost) { + return editCostOptions[editCostStringOptions[i]]; + } + } + return ged::Options::EditCosts::CONSTANT; +} + +/*! + * @brief Returns the enum IniType which correspond to the string parameter + * @param initOption Select initialization options. + * @return The init Type which correspond in the init options map. + */ +ged::Options::InitType translateInitOptions(std::string initOption) { + for (std::size_t i = 0; i != initStringOptions.size(); i++) { + if (initStringOptions[i] == initOption) { + return initOptions[initStringOptions[i]]; + } + } + return ged::Options::InitType::EAGER_WITHOUT_SHUFFLED_COPIES; +} + +/*! + * @brief Returns the string correspond to the enum IniType. + * @param initOption Select initialization options. + * @return The string which correspond to the enum IniType @p initOption. + */ + std::string initOptionsToString(ged::Options::InitType initOption) { + for (std::size_t i = 0; i != initOptions.size(); i++) { + if (initOptions[initStringOptions[i]] == initOption) { + return initStringOptions[i]; + } + } + return "EAGER_WITHOUT_SHUFFLED_COPIES"; +} + +/*! + * @brief Returns the enum Method which correspond to the string parameter + * @param method Select the method that is to be used. + * @return The computation method which correspond in the edit cost functions map. + */ +ged::Options::GEDMethod translateMethod(std::string method) { + for (std::size_t i = 0; i != methodStringOptions.size(); i++) { + if (methodStringOptions[i] == method) { + return methodOptions[methodStringOptions[i]]; + } + } + return ged::Options::GEDMethod::STAR; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(std::size_t* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back(pointer[i]); + } + return res; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The double pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(double* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back(pointer[i]); + } + return res; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values, with double type. + */ +std::vector translateAndConvertPointer(std::size_t* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back((double)pointer[i]); + } + return res; +} + +/*! + * @brief Returns the string which contains all element of a int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector) { + std::string res = ""; + + for (std::size_t i = 0; i != vector.size(); i++) + { + res += std::to_string(vector[i]) + " "; + } + + return res; +} + +/*! + * @brief Returns the string which contains all element of a unsigned long int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector) { + std::string res = ""; + + for (std::size_t i = 0; i != vector.size(); i++) + { + res += std::to_string(vector[i]) + " "; + } + + return res; +} + + +PyGEDEnvAttr::PyGEDEnvAttr () : env_(nullptr), initialized(false) { + env_ = new ged::GEDEnv(); +} + +PyGEDEnvAttr::~PyGEDEnvAttr () { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } +} + +// ======== Environment Public APIs ======== + +// bool initialized = false; //Initialization boolean (because Env has one but not accessible). + +bool PyGEDEnvAttr::isInitialized() { + return initialized; +} + +void PyGEDEnvAttr::restartEnv() { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } + env_ = new ged::GEDEnv(); + initialized = false; +} + +// void PyGEDEnvAttr::loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type) { +// std::vector tmp_graph_ids(env_->load_gxl_graph(pathFolder, pathXML, +// (node_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), +// (edge_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), +// std::unordered_set(), std::unordered_set())); +// } + +std::pair PyGEDEnvAttr::getGraphIds() const { + return env_->graph_ids(); +} + +std::vector PyGEDEnvAttr::getAllGraphIds() { + std::vector listID; + for (std::size_t i = env_->graph_ids().first; i != env_->graph_ids().second; i++) { + listID.push_back(i); + } + return listID; +} + +const std::string PyGEDEnvAttr::getGraphClass(std::size_t id) const { + return env_->get_graph_class(id); +} + +const std::string PyGEDEnvAttr::getGraphName(std::size_t id) const { + return env_->get_graph_name(id); +} + +std::size_t PyGEDEnvAttr::addGraph(const std::string & graph_name, const std::string & graph_class) { + ged::GEDGraph::GraphID newId = env_->add_graph(graph_name, graph_class); + initialized = false; + return std::stoi(std::to_string(newId)); +} + +// void PyGEDEnvAttr::addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel) { +// // todo: if this needs to be supported, we need to convert the string map to an AttrLabel +// env_->add_node(graphId, nodeId, nodeLabel); +// initialized = false; +// } + +void PyGEDEnvAttr::addNode( + std::size_t graphId, + const std::string& nodeId, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { +// // debug test only: +// std::cout << "The node labels received by the c++ bindings are: " << std::endl; +// printLabelMaps(str_map, int_map, float_map, list_str_map, list_int_map, list_float_map); + + // Merge the maps into AttrLabel: + ged::AttrLabel nodeLabel = PyGEDEnvAttr::constructAttrLabelFromMaps( + str_map, + int_map, + float_map, + list_str_map, + list_int_map, + list_float_map + ); + +// std::cout << "The node label passed to c++ env is: " << std::endl; +// printAttrLabel(nodeLabel); + + env_->add_node(graphId, nodeId, nodeLabel); + initialized = false; +} + +/*void addEdge(std::size_t graphId, ged::GXLNodeID tail, ged::GXLNodeID head, ged::GXLLabel edgeLabel) { + env_->add_edge(graphId, tail, head, edgeLabel); +}*/ + +// void PyGEDEnvAttr::addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates) { +// // todo: if this needs to be supported, we need to convert the string map to an AttrLabel +// env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); +// initialized = false; +// } + +void PyGEDEnvAttr::addEdge( + std::size_t graphId, + const std::string& tail, + const std::string& head, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map, + bool ignoreDuplicates +) { +// // debug test only: +// std::cout << "The edge labels received by the c++ bindings are: " << std::endl; +// printLabelMaps(str_map, int_map, float_map, list_str_map, list_int_map, list_float_map); + + // Merge the maps into AttrLabel: + ged::AttrLabel edgeLabel = PyGEDEnvAttr::constructAttrLabelFromMaps( + str_map, + int_map, + float_map, + list_str_map, + list_int_map, + list_float_map + ); + +// std::cout << "The edge label passed to c++ env is: " << std::endl; +// printAttrLabel(edgeLabel); + + env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); + initialized = false; +} + +void PyGEDEnvAttr::clearGraph(std::size_t graphId) { + env_->clear_graph(graphId); + initialized = false; +} + +// todo: check if ExchangeGraph supports AttrLabel +ged::ExchangeGraph PyGEDEnvAttr::getGraph(std::size_t graphId) const { +// static_assert(std::is_same_v< +// decltype(env_->get_graph(graphId)), +// ged::ExchangeGraph +// >, "get_graph() 返回的不是 AttrLabel 类型"); +// std::cout << "get_graph() 返回的是 AttrLabel 类型: " << std::endl; + + + return env_->get_graph(graphId); +} + +std::size_t PyGEDEnvAttr::getGraphInternalId(std::size_t graphId) { + return getGraph(graphId).id; +} + +std::size_t PyGEDEnvAttr::getGraphNumNodes(std::size_t graphId) { + return getGraph(graphId).num_nodes; +} + +std::size_t PyGEDEnvAttr::getGraphNumEdges(std::size_t graphId) { + return getGraph(graphId).num_edges; +} + +std::vector PyGEDEnvAttr::getGraphOriginalNodeIds(std::size_t graphId) { + return getGraph(graphId).original_node_ids; +} + +std::vector PyGEDEnvAttr::getGraphNodeLabels(std::size_t graphId) { + return getGraph(graphId).node_labels; +} + +std::map, ged::AttrLabel> PyGEDEnvAttr::getGraphEdges(std::size_t graphId) { + return getGraph(graphId).edge_labels; +} + +std::vector> PyGEDEnvAttr::getGraphAdjacenceMatrix(std::size_t graphId) { + return getGraph(graphId).adj_matrix; +} + +void PyGEDEnvAttr::setEditCost( + std::string editCost, std::vector editCostConstants, + std::unordered_map editCostConfigStr, + std::unordered_map editCostConfigBool +) { + // Convert edit cost config values to std::any: + std::unordered_map editCostConstantsAny; + for (const auto& pair : editCostConfigStr) { + editCostConstantsAny[pair.first] = pair.second; + } + for (const auto& pair : editCostConfigBool) { + editCostConstantsAny[pair.first] = pair.second; + } + +// // debug test only: +// std::cout << "[gedlib_bind_attr.ipp] edit cost config passed to C++ GEDEnv is: " << std::endl; +// for (const auto& pair : editCostConstantsAny) { +// std::cout << pair.first << ": "; +// if (pair.second.type() == typeid(std::string)) { +// std::cout << std::any_cast(pair.second); +// } else if (pair.second.type() == typeid(bool)) { +// std::cout << std::any_cast(pair.second); +// } else { +// std::cout << "Unknown type"; +// } +// std::cout << std::endl; +// } + + env_->set_edit_costs(translateEditCost(editCost), editCostConstants, editCostConstantsAny); +} + +void PyGEDEnvAttr::setPersonalEditCost(std::vector editCostConstants) { + //env_->set_edit_costs(Your EditCost Class(editCostConstants)); +} + +// void PyGEDEnvAttr::initEnv() { +// env_->init(); +// initialized = true; +// } + +void PyGEDEnvAttr::initEnv(std::string initOption, bool print_to_stdout) { + env_->init(translateInitOptions(initOption), print_to_stdout); + initialized = true; +} + +void PyGEDEnvAttr::setMethod(std::string method, const std::string & options) { + env_->set_method(translateMethod(method), options); +} + +void PyGEDEnvAttr::initMethod() { + env_->init_method(); +} + +double PyGEDEnvAttr::getInitime() const { + return env_->get_init_time(); +} + +void PyGEDEnvAttr::runMethod(std::size_t g, std::size_t h) { + env_->run_method(g, h); +} + +double PyGEDEnvAttr::getUpperBound(std::size_t g, std::size_t h) const { + return env_->get_upper_bound(g, h); +} + +double PyGEDEnvAttr::getLowerBound(std::size_t g, std::size_t h) const { + return env_->get_lower_bound(g, h); +} + +std::vector PyGEDEnvAttr::getForwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_forward_map(); +} + +std::vector PyGEDEnvAttr::getBackwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_backward_map(); +} + +std::size_t PyGEDEnvAttr::getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).image(nodeId); +} + +std::size_t PyGEDEnvAttr::getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).pre_image(nodeId); +} + +double PyGEDEnvAttr::getInducedCost(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).induced_cost(); +} + +std::vector> PyGEDEnvAttr::getNodeMap(std::size_t g, std::size_t h) { + std::vector> res; + std::vector relation; + env_->get_node_map(g, h).as_relation(relation); + for (const auto & assignment : relation) { + res.push_back(std::make_pair(assignment.first, assignment.second)); + } + return res; +} + +std::vector> PyGEDEnvAttr::getAssignmentMatrix(std::size_t g, std::size_t h) { + std::vector> res; + for(std::size_t i = 0; i != getForwardMap(g, h).size(); i++) { + std::vector newLine; + bool have1 = false; + for(std::size_t j = 0; j != getBackwardMap(g, h).size(); j++) { + if (getNodeImage(g, h, i) == j) { + newLine.push_back(1); + have1 = true; + } + else{ + newLine.push_back(0); + } + } + if(have1) { + newLine.push_back(0); + } + else{ + newLine.push_back(1); + } + res.push_back(newLine); + } + std::vector lastLine; + for (size_t k = 0; k != getBackwardMap(g,h).size(); k++) { + if (getBackwardMap(g,h)[k] == ged::GEDGraph::dummy_node()) { + lastLine.push_back(1); + } + else{ + lastLine.push_back(0); + } + } + res.push_back(lastLine); + return res; +} + +std::vector> PyGEDEnvAttr::getAllMap(std::size_t g, std::size_t h) { + std::vector> res; + res.push_back(getForwardMap(g, h)); + res.push_back(getBackwardMap(g,h)); + return res; +} + +double PyGEDEnvAttr::getRuntime(std::size_t g, std::size_t h) const { + return env_->get_runtime(g, h); +} + +bool PyGEDEnvAttr::quasimetricCosts() const { + return env_->quasimetric_costs(); +} + +std::vector> PyGEDEnvAttr::hungarianLSAP(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows], *varrho = new std::size_t[ncols]; + std::size_t *u = new std::size_t[nrows], *v = new std::size_t[ncols]; + std::size_t *C = new std::size_t[nrows*ncols]; + // std::size_t i = 0, j; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAP(C,nrows,ncols,rho,u,v,varrho); + std::vector> res; + res.push_back(translatePointer(rho, nrows)); + res.push_back(translatePointer(varrho, ncols)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::vector> PyGEDEnvAttr::hungarianLSAPE(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows-1], *varrho = new std::size_t[ncols-1]; + double *u = new double[nrows], *v = new double[ncols]; + double *C = new double[nrows*ncols]; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAPE(C,nrows,ncols,rho,varrho,u,v); + std::vector> res; + res.push_back(translateAndConvertPointer(rho, nrows-1)); + res.push_back(translateAndConvertPointer(varrho, ncols-1)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::size_t PyGEDEnvAttr::getNumGraphs() const { + return env_->num_graphs(); +} + +std::size_t PyGEDEnvAttr::getNumNodeLabels() const { + return env_->num_node_labels(); +} + +ged::AttrLabel PyGEDEnvAttr::getNodeLabel(std::size_t label_id) const { + return env_->get_node_label(label_id); +} + +std::size_t PyGEDEnvAttr::getNumEdgeLabels() const { + return env_->num_edge_labels(); +} + +ged::AttrLabel PyGEDEnvAttr::getEdgeLabel(std::size_t label_id) const { + return env_->get_edge_label(label_id); +} + +// std::size_t PyGEDEnvAttr::getNumNodes(std::size_t graph_id) const { +// return env_->get_num_nodes(graph_id); +// } + +double PyGEDEnvAttr::getAvgNumNodes() const { + return env_->get_avg_num_nodes(); +} + +double PyGEDEnvAttr::getNodeRelCost(const ged::AttrLabel & node_label_1, const ged::AttrLabel & node_label_2) const { + return env_->node_rel_cost(node_label_1, node_label_2); +} + +double PyGEDEnvAttr::getNodeDelCost(const ged::AttrLabel & node_label) const { + return env_->node_del_cost(node_label); +} + +double PyGEDEnvAttr::getNodeInsCost(const ged::AttrLabel & node_label) const { + return env_->node_ins_cost(node_label); +} + +ged::AttrLabel PyGEDEnvAttr::getMedianNodeLabel(const std::vector & node_labels) const { + return env_->median_node_label(node_labels); +} + +double PyGEDEnvAttr::getEdgeRelCost(const ged::AttrLabel & edge_label_1, const ged::AttrLabel & edge_label_2) const { + return env_->edge_rel_cost(edge_label_1, edge_label_2); +} + +double PyGEDEnvAttr::getEdgeDelCost(const ged::AttrLabel & edge_label) const { + return env_->edge_del_cost(edge_label); +} + +double PyGEDEnvAttr::getEdgeInsCost(const ged::AttrLabel & edge_label) const { + return env_->edge_ins_cost(edge_label); +} + +ged::AttrLabel PyGEDEnvAttr::getMedianEdgeLabel(const std::vector & edge_labels) const { + return env_->median_edge_label(edge_labels); +} + +std::string PyGEDEnvAttr::getInitType() const { + return initOptionsToString(env_->get_init_type()); +} + +double PyGEDEnvAttr::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const { + ged::NodeMap node_map = ged::NodeMap(env_->get_num_nodes(g_id), env_->get_num_nodes(h_id)); + for (const auto & assignment : relation) { + node_map.add_assignment(assignment.first, assignment.second); + // std::cout << assignment.first << assignment.second << endl; + } + const std::vector forward_map = node_map.get_forward_map(); + for (std::size_t i{0}; i < node_map.num_source_nodes(); i++) { + if (forward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(i, ged::GEDGraph::dummy_node()); + } + } + const std::vector backward_map = node_map.get_backward_map(); + for (std::size_t i{0}; i < node_map.num_target_nodes(); i++) { + if (backward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(ged::GEDGraph::dummy_node(), i); + } + } + // for (auto & map : node_map.get_forward_map()) { + // std::cout << map << ", "; + // } + // std::cout << endl; + // for (auto & map : node_map.get_backward_map()) { + // std::cout << map << ", "; + // } + env_->compute_induced_cost(g_id, h_id, node_map); + return node_map.induced_cost(); +} + + +ged::AttrLabel +PyGEDEnvAttr::constructAttrLabelFromMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { + // using ged::AttrLabel = std::unordered_map, std::vector, std::vector>>; + ged::AttrLabel attr_label; + for (const auto& pair : str_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : int_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : float_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_str_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_int_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_float_map) { + attr_label[pair.first] = pair.second; + } + return attr_label; +} + + +void printLabelMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { + // Print the label maps for debugging purposes + std::cout << "String map: "; + for (const auto& pair : str_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nInt map: "; + for (const auto& pair : int_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nFloat map: "; + for (const auto& pair : float_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nList of strings map: "; + for (const auto& pair : list_str_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + std::cout << "\nList of ints map: "; + for (const auto& pair : list_int_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + std::cout << "\nList of floats map: "; + for (const auto& pair : list_float_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + + std::cout << std::endl; + +} + +void printAttrLabel(const ged::AttrLabel & attr_label) { + std::cout << "AttrLabel: "; + for (const auto& pair : attr_label) { + std::cout << pair.first << ": "; + if (std::holds_alternative(pair.second)) { + std::cout << std::get(pair.second); + } else if (std::holds_alternative(pair.second)) { + std::cout << std::get(pair.second); + } else if (std::holds_alternative(pair.second)) { + std::cout << std::get(pair.second); + } else if (std::holds_alternative>(pair.second)) { + const auto& vec = std::get>(pair.second); + std::cout << "["; + for (const auto& item : vec) { + std::cout << item << ", "; + } + std::cout << "]"; + } else if (std::holds_alternative>(pair.second)) { + const auto& vec = std::get>(pair.second); + std::cout << "["; + for (const auto& item : vec) { + std::cout << item << ", "; + } + std::cout << "]"; + } else if (std::holds_alternative>(pair.second)) { + const auto& vec = std::get>(pair.second); + std::cout << "["; + for (const auto& item : vec) { + std::cout << item << ", "; + } + std::cout << "]"; + } + std::cout << ", "; + } + std::cout << std::endl; +} + +} // namespace pyged + +//#endif /* SRC_GEDLIB_BIND_ATTR_IPP */ diff --git a/gklearn/gedlib/src/gedlib_bind_gxl.cpp b/gklearn/gedlib/src/gedlib_bind_gxl.cpp new file mode 100644 index 0000000000..1e52add2af --- /dev/null +++ b/gklearn/gedlib/src/gedlib_bind_gxl.cpp @@ -0,0 +1,735 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_gxl.cpp + * @brief implementations of classes and functions to call easily GebLib in Python without Gedlib's types + */ +#pragma once +//#ifndef GEDLIBBIND_IPP +//#define GEDLIBBIND_IPP + +//Include standard libraries + GedLib library +// #include +// #include "GedLibBind.h" +// #include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/median/src/median_graph_estimator.hpp" +#include "gedlib_bind_gxl.hpp" + +// Instantiation of GEDEnv for GXLNodeID and GXLLabel: +namespace ged { + +extern template class GEDEnv; + +} // namespace ged + + +using namespace std; + +//Definition of types and templates used in this code for my human's memory :). +//ged::GEDEnv env; +//template struct ExchangeGraph + +//typedef std::map GXLLabel; +//typedef std::string GXLNodeID; + + +namespace pyged { + +//extern template class ged::GEDEnv; + +//!< List of available edit cost functions readable by Python. +std::vector editCostStringOptions = { + "CHEM_1", + "CHEM_2", + "CMU", + "GREC_1", + "GREC_2", + "LETTER", + "LETTER2", + "NON_SYMBOLIC", + "FINGERPRINT", + "PROTEIN", + "CONSTANT" +}; + +//!< Map of available edit cost functions between enum type in C++ and string in Python +std::map editCostOptions = { + {"CHEM_1", ged::Options::EditCosts::CHEM_1}, + {"CHEM_2", ged::Options::EditCosts::CHEM_2}, + {"CMU", ged::Options::EditCosts::CMU}, + {"GREC_1", ged::Options::EditCosts::GREC_1}, + {"GREC_2", ged::Options::EditCosts::GREC_2}, + {"LETTER", ged::Options::EditCosts::LETTER}, + {"LETTER2", ged::Options::EditCosts::LETTER2}, + {"NON_SYMBOLIC", ged::Options::EditCosts::NON_SYMBOLIC}, + {"FINGERPRINT", ged::Options::EditCosts::FINGERPRINT}, + {"PROTEIN", ged::Options::EditCosts::PROTEIN}, + {"CONSTANT", ged::Options::EditCosts::CONSTANT} +}; + + //!< List of available computation methods readable by Python. +std::vector methodStringOptions = { + "BRANCH", + "BRANCH_FAST", + "BRANCH_TIGHT", + "BRANCH_UNIFORM", + "BRANCH_COMPACT", + "PARTITION", + "HYBRID", + "RING", + "ANCHOR_AWARE_GED", + "WALKS", + "IPFP", + "BIPARTITE", + "SUBGRAPH", + "NODE", + "RING_ML", + "BIPARTITE_ML", + "REFINE", + "BP_BEAM", + "SIMULATED_ANNEALING", + "HED", + "STAR" +}; + +//!< Map of available computation methods readables between enum type in C++ and string in Python +std::map methodOptions = { + {"BRANCH", ged::Options::GEDMethod::BRANCH}, + {"BRANCH_FAST", ged::Options::GEDMethod::BRANCH_FAST}, + {"BRANCH_TIGHT", ged::Options::GEDMethod::BRANCH_TIGHT}, + {"BRANCH_UNIFORM", ged::Options::GEDMethod::BRANCH_UNIFORM}, + {"BRANCH_COMPACT", ged::Options::GEDMethod::BRANCH_COMPACT}, + {"PARTITION", ged::Options::GEDMethod::PARTITION}, + {"HYBRID", ged::Options::GEDMethod::HYBRID}, + {"RING", ged::Options::GEDMethod::RING}, + {"ANCHOR_AWARE_GED", ged::Options::GEDMethod::ANCHOR_AWARE_GED}, + {"WALKS", ged::Options::GEDMethod::WALKS}, + {"IPFP", ged::Options::GEDMethod::IPFP}, + {"BIPARTITE", ged::Options::GEDMethod::BIPARTITE}, + {"SUBGRAPH", ged::Options::GEDMethod::SUBGRAPH}, + {"NODE", ged::Options::GEDMethod::NODE}, + {"RING_ML", ged::Options::GEDMethod::RING_ML}, + {"BIPARTITE_ML",ged::Options::GEDMethod::BIPARTITE_ML}, + {"REFINE",ged::Options::GEDMethod::REFINE}, + {"BP_BEAM", ged::Options::GEDMethod::BP_BEAM}, + {"SIMULATED_ANNEALING", ged::Options::GEDMethod::SIMULATED_ANNEALING}, + {"HED", ged::Options::GEDMethod::HED}, + {"STAR" , ged::Options::GEDMethod::STAR}, +}; + +//! initStringOptions = { + "LAZY_WITHOUT_SHUFFLED_COPIES", + "EAGER_WITHOUT_SHUFFLED_COPIES", + "LAZY_WITH_SHUFFLED_COPIES", + "EAGER_WITH_SHUFFLED_COPIES" +}; + +//!< Map of available initilaization options readables between enum type in C++ and string in Python +std::map initOptions = { + {"LAZY_WITHOUT_SHUFFLED_COPIES", ged::Options::InitType::LAZY_WITHOUT_SHUFFLED_COPIES}, + {"EAGER_WITHOUT_SHUFFLED_COPIES", ged::Options::InitType::EAGER_WITHOUT_SHUFFLED_COPIES}, + {"LAZY_WITH_SHUFFLED_COPIES", ged::Options::InitType::LAZY_WITH_SHUFFLED_COPIES}, + {"EAGER_WITH_SHUFFLED_COPIES", ged::Options::InitType::EAGER_WITH_SHUFFLED_COPIES} +}; + +std::vector getEditCostStringOptions() { + return editCostStringOptions; +} + +std::vector getMethodStringOptions() { + return methodStringOptions; +} + +std::vector getInitStringOptions() { + return initStringOptions; +} + +static std::size_t getDummyNode() { + return ged::GEDGraph::dummy_node(); +} + + +/*! + * @brief Returns the enum EditCost which correspond to the string parameter + * @param editCost Select one of the predefined edit costs in the list. + * @return The edit cost function which correspond in the edit cost functions map. + */ +ged::Options::EditCosts translateEditCost(std::string editCost) { + for (std::size_t i = 0; i != editCostStringOptions.size(); i++) { + if (editCostStringOptions[i] == editCost) { + return editCostOptions[editCostStringOptions[i]]; + } + } + return ged::Options::EditCosts::CONSTANT; +} + +/*! + * @brief Returns the enum IniType which correspond to the string parameter + * @param initOption Select initialization options. + * @return The init Type which correspond in the init options map. + */ +ged::Options::InitType translateInitOptions(std::string initOption) { + for (std::size_t i = 0; i != initStringOptions.size(); i++) { + if (initStringOptions[i] == initOption) { + return initOptions[initStringOptions[i]]; + } + } + return ged::Options::InitType::EAGER_WITHOUT_SHUFFLED_COPIES; +} + +/*! + * @brief Returns the string correspond to the enum IniType. + * @param initOption Select initialization options. + * @return The string which correspond to the enum IniType @p initOption. + */ + std::string initOptionsToString(ged::Options::InitType initOption) { + for (std::size_t i = 0; i != initOptions.size(); i++) { + if (initOptions[initStringOptions[i]] == initOption) { + return initStringOptions[i]; + } + } + return "EAGER_WITHOUT_SHUFFLED_COPIES"; +} + +/*! + * @brief Returns the enum Method which correspond to the string parameter + * @param method Select the method that is to be used. + * @return The computation method which correspond in the edit cost functions map. + */ +ged::Options::GEDMethod translateMethod(std::string method) { + for (std::size_t i = 0; i != methodStringOptions.size(); i++) { + if (methodStringOptions[i] == method) { + return methodOptions[methodStringOptions[i]]; + } + } + return ged::Options::GEDMethod::STAR; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(std::size_t* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back(pointer[i]); + } + return res; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The double pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(double* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back(pointer[i]); + } + return res; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values, with double type. + */ +std::vector translateAndConvertPointer(std::size_t* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back((double)pointer[i]); + } + return res; +} + +/*! + * @brief Returns the string which contains all element of a int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector) { + std::string res = ""; + + for (std::size_t i = 0; i != vector.size(); i++) + { + res += std::to_string(vector[i]) + " "; + } + + return res; +} + +/*! + * @brief Returns the string which contains all element of a unsigned long int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector) { + std::string res = ""; + + for (std::size_t i = 0; i != vector.size(); i++) + { + res += std::to_string(vector[i]) + " "; + } + + return res; +} + + +PyGEDEnvGXL::PyGEDEnvGXL () { + env_ = new ged::GEDEnv(); + this->initialized = false; +} + +PyGEDEnvGXL::~PyGEDEnvGXL () { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } +} + +// bool initialized = false; //Initialization boolean (because Env has one but not accessible). + +bool PyGEDEnvGXL::isInitialized() { + return initialized; +} + +void PyGEDEnvGXL::restartEnv() { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } + env_ = new ged::GEDEnv(); + initialized = false; +} + +void PyGEDEnvGXL::loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type) { + std::vector tmp_graph_ids(env_->load_gxl_graph(pathFolder, pathXML, + (node_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), + (edge_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), + std::unordered_set(), std::unordered_set())); +} + +std::pair PyGEDEnvGXL::getGraphIds() const { + return env_->graph_ids(); +} + +std::vector PyGEDEnvGXL::getAllGraphIds() { + std::vector listID; + for (std::size_t i = env_->graph_ids().first; i != env_->graph_ids().second; i++) { + listID.push_back(i); + } + return listID; +} + +const std::string PyGEDEnvGXL::getGraphClass(std::size_t id) const { + return env_->get_graph_class(id); +} + +const std::string PyGEDEnvGXL::getGraphName(std::size_t id) const { + return env_->get_graph_name(id); +} + +std::size_t PyGEDEnvGXL::addGraph(const std::string & graph_name, const std::string & graph_class) { + ged::GEDGraph::GraphID newId = env_->add_graph(graph_name, graph_class); + initialized = false; + return std::stoi(std::to_string(newId)); +} + +void PyGEDEnvGXL::addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel) { + env_->add_node(graphId, nodeId, nodeLabel); + initialized = false; +} + +/*void addEdge(std::size_t graphId, ged::GXLNodeID tail, ged::GXLNodeID head, ged::GXLLabel edgeLabel) { + env_->add_edge(graphId, tail, head, edgeLabel); +}*/ + +void PyGEDEnvGXL::addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates) { + env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); + initialized = false; +} + +void PyGEDEnvGXL::clearGraph(std::size_t graphId) { + env_->clear_graph(graphId); + initialized = false; +} + +ged::ExchangeGraph PyGEDEnvGXL::getGraph(std::size_t graphId) const { + return env_->get_graph(graphId); +} + +std::size_t PyGEDEnvGXL::getGraphInternalId(std::size_t graphId) { + return getGraph(graphId).id; +} + +std::size_t PyGEDEnvGXL::getGraphNumNodes(std::size_t graphId) { + return getGraph(graphId).num_nodes; +} + +std::size_t PyGEDEnvGXL::getGraphNumEdges(std::size_t graphId) { + return getGraph(graphId).num_edges; +} + +std::vector PyGEDEnvGXL::getGraphOriginalNodeIds(std::size_t graphId) { + return getGraph(graphId).original_node_ids; +} + +std::vector> PyGEDEnvGXL::getGraphNodeLabels(std::size_t graphId) { + return getGraph(graphId).node_labels; +} + +std::map, std::map> PyGEDEnvGXL::getGraphEdges(std::size_t graphId) { + return getGraph(graphId).edge_labels; +} + +std::vector> PyGEDEnvGXL::getGraphAdjacenceMatrix(std::size_t graphId) { + return getGraph(graphId).adj_matrix; +} + +void PyGEDEnvGXL::setEditCost(std::string editCost, std::vector editCostConstants) { + env_->set_edit_costs(translateEditCost(editCost), editCostConstants); +} + +void PyGEDEnvGXL::setPersonalEditCost(std::vector editCostConstants) { + //env_->set_edit_costs(Your EditCost Class(editCostConstants)); +} + +// void PyGEDEnvGXL::initEnv() { +// env_->init(); +// initialized = true; +// } + +void PyGEDEnvGXL::initEnv(std::string initOption, bool print_to_stdout) { + env_->init(translateInitOptions(initOption), print_to_stdout); + initialized = true; +} + +void PyGEDEnvGXL::setMethod(std::string method, const std::string & options) { + env_->set_method(translateMethod(method), options); +} + +void PyGEDEnvGXL::initMethod() { + env_->init_method(); +} + +double PyGEDEnvGXL::getInitime() const { + return env_->get_init_time(); +} + +void PyGEDEnvGXL::runMethod(std::size_t g, std::size_t h) { + env_->run_method(g, h); +} + +double PyGEDEnvGXL::getUpperBound(std::size_t g, std::size_t h) const { + return env_->get_upper_bound(g, h); +} + +double PyGEDEnvGXL::getLowerBound(std::size_t g, std::size_t h) const { + return env_->get_lower_bound(g, h); +} + +std::vector PyGEDEnvGXL::getForwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_forward_map(); +} + +std::vector PyGEDEnvGXL::getBackwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_backward_map(); +} + +std::size_t PyGEDEnvGXL::getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).image(nodeId); +} + +std::size_t PyGEDEnvGXL::getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).pre_image(nodeId); +} + +double PyGEDEnvGXL::getInducedCost(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).induced_cost(); +} + +std::vector> PyGEDEnvGXL::getNodeMap(std::size_t g, std::size_t h) { + std::vector> res; + std::vector relation; + env_->get_node_map(g, h).as_relation(relation); + for (const auto & assignment : relation) { + res.push_back(std::make_pair(assignment.first, assignment.second)); + } + return res; +} + +std::vector> PyGEDEnvGXL::getAssignmentMatrix(std::size_t g, std::size_t h) { + std::vector> res; + for(std::size_t i = 0; i != getForwardMap(g, h).size(); i++) { + std::vector newLine; + bool have1 = false; + for(std::size_t j = 0; j != getBackwardMap(g, h).size(); j++) { + if (getNodeImage(g, h, i) == j) { + newLine.push_back(1); + have1 = true; + } + else{ + newLine.push_back(0); + } + } + if(have1) { + newLine.push_back(0); + } + else{ + newLine.push_back(1); + } + res.push_back(newLine); + } + std::vector lastLine; + for (size_t k = 0; k != getBackwardMap(g,h).size(); k++) { + if (getBackwardMap(g,h)[k] == ged::GEDGraph::dummy_node()) { + lastLine.push_back(1); + } + else{ + lastLine.push_back(0); + } + } + res.push_back(lastLine); + return res; +} + +std::vector> PyGEDEnvGXL::getAllMap(std::size_t g, std::size_t h) { + std::vector> res; + res.push_back(getForwardMap(g, h)); + res.push_back(getBackwardMap(g,h)); + return res; +} + +double PyGEDEnvGXL::getRuntime(std::size_t g, std::size_t h) const { + return env_->get_runtime(g, h); +} + +bool PyGEDEnvGXL::quasimetricCosts() const { + return env_->quasimetric_costs(); +} + +std::vector> PyGEDEnvGXL::hungarianLSAP(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows], *varrho = new std::size_t[ncols]; + std::size_t *u = new std::size_t[nrows], *v = new std::size_t[ncols]; + std::size_t *C = new std::size_t[nrows*ncols]; + // std::size_t i = 0, j; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAP(C,nrows,ncols,rho,u,v,varrho); + std::vector> res; + res.push_back(translatePointer(rho, nrows)); + res.push_back(translatePointer(varrho, ncols)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::vector> PyGEDEnvGXL::hungarianLSAPE(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows-1], *varrho = new std::size_t[ncols-1]; + double *u = new double[nrows], *v = new double[ncols]; + double *C = new double[nrows*ncols]; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAPE(C,nrows,ncols,rho,varrho,u,v); + std::vector> res; + res.push_back(translateAndConvertPointer(rho, nrows-1)); + res.push_back(translateAndConvertPointer(varrho, ncols-1)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::size_t PyGEDEnvGXL::getNumNodeLabels() const { + return env_->num_node_labels(); +} + +std::map PyGEDEnvGXL::getNodeLabel(std::size_t label_id) const { + return env_->get_node_label(label_id); +} + +std::size_t PyGEDEnvGXL::getNumEdgeLabels() const { + return env_->num_edge_labels(); +} + +std::map PyGEDEnvGXL::getEdgeLabel(std::size_t label_id) const { + return env_->get_edge_label(label_id); +} + +// std::size_t PyGEDEnvGXL::getNumNodes(std::size_t graph_id) const { +// return env_->get_num_nodes(graph_id); +// } + +double PyGEDEnvGXL::getAvgNumNodes() const { + return env_->get_avg_num_nodes(); +} + +double PyGEDEnvGXL::getNodeRelCost(const std::map & node_label_1, const std::map & node_label_2) const { + return env_->node_rel_cost(node_label_1, node_label_2); +} + +double PyGEDEnvGXL::getNodeDelCost(const std::map & node_label) const { + return env_->node_del_cost(node_label); +} + +double PyGEDEnvGXL::getNodeInsCost(const std::map & node_label) const { + return env_->node_ins_cost(node_label); +} + +std::map PyGEDEnvGXL::getMedianNodeLabel(const std::vector> & node_labels) const { + return env_->median_node_label(node_labels); +} + +double PyGEDEnvGXL::getEdgeRelCost(const std::map & edge_label_1, const std::map & edge_label_2) const { + return env_->edge_rel_cost(edge_label_1, edge_label_2); +} + +double PyGEDEnvGXL::getEdgeDelCost(const std::map & edge_label) const { + return env_->edge_del_cost(edge_label); +} + +double PyGEDEnvGXL::getEdgeInsCost(const std::map & edge_label) const { + return env_->edge_ins_cost(edge_label); +} + +std::map PyGEDEnvGXL::getMedianEdgeLabel(const std::vector> & edge_labels) const { + return env_->median_edge_label(edge_labels); +} + +std::string PyGEDEnvGXL::getInitType() const { + return initOptionsToString(env_->get_init_type()); +} + +double PyGEDEnvGXL::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const { + ged::NodeMap node_map = ged::NodeMap(env_->get_num_nodes(g_id), env_->get_num_nodes(h_id)); + for (const auto & assignment : relation) { + node_map.add_assignment(assignment.first, assignment.second); + // std::cout << assignment.first << assignment.second << endl; + } + const std::vector forward_map = node_map.get_forward_map(); + for (std::size_t i{0}; i < node_map.num_source_nodes(); i++) { + if (forward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(i, ged::GEDGraph::dummy_node()); + } + } + const std::vector backward_map = node_map.get_backward_map(); + for (std::size_t i{0}; i < node_map.num_target_nodes(); i++) { + if (backward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(ged::GEDGraph::dummy_node(), i); + } + } + // for (auto & map : node_map.get_forward_map()) { + // std::cout << map << ", "; + // } + // std::cout << endl; + // for (auto & map : node_map.get_backward_map()) { + // std::cout << map << ", "; + // } + env_->compute_induced_cost(g_id, h_id, node_map); + return node_map.induced_cost(); +} + + + + +// double PyGEDEnvGXL::getNodeCost(std::size_t label1, std::size_t label2) const { +// return env_->ged_data_node_cost(label1, label2); +// } + + +/*void medianLetter(pathFolder, pathXML, editCost, method, options="", initOption = "EAGER_WITHOUT_SHUFFLED_COPIES") { + + if(isInitialized()) { + restartEnv(); + } + setEditCost(editCost);*/ + + /*std::string letter_class("A"); + if (argc > 1) { + letter_class = std::string(argv[1]); + }*/ + //std::string seed("0"); + /*if (argc > 2) { + seed = std::string(argv[2]); + }*/ + + /*loadGXLGraph(pathFolder, pathXML); + std::vector graph_ids = getAllGraphIds(); + std::size_t median_id = env_->add_graph("median", ""); + + initEnv(initOption); + + setMethod(method); + + ged::MedianGraphEstimator median_estimator(&env, false); + median_estimator.set_options("--init-type RANDOM --randomness PSEUDO --seed " + seed); + median_estimator.run(graph_ids, median_id); + std::string gxl_file_name("../output/gen_median_Letter_HIGH_" + letter_class + ".gxl"); + env_->save_as_gxl_graph(median_id, gxl_file_name);*/ + + /*std::string tikz_file_name("../output/gen_median_Letter_HIGH_" + letter_class + ".tex"); + save_letter_graph_as_tikz_file(env_->get_graph(median_id), tikz_file_name);*/ +//} + +} // namespace pyged + +//#endif /* SRC_GEDLIB_BIND_IPP */ + +// namespace shapes { + +// // Default constructor +// Rectangle::Rectangle () {} + +// // Overloaded constructor +// Rectangle::Rectangle (int x0, int y0, int x1, int y1) { +// this->x0 = x0; +// this->y0 = y0; +// this->x1 = x1; +// this->y1 = y1; +// } + +// // Destructor +// Rectangle::~Rectangle () {} + +// // Return the area of the rectangle +// int Rectangle::getArea () { +// return (this->x1 - this->x0) * (this->y1 - this->y0); +// } + +// // Get the size of the rectangle. +// // Put the size in the pointer args +// void Rectangle::getSize (int *width, int *height) { +// (*width) = x1 - x0; +// (*height) = y1 - y0; +// } + +// // Move the rectangle by dx dy +// void Rectangle::move (int dx, int dy) { +// this->x0 += dx; +// this->y0 += dy; +// this->x1 += dx; +// this->y1 += dy; +// } +// } \ No newline at end of file diff --git a/gklearn/gedlib/src/gedlib_bind_gxl.hpp b/gklearn/gedlib/src/gedlib_bind_gxl.hpp new file mode 100644 index 0000000000..1daf704c4a --- /dev/null +++ b/gklearn/gedlib/src/gedlib_bind_gxl.hpp @@ -0,0 +1,513 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_gxl.hpp + * @brief Class and function declarations to call easily GebLib in Python without Gedlib's types + * with the GXLLabel (string) + */ +#pragma once +//#ifndef SRC_GEDLIB_BIND_GXL_HPP +//#define SRC_GEDLIB_BIND_GXL_HPP + +//Include standard libraries. +#include +#include +#include +#include +#include +#include "../include/gedlib-master/src/env/ged_env.hpp" +#include "../include/gedlib-master/src/env/node_map.hpp" + + +/*! + * @namespace pyged + * @brief Global namespace for gedlibpy. + */ +namespace pyged { + +/*! +* @brief Get list of available edit cost functions readable by Python. +*/ +std::vector getEditCostStringOptions(); + +/*! +* @brief Get list of available computation methods readable by Python. +*/ +std::vector getMethodStringOptions(); + +/*! +* @brief Get list of available initilaization options readable by Python. +*/ +std::vector getInitStringOptions(); + +/*! +* @brief Returns a dummy node. +* @return ID of dummy node. +*/ +static std::size_t getDummyNode(); + + +/*! +* @brief Provides the API of GEDLIB for Python for gxl labels (string labels). +*/ +class PyGEDEnvGXL { + +public: + + /*! + * @brief Constructor. + */ + PyGEDEnvGXL(); + + // PyGEDEnvGXL(); + + /*! + * @brief Destructor. + */ + ~PyGEDEnvGXL(); + + /*! + * @brief Tests if the environment is initialized or not. + * @return Boolean @p true if the environment is initialized and @p false otherwise. + */ + bool isInitialized(); + + /*! + * @brief Restart the environment (recall a new empty environment). + */ + void restartEnv(); + + /*! + * @brief Loads graph given in the [GXL file format](http://www.gupro.de/GXL/). + * @param[in] pathFolder The path to the directory containing the graphs. + * @param[in] pathXML The path to a XML file thats lists the graphs contained in @p pathFolder that should be loaded. + * @param[in] node_type Select if nodes are labeled or unlabeled. + * @param[in] edge_type Select if edges are labeled or unlabeled. + */ + void loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type); + + /*! + * @brief Provides access to the IDs of the graphs contained in the environment. + * @return Pair (ID of first graphs, ID of last graph + 1) of graph IDs. + * If both entries equal 0, the environment does not contain any graphs. + */ + std::pair getGraphIds() const; + + /*! + * @brief Returns the list of graphs IDs which are loaded in the environment. + * @return A vector which contains all the graphs Ids. + */ + std::vector getAllGraphIds(); + + /*! + * @brief Returns the graph class. + * @param[in] id ID of an input graph that has been added to the environment. + * @return Class of the input graph. + */ + const std::string getGraphClass(std::size_t id) const; + + /*! + * @brief Returns the graph name. + * @param[in] id ID of an input graph that has been added to the environment. + * @return Name of the input graph. + */ + const std::string getGraphName(std::size_t id) const; + + /*! + * @brief Adds a new uninitialized graph to the environment. Call initEnv() after calling this method. + * @param[in] name The name of the added graph. Empty if not specified. + * @param[in] class The class of the added graph. Empty if not specified. + * @return The ID of the newly added graph. + */ + std::size_t addGraph(const std::string & graph_name, const std::string & graph_class); + + /*! + * @brief Adds a labeled node. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] nodeId The user-specific ID of the vertex that has to be added. + * @param[in] nodeLabel The label of the vertex that has to be added. + */ + void addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel); + + /*! + * @brief Adds a labeled edge. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] tail The user-specific ID of the tail of the edge that has to be added. + * @param[in] head The user-specific ID of the head of the edge that has to be added. + * @param[in] edgeLabel The label of the vertex that has to be added. + * @param[in] ignoreDuplicates If @p true, duplicate edges are ignores. Otherwise, an exception is thrown if an existing edge is added to the graph. + */ + void addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates = true); + + /*! + * @brief Clears and de-initializes a graph that has previously been added to the environment. Call initEnv() after calling this method. + * @param[in] graphId ID of graph that has to be cleared. + */ + void clearGraph(std::size_t graphId); + + /*! + * @brief Returns ged::ExchangeGraph representation. + * @param graphId ID of the selected graph. + * @return ged::ExchangeGraph representation of the selected graph. + */ + ged::ExchangeGraph getGraph(std::size_t graphId) const; + + /*! + * @brief Returns the internal Id of a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The internal ID of the selected graph + */ + std::size_t getGraphInternalId(std::size_t graphId); + + /*! + * @brief Returns all the number of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The number of nodes on the selected graph + */ + std::size_t getGraphNumNodes(std::size_t graphId); + + /*! + * @brief Returns all the number of edges on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The number of edges on the selected graph + */ + std::size_t getGraphNumEdges(std::size_t graphId); + + /*! + * @brief Returns all th Ids of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of IDs's nodes on the selected graph + */ + std::vector getGraphOriginalNodeIds(std::size_t graphId); + + /*! + * @brief Returns all the labels of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of labels's nodes on the selected graph + */ + std::vector> getGraphNodeLabels(std::size_t graphId); + + /*! + * @brief Returns all the edges on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of edges on the selected graph + */ + std::map, std::map> getGraphEdges(std::size_t graphId); + + /*! + * @brief Returns the adjacence list of a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The adjacence list of the selected graph + */ + std::vector> getGraphAdjacenceMatrix(std::size_t graphId); + + /*! + * @brief Sets the edit costs to one of the predefined edit costs. + * @param[in] editCost Select one of the predefined edit costs. + * @param[in] editCostConstants Parameters for the edit cost, empty by default. + */ + void setEditCost(std::string editCost, std::vector editCostConstants = {}); + + /*! + * @brief Sets the edit costs to a personal Edit Cost Class. + * @param[in] editCostConstants Parameters for the edit cost, empty by default. + * @note You have to add your class, which should inherit from EditCost class, in the function. After that, you can compile and use it in Python + */ + void setPersonalEditCost(std::vector editCostConstants = {}); + + /*! + * @brief Initializes the environment. + * @param[in] initOption Select initialization options. + * @param[in] print_to_stdout If set to @p true, the progress of the initialization is printed to std::out. + */ + void initEnv(std::string initOption = "EAGER_WITH_SHUFFLED_COPIES", bool print_to_stdout = false); + + /*! + * @brief Sets the GEDMethod to be used by run_method(). + * @param[in] method Select the method that is to be used. + * @param[in] options An options string of the form @"[--@ @] [...]@" passed to the selected method. + */ + void setMethod(std::string method, const std::string & options); + + /*! + * @brief Initializes the method specified by call to set_method(). + */ + void initMethod(); + + /*! + * @brief Returns initialization time. + * @return Runtime of the last call to init_method(). + */ + double getInitime() const; + + /*! + * @brief Runs the GED method specified by call to set_method() between the graphs with IDs @p g and @p h. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + */ + void runMethod(std::size_t g, std::size_t h); + + /*! + * @brief Returns upper bound for edit distance between the input graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Upper bound computed by the last call to run_method() with arguments @p g and @p h. + */ + double getUpperBound(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns lower bound for edit distance between the input graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Lower bound computed by the last call to run_method() with arguments @p g and @p h. + */ + double getLowerBound(std::size_t g,std::size_t h) const; + + /*! + * @brief Returns the forward map between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The forward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector getForwardMap(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns the backward map between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The backward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector getBackwardMap(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns image of a node. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @param[in] nodeId Node whose image is to be returned. + * @return Node to which node @p node is assigned. + */ + std::size_t getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const; + + /*! + * @brief Returns pre-image of a node. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @param[in] nodeId Node whose pre-image is to be returned. + * @return Node to which node @p node is assigned. + */ + std::size_t getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const; + + /*! + * @brief Returns the induced cost between the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The induced cost between the two indicated graphs. + */ + double getInducedCost(std::size_t g, std::size_t h) const; + + + /*! + * @brief Returns node map between the input graphs. This function duplicates datas. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Node map computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getNodeMap(std::size_t g, std::size_t h); + + /*! + * @brief Returns assignment matrix between the input graphs. This function duplicates datas. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Assignment matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getAssignmentMatrix(std::size_t g, std::size_t h); + + /*! + * @brief Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The forward and backward maps to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getAllMap(std::size_t g, std::size_t h); + + /*! + * @brief Returns runtime. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Runtime of last call to run_method() with arguments @p g and @p h. + */ + double getRuntime(std::size_t g, std::size_t h) const; + + /*! + * @brief Checks if the edit costs are quasimetric. + * @return Boolean @p true if the edit costs are quasimetric and @p false, otherwise. + */ + bool quasimetricCosts() const; + + /*! + * @brief Applies the hungarian algorithm (LSAP) to a matrix cost. + * @param[in] matrixCost The matrix cost. + * @return the values of rho, varrho, u and v, in this order. + */ + std::vector> hungarianLSAP(std::vector> matrixCost); + + /*! + * @brief Applies the hungarian algorithm (LSAPE) to a matrix cost. + * @param[in] matrixCost The matrix cost. + * @return the values of rho, varrho, u and v, in this order. + */ + std::vector> hungarianLSAPE(std::vector> matrixCost); + + /*! + * @brief Returns the number of node labels. + * @return Number of pairwise different node labels contained in the environment. + * @note If @p 1 is returned, the nodes are unlabeled. + */ + std::size_t getNumNodeLabels() const; + + /*! + * @brief Returns node label. + * @param[in] label_id ID of node label that should be returned. Must be between 1 and num_node_labels(). + * @return Node label for selected label ID. + */ + std::map getNodeLabel(std::size_t label_id) const; + + /*! + * @brief Returns the number of edge labels. + * @return Number of pairwise different edge labels contained in the environment. + * @note If @p 1 is returned, the edges are unlabeled. + */ + std::size_t getNumEdgeLabels() const; + + /*! + * @brief Returns edge label. + * @param[in] label_id ID of edge label that should be returned. Must be between 1 and num_node_labels(). + * @return Edge label for selected label ID. + */ + std::map getEdgeLabel(std::size_t label_id) const; + + // /*! + // * @brief Returns the number of nodes. + // * @param[in] graph_id ID of an input graph that has been added to the environment. + // * @return Number of nodes in the graph. + // */ + // std::size_t getNumNodes(std::size_t graph_id) const; + + /*! + * @brief Returns average number of nodes. + * @return Average number of nodes of the graphs contained in the environment. + */ + double getAvgNumNodes() const; + + /*! + * @brief Returns node relabeling cost. + * @param[in] node_label_1 First node label. + * @param[in] node_label_2 Second node label. + * @return Node relabeling cost for the given node labels. + */ + double getNodeRelCost(const std::map & node_label_1, const std::map & node_label_2) const; + + /*! + * @brief Returns node deletion cost. + * @param[in] node_label Node label. + * @return Cost of deleting node with given label. + */ + double getNodeDelCost(const std::map & node_label) const; + + /*! + * @brief Returns node insertion cost. + * @param[in] node_label Node label. + * @return Cost of inserting node with given label. + */ + double getNodeInsCost(const std::map & node_label) const; + + /*! + * @brief Computes median node label. + * @param[in] node_labels The node labels whose median should be computed. + * @return Median of the given node labels. + */ + std::map getMedianNodeLabel(const std::vector> & node_labels) const; + + /*! + * @brief Returns edge relabeling cost. + * @param[in] edge_label_1 First edge label. + * @param[in] edge_label_2 Second edge label. + * @return Edge relabeling cost for the given edge labels. + */ + double getEdgeRelCost(const std::map & edge_label_1, const std::map & edge_label_2) const; + + /*! + * @brief Returns edge deletion cost. + * @param[in] edge_label Edge label. + * @return Cost of deleting edge with given label. + */ + double getEdgeDelCost(const std::map & edge_label) const; + + /*! + * @brief Returns edge insertion cost. + * @param[in] edge_label Edge label. + * @return Cost of inserting edge with given label. + */ + double getEdgeInsCost(const std::map & edge_label) const; + + /*! + * @brief Computes median edge label. + * @param[in] edge_labels The edge labels whose median should be computed. + * @return Median of the given edge labels. + */ + std::map getMedianEdgeLabel(const std::vector> & edge_labels) const; + + /*! + * @brief Returns the initialization type of the last initialization. + * @return Initialization type in string. + */ + std::string getInitType() const; + + /*! + * @brief Computes the edit cost between two graphs induced by a node map. + * @param[in] g_id ID of input graph. + * @param[in] h_id ID of input graph. + * @return Computed induced cost. + */ + double computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const; + + // /*! + // * @brief Returns node relabeling, insertion, or deletion cost. + // * @param[in] label1 First node label. + // * @param[in] label2 Second node label. + // * @return Node relabeling cost if @p label1 and @p label2 are both different from ged::dummy_label(), + // * node insertion cost if @p label1 equals ged::dummy_label and @p label2 does not, + // * node deletion cost if @p label1 does not equal ged::dummy_label and @p label2 does, + // * and 0 otherwise. + // */ + // double getNodeCost(std::size_t label1, std::size_t label2) const; + + +private: + + ged::GEDEnv * env_; // environment variable + + bool initialized; // initialization boolean (because env has one but not accessible) + +}; // end of class PyGEDEnvGXL + +} // namespace pyged + +// According to some Chat AI, if implementation is in the .ipp file, it will be +// recognized by the compiler as inline code, so I can include it here and does not +// cause the multiple definition error. +// I will not use .cpp files to implement, it is a PAIN In The A to compile and link +// while avoiding multiple definition errors... +#include "gedlib_bind_gxl.ipp" + +//#endif /* SRC_GEDLIB_BIND_GXL_HPP */ \ No newline at end of file diff --git a/gklearn/gedlib/src/GedLibBind.ipp b/gklearn/gedlib/src/gedlib_bind_gxl.ipp old mode 100755 new mode 100644 similarity index 76% rename from gklearn/gedlib/src/GedLibBind.ipp rename to gklearn/gedlib/src/gedlib_bind_gxl.ipp index 2928e0bf63..6cb9cadf5e --- a/gklearn/gedlib/src/GedLibBind.ipp +++ b/gklearn/gedlib/src/gedlib_bind_gxl.ipp @@ -1,7 +1,7 @@ /**************************************************************************** * * - * Copyright (C) 2019-2020 by Natacha Lambert, David B. Blumenthal and * - * Linlin Jia * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * * * * This file should be used by Python. * * Please call the Python module if you want to use GedLib with this code.* @@ -11,17 +11,19 @@ ***************************************************************************/ /*! - * @file GedLibBind.ipp - * @brief Classe and function definitions to call easly GebLib in Python without Gedlib's types + * @file gedlib_bind_gxl.ipp + * @brief implementations of classes and functions to call easily GebLib in Python without Gedlib's types */ -#ifndef GEDLIBBIND_IPP -#define GEDLIBBIND_IPP +#pragma once +//#ifndef SRC_GEDLIB_BIND_GXL_IPP +//#define SRC_GEDLIB_BIND_GXL_IPP //Include standard libraries + GedLib library // #include // #include "GedLibBind.h" // #include "../include/gedlib-master/src/env/ged_env.hpp" //#include "../include/gedlib-master/median/src/median_graph_estimator.hpp" +// #include "gedlib_bind_gxl.hpp" using namespace std; @@ -34,7 +36,9 @@ using namespace std; namespace pyged { - + +//extern template class ged::GEDEnv; + //!< List of available edit cost functions readable by Python. std::vector editCostStringOptions = { "CHEM_1", @@ -276,25 +280,26 @@ std::string toStringVectorInt(std::vector vector) { } -PyGEDEnv::PyGEDEnv () { +PyGEDEnvGXL::PyGEDEnvGXL () : env_(nullptr), initialized(false) { env_ = new ged::GEDEnv(); - this->initialized = false; } -PyGEDEnv::~PyGEDEnv () { +PyGEDEnvGXL::~PyGEDEnvGXL () { if (env_ != NULL) { - delete env_; + delete env_; env_ = NULL; - } + } } +// ======== Environment Public APIs ======== + // bool initialized = false; //Initialization boolean (because Env has one but not accessible). -bool PyGEDEnv::isInitialized() { +bool PyGEDEnvGXL::isInitialized() { return initialized; } -void PyGEDEnv::restartEnv() { +void PyGEDEnvGXL::restartEnv() { if (env_ != NULL) { delete env_; env_ = NULL; @@ -303,18 +308,18 @@ void PyGEDEnv::restartEnv() { initialized = false; } -void PyGEDEnv::loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type) { - std::vector tmp_graph_ids(env_->load_gxl_graph(pathFolder, pathXML, +void PyGEDEnvGXL::loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type) { + std::vector tmp_graph_ids(env_->load_gxl_graph(pathFolder, pathXML, (node_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), (edge_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), std::unordered_set(), std::unordered_set())); } -std::pair PyGEDEnv::getGraphIds() const { +std::pair PyGEDEnvGXL::getGraphIds() const { return env_->graph_ids(); } -std::vector PyGEDEnv::getAllGraphIds() { +std::vector PyGEDEnvGXL::getAllGraphIds() { std::vector listID; for (std::size_t i = env_->graph_ids().first; i != env_->graph_ids().second; i++) { listID.push_back(i); @@ -322,21 +327,21 @@ std::vector PyGEDEnv::getAllGraphIds() { return listID; } -const std::string PyGEDEnv::getGraphClass(std::size_t id) const { +const std::string PyGEDEnvGXL::getGraphClass(std::size_t id) const { return env_->get_graph_class(id); } -const std::string PyGEDEnv::getGraphName(std::size_t id) const { +const std::string PyGEDEnvGXL::getGraphName(std::size_t id) const { return env_->get_graph_name(id); } -std::size_t PyGEDEnv::addGraph(const std::string & graph_name, const std::string & graph_class) { +std::size_t PyGEDEnvGXL::addGraph(const std::string & graph_name, const std::string & graph_class) { ged::GEDGraph::GraphID newId = env_->add_graph(graph_name, graph_class); initialized = false; return std::stoi(std::to_string(newId)); } -void PyGEDEnv::addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel) { +void PyGEDEnvGXL::addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel) { env_->add_node(graphId, nodeId, nodeLabel); initialized = false; } @@ -345,111 +350,111 @@ void PyGEDEnv::addNode(std::size_t graphId, const std::string & nodeId, const st env_->add_edge(graphId, tail, head, edgeLabel); }*/ -void PyGEDEnv::addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates) { +void PyGEDEnvGXL::addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates) { env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); initialized = false; } -void PyGEDEnv::clearGraph(std::size_t graphId) { +void PyGEDEnvGXL::clearGraph(std::size_t graphId) { env_->clear_graph(graphId); initialized = false; } -ged::ExchangeGraph PyGEDEnv::getGraph(std::size_t graphId) const { +ged::ExchangeGraph PyGEDEnvGXL::getGraph(std::size_t graphId) const { return env_->get_graph(graphId); } -std::size_t PyGEDEnv::getGraphInternalId(std::size_t graphId) { +std::size_t PyGEDEnvGXL::getGraphInternalId(std::size_t graphId) { return getGraph(graphId).id; } -std::size_t PyGEDEnv::getGraphNumNodes(std::size_t graphId) { +std::size_t PyGEDEnvGXL::getGraphNumNodes(std::size_t graphId) { return getGraph(graphId).num_nodes; } -std::size_t PyGEDEnv::getGraphNumEdges(std::size_t graphId) { +std::size_t PyGEDEnvGXL::getGraphNumEdges(std::size_t graphId) { return getGraph(graphId).num_edges; } -std::vector PyGEDEnv::getGraphOriginalNodeIds(std::size_t graphId) { +std::vector PyGEDEnvGXL::getGraphOriginalNodeIds(std::size_t graphId) { return getGraph(graphId).original_node_ids; } -std::vector> PyGEDEnv::getGraphNodeLabels(std::size_t graphId) { +std::vector> PyGEDEnvGXL::getGraphNodeLabels(std::size_t graphId) { return getGraph(graphId).node_labels; } -std::map, std::map> PyGEDEnv::getGraphEdges(std::size_t graphId) { +std::map, std::map> PyGEDEnvGXL::getGraphEdges(std::size_t graphId) { return getGraph(graphId).edge_labels; } -std::vector> PyGEDEnv::getGraphAdjacenceMatrix(std::size_t graphId) { +std::vector> PyGEDEnvGXL::getGraphAdjacenceMatrix(std::size_t graphId) { return getGraph(graphId).adj_matrix; } -void PyGEDEnv::setEditCost(std::string editCost, std::vector editCostConstants) { +void PyGEDEnvGXL::setEditCost(std::string editCost, std::vector editCostConstants) { env_->set_edit_costs(translateEditCost(editCost), editCostConstants); } -void PyGEDEnv::setPersonalEditCost(std::vector editCostConstants) { +void PyGEDEnvGXL::setPersonalEditCost(std::vector editCostConstants) { //env_->set_edit_costs(Your EditCost Class(editCostConstants)); } -// void PyGEDEnv::initEnv() { +// void PyGEDEnvGXL::initEnv() { // env_->init(); // initialized = true; // } -void PyGEDEnv::initEnv(std::string initOption, bool print_to_stdout) { +void PyGEDEnvGXL::initEnv(std::string initOption, bool print_to_stdout) { env_->init(translateInitOptions(initOption), print_to_stdout); initialized = true; } -void PyGEDEnv::setMethod(std::string method, const std::string & options) { +void PyGEDEnvGXL::setMethod(std::string method, const std::string & options) { env_->set_method(translateMethod(method), options); } -void PyGEDEnv::initMethod() { +void PyGEDEnvGXL::initMethod() { env_->init_method(); } -double PyGEDEnv::getInitime() const { +double PyGEDEnvGXL::getInitime() const { return env_->get_init_time(); } -void PyGEDEnv::runMethod(std::size_t g, std::size_t h) { +void PyGEDEnvGXL::runMethod(std::size_t g, std::size_t h) { env_->run_method(g, h); } -double PyGEDEnv::getUpperBound(std::size_t g, std::size_t h) const { +double PyGEDEnvGXL::getUpperBound(std::size_t g, std::size_t h) const { return env_->get_upper_bound(g, h); } -double PyGEDEnv::getLowerBound(std::size_t g, std::size_t h) const { +double PyGEDEnvGXL::getLowerBound(std::size_t g, std::size_t h) const { return env_->get_lower_bound(g, h); } -std::vector PyGEDEnv::getForwardMap(std::size_t g, std::size_t h) const { +std::vector PyGEDEnvGXL::getForwardMap(std::size_t g, std::size_t h) const { return env_->get_node_map(g, h).get_forward_map(); } -std::vector PyGEDEnv::getBackwardMap(std::size_t g, std::size_t h) const { +std::vector PyGEDEnvGXL::getBackwardMap(std::size_t g, std::size_t h) const { return env_->get_node_map(g, h).get_backward_map(); } -std::size_t PyGEDEnv::getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const { +std::size_t PyGEDEnvGXL::getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const { return env_->get_node_map(g, h).image(nodeId); } -std::size_t PyGEDEnv::getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const { +std::size_t PyGEDEnvGXL::getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const { return env_->get_node_map(g, h).pre_image(nodeId); } -double PyGEDEnv::getInducedCost(std::size_t g, std::size_t h) const { +double PyGEDEnvGXL::getInducedCost(std::size_t g, std::size_t h) const { return env_->get_node_map(g, h).induced_cost(); } -std::vector> PyGEDEnv::getNodeMap(std::size_t g, std::size_t h) { +std::vector> PyGEDEnvGXL::getNodeMap(std::size_t g, std::size_t h) { std::vector> res; std::vector relation; env_->get_node_map(g, h).as_relation(relation); @@ -459,7 +464,7 @@ std::vector> PyGEDEnv::getNodeMap(std::size_t g, return res; } -std::vector> PyGEDEnv::getAssignmentMatrix(std::size_t g, std::size_t h) { +std::vector> PyGEDEnvGXL::getAssignmentMatrix(std::size_t g, std::size_t h) { std::vector> res; for(std::size_t i = 0; i != getForwardMap(g, h).size(); i++) { std::vector newLine; @@ -494,22 +499,22 @@ std::vector> PyGEDEnv::getAssignmentMatrix(std::size_t g, std:: return res; } -std::vector> PyGEDEnv::getAllMap(std::size_t g, std::size_t h) { +std::vector> PyGEDEnvGXL::getAllMap(std::size_t g, std::size_t h) { std::vector> res; res.push_back(getForwardMap(g, h)); res.push_back(getBackwardMap(g,h)); return res; } -double PyGEDEnv::getRuntime(std::size_t g, std::size_t h) const { +double PyGEDEnvGXL::getRuntime(std::size_t g, std::size_t h) const { return env_->get_runtime(g, h); } -bool PyGEDEnv::quasimetricCosts() const { +bool PyGEDEnvGXL::quasimetricCosts() const { return env_->quasimetric_costs(); } -std::vector> PyGEDEnv::hungarianLSAP(std::vector> matrixCost) { +std::vector> PyGEDEnvGXL::hungarianLSAP(std::vector> matrixCost) { std::size_t nrows = matrixCost.size(); std::size_t ncols = matrixCost[0].size(); std::size_t *rho = new std::size_t[nrows], *varrho = new std::size_t[ncols]; @@ -530,7 +535,7 @@ std::vector> PyGEDEnv::hungarianLSAP(std::vector> PyGEDEnv::hungarianLSAPE(std::vector> matrixCost) { +std::vector> PyGEDEnvGXL::hungarianLSAPE(std::vector> matrixCost) { std::size_t nrows = matrixCost.size(); std::size_t ncols = matrixCost[0].size(); std::size_t *rho = new std::size_t[nrows-1], *varrho = new std::size_t[ncols-1]; @@ -550,67 +555,67 @@ std::vector> PyGEDEnv::hungarianLSAPE(std::vectornum_node_labels(); } -std::map PyGEDEnv::getNodeLabel(std::size_t label_id) const { +std::map PyGEDEnvGXL::getNodeLabel(std::size_t label_id) const { return env_->get_node_label(label_id); } -std::size_t PyGEDEnv::getNumEdgeLabels() const { +std::size_t PyGEDEnvGXL::getNumEdgeLabels() const { return env_->num_edge_labels(); } -std::map PyGEDEnv::getEdgeLabel(std::size_t label_id) const { +std::map PyGEDEnvGXL::getEdgeLabel(std::size_t label_id) const { return env_->get_edge_label(label_id); } -// std::size_t PyGEDEnv::getNumNodes(std::size_t graph_id) const { +// std::size_t PyGEDEnvGXL::getNumNodes(std::size_t graph_id) const { // return env_->get_num_nodes(graph_id); // } -double PyGEDEnv::getAvgNumNodes() const { +double PyGEDEnvGXL::getAvgNumNodes() const { return env_->get_avg_num_nodes(); } -double PyGEDEnv::getNodeRelCost(const std::map & node_label_1, const std::map & node_label_2) const { +double PyGEDEnvGXL::getNodeRelCost(const std::map & node_label_1, const std::map & node_label_2) const { return env_->node_rel_cost(node_label_1, node_label_2); } -double PyGEDEnv::getNodeDelCost(const std::map & node_label) const { +double PyGEDEnvGXL::getNodeDelCost(const std::map & node_label) const { return env_->node_del_cost(node_label); } -double PyGEDEnv::getNodeInsCost(const std::map & node_label) const { +double PyGEDEnvGXL::getNodeInsCost(const std::map & node_label) const { return env_->node_ins_cost(node_label); } -std::map PyGEDEnv::getMedianNodeLabel(const std::vector> & node_labels) const { +std::map PyGEDEnvGXL::getMedianNodeLabel(const std::vector> & node_labels) const { return env_->median_node_label(node_labels); } -double PyGEDEnv::getEdgeRelCost(const std::map & edge_label_1, const std::map & edge_label_2) const { +double PyGEDEnvGXL::getEdgeRelCost(const std::map & edge_label_1, const std::map & edge_label_2) const { return env_->edge_rel_cost(edge_label_1, edge_label_2); } -double PyGEDEnv::getEdgeDelCost(const std::map & edge_label) const { +double PyGEDEnvGXL::getEdgeDelCost(const std::map & edge_label) const { return env_->edge_del_cost(edge_label); } -double PyGEDEnv::getEdgeInsCost(const std::map & edge_label) const { +double PyGEDEnvGXL::getEdgeInsCost(const std::map & edge_label) const { return env_->edge_ins_cost(edge_label); } -std::map PyGEDEnv::getMedianEdgeLabel(const std::vector> & edge_labels) const { +std::map PyGEDEnvGXL::getMedianEdgeLabel(const std::vector> & edge_labels) const { return env_->median_edge_label(edge_labels); } -std::string PyGEDEnv::getInitType() const { +std::string PyGEDEnvGXL::getInitType() const { return initOptionsToString(env_->get_init_type()); } -double PyGEDEnv::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const { +double PyGEDEnvGXL::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const { ged::NodeMap node_map = ged::NodeMap(env_->get_num_nodes(g_id), env_->get_num_nodes(h_id)); for (const auto & assignment : relation) { node_map.add_assignment(assignment.first, assignment.second); @@ -642,7 +647,7 @@ double PyGEDEnv::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vec -// double PyGEDEnv::getNodeCost(std::size_t label1, std::size_t label2) const { +// double PyGEDEnvGXL::getNodeCost(std::size_t label1, std::size_t label2) const { // return env_->ged_data_node_cost(label1, label2); // } @@ -681,9 +686,9 @@ double PyGEDEnv::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vec save_letter_graph_as_tikz_file(env_->get_graph(median_id), tikz_file_name);*/ //} -} +} // namespace pyged -#endif /* SRC_GEDLIB_BIND_IPP */ +//#endif /* SRC_GEDLIB_BIND_GXL_IPP */ // namespace shapes { diff --git a/gklearn/gedlib/src/single_so_version_bk/gedlib_bind.hpp b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind.hpp new file mode 100644 index 0000000000..303acd4d96 --- /dev/null +++ b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind.hpp @@ -0,0 +1,21 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind.hpp + * @brief header file for GedLibBind class and function declarations to call easily GedLib in Python + */ +#pragma once + +#include "gedlib_bind_util.hpp" +#include "gedlib_bind_gxl.hpp" +#include "gedlib_bind_attr.hpp" \ No newline at end of file diff --git a/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_attr.cpp b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_attr.cpp new file mode 100644 index 0000000000..fa01811ff7 --- /dev/null +++ b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_attr.cpp @@ -0,0 +1,605 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_attr.cpp + * @brief Implementations of classes and functions to call easily GebLib in Python without Gedlib's types + */ +#pragma once +//#ifndef GEDLIBBIND_ATTR_IPP +//#define GEDLIBBIND_ATTR_IPP + +//Include standard libraries + GedLib library +// #include +// #include "GedLibBind.h" +// #include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/median/src/median_graph_estimator.hpp" +#include "gedlib_bind_attr.hpp" + +using namespace std; + +//Definition of types and templates used in this code for my human's memory :). +//ged::GEDEnv env; +//template struct ExchangeGraph + +//typedef std::map GXLLabel; +//typedef std::string GXLNodeID; + + +namespace pyged { + +PyGEDEnvAttr::PyGEDEnvAttr () : env_(nullptr), initialized(false) { + env_ = new ged::GEDEnv(); +} + +PyGEDEnvAttr::~PyGEDEnvAttr () { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } +} + +// ======== Environment Public APIs ======== + +// bool initialized = false; //Initialization boolean (because Env has one but not accessible). + +bool PyGEDEnvAttr::isInitialized() { + return initialized; +} + +void PyGEDEnvAttr::restartEnv() { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } + env_ = new ged::GEDEnv(); + initialized = false; +} + +void PyGEDEnvAttr::loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type) { + std::vector tmp_graph_ids(env_->load_gxl_graph(pathFolder, pathXML, + (node_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), + (edge_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), + std::unordered_set(), std::unordered_set())); +} + +std::pair PyGEDEnvAttr::getGraphIds() const { + return env_->graph_ids(); +} + +std::vector PyGEDEnvAttr::getAllGraphIds() { + std::vector listID; + for (std::size_t i = env_->graph_ids().first; i != env_->graph_ids().second; i++) { + listID.push_back(i); + } + return listID; +} + +const std::string PyGEDEnvAttr::getGraphClass(std::size_t id) const { + return env_->get_graph_class(id); +} + +const std::string PyGEDEnvAttr::getGraphName(std::size_t id) const { + return env_->get_graph_name(id); +} + +std::size_t PyGEDEnvAttr::addGraph(const std::string & graph_name, const std::string & graph_class) { + ged::GEDGraph::GraphID newId = env_->add_graph(graph_name, graph_class); + initialized = false; + return std::stoi(std::to_string(newId)); +} + +// void PyGEDEnvAttr::addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel) { +// // todo: if this needs to be supported, we need to convert the string map to an AttrLabel +// env_->add_node(graphId, nodeId, nodeLabel); +// initialized = false; +// } + +void PyGEDEnvAttr::addNode( + std::size_t graphId, + const std::string& nodeId, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { + // fixme: debug test only: + std::cout << "The node labels received by the c++ bindings are: " << std::endl; + printLabelMaps(str_map, int_map, float_map, list_str_map, list_int_map, list_float_map); + + // Merge the maps into AttrLabel: + ged::AttrLabel nodeLabel = PyGEDEnvAttr::constructAttrLabelFromMaps( + str_map, + int_map, + float_map, + list_str_map, + list_int_map, + list_float_map + ); + + std::cout << "The node label passed to c++ env is: " << std::endl; + printAttrLabel(nodeLabel); + + env_->add_node(graphId, nodeId, nodeLabel); + initialized = false; +} + +/*void addEdge(std::size_t graphId, ged::GXLNodeID tail, ged::GXLNodeID head, ged::GXLLabel edgeLabel) { + env_->add_edge(graphId, tail, head, edgeLabel); +}*/ + +// void PyGEDEnvAttr::addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates) { +// // todo: if this needs to be supported, we need to convert the string map to an AttrLabel +// env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); +// initialized = false; +// } + +void PyGEDEnvAttr::addEdge( + std::size_t graphId, + const std::string& tail, + const std::string& head, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map, + bool ignoreDuplicates +) { + // fixme: debug test only: + std::cout << "The edge labels received by the c++ bindings are: " << std::endl; + printLabelMaps(str_map, int_map, float_map, list_str_map, list_int_map, list_float_map); + + // Merge the maps into AttrLabel: + ged::AttrLabel edgeLabel = PyGEDEnvAttr::constructAttrLabelFromMaps( + str_map, + int_map, + float_map, + list_str_map, + list_int_map, + list_float_map + ); + + std::cout << "The edge label passed to c++ env is: " << std::endl; + printAttrLabel(edgeLabel); + + env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); + initialized = false; +} + +void PyGEDEnvAttr::clearGraph(std::size_t graphId) { + env_->clear_graph(graphId); + initialized = false; +} + +// todo: check if ExchangeGraph supports AttrLabel +ged::ExchangeGraph PyGEDEnvAttr::getGraph(std::size_t graphId) const { +// static_assert(std::is_same_v< +// decltype(env_->get_graph(graphId)), +// ged::ExchangeGraph +// >, "get_graph() 返回的不是 AttrLabel 类型"); +// std::cout << "get_graph() 返回的是 AttrLabel 类型: " << std::endl; + + + return env_->get_graph(graphId); +} + +std::size_t PyGEDEnvAttr::getGraphInternalId(std::size_t graphId) { + return getGraph(graphId).id; +} + +std::size_t PyGEDEnvAttr::getGraphNumNodes(std::size_t graphId) { + return getGraph(graphId).num_nodes; +} + +std::size_t PyGEDEnvAttr::getGraphNumEdges(std::size_t graphId) { + return getGraph(graphId).num_edges; +} + +std::vector PyGEDEnvAttr::getGraphOriginalNodeIds(std::size_t graphId) { + return getGraph(graphId).original_node_ids; +} + +std::vector PyGEDEnvAttr::getGraphNodeLabels(std::size_t graphId) { + return getGraph(graphId).node_labels; +} + +std::map, ged::AttrLabel> PyGEDEnvAttr::getGraphEdges(std::size_t graphId) { + return getGraph(graphId).edge_labels; +} + +std::vector> PyGEDEnvAttr::getGraphAdjacenceMatrix(std::size_t graphId) { + return getGraph(graphId).adj_matrix; +} + +void PyGEDEnvAttr::setEditCost(std::string editCost, std::vector editCostConstants) { + env_->set_edit_costs(translateEditCost(editCost), editCostConstants); +} + +void PyGEDEnvAttr::setPersonalEditCost(std::vector editCostConstants) { + //env_->set_edit_costs(Your EditCost Class(editCostConstants)); +} + +// void PyGEDEnvAttr::initEnv() { +// env_->init(); +// initialized = true; +// } + +void PyGEDEnvAttr::initEnv(std::string initOption, bool print_to_stdout) { + env_->init(translateInitOptions(initOption), print_to_stdout); + initialized = true; +} + +void PyGEDEnvAttr::setMethod(std::string method, const std::string & options) { + env_->set_method(translateMethod(method), options); +} + +void PyGEDEnvAttr::initMethod() { + env_->init_method(); +} + +double PyGEDEnvAttr::getInitime() const { + return env_->get_init_time(); +} + +void PyGEDEnvAttr::runMethod(std::size_t g, std::size_t h) { + env_->run_method(g, h); +} + +double PyGEDEnvAttr::getUpperBound(std::size_t g, std::size_t h) const { + return env_->get_upper_bound(g, h); +} + +double PyGEDEnvAttr::getLowerBound(std::size_t g, std::size_t h) const { + return env_->get_lower_bound(g, h); +} + +std::vector PyGEDEnvAttr::getForwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_forward_map(); +} + +std::vector PyGEDEnvAttr::getBackwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_backward_map(); +} + +std::size_t PyGEDEnvAttr::getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).image(nodeId); +} + +std::size_t PyGEDEnvAttr::getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).pre_image(nodeId); +} + +double PyGEDEnvAttr::getInducedCost(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).induced_cost(); +} + +std::vector> PyGEDEnvAttr::getNodeMap(std::size_t g, std::size_t h) { + std::vector> res; + std::vector relation; + env_->get_node_map(g, h).as_relation(relation); + for (const auto & assignment : relation) { + res.push_back(std::make_pair(assignment.first, assignment.second)); + } + return res; +} + +std::vector> PyGEDEnvAttr::getAssignmentMatrix(std::size_t g, std::size_t h) { + std::vector> res; + for(std::size_t i = 0; i != getForwardMap(g, h).size(); i++) { + std::vector newLine; + bool have1 = false; + for(std::size_t j = 0; j != getBackwardMap(g, h).size(); j++) { + if (getNodeImage(g, h, i) == j) { + newLine.push_back(1); + have1 = true; + } + else{ + newLine.push_back(0); + } + } + if(have1) { + newLine.push_back(0); + } + else{ + newLine.push_back(1); + } + res.push_back(newLine); + } + std::vector lastLine; + for (size_t k = 0; k != getBackwardMap(g,h).size(); k++) { + if (getBackwardMap(g,h)[k] == ged::GEDGraph::dummy_node()) { + lastLine.push_back(1); + } + else{ + lastLine.push_back(0); + } + } + res.push_back(lastLine); + return res; +} + +std::vector> PyGEDEnvAttr::getAllMap(std::size_t g, std::size_t h) { + std::vector> res; + res.push_back(getForwardMap(g, h)); + res.push_back(getBackwardMap(g,h)); + return res; +} + +double PyGEDEnvAttr::getRuntime(std::size_t g, std::size_t h) const { + return env_->get_runtime(g, h); +} + +bool PyGEDEnvAttr::quasimetricCosts() const { + return env_->quasimetric_costs(); +} + +std::vector> PyGEDEnvAttr::hungarianLSAP(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows], *varrho = new std::size_t[ncols]; + std::size_t *u = new std::size_t[nrows], *v = new std::size_t[ncols]; + std::size_t *C = new std::size_t[nrows*ncols]; + // std::size_t i = 0, j; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAP(C,nrows,ncols,rho,u,v,varrho); + std::vector> res; + res.push_back(translatePointer(rho, nrows)); + res.push_back(translatePointer(varrho, ncols)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::vector> PyGEDEnvAttr::hungarianLSAPE(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows-1], *varrho = new std::size_t[ncols-1]; + double *u = new double[nrows], *v = new double[ncols]; + double *C = new double[nrows*ncols]; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAPE(C,nrows,ncols,rho,varrho,u,v); + std::vector> res; + res.push_back(translateAndConvertPointer(rho, nrows-1)); + res.push_back(translateAndConvertPointer(varrho, ncols-1)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::size_t PyGEDEnvAttr::getNumGraphs() const { + return env_->num_graphs(); +} + +std::size_t PyGEDEnvAttr::getNumNodeLabels() const { + return env_->num_node_labels(); +} + +ged::AttrLabel PyGEDEnvAttr::getNodeLabel(std::size_t label_id) const { + return env_->get_node_label(label_id); +} + +std::size_t PyGEDEnvAttr::getNumEdgeLabels() const { + return env_->num_edge_labels(); +} + +ged::AttrLabel PyGEDEnvAttr::getEdgeLabel(std::size_t label_id) const { + return env_->get_edge_label(label_id); +} + +// std::size_t PyGEDEnvAttr::getNumNodes(std::size_t graph_id) const { +// return env_->get_num_nodes(graph_id); +// } + +double PyGEDEnvAttr::getAvgNumNodes() const { + return env_->get_avg_num_nodes(); +} + +double PyGEDEnvAttr::getNodeRelCost(const ged::AttrLabel & node_label_1, const ged::AttrLabel & node_label_2) const { + return env_->node_rel_cost(node_label_1, node_label_2); +} + +double PyGEDEnvAttr::getNodeDelCost(const ged::AttrLabel & node_label) const { + return env_->node_del_cost(node_label); +} + +double PyGEDEnvAttr::getNodeInsCost(const ged::AttrLabel & node_label) const { + return env_->node_ins_cost(node_label); +} + +ged::AttrLabel PyGEDEnvAttr::getMedianNodeLabel(const std::vector & node_labels) const { + return env_->median_node_label(node_labels); +} + +double PyGEDEnvAttr::getEdgeRelCost(const ged::AttrLabel & edge_label_1, const ged::AttrLabel & edge_label_2) const { + return env_->edge_rel_cost(edge_label_1, edge_label_2); +} + +double PyGEDEnvAttr::getEdgeDelCost(const ged::AttrLabel & edge_label) const { + return env_->edge_del_cost(edge_label); +} + +double PyGEDEnvAttr::getEdgeInsCost(const ged::AttrLabel & edge_label) const { + return env_->edge_ins_cost(edge_label); +} + +ged::AttrLabel PyGEDEnvAttr::getMedianEdgeLabel(const std::vector & edge_labels) const { + return env_->median_edge_label(edge_labels); +} + +std::string PyGEDEnvAttr::getInitType() const { + return initOptionsToString(env_->get_init_type()); +} + +double PyGEDEnvAttr::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const { + ged::NodeMap node_map = ged::NodeMap(env_->get_num_nodes(g_id), env_->get_num_nodes(h_id)); + for (const auto & assignment : relation) { + node_map.add_assignment(assignment.first, assignment.second); + // std::cout << assignment.first << assignment.second << endl; + } + const std::vector forward_map = node_map.get_forward_map(); + for (std::size_t i{0}; i < node_map.num_source_nodes(); i++) { + if (forward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(i, ged::GEDGraph::dummy_node()); + } + } + const std::vector backward_map = node_map.get_backward_map(); + for (std::size_t i{0}; i < node_map.num_target_nodes(); i++) { + if (backward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(ged::GEDGraph::dummy_node(), i); + } + } + // for (auto & map : node_map.get_forward_map()) { + // std::cout << map << ", "; + // } + // std::cout << endl; + // for (auto & map : node_map.get_backward_map()) { + // std::cout << map << ", "; + // } + env_->compute_induced_cost(g_id, h_id, node_map); + return node_map.induced_cost(); +} + + +ged::AttrLabel +PyGEDEnvAttr::constructAttrLabelFromMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { + // using ged::AttrLabel = std::unordered_map, std::vector, std::vector>>; + ged::AttrLabel attr_label; + for (const auto& pair : str_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : int_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : float_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_str_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_int_map) { + attr_label[pair.first] = pair.second; + } + for (const auto& pair : list_float_map) { + attr_label[pair.first] = pair.second; + } + return attr_label; +} + + +void printLabelMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +) { + // Print the label maps for debugging purposes + std::cout << "String map: "; + for (const auto& pair : str_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nInt map: "; + for (const auto& pair : int_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nFloat map: "; + for (const auto& pair : float_map) { + std::cout << pair.first << ": " << pair.second << ", "; + } + std::cout << "\nList of strings map: "; + for (const auto& pair : list_str_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + std::cout << "\nList of ints map: "; + for (const auto& pair : list_int_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + std::cout << "\nList of floats map: "; + for (const auto& pair : list_float_map) { + std::cout << pair.first << ": ["; + for (const auto& item : pair.second) { + std::cout << item << ", "; + } + std::cout << "], "; + } + + std::cout << std::endl; + +} + +void printAttrLabel(const ged::AttrLabel & attr_label) { + std::cout << "AttrLabel: "; + for (const auto& pair : attr_label) { + std::cout << pair.first << ": "; + if (std::holds_alternative(pair.second)) { + std::cout << std::get(pair.second); + } else if (std::holds_alternative(pair.second)) { + std::cout << std::get(pair.second); + } else if (std::holds_alternative(pair.second)) { + std::cout << std::get(pair.second); + } else if (std::holds_alternative>(pair.second)) { + const auto& vec = std::get>(pair.second); + std::cout << "["; + for (const auto& item : vec) { + std::cout << item << ", "; + } + std::cout << "]"; + } else if (std::holds_alternative>(pair.second)) { + const auto& vec = std::get>(pair.second); + std::cout << "["; + for (const auto& item : vec) { + std::cout << item << ", "; + } + std::cout << "]"; + } else if (std::holds_alternative>(pair.second)) { + const auto& vec = std::get>(pair.second); + std::cout << "["; + for (const auto& item : vec) { + std::cout << item << ", "; + } + std::cout << "]"; + } + std::cout << ", "; + } + std::cout << std::endl; +} + +} // namespace pyged + +//#endif /* SRC_GEDLIB_BIND_ATTR_IPP */ diff --git a/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_attr.hpp b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_attr.hpp new file mode 100644 index 0000000000..61c5db9175 --- /dev/null +++ b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_attr.hpp @@ -0,0 +1,580 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_attr.hpp + * @brief Class and function declarations to call easily GebLib in Python without Gedlib's types + * with the AttrLabel. + * @todo: it is better to refactor it along with GedLibBindGXL.hpp or use pybind11. + */ +#pragma once +//#ifndef GEDLIBBIND_ATTR_HPP +//#define GEDLIBBIND_ATTR_HPP + +//Include standard libraries. +#include +#include +#include +#include +#include +#include +//#include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/src/env/node_map.hpp" +//#include "../include/gedlib-master/src/env/common_types.hpp" +#include "gedlib_header.hpp" +#include "gedlib_bind_util.hpp" + + +/*! + * @namespace pyged + * @brief Global namespace for gedlibpy. + */ +namespace pyged { + +/*! +* @brief Provides the API of GEDLIB for Python. +*/ +class PyGEDEnvAttr { + + +public: + + /*! + * @brief Constructor. + */ + PyGEDEnvAttr(); + + // PyGEDEnvAttr(); + + /*! + * @brief Destructor. + */ + ~PyGEDEnvAttr(); + + // ======== Environment Public APIs ======== + + /*! + * @brief Tests if the environment is initialized or not. + * @return Boolean @p true if the environment is initialized and @p false otherwise. + */ + bool isInitialized(); + + /*! + * @brief Restart the environment (recall a new empty environment). + */ + void restartEnv(); + + /*! + * @brief Loads graph given in the [GXL file format](http://www.gupro.de/GXL/). + * @param[in] pathFolder The path to the directory containing the graphs. + * @param[in] pathXML The path to a XML file thats lists the graphs contained in @p pathFolder that should be loaded. + * @param[in] node_type Select if nodes are labeled or unlabeled. + * @param[in] edge_type Select if edges are labeled or unlabeled. + */ + void loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type); + + /*! + * @brief Provides access to the IDs of the graphs contained in the environment. + * @return Pair (ID of first graphs, ID of last graph + 1) of graph IDs. + * If both entries equal 0, the environment does not contain any graphs. + */ + std::pair getGraphIds() const; + + /*! + * @brief Returns the list of graphs IDs which are loaded in the environment. + * @return A vector which contains all the graphs Ids. + */ + std::vector getAllGraphIds(); + + /*! + * @brief Returns the graph class. + * @param[in] id ID of an input graph that has been added to the environment. + * @return Class of the input graph. + */ + const std::string getGraphClass(std::size_t id) const; + + /*! + * @brief Returns the graph name. + * @param[in] id ID of an input graph that has been added to the environment. + * @return Name of the input graph. + */ + const std::string getGraphName(std::size_t id) const; + + /*! + * @brief Adds a new uninitialized graph to the environment. Call initEnv() after calling this method. + * @param[in] name The name of the added graph. Empty if not specified. + * @param[in] class The class of the added graph. Empty if not specified. + * @return The ID of the newly added graph. + */ + std::size_t addGraph(const std::string & graph_name, const std::string & graph_class); + +// /*! +// * @brief Adds a labeled node. +// * @param[in] graphId ID of graph that has been added to the environment. +// * @param[in] nodeId The user-specific ID of the vertex that has to be added. +// * @param[in] nodeLabel The label of the vertex that has to be added. Only supports string labels. +// */ +// void addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel); + + /*! + * @brief Adds a labeled node. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] nodeId The user-specific ID of the vertex that has to be added. + * @param[in] str_map The string attributes of the node. + * @param[in] int_map The integer attributes of the node. + * @param[in] float_map The float attributes of the node. + * @param[in] list_int_map The list of integer attributes of the node. + * @param[in] list_float_map The list of float attributes of the node. + * @param[in] list_str_map The list of string attributes of the node. + */ + void addNode( + std::size_t graphId, + const std::string& nodeId, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map + ); + +// /*! +// * @brief Adds a labeled edge. +// * @param[in] graphId ID of graph that has been added to the environment. +// * @param[in] tail The user-specific ID of the tail of the edge that has to be added. +// * @param[in] head The user-specific ID of the head of the edge that has to be added. +// * @param[in] edgeLabel The label of the vertex that has to be added. Only supports string labels. +// * @param[in] ignoreDuplicates If @p true, duplicate edges are ignores. Otherwise, an exception is thrown if an existing edge is added to the graph. +// */ +// void addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates = true); + + /*! + * @brief Adds a labeled edge. + * @param[in] graphId ID of graph that has been added to the environment. + * @param[in] tail The user-specific ID of the tail of the edge that has to be added. + * @param[in] head The user-specific ID of the head of the edge that has to be added. + * @param[in] str_map The string attributes of the edge. + * @param[in] int_map The integer attributes of the edge. + * @param[in] float_map The float attributes of the edge. + * @param[in] list_int_map The list of integer attributes of the edge. + * @param[in] list_float_map The list of float attributes of the edge. + * @param[in] list_str_map The list of string attributes of the edge. + * @param[in] ignoreDuplicates If @p true, duplicate edges are ignores. Otherwise, an exception is thrown if an existing edge is added to the graph. + */ + void addEdge( + std::size_t graphId, + const std::string& tail, + const std::string& head, + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map, + bool ignoreDuplicates + ); + + /*! + * @brief Clears and de-initializes a graph that has previously been added to the environment. Call initEnv() after calling this method. + * @param[in] graphId ID of graph that has to be cleared. + */ + void clearGraph(std::size_t graphId); + + /*! + * @brief Returns ged::ExchangeGraph representation. + * @param graphId ID of the selected graph. + * @return ged::ExchangeGraph representation of the selected graph. + */ + ged::ExchangeGraph getGraph(std::size_t graphId) const; + + /*! + * @brief Returns the internal Id of a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The internal ID of the selected graph + */ + std::size_t getGraphInternalId(std::size_t graphId); + + /*! + * @brief Returns all the number of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The number of nodes on the selected graph + */ + std::size_t getGraphNumNodes(std::size_t graphId); + + /*! + * @brief Returns all the number of edges on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The number of edges on the selected graph + */ + std::size_t getGraphNumEdges(std::size_t graphId); + + /*! + * @brief Returns all th Ids of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of IDs's nodes on the selected graph + */ + std::vector getGraphOriginalNodeIds(std::size_t graphId); + + /*! + * @brief Returns all the labels of nodes on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of labels's nodes on the selected graph + */ + std::vector getGraphNodeLabels(std::size_t graphId); + + /*! + * @brief Returns all the edges on a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The list of edges on the selected graph + */ + std::map, ged::AttrLabel> getGraphEdges(std::size_t graphId); + + /*! + * @brief Returns the adjacence list of a graph, selected by its ID. + * @param[in] graphId ID of an input graph that has been added to the environment. + * @return The adjacence list of the selected graph + */ + std::vector> getGraphAdjacenceMatrix(std::size_t graphId); + + /*! + * @brief Sets the edit costs to one of the predefined edit costs. + * @param[in] editCost Select one of the predefined edit costs. + * @param[in] editCostConstants Parameters for the edit cost, empty by default. + */ + void setEditCost(std::string editCost, std::vector editCostConstants = {}); + + /*! + * @brief Sets the edit costs to a personal Edit Cost Class. + * @param[in] editCostConstants Parameters for the edit cost, empty by default. + * @note You have to add your class, which should inherit from EditCost class, in the function. After that, you can compile and use it in Python + */ + void setPersonalEditCost(std::vector editCostConstants = {}); + + /*! + * @brief Initializes the environment. + * @param[in] initOption Select initialization options. + * @param[in] print_to_stdout If set to @p true, the progress of the initialization is printed to std::out. + */ + void initEnv(std::string initOption = "EAGER_WITH_SHUFFLED_COPIES", bool print_to_stdout = false); + + /*! + * @brief Sets the GEDMethod to be used by run_method(). + * @param[in] method Select the method that is to be used. + * @param[in] options An options string of the form @"[--@ @] [...]@" passed to the selected method. + */ + void setMethod(std::string method, const std::string & options); + + /*! + * @brief Initializes the method specified by call to set_method(). + */ + void initMethod(); + + /*! + * @brief Returns initialization time. + * @return Runtime of the last call to init_method(). + */ + double getInitime() const; + + /*! + * @brief Runs the GED method specified by call to set_method() between the graphs with IDs @p g and @p h. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + */ + void runMethod(std::size_t g, std::size_t h); + + /*! + * @brief Returns upper bound for edit distance between the input graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Upper bound computed by the last call to run_method() with arguments @p g and @p h. + */ + double getUpperBound(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns lower bound for edit distance between the input graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Lower bound computed by the last call to run_method() with arguments @p g and @p h. + */ + double getLowerBound(std::size_t g,std::size_t h) const; + + /*! + * @brief Returns the forward map between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The forward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector getForwardMap(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns the backward map between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The backward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector getBackwardMap(std::size_t g, std::size_t h) const; + + /*! + * @brief Returns image of a node. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @param[in] nodeId Node whose image is to be returned. + * @return Node to which node @p node is assigned. + */ + std::size_t getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const; + + /*! + * @brief Returns pre-image of a node. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @param[in] nodeId Node whose pre-image is to be returned. + * @return Node to which node @p node is assigned. + */ + std::size_t getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const; + + /*! + * @brief Returns the induced cost between the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The induced cost between the two indicated graphs. + */ + double getInducedCost(std::size_t g, std::size_t h) const; + + + /*! + * @brief Returns node map between the input graphs. This function duplicates datas. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Node map computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getNodeMap(std::size_t g, std::size_t h); + + /*! + * @brief Returns assignment matrix between the input graphs. This function duplicates datas. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Assignment matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getAssignmentMatrix(std::size_t g, std::size_t h); + + /*! + * @brief Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return The forward and backward maps to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. + */ + std::vector> getAllMap(std::size_t g, std::size_t h); + + /*! + * @brief Returns runtime. + * @param[in] g ID of an input graph that has been added to the environment. + * @param[in] h ID of an input graph that has been added to the environment. + * @return Runtime of last call to run_method() with arguments @p g and @p h. + */ + double getRuntime(std::size_t g, std::size_t h) const; + + /*! + * @brief Checks if the edit costs are quasimetric. + * @return Boolean @p true if the edit costs are quasimetric and @p false, otherwise. + */ + bool quasimetricCosts() const; + + /*! + * @brief Applies the hungarian algorithm (LSAP) to a matrix cost. + * @param[in] matrixCost The matrix cost. + * @return the values of rho, varrho, u and v, in this order. + */ + std::vector> hungarianLSAP(std::vector> matrixCost); + + /*! + * @brief Applies the hungarian algorithm (LSAPE) to a matrix cost. + * @param[in] matrixCost The matrix cost. + * @return the values of rho, varrho, u and v, in this order. + */ + std::vector> hungarianLSAPE(std::vector> matrixCost); + + + /*! + * @brief Returns the number of graphs. + * @return Number of graphs contained in the environment. + */ + std::size_t getNumGraphs() const; + + /*! + * @brief Returns the number of node labels. + * @return Number of pairwise different node labels contained in the environment. + * @note If @p 1 is returned, the nodes are unlabeled. + */ + std::size_t getNumNodeLabels() const; + + /*! + * @brief Returns node label. + * @param[in] label_id ID of node label that should be returned. Must be between 1 and num_node_labels(). + * @return Node label for selected label ID. + */ + ged::AttrLabel getNodeLabel(std::size_t label_id) const; + + /*! + * @brief Returns the number of edge labels. + * @return Number of pairwise different edge labels contained in the environment. + * @note If @p 1 is returned, the edges are unlabeled. + */ + std::size_t getNumEdgeLabels() const; + + /*! + * @brief Returns edge label. + * @param[in] label_id ID of edge label that should be returned. Must be between 1 and num_node_labels(). + * @return Edge label for selected label ID. + */ + ged::AttrLabel getEdgeLabel(std::size_t label_id) const; + + // /*! + // * @brief Returns the number of nodes. + // * @param[in] graph_id ID of an input graph that has been added to the environment. + // * @return Number of nodes in the graph. + // */ + // std::size_t getNumNodes(std::size_t graph_id) const; + + /*! + * @brief Returns average number of nodes. + * @return Average number of nodes of the graphs contained in the environment. + */ + double getAvgNumNodes() const; + + /*! + * @brief Returns node relabeling cost. + * @param[in] node_label_1 First node label. + * @param[in] node_label_2 Second node label. + * @return Node relabeling cost for the given node labels. + */ + double getNodeRelCost(const ged::AttrLabel & node_label_1, const ged::AttrLabel & node_label_2) const; + + /*! + * @brief Returns node deletion cost. + * @param[in] node_label Node label. + * @return Cost of deleting node with given label. + */ + double getNodeDelCost(const ged::AttrLabel & node_label) const; + + /*! + * @brief Returns node insertion cost. + * @param[in] node_label Node label. + * @return Cost of inserting node with given label. + */ + double getNodeInsCost(const ged::AttrLabel & node_label) const; + + /*! + * @brief Computes median node label. + * @param[in] node_labels The node labels whose median should be computed. + * @return Median of the given node labels. + */ + ged::AttrLabel getMedianNodeLabel(const std::vector & node_labels) const; + + /*! + * @brief Returns edge relabeling cost. + * @param[in] edge_label_1 First edge label. + * @param[in] edge_label_2 Second edge label. + * @return Edge relabeling cost for the given edge labels. + */ + double getEdgeRelCost(const ged::AttrLabel & edge_label_1, const ged::AttrLabel & edge_label_2) const; + + /*! + * @brief Returns edge deletion cost. + * @param[in] edge_label Edge label. + * @return Cost of deleting edge with given label. + */ + double getEdgeDelCost(const ged::AttrLabel & edge_label) const; + + /*! + * @brief Returns edge insertion cost. + * @param[in] edge_label Edge label. + * @return Cost of inserting edge with given label. + */ + double getEdgeInsCost(const ged::AttrLabel & edge_label) const; + + /*! + * @brief Computes median edge label. + * @param[in] edge_labels The edge labels whose median should be computed. + * @return Median of the given edge labels. + */ + ged::AttrLabel getMedianEdgeLabel(const std::vector & edge_labels) const; + + /*! + * @brief Returns the initialization type of the last initialization. + * @return Initialization type in string. + */ + std::string getInitType() const; + + /*! + * @brief Computes the edit cost between two graphs induced by a node map. + * @param[in] g_id ID of input graph. + * @param[in] h_id ID of input graph. + * @return Computed induced cost. + */ + double computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const; + + // /*! + // * @brief Returns node relabeling, insertion, or deletion cost. + // * @param[in] label1 First node label. + // * @param[in] label2 Second node label. + // * @return Node relabeling cost if @p label1 and @p label2 are both different from ged::dummy_label(), + // * node insertion cost if @p label1 equals ged::dummy_label and @p label2 does not, + // * node deletion cost if @p label1 does not equal ged::dummy_label and @p label2 does, + // * and 0 otherwise. + // */ + // double getNodeCost(std::size_t label1, std::size_t label2) const; + + + /*! + * @brief Constructs a ged::AttrLabel from the given attribute maps. + * @param str_map The string attributes of the node/edge. + * @param int_map The integer attributes of the node/edge. + * @param float_map The float attributes of the node/edge. + * @param list_str_map The list of string attributes of the node/edge. + * @param list_int_map The list of integer attributes of the node/edge. + * @param list_float_map The list of float attributes of the node/edge. + * @return A ged::AttrLabel constructed from the given maps. + */ + static ged::AttrLabel constructAttrLabelFromMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map + ); + + +private: + + ged::GEDEnv * env_; // environment variable + + bool initialized; // initialization boolean (because env has one but not accessible) + +}; // class PyGEDEnvAttr + +static void printLabelMaps( + const std::unordered_map& str_map, + const std::unordered_map& int_map, + const std::unordered_map& float_map, + const std::unordered_map>& list_str_map, + const std::unordered_map>& list_int_map, + const std::unordered_map>& list_float_map +); + +static void printAttrLabel(const ged::AttrLabel & attr_label); + +} // namespace pyged + +//#include "GedLibBindAttr.ipp" + +//#endif /* SRC_GEDLIB_BIND_ATTR_HPP */ \ No newline at end of file diff --git a/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_gxl.cpp b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_gxl.cpp new file mode 100644 index 0000000000..1331463028 --- /dev/null +++ b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_gxl.cpp @@ -0,0 +1,447 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_gxl.cpp + * @brief implementations of classes and functions to call easily GebLib in Python without Gedlib's types + */ +#pragma once +//#ifndef GEDLIBBIND_IPP +//#define GEDLIBBIND_IPP + +//Include standard libraries + GedLib library +// #include +// #include "GedLibBind.h" +// #include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/median/src/median_graph_estimator.hpp" +#include "gedlib_bind_gxl.hpp" + +using namespace std; + +//Definition of types and templates used in this code for my human's memory :). +//ged::GEDEnv env; +//template struct ExchangeGraph + +//typedef std::map GXLLabel; +//typedef std::string GXLNodeID; + + +namespace pyged { + +PyGEDEnvGXL::PyGEDEnvGXL () { + env_ = new ged::GEDEnv(); + this->initialized = false; +} + +PyGEDEnvGXL::~PyGEDEnvGXL () { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } +} + +// bool initialized = false; //Initialization boolean (because Env has one but not accessible). + +bool PyGEDEnvGXL::isInitialized() { + return initialized; +} + +void PyGEDEnvGXL::restartEnv() { + if (env_ != NULL) { + delete env_; + env_ = NULL; + } + env_ = new ged::GEDEnv(); + initialized = false; +} + +void PyGEDEnvGXL::loadGXLGraph(const std::string & pathFolder, const std::string & pathXML, bool node_type, bool edge_type) { + std::vector tmp_graph_ids(env_->load_gxl_graph(pathFolder, pathXML, + (node_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), + (edge_type ? ged::Options::GXLNodeEdgeType::LABELED : ged::Options::GXLNodeEdgeType::UNLABELED), + std::unordered_set(), std::unordered_set())); +} + +std::pair PyGEDEnvGXL::getGraphIds() const { + return env_->graph_ids(); +} + +std::vector PyGEDEnvGXL::getAllGraphIds() { + std::vector listID; + for (std::size_t i = env_->graph_ids().first; i != env_->graph_ids().second; i++) { + listID.push_back(i); + } + return listID; +} + +const std::string PyGEDEnvGXL::getGraphClass(std::size_t id) const { + return env_->get_graph_class(id); +} + +const std::string PyGEDEnvGXL::getGraphName(std::size_t id) const { + return env_->get_graph_name(id); +} + +std::size_t PyGEDEnvGXL::addGraph(const std::string & graph_name, const std::string & graph_class) { + ged::GEDGraph::GraphID newId = env_->add_graph(graph_name, graph_class); + initialized = false; + return std::stoi(std::to_string(newId)); +} + +void PyGEDEnvGXL::addNode(std::size_t graphId, const std::string & nodeId, const std::map & nodeLabel) { + env_->add_node(graphId, nodeId, nodeLabel); + initialized = false; +} + +/*void addEdge(std::size_t graphId, ged::GXLNodeID tail, ged::GXLNodeID head, ged::GXLLabel edgeLabel) { + env_->add_edge(graphId, tail, head, edgeLabel); +}*/ + +void PyGEDEnvGXL::addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates) { + env_->add_edge(graphId, tail, head, edgeLabel, ignoreDuplicates); + initialized = false; +} + +void PyGEDEnvGXL::clearGraph(std::size_t graphId) { + env_->clear_graph(graphId); + initialized = false; +} + +ged::ExchangeGraph PyGEDEnvGXL::getGraph(std::size_t graphId) const { + return env_->get_graph(graphId); +} + +std::size_t PyGEDEnvGXL::getGraphInternalId(std::size_t graphId) { + return getGraph(graphId).id; +} + +std::size_t PyGEDEnvGXL::getGraphNumNodes(std::size_t graphId) { + return getGraph(graphId).num_nodes; +} + +std::size_t PyGEDEnvGXL::getGraphNumEdges(std::size_t graphId) { + return getGraph(graphId).num_edges; +} + +std::vector PyGEDEnvGXL::getGraphOriginalNodeIds(std::size_t graphId) { + return getGraph(graphId).original_node_ids; +} + +std::vector> PyGEDEnvGXL::getGraphNodeLabels(std::size_t graphId) { + return getGraph(graphId).node_labels; +} + +std::map, std::map> PyGEDEnvGXL::getGraphEdges(std::size_t graphId) { + return getGraph(graphId).edge_labels; +} + +std::vector> PyGEDEnvGXL::getGraphAdjacenceMatrix(std::size_t graphId) { + return getGraph(graphId).adj_matrix; +} + +void PyGEDEnvGXL::setEditCost(std::string editCost, std::vector editCostConstants) { + env_->set_edit_costs(translateEditCost(editCost), editCostConstants); +} + +void PyGEDEnvGXL::setPersonalEditCost(std::vector editCostConstants) { + //env_->set_edit_costs(Your EditCost Class(editCostConstants)); +} + +// void PyGEDEnvGXL::initEnv() { +// env_->init(); +// initialized = true; +// } + +void PyGEDEnvGXL::initEnv(std::string initOption, bool print_to_stdout) { + env_->init(translateInitOptions(initOption), print_to_stdout); + initialized = true; +} + +void PyGEDEnvGXL::setMethod(std::string method, const std::string & options) { + env_->set_method(translateMethod(method), options); +} + +void PyGEDEnvGXL::initMethod() { + env_->init_method(); +} + +double PyGEDEnvGXL::getInitime() const { + return env_->get_init_time(); +} + +void PyGEDEnvGXL::runMethod(std::size_t g, std::size_t h) { + env_->run_method(g, h); +} + +double PyGEDEnvGXL::getUpperBound(std::size_t g, std::size_t h) const { + return env_->get_upper_bound(g, h); +} + +double PyGEDEnvGXL::getLowerBound(std::size_t g, std::size_t h) const { + return env_->get_lower_bound(g, h); +} + +std::vector PyGEDEnvGXL::getForwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_forward_map(); +} + +std::vector PyGEDEnvGXL::getBackwardMap(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).get_backward_map(); +} + +std::size_t PyGEDEnvGXL::getNodeImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).image(nodeId); +} + +std::size_t PyGEDEnvGXL::getNodePreImage(std::size_t g, std::size_t h, std::size_t nodeId) const { + return env_->get_node_map(g, h).pre_image(nodeId); +} + +double PyGEDEnvGXL::getInducedCost(std::size_t g, std::size_t h) const { + return env_->get_node_map(g, h).induced_cost(); +} + +std::vector> PyGEDEnvGXL::getNodeMap(std::size_t g, std::size_t h) { + std::vector> res; + std::vector relation; + env_->get_node_map(g, h).as_relation(relation); + for (const auto & assignment : relation) { + res.push_back(std::make_pair(assignment.first, assignment.second)); + } + return res; +} + +std::vector> PyGEDEnvGXL::getAssignmentMatrix(std::size_t g, std::size_t h) { + std::vector> res; + for(std::size_t i = 0; i != getForwardMap(g, h).size(); i++) { + std::vector newLine; + bool have1 = false; + for(std::size_t j = 0; j != getBackwardMap(g, h).size(); j++) { + if (getNodeImage(g, h, i) == j) { + newLine.push_back(1); + have1 = true; + } + else{ + newLine.push_back(0); + } + } + if(have1) { + newLine.push_back(0); + } + else{ + newLine.push_back(1); + } + res.push_back(newLine); + } + std::vector lastLine; + for (size_t k = 0; k != getBackwardMap(g,h).size(); k++) { + if (getBackwardMap(g,h)[k] == ged::GEDGraph::dummy_node()) { + lastLine.push_back(1); + } + else{ + lastLine.push_back(0); + } + } + res.push_back(lastLine); + return res; +} + +std::vector> PyGEDEnvGXL::getAllMap(std::size_t g, std::size_t h) { + std::vector> res; + res.push_back(getForwardMap(g, h)); + res.push_back(getBackwardMap(g,h)); + return res; +} + +double PyGEDEnvGXL::getRuntime(std::size_t g, std::size_t h) const { + return env_->get_runtime(g, h); +} + +bool PyGEDEnvGXL::quasimetricCosts() const { + return env_->quasimetric_costs(); +} + +std::vector> PyGEDEnvGXL::hungarianLSAP(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows], *varrho = new std::size_t[ncols]; + std::size_t *u = new std::size_t[nrows], *v = new std::size_t[ncols]; + std::size_t *C = new std::size_t[nrows*ncols]; + // std::size_t i = 0, j; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAP(C,nrows,ncols,rho,u,v,varrho); + std::vector> res; + res.push_back(translatePointer(rho, nrows)); + res.push_back(translatePointer(varrho, ncols)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::vector> PyGEDEnvGXL::hungarianLSAPE(std::vector> matrixCost) { + std::size_t nrows = matrixCost.size(); + std::size_t ncols = matrixCost[0].size(); + std::size_t *rho = new std::size_t[nrows-1], *varrho = new std::size_t[ncols-1]; + double *u = new double[nrows], *v = new double[ncols]; + double *C = new double[nrows*ncols]; + for (std::size_t i = 0; i < nrows; i++) { + for (std::size_t j = 0; j < ncols; j++) { + C[j*nrows+i] = matrixCost[i][j]; + } + } + lsape::hungarianLSAPE(C,nrows,ncols,rho,varrho,u,v); + std::vector> res; + res.push_back(translateAndConvertPointer(rho, nrows-1)); + res.push_back(translateAndConvertPointer(varrho, ncols-1)); + res.push_back(translatePointer(u, nrows)); + res.push_back(translatePointer(v, ncols)); + return res; +} + +std::size_t PyGEDEnvGXL::getNumNodeLabels() const { + return env_->num_node_labels(); +} + +std::map PyGEDEnvGXL::getNodeLabel(std::size_t label_id) const { + return env_->get_node_label(label_id); +} + +std::size_t PyGEDEnvGXL::getNumEdgeLabels() const { + return env_->num_edge_labels(); +} + +std::map PyGEDEnvGXL::getEdgeLabel(std::size_t label_id) const { + return env_->get_edge_label(label_id); +} + +// std::size_t PyGEDEnvGXL::getNumNodes(std::size_t graph_id) const { +// return env_->get_num_nodes(graph_id); +// } + +double PyGEDEnvGXL::getAvgNumNodes() const { + return env_->get_avg_num_nodes(); +} + +double PyGEDEnvGXL::getNodeRelCost(const std::map & node_label_1, const std::map & node_label_2) const { + return env_->node_rel_cost(node_label_1, node_label_2); +} + +double PyGEDEnvGXL::getNodeDelCost(const std::map & node_label) const { + return env_->node_del_cost(node_label); +} + +double PyGEDEnvGXL::getNodeInsCost(const std::map & node_label) const { + return env_->node_ins_cost(node_label); +} + +std::map PyGEDEnvGXL::getMedianNodeLabel(const std::vector> & node_labels) const { + return env_->median_node_label(node_labels); +} + +double PyGEDEnvGXL::getEdgeRelCost(const std::map & edge_label_1, const std::map & edge_label_2) const { + return env_->edge_rel_cost(edge_label_1, edge_label_2); +} + +double PyGEDEnvGXL::getEdgeDelCost(const std::map & edge_label) const { + return env_->edge_del_cost(edge_label); +} + +double PyGEDEnvGXL::getEdgeInsCost(const std::map & edge_label) const { + return env_->edge_ins_cost(edge_label); +} + +std::map PyGEDEnvGXL::getMedianEdgeLabel(const std::vector> & edge_labels) const { + return env_->median_edge_label(edge_labels); +} + +std::string PyGEDEnvGXL::getInitType() const { + return initOptionsToString(env_->get_init_type()); +} + +double PyGEDEnvGXL::computeInducedCost(std::size_t g_id, std::size_t h_id, std::vector> relation) const { + ged::NodeMap node_map = ged::NodeMap(env_->get_num_nodes(g_id), env_->get_num_nodes(h_id)); + for (const auto & assignment : relation) { + node_map.add_assignment(assignment.first, assignment.second); + // std::cout << assignment.first << assignment.second << endl; + } + const std::vector forward_map = node_map.get_forward_map(); + for (std::size_t i{0}; i < node_map.num_source_nodes(); i++) { + if (forward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(i, ged::GEDGraph::dummy_node()); + } + } + const std::vector backward_map = node_map.get_backward_map(); + for (std::size_t i{0}; i < node_map.num_target_nodes(); i++) { + if (backward_map.at(i) == ged::GEDGraph::undefined_node()) { + node_map.add_assignment(ged::GEDGraph::dummy_node(), i); + } + } + // for (auto & map : node_map.get_forward_map()) { + // std::cout << map << ", "; + // } + // std::cout << endl; + // for (auto & map : node_map.get_backward_map()) { + // std::cout << map << ", "; + // } + env_->compute_induced_cost(g_id, h_id, node_map); + return node_map.induced_cost(); +} + + + + +// double PyGEDEnvGXL::getNodeCost(std::size_t label1, std::size_t label2) const { +// return env_->ged_data_node_cost(label1, label2); +// } + + +/*void medianLetter(pathFolder, pathXML, editCost, method, options="", initOption = "EAGER_WITHOUT_SHUFFLED_COPIES") { + + if(isInitialized()) { + restartEnv(); + } + setEditCost(editCost);*/ + + /*std::string letter_class("A"); + if (argc > 1) { + letter_class = std::string(argv[1]); + }*/ + //std::string seed("0"); + /*if (argc > 2) { + seed = std::string(argv[2]); + }*/ + + /*loadGXLGraph(pathFolder, pathXML); + std::vector graph_ids = getAllGraphIds(); + std::size_t median_id = env_->add_graph("median", ""); + + initEnv(initOption); + + setMethod(method); + + ged::MedianGraphEstimator median_estimator(&env, false); + median_estimator.set_options("--init-type RANDOM --randomness PSEUDO --seed " + seed); + median_estimator.run(graph_ids, median_id); + std::string gxl_file_name("../output/gen_median_Letter_HIGH_" + letter_class + ".gxl"); + env_->save_as_gxl_graph(median_id, gxl_file_name);*/ + + /*std::string tikz_file_name("../output/gen_median_Letter_HIGH_" + letter_class + ".tex"); + save_letter_graph_as_tikz_file(env_->get_graph(median_id), tikz_file_name);*/ +//} + +} // namespace pyged + +//#endif /* SRC_GEDLIB_BIND_IPP */ \ No newline at end of file diff --git a/gklearn/gedlib/src/GedLibBind.hpp b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_gxl.hpp old mode 100755 new mode 100644 similarity index 94% rename from gklearn/gedlib/src/GedLibBind.hpp rename to gklearn/gedlib/src/single_so_version_bk/gedlib_bind_gxl.hpp index 63e0db396e..3388dc1e26 --- a/gklearn/gedlib/src/GedLibBind.hpp +++ b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_gxl.hpp @@ -4,27 +4,30 @@ * Linlin Jia * * * * This file should be used by Python. * - * Please call the Python module if you want to use GedLib with this code.* + * Please call the Python module if you want to use GedLib with this code.* * * * Otherwise, you can directly use GedLib for C++. * * * ***************************************************************************/ - + /*! - * @file GedLibBind.hpp - * @brief Classe and function declarations to call easly GebLib in Python without Gedlib's types + * @file gedlib_bind_gxl.hpp + * @brief Class and function declarations to call easily GebLib in Python without Gedlib's types */ -#ifndef GEDLIBBIND_HPP -#define GEDLIBBIND_HPP - +#pragma once +//#ifndef GEDLIBBIND_HPP +//#define GEDLIBBIND_HPP + //Include standard libraries. #include #include #include #include #include -#include "../include/gedlib-master/src/env/ged_env.hpp" -#include "../include/gedlib-master/src/env/node_map.hpp" +//#include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/src/env/node_map.hpp" +#include "gedlib_header.hpp" +#include "gedlib_bind_util.hpp" /*! @@ -34,48 +37,26 @@ namespace pyged { /*! -* @brief Get list of available edit cost functions readable by Python. -*/ -std::vector getEditCostStringOptions(); - -/*! -* @brief Get list of available computation methods readable by Python. -*/ -std::vector getMethodStringOptions(); - -/*! -* @brief Get list of available initilaization options readable by Python. -*/ -std::vector getInitStringOptions(); - -/*! -* @brief Returns a dummy node. -* @return ID of dummy node. -*/ -static std::size_t getDummyNode(); - - -/*! -* @brief Provides the API of GEDLIB for Python. +* @brief Provides the API of GEDLIB for Python for gxl labels (string labels). */ -class PyGEDEnv { +class PyGEDEnvGXL { public: /*! * @brief Constructor. */ - PyGEDEnv(); + PyGEDEnvGXL(); - // PyGEDEnv(); + // PyGEDEnvGXL(); /*! * @brief Destructor. */ - ~PyGEDEnv(); + ~PyGEDEnvGXL(); /*! - * @brief Tests if the environment is initialized or not. + * @brief Tests if the environment is initialized or not. * @return Boolean @p true if the environment is initialized and @p false otherwise. */ bool isInitialized(); @@ -102,8 +83,8 @@ class PyGEDEnv { std::pair getGraphIds() const; /*! - * @brief Returns the list of graphs IDs which are loaded in the environment. - * @return A vector which contains all the graphs Ids. + * @brief Returns the list of graphs IDs which are loaded in the environment. + * @return A vector which contains all the graphs Ids. */ std::vector getAllGraphIds(); @@ -142,7 +123,7 @@ class PyGEDEnv { * @param[in] graphId ID of graph that has been added to the environment. * @param[in] tail The user-specific ID of the tail of the edge that has to be added. * @param[in] head The user-specific ID of the head of the edge that has to be added. - * @param[in] edgeLabel The label of the vertex that has to be added. + * @param[in] edgeLabel The label of the vertex that has to be added. * @param[in] ignoreDuplicates If @p true, duplicate edges are ignores. Otherwise, an exception is thrown if an existing edge is added to the graph. */ void addEdge(std::size_t graphId, const std::string & tail, const std::string & head, const std::map & edgeLabel, bool ignoreDuplicates = true); @@ -272,7 +253,7 @@ class PyGEDEnv { double getLowerBound(std::size_t g,std::size_t h) const; /*! - * @brief Returns the forward map between nodes of the two indicated graphs. + * @brief Returns the forward map between nodes of the two indicated graphs. * @param[in] g ID of an input graph that has been added to the environment. * @param[in] h ID of an input graph that has been added to the environment. * @return The forward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. @@ -280,7 +261,7 @@ class PyGEDEnv { std::vector getForwardMap(std::size_t g, std::size_t h) const; /*! - * @brief Returns the backward map between nodes of the two indicated graphs. + * @brief Returns the backward map between nodes of the two indicated graphs. * @param[in] g ID of an input graph that has been added to the environment. * @param[in] h ID of an input graph that has been added to the environment. * @return The backward map to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. @@ -312,10 +293,10 @@ class PyGEDEnv { * @return The induced cost between the two indicated graphs. */ double getInducedCost(std::size_t g, std::size_t h) const; - + /*! - * @brief Returns node map between the input graphs. This function duplicates datas. + * @brief Returns node map between the input graphs. This function duplicates datas. * @param[in] g ID of an input graph that has been added to the environment. * @param[in] h ID of an input graph that has been added to the environment. * @return Node map computed by the last call to run_method() with arguments @p g and @p h. @@ -323,7 +304,7 @@ class PyGEDEnv { std::vector> getNodeMap(std::size_t g, std::size_t h); /*! - * @brief Returns assignment matrix between the input graphs. This function duplicates datas. + * @brief Returns assignment matrix between the input graphs. This function duplicates datas. * @param[in] g ID of an input graph that has been added to the environment. * @param[in] h ID of an input graph that has been added to the environment. * @return Assignment matrix computed by the last call to run_method() with arguments @p g and @p h. @@ -331,7 +312,7 @@ class PyGEDEnv { std::vector> getAssignmentMatrix(std::size_t g, std::size_t h); /*! - * @brief Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. + * @brief Returns a vector which contains the forward and the backward maps between nodes of the two indicated graphs. * @param[in] g ID of an input graph that has been added to the environment. * @param[in] h ID of an input graph that has been added to the environment. * @return The forward and backward maps to the adjacence matrix computed by the last call to run_method() with arguments @p g and @p h. @@ -497,21 +478,13 @@ class PyGEDEnv { bool initialized; // initialization boolean (because env has one but not accessible) -}; - -} - -#include "GedLibBind.ipp" - -#endif /* SRC_GEDLIB_BIND_HPP */ - - - - - +}; // end of class PyGEDEnvGXL +} // namespace pyged +//#include "GedLibBind.ipp" +//#endif /* SRC_GEDLIB_BIND_HPP */ diff --git a/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_util.cpp b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_util.cpp new file mode 100644 index 0000000000..84a0cd58eb --- /dev/null +++ b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_util.cpp @@ -0,0 +1,355 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_util.cpp + * @brief implementation of util class and function declarations to call easily GedLib in Python + */ +#pragma once +//#ifndef GEDLIB_BIND_UTIL_CPP +//#define GEDLIB_BIND_UTIL_CPP + +//Include standard libraries + GedLib library +// #include +// #include "GedLibBind.h" +// #include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/median/src/median_graph_estimator.hpp" +#include "gedlib_bind_util.hpp" + +using namespace std; + +//Definition of types and templates used in this code for my human's memory :). +//ged::GEDEnv env; +//template struct ExchangeGraph + +//typedef std::map GXLLabel; +//typedef std::string GXLNodeID; + + +namespace pyged { + +//!< List of available edit cost functions readable by Python. +std::vector editCostStringOptions = { + "CHEM_1", + "CHEM_2", + "CMU", + "GREC_1", + "GREC_2", + "LETTER", + "LETTER2", + "NON_SYMBOLIC", + "FINGERPRINT", + "PROTEIN", + "CONSTANT" +}; + +//!< Map of available edit cost functions between enum type in C++ and string in Python +std::map editCostOptions = { + {"CHEM_1", ged::Options::EditCosts::CHEM_1}, + {"CHEM_2", ged::Options::EditCosts::CHEM_2}, + {"CMU", ged::Options::EditCosts::CMU}, + {"GREC_1", ged::Options::EditCosts::GREC_1}, + {"GREC_2", ged::Options::EditCosts::GREC_2}, + {"LETTER", ged::Options::EditCosts::LETTER}, + {"LETTER2", ged::Options::EditCosts::LETTER2}, + {"NON_SYMBOLIC", ged::Options::EditCosts::NON_SYMBOLIC}, + {"FINGERPRINT", ged::Options::EditCosts::FINGERPRINT}, + {"PROTEIN", ged::Options::EditCosts::PROTEIN}, + {"CONSTANT", ged::Options::EditCosts::CONSTANT} +}; + + //!< List of available computation methods readable by Python. +std::vector methodStringOptions = { + "BRANCH", + "BRANCH_FAST", + "BRANCH_TIGHT", + "BRANCH_UNIFORM", + "BRANCH_COMPACT", + "PARTITION", + "HYBRID", + "RING", + "ANCHOR_AWARE_GED", + "WALKS", + "IPFP", + "BIPARTITE", + "SUBGRAPH", + "NODE", + "RING_ML", + "BIPARTITE_ML", + "REFINE", + "BP_BEAM", + "SIMULATED_ANNEALING", + "HED", + "STAR" +}; + +//!< Map of available computation methods readables between enum type in C++ and string in Python +std::map methodOptions = { + {"BRANCH", ged::Options::GEDMethod::BRANCH}, + {"BRANCH_FAST", ged::Options::GEDMethod::BRANCH_FAST}, + {"BRANCH_TIGHT", ged::Options::GEDMethod::BRANCH_TIGHT}, + {"BRANCH_UNIFORM", ged::Options::GEDMethod::BRANCH_UNIFORM}, + {"BRANCH_COMPACT", ged::Options::GEDMethod::BRANCH_COMPACT}, + {"PARTITION", ged::Options::GEDMethod::PARTITION}, + {"HYBRID", ged::Options::GEDMethod::HYBRID}, + {"RING", ged::Options::GEDMethod::RING}, + {"ANCHOR_AWARE_GED", ged::Options::GEDMethod::ANCHOR_AWARE_GED}, + {"WALKS", ged::Options::GEDMethod::WALKS}, + {"IPFP", ged::Options::GEDMethod::IPFP}, + {"BIPARTITE", ged::Options::GEDMethod::BIPARTITE}, + {"SUBGRAPH", ged::Options::GEDMethod::SUBGRAPH}, + {"NODE", ged::Options::GEDMethod::NODE}, + {"RING_ML", ged::Options::GEDMethod::RING_ML}, + {"BIPARTITE_ML",ged::Options::GEDMethod::BIPARTITE_ML}, + {"REFINE",ged::Options::GEDMethod::REFINE}, + {"BP_BEAM", ged::Options::GEDMethod::BP_BEAM}, + {"SIMULATED_ANNEALING", ged::Options::GEDMethod::SIMULATED_ANNEALING}, + {"HED", ged::Options::GEDMethod::HED}, + {"STAR" , ged::Options::GEDMethod::STAR}, +}; + +//! initStringOptions = { + "LAZY_WITHOUT_SHUFFLED_COPIES", + "EAGER_WITHOUT_SHUFFLED_COPIES", + "LAZY_WITH_SHUFFLED_COPIES", + "EAGER_WITH_SHUFFLED_COPIES" +}; + +//!< Map of available initilaization options readables between enum type in C++ and string in Python +std::map initOptions = { + {"LAZY_WITHOUT_SHUFFLED_COPIES", ged::Options::InitType::LAZY_WITHOUT_SHUFFLED_COPIES}, + {"EAGER_WITHOUT_SHUFFLED_COPIES", ged::Options::InitType::EAGER_WITHOUT_SHUFFLED_COPIES}, + {"LAZY_WITH_SHUFFLED_COPIES", ged::Options::InitType::LAZY_WITH_SHUFFLED_COPIES}, + {"EAGER_WITH_SHUFFLED_COPIES", ged::Options::InitType::EAGER_WITH_SHUFFLED_COPIES} +}; + +std::vector getEditCostStringOptions() { + return editCostStringOptions; +} + +std::vector getMethodStringOptions() { + return methodStringOptions; +} + +std::vector getInitStringOptions() { + return initStringOptions; +} + +static std::size_t getDummyNode() { + return ged::GEDGraph::dummy_node(); +} + + +/*! + * @brief Returns the enum EditCost which correspond to the string parameter + * @param editCost Select one of the predefined edit costs in the list. + * @return The edit cost function which correspond in the edit cost functions map. + */ +ged::Options::EditCosts translateEditCost(std::string editCost) { + for (std::size_t i = 0; i != editCostStringOptions.size(); i++) { + if (editCostStringOptions[i] == editCost) { + return editCostOptions[editCostStringOptions[i]]; + } + } + return ged::Options::EditCosts::CONSTANT; +} + +/*! + * @brief Returns the enum IniType which correspond to the string parameter + * @param initOption Select initialization options. + * @return The init Type which correspond in the init options map. + */ +ged::Options::InitType translateInitOptions(std::string initOption) { + for (std::size_t i = 0; i != initStringOptions.size(); i++) { + if (initStringOptions[i] == initOption) { + return initOptions[initStringOptions[i]]; + } + } + return ged::Options::InitType::EAGER_WITHOUT_SHUFFLED_COPIES; +} + +/*! + * @brief Returns the string correspond to the enum IniType. + * @param initOption Select initialization options. + * @return The string which correspond to the enum IniType @p initOption. + */ + std::string initOptionsToString(ged::Options::InitType initOption) { + for (std::size_t i = 0; i != initOptions.size(); i++) { + if (initOptions[initStringOptions[i]] == initOption) { + return initStringOptions[i]; + } + } + return "EAGER_WITHOUT_SHUFFLED_COPIES"; +} + +/*! + * @brief Returns the enum Method which correspond to the string parameter + * @param method Select the method that is to be used. + * @return The computation method which correspond in the edit cost functions map. + */ +ged::Options::GEDMethod translateMethod(std::string method) { + for (std::size_t i = 0; i != methodStringOptions.size(); i++) { + if (methodStringOptions[i] == method) { + return methodOptions[methodStringOptions[i]]; + } + } + return ged::Options::GEDMethod::STAR; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(std::size_t* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back(pointer[i]); + } + return res; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The double pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(double* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back(pointer[i]); + } + return res; +} + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values, with double type. + */ +std::vector translateAndConvertPointer(std::size_t* pointer, std::size_t dataSize ) { + std::vector res; + for(std::size_t i = 0; i < dataSize; i++) { + res.push_back((double)pointer[i]); + } + return res; +} + +/*! + * @brief Returns the string which contains all element of a int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector) { + std::string res = ""; + + for (std::size_t i = 0; i != vector.size(); i++) + { + res += std::to_string(vector[i]) + " "; + } + + return res; +} + +/*! + * @brief Returns the string which contains all element of a unsigned long int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector) { + std::string res = ""; + + for (std::size_t i = 0; i != vector.size(); i++) + { + res += std::to_string(vector[i]) + " "; + } + + return res; +} + +/*void medianLetter(pathFolder, pathXML, editCost, method, options="", initOption = "EAGER_WITHOUT_SHUFFLED_COPIES") { + + if(isInitialized()) { + restartEnv(); + } + setEditCost(editCost);*/ + + /*std::string letter_class("A"); + if (argc > 1) { + letter_class = std::string(argv[1]); + }*/ + //std::string seed("0"); + /*if (argc > 2) { + seed = std::string(argv[2]); + }*/ + + /*loadGXLGraph(pathFolder, pathXML); + std::vector graph_ids = getAllGraphIds(); + std::size_t median_id = env_->add_graph("median", ""); + + initEnv(initOption); + + setMethod(method); + + ged::MedianGraphEstimator median_estimator(&env, false); + median_estimator.set_options("--init-type RANDOM --randomness PSEUDO --seed " + seed); + median_estimator.run(graph_ids, median_id); + std::string gxl_file_name("../output/gen_median_Letter_HIGH_" + letter_class + ".gxl"); + env_->save_as_gxl_graph(median_id, gxl_file_name);*/ + + /*std::string tikz_file_name("../output/gen_median_Letter_HIGH_" + letter_class + ".tex"); + save_letter_graph_as_tikz_file(env_->get_graph(median_id), tikz_file_name);*/ +//} + +} // namespace pyged + +//#endif /* GEDLIB_BIND_UTIL_CPP */ + + + +// namespace shapes { + +// // Default constructor +// Rectangle::Rectangle () {} + +// // Overloaded constructor +// Rectangle::Rectangle (int x0, int y0, int x1, int y1) { +// this->x0 = x0; +// this->y0 = y0; +// this->x1 = x1; +// this->y1 = y1; +// } + +// // Destructor +// Rectangle::~Rectangle () {} + +// // Return the area of the rectangle +// int Rectangle::getArea () { +// return (this->x1 - this->x0) * (this->y1 - this->y0); +// } + +// // Get the size of the rectangle. +// // Put the size in the pointer args +// void Rectangle::getSize (int *width, int *height) { +// (*width) = x1 - x0; +// (*height) = y1 - y0; +// } + +// // Move the rectangle by dx dy +// void Rectangle::move (int dx, int dy) { +// this->x0 += dx; +// this->y0 += dy; +// this->x1 += dx; +// this->y1 += dy; +// } +// } \ No newline at end of file diff --git a/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_util.hpp b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_util.hpp new file mode 100644 index 0000000000..80666e6056 --- /dev/null +++ b/gklearn/gedlib/src/single_so_version_bk/gedlib_bind_util.hpp @@ -0,0 +1,141 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_bind_util.hpp + * @brief Util class and function declarations to call easily GebLib in Python + */ +#pragma once +//#ifndef GEDLIB_BIND_UTIL_HPP +//#define GEDLIB_BIND_UTIL_HPP + +//Include standard libraries. +#include +#include +#include +//#include "../include/gedlib-master/src/env/ged_env.hpp" +//#include "../include/gedlib-master/src/env/node_map.hpp" +#include "gedlib_header.hpp" + +/*! + * @namespace pyged + * @brief Global namespace for gedlibpy. + */ +namespace pyged { +// +////!< List of available edit cost functions readable by Python. +//extern std::vector editCostStringOptions; +// +////!< Map of available edit cost functions between enum type in C++ and string in Python +//extern std::map editCostOptions; +// +// //!< List of available computation methods readable by Python. +//extern std::vector methodStringOptions; +// +////!< Map of available computation methods readables between enum type in C++ and string in Python +//extern std::map methodOptions; +// +////! initStringOptions; +// +////!< Map of available initilaization options readables between enum type in C++ and string in Python +//extern std::map initOptions; + + +/*! +* @brief Get list of available edit cost functions readable by Python. +*/ +std::vector getEditCostStringOptions(); + +/*! +* @brief Get list of available computation methods readable by Python. +*/ +std::vector getMethodStringOptions(); + +/*! +* @brief Get list of available initilaization options readable by Python. +*/ +std::vector getInitStringOptions(); + +/*! +* @brief Returns a dummy node. +* @return ID of dummy node. +*/ +static std::size_t getDummyNode(); + +/*! + * @brief Returns the enum EditCost which correspond to the string parameter + * @param editCost Select one of the predefined edit costs in the list. + * @return The edit cost function which correspond in the edit cost functions map. + */ +ged::Options::EditCosts translateEditCost(std::string editCost); + + +/*! + * @brief Returns the enum IniType which correspond to the string parameter + * @param initOption Select initialization options. + * @return The init Type which correspond in the init options map. + */ +ged::Options::InitType translateInitOptions(std::string initOption); + +/*! + * @brief Returns the string correspond to the enum IniType. + * @param initOption Select initialization options. + * @return The string which correspond to the enum IniType @p initOption. + */ + std::string initOptionsToString(ged::Options::InitType initOption); + + /*! + * @brief Returns the enum Method which correspond to the string parameter + * @param method Select the method that is to be used. + * @return The computation method which correspond in the edit cost functions map. + */ +ged::Options::GEDMethod translateMethod(std::string method); + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(std::size_t* pointer, std::size_t dataSize ); + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The double pointer to convert. + * @return The vector which contains the pointer's values. + */ +std::vector translatePointer(double* pointer, std::size_t dataSize ); + +/*! + * @brief Returns the vector of values which correspond to the pointer parameter. + * @param pointer The size_t pointer to convert. + * @return The vector which contains the pointer's values, with double type. + */ +std::vector translateAndConvertPointer(std::size_t* pointer, std::size_t dataSize ); + +/*! + * @brief Returns the string which contains all element of a int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector); + +/*! + * @brief Returns the string which contains all element of a unsigned long int list. + * @param vector The vector to translate. + * @return The string which contains all elements separated with a blank space. + */ +std::string toStringVectorInt(std::vector vector); + +} // namespace pyged + +//#endif /* GEDLIB_BIND_UTIL_HPP */ \ No newline at end of file diff --git a/gklearn/gedlib/src/single_so_version_bk/gedlib_header.hpp b/gklearn/gedlib/src/single_so_version_bk/gedlib_header.hpp new file mode 100644 index 0000000000..ad0432fd48 --- /dev/null +++ b/gklearn/gedlib/src/single_so_version_bk/gedlib_header.hpp @@ -0,0 +1,23 @@ +/**************************************************************************** + * * + * Copyright (C) 2019-2025 by Linlin Jia, Natacha Lambert, and David B. * + * Blumenthal * + * * + * This file should be used by Python. * + * Please call the Python module if you want to use GedLib with this code.* + * * + * Otherwise, you can directly use GedLib for C++. * + * * + ***************************************************************************/ + +/*! + * @file gedlib_header.hpp + * @brief Include all headers of GEDLIB to avoid multiple includes. + */ +#pragma once + +#define SRC_ENV_GED_ENV_GXL_CPP_ + +#include "../include/gedlib-master/src/env/ged_env.hpp" +#include "../include/gedlib-master/src/env/node_map.hpp" +#include "../include/gedlib-master/src/env/common_types.hpp" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..cbbc7d2c16 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "graphkit-learn" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "control>=0.10.1", + "cvxopt>=1.3.2", + "cvxpy>=1.6.5", + "cython>=0.29.37", + "matplotlib>=3.10.3", + "mosek>=11.0.20", + "networkx>=3.4.2", + "numpy>=2.2.6", + "pip>=25.1.1", + "scikit-learn>=1.6.1", + "scipy>=1.15.3", + "setuptools>=80.8.0", + "tabulate>=0.9.0", + "tqdm>=4.67.1", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..4618e19567 --- /dev/null +++ b/uv.lock @@ -0,0 +1,926 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "clarabel" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/83/f3e550b56188b9aa363351e87f6608c67cc2c7551feece1006d5ff9eb0c3/clarabel-0.10.0.tar.gz", hash = "sha256:a8a2105058fd7db54718be53c48715a50910500b10ff0b8f5380434e69c10a10", size = 212927 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/18/a537f020338349943db49afb305a75e8d41ba9b1383df6341bde8decfde7/clarabel-0.10.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ac0375778b351ed0a6a209a3a671e438181f640e98ea56761acf44681f05f211", size = 1737235 }, + { url = "https://files.pythonhosted.org/packages/15/6c/eb05273543a80f4f9d4196fde5d2e20dc73102b4e5255d965018bd9c9031/clarabel-0.10.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:385c29169918a0fbf7eaece919db381120519241d9806f65b291444ef52deccc", size = 903609 }, + { url = "https://files.pythonhosted.org/packages/44/23/f3bc9dcb9b5e9b821702dcd2a5da03e0583ad96609641c069a31ccab6db9/clarabel-0.10.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325468980cd4495005926d413ccca4bb534e52ecc9e43fbd915f5d0e63859bd8", size = 940233 }, + { url = "https://files.pythonhosted.org/packages/fa/fa/5faf0fcbad99a70ee416443c5194af8c7f1c65d74f6570bfa1560e69af8e/clarabel-0.10.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9be6910e0ae1694996aa0c5f3db9b99ab6b619f6735ad178086b6f1e3eeef5e2", size = 1011382 }, + { url = "https://files.pythonhosted.org/packages/e6/ba/67a0defb70ed910877049e915e6738fc0432edd5e46b97743ce78a1e5b95/clarabel-0.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:7871b6f499ad66f71d4e7fb40754c4d986d4316f242beb62ff4f63a69785a50c", size = 742536 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, +] + +[[package]] +name = "control" +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/c2/c480f2af22bfb3f2fbb0e791a26e7b8d7d3a63b6873f644c22b161dd5299/control-0.10.1.tar.gz", hash = "sha256:3bba142c9274d896efda8c9a21aaf61dd38d673d6d40714a7989d143428e738c", size = 9474344 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/9b/972af6f963631c10e98e35fe53a46f6f4ec1e2dd05c5d88f63f3c75c3d21/control-0.10.1-py3-none-any.whl", hash = "sha256:ff4c7620addb7edcafa24f712b9cad61dcdf50a40018f16b8dcb0b26c7774524", size = 549597 }, +] + +[[package]] +name = "cvxopt" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/12/8467d16008ab7577259d32f1e59c4d84edda22b7729ab4a1a0dfd5f0550b/cvxopt-1.3.2.tar.gz", hash = "sha256:3461fa42c1b2240ba4da1d985ca73503914157fc4c77417327ed6d7d85acdbe6", size = 4108454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/ab/78b8dcaf31f034184c4d9051562631856212614f34b9246f694dfb3e105b/cvxopt-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd4a1bba537a34808b92f1e793e3499029d339a7a2ab6d989f82e395b7b740ff", size = 13835104 }, + { url = "https://files.pythonhosted.org/packages/44/b1/b27dcf10dc6b61ffeb84bcf684d83ca90557b717d80b78a4758576c17010/cvxopt-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3cd2db913b1cf64d84cdb7bc467a8a15adbd1f0f83a7a45a7167ad590f79408", size = 11103451 }, + { url = "https://files.pythonhosted.org/packages/41/6d/98814860dbb9cdc27dcb6651b35124d7adca3bfe281f3351abb02a8a3f72/cvxopt-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6874e1b9aa002f9d796da9d02bdca76b15aa3d4b2f83ca5064ac4c7894b92ece", size = 13578154 }, + { url = "https://files.pythonhosted.org/packages/ef/67/3c577c9b4a09c3006e994a581fb540f48cf0378d8f3785cc1fe00fd48b87/cvxopt-1.3.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:32d9f88940464bffddfc0601fe3156ab16bf5a92393483e32342df0272fa64ce", size = 13814850 }, + { url = "https://files.pythonhosted.org/packages/89/91/a68d87b421c4bfe936c756778d58c7220abd9292e8e2dac951a3e3f64505/cvxopt-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9eb704be0918f04691af1267107539222cc2277bca888fdc385733bcab30f734", size = 9499915 }, + { url = "https://files.pythonhosted.org/packages/5b/10/429440cf9b841a5f8645f0aacc6a8da0a87cce4846d45e836f6b5f83be34/cvxopt-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:22d12b88190e047c0cedde165711222aa0dcdc325a229b876c36f746dd4a6f12", size = 12844564 }, + { url = "https://files.pythonhosted.org/packages/c1/43/f626c353802fb5ed37a087a0e41ad92246a1e1189869d47865853a980927/cvxopt-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a459b6ee9f99fc34861cbcf679a196af2d930ec70d95018a94f2e6dbe46c8c24", size = 13835210 }, + { url = "https://files.pythonhosted.org/packages/08/4d/2b2cc805f7db0636896b185dc8204556d363ccadbdca67e1a60e7aab4be6/cvxopt-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ae730ebc130461f743922f11d00c2d59a79492e57a1f5d245d4a6c731b7e334", size = 11095304 }, + { url = "https://files.pythonhosted.org/packages/8b/59/5e617916304022f5ad421459aa3f6e631537317d7a804c8128b32c6c29e6/cvxopt-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994dab68c193bea405a3a89a88b8703dd2c79bb790a330c8d459f0454cca71ef", size = 13578119 }, + { url = "https://files.pythonhosted.org/packages/e8/45/16b1719c489f734c76a6d9187f6dcdc41a1b923cd91c081aa0f4bedb923d/cvxopt-1.3.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ede23c1aaacdbfd3b8fd192121b3024b41d00a97f2e9fc8f106be922ea05523d", size = 13840609 }, + { url = "https://files.pythonhosted.org/packages/1e/cd/cd01bd7f4052d2ca336d67da4ecae4ffef34289ff408e8f654e14ee44b96/cvxopt-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a8c92308165b632bc43dc39acee052180037a1209d4a57b5c3d10136a2f563a4", size = 9524719 }, + { url = "https://files.pythonhosted.org/packages/a3/52/2237d72cf007e6c36367ab8a776388a9f13511e4cfa8a71b79101ad6e0fa/cvxopt-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:0c45f663e40b3ed2e2320e7ae8d50fcf09b5ac72c5af4c66aa523e0045453311", size = 12844638 }, + { url = "https://files.pythonhosted.org/packages/10/dc/1c21715e1267ca29f562e4450426d1ff8a7ffcc3e670100cec332a105b95/cvxopt-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25adbeb0efd50d7ea4f07e5f5bd390a3c807df907f03efb86b018807c2c8cfbe", size = 13836586 }, + { url = "https://files.pythonhosted.org/packages/cd/c8/a04048143d0329ccd36403951746c1a6b5f1fc56c479e5a0a77efb2064b2/cvxopt-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c10e27cb7a27b55f17e0df30c6b85e98c9672a7bdb7000a7509560eee7679137", size = 12765513 }, + { url = "https://files.pythonhosted.org/packages/c7/17/ee82c745c5bda340a4dd812652c42fb71efd45f663554a10c3ec45f230df/cvxopt-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8bcf71a5016aeb24e597dc099564e8de809e0bc5d6af21e26422586aea26718", size = 17870231 }, + { url = "https://files.pythonhosted.org/packages/c6/f9/467c3f4682f3dbfbd7ff67f2307ed746a86b6dcc6b0b62cf1eeaebbd9d74/cvxopt-1.3.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a581e6c87a06371210184f64353055ff7c917d49363901ae0c527da139095082", size = 13846494 }, + { url = "https://files.pythonhosted.org/packages/41/8e/c3869928250e12ad9264da388bc70150a9de039e233b815a6a3bd2b8b8ae/cvxopt-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be7800ac4556d8920aaf8e4e2d89348aafd5d585642aabf9eeecb09a2659fbca", size = 9529949 }, + { url = "https://files.pythonhosted.org/packages/9f/ad/edce467c24529c536fc9de787546a1c8eca293009383a872b6f638d22eae/cvxopt-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:a92ebfc5df77fea57544f8ad2102bfc45af0e77ac4dfe98ed1b9628e8bba77c3", size = 12845277 }, + { url = "https://files.pythonhosted.org/packages/3e/c5/3e70e50c4c478acd3fefe3ea51b7e42ad661ce5a265a72b3dba175ce10fc/cvxopt-1.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2f9135eea23c9b781574e0cadc5738cf5651a8fd8de822b6de1260411523bfd1", size = 16873224 }, + { url = "https://files.pythonhosted.org/packages/61/96/e42b9ec38e1bbe9bf85a5fc9cc7feb173de5a874889735072b49a7d4d8d0/cvxopt-1.3.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d7921768712db156e6ec92ac21f7ce52069feb1fb994868d0ca795498111fbac", size = 12424739 }, + { url = "https://files.pythonhosted.org/packages/32/08/2c621ad782e9ff7f921c2244c6b4bcbc72ca756cb33021295c288123c465/cvxopt-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af63db45ba559e3e15180fbec140d8a4ff612d8f21d989181a4e8479fa3b8b6", size = 17869707 }, + { url = "https://files.pythonhosted.org/packages/62/60/583a1ef8e2e259bdd1bf32fccd4ea15aef4aad5854746ec59cbb2462eb92/cvxopt-1.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:8fe178ac780a8bccf425a08004d853eae43b3ddcf7617521fb35c63550077b17", size = 13846614 }, + { url = "https://files.pythonhosted.org/packages/e4/2b/d8721b046a3c8bff494490a01ef1eeacf1f970f0d1274448856ccbe0475c/cvxopt-1.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a47a95d7848e6fe768b55910bac8bb114c5f1f355f5a6590196d5e9bdf775d2f", size = 21277032 }, + { url = "https://files.pythonhosted.org/packages/6a/19/b1e1c16895a36cc504bf7a940e88431b82b18ca10cbce81072860b9e3d60/cvxopt-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e863238d64a4b4443b8be53a08f6b94eda6ec1727038c330da02014f7c19e1be", size = 9530674 }, + { url = "https://files.pythonhosted.org/packages/42/cc/ac0705749f96cc52f8d30c9c06e54dc8d4c04ef9c2d21aeed1ae2ee63dab/cvxopt-1.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c56965415afd8a493cc4af3587960751f8780057ca3de8c6be97217156e4633", size = 13725340 }, + { url = "https://files.pythonhosted.org/packages/76/f2/7e3c3f51e8e6b325bf00bfc37036f1f58bd9a5c29bbd88fb2eef2ebc0ac2/cvxopt-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:85c3b52c1353b294c597b169cc901f5274d8bb8776908ccad66fec7a14b69519", size = 16226402 }, + { url = "https://files.pythonhosted.org/packages/b9/55/90b40b489a235a9f35a532eb77cec81782e466779d9a531ffda6b2f99410/cvxopt-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:0a0987966009ad383de0918e61255d34ed9ebc783565bcb15470d4155010b6bf", size = 12845323 }, +] + +[[package]] +name = "cvxpy" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "clarabel" }, + { name = "numpy" }, + { name = "osqp" }, + { name = "scipy" }, + { name = "scs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/9e/0aeb94d435f4aad947b45eb9354fba31b23066b5ed78c42bbf71a9e4105e/cvxpy-1.6.5.tar.gz", hash = "sha256:666081b9c1f6db8947bcfc3c6f250174f934fa1ba8e30b38e3d32eba779ff785", size = 1610956 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/d4/6fcae39e6be6556f49752e1b2d1ccdfea1e9599aa086adfb7f038019c771/cvxpy-1.6.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:26ac571ffed3f98ad59c2ddfb88eaad7280003faa96b5f509c353e46fb79e787", size = 1485548 }, + { url = "https://files.pythonhosted.org/packages/6a/00/e1a1f8ca3ec4410e9d17b3190f73b00ac404c3b22577c6458a53e74191ba/cvxpy-1.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b35a3b5c6c7b43b85179ee77d895ac7a49afdccbe7ebcf60f5f74cd9186cedd8", size = 1151746 }, + { url = "https://files.pythonhosted.org/packages/5f/a9/0b56ee6107410548914dcb6ad041b3aed1cf0f75e8ade3cc0690290f59da/cvxpy-1.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:374b18b023a9266d888bf753b6f012d1e58199bb9ba7d2e71bcbbd1f2c677ddf", size = 1204806 }, + { url = "https://files.pythonhosted.org/packages/de/81/8e6b99e5f53bfd107cea01dbbbcc2bd832146b6bf313b632cf5be0c90f79/cvxpy-1.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e39e83fab103c049209d93e38a439c9ce0d09b59874c1b026c82b655aad2d9", size = 1229089 }, + { url = "https://files.pythonhosted.org/packages/b9/90/9c36f1274d7fe055c5bd190fc197d62695e29fe715a3ce7be50d56c2e992/cvxpy-1.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:ff94cad9bcb0897bdecadd0b34836fba7d44b5c95237837132f09f979dadafe0", size = 1096536 }, + { url = "https://files.pythonhosted.org/packages/c5/43/eb11a345f504c600add4e58f7420d19630b11903db053e00f8dce7b0ec4c/cvxpy-1.6.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9dfc3fda013b7387e4d8820d01c81739330519231254b73dfaa79bebdd509937", size = 1487826 }, + { url = "https://files.pythonhosted.org/packages/66/e4/e38fa091c4a3bdeaec92c31764e92382956d82f6997b481b9ec894c7b188/cvxpy-1.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7a7debf1bf36550c74bc6b5625592a9bec92d9f3a884b7e0a9d49f9e302ce6e4", size = 1153256 }, + { url = "https://files.pythonhosted.org/packages/72/21/199abe12ba335c989574ccf12113b27ddfdfd67d888f74163323164a5b95/cvxpy-1.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93d898722b772438ae7e41b043d89d5896ff33c0ba764429c0282dc7e7db80e6", size = 1206204 }, + { url = "https://files.pythonhosted.org/packages/3d/ed/9cf4defd1ddd44a96b54c7482117de5d032ab44e473de2517027735db360/cvxpy-1.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de121b64c4a876a0d1b005fbd68a893f0fc8640d2d02f58f2f3f0941f48a6742", size = 1230283 }, + { url = "https://files.pythonhosted.org/packages/11/3f/9f58f7e6537b35c10b5de30a3d6496f765e65ede72fa35db3ac76512e1f0/cvxpy-1.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:9752baf15c2339c24d4d8bd974563c14f7cc8020a338d31dbcb80149fcbb7971", size = 1097721 }, + { url = "https://files.pythonhosted.org/packages/30/ae/4acbab3ab701ec0873d54c2d704472cbbbe58d29090f28685ad1df4028ae/cvxpy-1.6.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c3365de01866f3f3a14f2c754d52f1aa361184c4f5f004b7257622b2c177237", size = 1490248 }, + { url = "https://files.pythonhosted.org/packages/40/a5/39ae4adec78e063e5a5a3f751701ff5d72d5b1548712771ec337b35883d5/cvxpy-1.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9f70e39a41e1691783a4e55a73440f9e68b852fe0e0498c4d0c5a1505f3a2640", size = 1154518 }, + { url = "https://files.pythonhosted.org/packages/c2/ba/ab502c2cc25e4b6eeaf7833b6bef5f1a5aa14c394a9bf9822bdcd7efcf9d/cvxpy-1.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:579b0039fa097e2e20272028bd2d4b592de7c67e60fd8eb6991629b5d53204a2", size = 1205970 }, + { url = "https://files.pythonhosted.org/packages/27/e7/56c6fea9afc1ff1a8abcd60a40a21370bea37b620902237045b5083b9044/cvxpy-1.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a82c6de45a39065dd8c5ff5b30bee21b09ca85eac6b6dcbd3f5ed1b19986bec1", size = 1231908 }, + { url = "https://files.pythonhosted.org/packages/de/0c/d0dd074b17076665e5a28145944c88eac07e8ecf05d8ece9a06dbd495b8b/cvxpy-1.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:436aed23d0ca84df81944018d971cf8bda8f19bfa2362ae3c540313d5183eca6", size = 1098416 }, + { url = "https://files.pythonhosted.org/packages/22/68/15aeafcc7aab50853bfd96e7bb6019b7a8b2c764f4c54c690cdc1289b736/cvxpy-1.6.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c16478107e3bd8bbda85d7a2d12ae737151e44a5fc43f695a84d387311c3cce9", size = 1490440 }, + { url = "https://files.pythonhosted.org/packages/e1/ef/a1ad85c1e2a43a1afcfe2cdc96f99d9ddf26a2c704ff124e68b2f29c4214/cvxpy-1.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5f5ca05999a61b7eed4fc3369e19368d0241538f1890d5510c31f35f7daff021", size = 1154603 }, + { url = "https://files.pythonhosted.org/packages/72/02/2d165243905ea567f103748d69eb06b5f0976da3e2aeef4686b7e08c8c13/cvxpy-1.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13eb878e89029d00c0569c6f151df31d136959b0ac1a9f11d567e77579a9a108", size = 1205948 }, + { url = "https://files.pythonhosted.org/packages/d1/31/0fae4e2553412b56fc0b0806f4450a874a3293aba1faed6c1337fa868cf5/cvxpy-1.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47c0edd990a200de5115427fdc5526cf8cd462951318af55800e6d5abcf72d32", size = 1232004 }, + { url = "https://files.pythonhosted.org/packages/a0/3c/86682ec2b72f72e46e359903d7c18686e5314aa4baa96c77f6595c58fb03/cvxpy-1.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:51161b0e8b0d83dc07355bab938bd0734dd5531c98dca8d6faaa8b847c651339", size = 1098576 }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "cython" +version = "0.29.37" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/97/8cc3fe7c6de4796921236a64d00ca8a95565772e57f0d3caae68d880b592/Cython-0.29.37.tar.gz", hash = "sha256:f813d4a6dd94adee5d4ff266191d1d95bf6d4164a4facc535422c021b2504cfb", size = 2099621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/2e/9116c533bd9fd36db3faa50d60e95305b6a6f1ed4c4d2abb214b6db78ed7/Cython-0.29.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4658499a41255431f6bbdca7e634e9c8d3a4c190bf24b4aa1646dac751d3da4d", size = 1807840 }, + { url = "https://files.pythonhosted.org/packages/5b/b7/6e35bbbf253ff6c383eec57baab1c6814107b2b9caf26b10e9c25014bb1e/Cython-0.29.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:12192ab269e7185720f2d2f8894587bf1da4276db1b9b869e4622a093f18cae6", size = 1910402 }, + { url = "https://files.pythonhosted.org/packages/20/88/af83962f9134a92949b8e780205d824be8aab704fdb711f379d434f3e9af/Cython-0.29.37-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:9450e0766ab65947f8a2a36f9e59079fc879c3807ec936c61725a48c97741a52", size = 1911975 }, + { url = "https://files.pythonhosted.org/packages/45/2e/9c904296381b3afc6cd8b16f3c4cb908265c969b807cddf0b65c07449758/Cython-0.29.37-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:177481b0a7e003e5c49e2bf0dda1d6fe610c239f17642a5da9f18c2ad0c5f6b6", size = 2006645 }, + { url = "https://files.pythonhosted.org/packages/c2/ba/eddee5c048c621607023f7438e27f9e559d4c34149d758049d50150c2b2f/Cython-0.29.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b048354fd380278f2fa096e7526973beb6e0491a9d44d7e4e29df52612d25776", size = 1808964 }, + { url = "https://files.pythonhosted.org/packages/22/99/2b01e0164ff1ea592e3515e473517674780f0f3d49f48af30324b85ac94d/Cython-0.29.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ea6d208be1906c5df25b674777d5905c6d8e9ef0b201b830849e0729ba08caba", size = 1910527 }, + { url = "https://files.pythonhosted.org/packages/6a/2f/b3f3694aa2ea48a39c2ef1b218b6a25f4b0d62836495f9a65495f060969a/Cython-0.29.37-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:af03854571738307a5f30cc6b724081d72db12f907699e7fdfc04c12c839158e", size = 1910933 }, + { url = "https://files.pythonhosted.org/packages/3d/7f/f1a8ec07e0e7e2af84940c0155e6f8bb383671da34a785f441a19f2cff4e/Cython-0.29.37-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c33508ede9172a6f6f99d5a6dadc7fee23c840423b411ef8b5a403c04e530297", size = 2006522 }, + { url = "https://files.pythonhosted.org/packages/c9/aa/99a0eac01136c0c75feb3210d107c49f93d49d5cb97f19e99318b9ecefdd/Cython-0.29.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8af5975ecfae254d8c0051204fca995dda8f93cf9f0bbf7571e3cda2b0cef4d", size = 1951525 }, + { url = "https://files.pythonhosted.org/packages/14/5f/f5efba4409474892d650ba7e57349afa5d4c41d06f5ba3356c32891c75a6/Cython-0.29.37-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29415d8eb2fdc1ea518ca4810c50a2d062b387d4c9fbcfb3352346e93db22c6d", size = 1950936 }, + { url = "https://files.pythonhosted.org/packages/7e/26/9d8de10005fedb1eceabe713348d43bae1dbab1786042ca0751a2e2b0f8c/Cython-0.29.37-py2.py3-none-any.whl", hash = "sha256:95f1d6a83ef2729e67b3fa7318c829ce5b07ac64c084cd6af11c228e0364662c", size = 989503 }, +] + +[[package]] +name = "fonttools" +version = "4.58.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/4d037663e2a1fe30fddb655d755d76e18624be44ad467c07412c2319ab97/fonttools-4.58.0.tar.gz", hash = "sha256:27423d0606a2c7b336913254bf0b1193ebd471d5f725d665e875c5e88a011a43", size = 3514522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/07/06d01b7239d6632a0984ef29ab496928531862b827cd3aa78309b205850d/fonttools-4.58.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0bcaa65cddbc7d32c77bd0af0b41fdd6448bad0e84365ca79cf8923c27b21e46", size = 2731632 }, + { url = "https://files.pythonhosted.org/packages/1d/c7/47d26d48d779b1b084ebc0d9ec07035167992578768237ef553a3eecc8db/fonttools-4.58.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:25590272f89e94ab5a292d518c549f3a88e6a34fa1193797b7047dfea111b048", size = 2303941 }, + { url = "https://files.pythonhosted.org/packages/79/2e/ac80c0fea501f1aa93e2b22d72c97a8c0d14239582b7e8c722185a0540a7/fonttools-4.58.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:614435e9a87abe18bd7bc7ceeb8029e8f181c571317161e89fa3e6e0a4f20f5d", size = 4712776 }, + { url = "https://files.pythonhosted.org/packages/f2/5c/b41f9c940dc397ecb41765654efc76e06782bfe0783c3e2affc534be181c/fonttools-4.58.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0154bd86d9a9e880f6e937e4d99c2139a624428dd9852072e12d7a85c79d611e", size = 4743251 }, + { url = "https://files.pythonhosted.org/packages/3d/c4/0d3807d922a788b603a3fff622af53e732464b88baf0049a181a90f9b1c6/fonttools-4.58.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5b3660df0b02c9cebbf7baf66952c2fd055e43e658aceb92cc95ba19e0a5c8b6", size = 4795635 }, + { url = "https://files.pythonhosted.org/packages/46/74/627bed8e2c7e641c9c572f09970b0980e5513fd29e57b394d4aee2261e30/fonttools-4.58.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c43b7f1d0b818427bb1cd20903d1168271abdcde10eb6247b1995c4e1ed63907", size = 4904720 }, + { url = "https://files.pythonhosted.org/packages/f9/f2/7e5d082a98eb61fc0c3055e8a0e061a1eb9fc2d93f0661854bf6cb63c519/fonttools-4.58.0-cp310-cp310-win32.whl", hash = "sha256:5450f40c385cdfa21133245f57b9cf8ce45018a04630a98de61eed8da14b8325", size = 2188180 }, + { url = "https://files.pythonhosted.org/packages/00/33/ffd914e3c3a585003d770457188c8eaf7266b7a1cceb6d234ab543a9f958/fonttools-4.58.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0553431696eacafee9aefe94dc3c2bf5d658fbdc7fdba5b341c588f935471c6", size = 2233120 }, + { url = "https://files.pythonhosted.org/packages/76/2e/9b9bd943872a50cb182382f8f4a99af92d76e800603d5f73e4343fdce61a/fonttools-4.58.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9345b1bb994476d6034996b31891c0c728c1059c05daa59f9ab57d2a4dce0f84", size = 2751920 }, + { url = "https://files.pythonhosted.org/packages/9b/8c/e8d6375da893125f610826c2e30e6d2597dfb8dad256f8ff5a54f3089fda/fonttools-4.58.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d93119ace1e2d39ff1340deb71097932f72b21c054bd3da727a3859825e24e5", size = 2313957 }, + { url = "https://files.pythonhosted.org/packages/4f/1b/a29cb00c8c20164b24f88780e298fafd0bbfb25cf8bc7b10c4b69331ad5d/fonttools-4.58.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c9e4f01bb04f19df272ae35314eb6349fdb2e9497a163cd22a21be999694bd", size = 4913808 }, + { url = "https://files.pythonhosted.org/packages/d1/ab/9b9507b65b15190cbfe1ccd3c08067d79268d8312ef20948b16d9f5aa905/fonttools-4.58.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62ecda1465d38248aaf9bee1c17a21cf0b16aef7d121d7d303dbb320a6fd49c2", size = 4935876 }, + { url = "https://files.pythonhosted.org/packages/15/e4/1395853bc775b0ab06a1c61cf261779afda7baff3f65cf1197bbd21aa149/fonttools-4.58.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29d0499bff12a26733c05c1bfd07e68465158201624b2fba4a40b23d96c43f94", size = 4974798 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/0358368ef5462f4653a198207b29885bee8d5e23c870f6125450ed88e693/fonttools-4.58.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1871abdb0af582e2d96cc12d88889e3bfa796928f491ec14d34a2e58ca298c7e", size = 5093560 }, + { url = "https://files.pythonhosted.org/packages/11/00/f64bc3659980c41eccf2c371e62eb15b40858f02a41a0e9c6258ef094388/fonttools-4.58.0-cp311-cp311-win32.whl", hash = "sha256:e292485d70402093eb94f6ab7669221743838b8bd4c1f45c84ca76b63338e7bf", size = 2186330 }, + { url = "https://files.pythonhosted.org/packages/c8/a0/0287be13a1ec7733abf292ffbd76417cea78752d4ce10fecf92d8b1252d6/fonttools-4.58.0-cp311-cp311-win_amd64.whl", hash = "sha256:6df3755fcf9ad70a74ad3134bd5c9738f73c9bb701a304b1c809877b11fe701c", size = 2234687 }, + { url = "https://files.pythonhosted.org/packages/6a/4e/1c6b35ec7c04d739df4cf5aace4b7ec284d6af2533a65de21972e2f237d9/fonttools-4.58.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:aa8316798f982c751d71f0025b372151ea36405733b62d0d94d5e7b8dd674fa6", size = 2737502 }, + { url = "https://files.pythonhosted.org/packages/fc/72/c6fcafa3c9ed2b69991ae25a1ba7a3fec8bf74928a96e8229c37faa8eda2/fonttools-4.58.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6db489511e867633b859b11aefe1b7c0d90281c5bdb903413edbb2ba77b97f1", size = 2307214 }, + { url = "https://files.pythonhosted.org/packages/52/11/1015cedc9878da6d8d1758049749eef857b693e5828d477287a959c8650f/fonttools-4.58.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:107bdb2dacb1f627db3c4b77fb16d065a10fe88978d02b4fc327b9ecf8a62060", size = 4811136 }, + { url = "https://files.pythonhosted.org/packages/32/b9/6a1bc1af6ec17eead5d32e87075e22d0dab001eace0b5a1542d38c6a9483/fonttools-4.58.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba7212068ab20f1128a0475f169068ba8e5b6e35a39ba1980b9f53f6ac9720ac", size = 4876598 }, + { url = "https://files.pythonhosted.org/packages/d8/46/b14584c7ea65ad1609fb9632251016cda8a2cd66b15606753b9f888d3677/fonttools-4.58.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f95ea3b6a3b9962da3c82db73f46d6a6845a6c3f3f968f5293b3ac1864e771c2", size = 4872256 }, + { url = "https://files.pythonhosted.org/packages/05/78/b2105a7812ca4ef9bf180cd741c82f4522316c652ce2a56f788e2eb54b62/fonttools-4.58.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:874f1225cc4ccfeac32009887f722d7f8b107ca5e867dcee067597eef9d4c80b", size = 5028710 }, + { url = "https://files.pythonhosted.org/packages/8c/a9/a38c85ffd30d1f2c7a5460c8abfd1aa66e00c198df3ff0b08117f5c6fcd9/fonttools-4.58.0-cp312-cp312-win32.whl", hash = "sha256:5f3cde64ec99c43260e2e6c4fa70dfb0a5e2c1c1d27a4f4fe4618c16f6c9ff71", size = 2173593 }, + { url = "https://files.pythonhosted.org/packages/66/48/29752962a74b7ed95da976b5a968bba1fe611a4a7e50b9fefa345e6e7025/fonttools-4.58.0-cp312-cp312-win_amd64.whl", hash = "sha256:2aee08e2818de45067109a207cbd1b3072939f77751ef05904d506111df5d824", size = 2223230 }, + { url = "https://files.pythonhosted.org/packages/0c/d7/d77cae11c445916d767cace93ba8283b3f360197d95d7470b90a9e984e10/fonttools-4.58.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4809790f2371d8a08e59e1ce2b734c954cf09742e75642d7f4c46cfdac488fdd", size = 2728320 }, + { url = "https://files.pythonhosted.org/packages/77/48/7d8b3c519ef4b48081d40310262224a38785e39a8610ccb92a229a6f085d/fonttools-4.58.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b00f240280f204ce4546b05ff3515bf8ff47a9cae914c718490025ea2bb9b324", size = 2302570 }, + { url = "https://files.pythonhosted.org/packages/2c/48/156b83eb8fb7261056e448bfda1b495b90e761b28ec23cee10e3e19f1967/fonttools-4.58.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a62015ad463e1925544e9159dd6eefe33ebfb80938d5ab15d8b1c4b354ff47b", size = 4790066 }, + { url = "https://files.pythonhosted.org/packages/60/49/aaecb1b3cea2b9b9c7cea6240d6bc8090feb5489a6fbf93cb68003be979b/fonttools-4.58.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ceef6f6ab58061a811967e3e32e630747fcb823dcc33a9a2c80e2d0d17cb292", size = 4861076 }, + { url = "https://files.pythonhosted.org/packages/dc/c8/97cbb41bee81ea9daf6109e0f3f70a274a3c69418e5ac6b0193f5dacf506/fonttools-4.58.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c7be21ac52370b515cdbdd0f400803fd29432a4fa4ddb4244ac8b322e54f36c0", size = 4858394 }, + { url = "https://files.pythonhosted.org/packages/4d/23/c2c231457361f869a7d7374a557208e303b469d48a4a697c0fb249733ea1/fonttools-4.58.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:85836be4c3c4aacf6fcb7a6f263896d0e9ce431da9fa6fe9213d70f221f131c9", size = 5002160 }, + { url = "https://files.pythonhosted.org/packages/a9/e0/c2262f941a43b810c5c192db94b5d1ce8eda91bec2757f7e2416398f4072/fonttools-4.58.0-cp313-cp313-win32.whl", hash = "sha256:2b32b7130277bd742cb8c4379a6a303963597d22adea77a940343f3eadbcaa4c", size = 2171919 }, + { url = "https://files.pythonhosted.org/packages/8f/ee/e4aa7bb4ce510ad57a808d321df1bbed1eeb6e1dfb20aaee1a5d9c076849/fonttools-4.58.0-cp313-cp313-win_amd64.whl", hash = "sha256:75e68ee2ec9aaa173cf5e33f243da1d51d653d5e25090f2722bc644a78db0f1a", size = 2222972 }, + { url = "https://files.pythonhosted.org/packages/9b/1f/4417c26e26a1feab85a27e927f7a73d8aabc84544be8ba108ce4aa90eb1e/fonttools-4.58.0-py3-none-any.whl", hash = "sha256:c96c36880be2268be409df7b08c5b5dacac1827083461a6bc2cb07b8cbcec1d7", size = 1111440 }, +] + +[[package]] +name = "graphkit-learn" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "control" }, + { name = "cvxopt" }, + { name = "cvxpy" }, + { name = "cython" }, + { name = "matplotlib" }, + { name = "mosek" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "pip" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "setuptools" }, + { name = "slycot" }, + { name = "tabulate" }, + { name = "tqdm" }, +] + +[package.metadata] +requires-dist = [ + { name = "control", specifier = ">=0.10.1" }, + { name = "cvxopt", specifier = ">=1.3.2" }, + { name = "cvxpy", specifier = ">=1.6.5" }, + { name = "cython", specifier = ">=0.29.37" }, + { name = "matplotlib", specifier = ">=3.10.3" }, + { name = "mosek", specifier = ">=11.0.20" }, + { name = "networkx", specifier = ">=3.4.2" }, + { name = "numpy", specifier = ">=2.2.6" }, + { name = "pip", specifier = ">=25.1.1" }, + { name = "scikit-learn", specifier = ">=1.6.1" }, + { name = "scipy", specifier = ">=1.15.3" }, + { name = "setuptools", specifier = ">=80.8.0" }, + { name = "slycot", specifier = ">=0.6.0" }, + { name = "tabulate", specifier = ">=0.9.0" }, + { name = "tqdm", specifier = ">=4.67.1" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "joblib" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/08/8bd4a0250247861420a040b33ccf42f43c426ac91d99405374ef117e5872/joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5", size = 330234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d3/13ee227a148af1c693654932b8b0b02ed64af5e1f7406d56b088b57574cd/joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491", size = 307682 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862 }, + { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149 }, + { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719 }, + { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801 }, + { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111 }, + { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213 }, + { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873 }, + { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205 }, + { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823 }, + { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464 }, + { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103 }, + { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492 }, + { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689 }, + { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466 }, + { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252 }, + { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321 }, + { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972 }, + { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954 }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318 }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132 }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633 }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031 }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988 }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034 }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223 }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985 }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109 }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082 }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699 }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044 }, + { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896 }, + { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702 }, + { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298 }, +] + +[[package]] +name = "mosek" +version = "11.0.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/f5/d54f015f44bc8a2fbdef8d1c15453916cec97e59eb261029821fb6715ef6/mosek-11.0.20-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8fa64362fee4ba0f5d6c861374081394da007f47bfda6a8cfd7d3f030fbbb9c9", size = 8338476 }, + { url = "https://files.pythonhosted.org/packages/5c/38/3280321439a5e0b83c228a4e7c8e99bdcee82230b87950565da107f1ef20/mosek-11.0.20-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:7906e44d366394681f218c70e4974897dbf99b7ae317d1107ec8446258e36105", size = 14725578 }, + { url = "https://files.pythonhosted.org/packages/b9/d3/ed4cb75a7909df7e3a85c7368a31eaafd16d41cfe60673faf83775797723/mosek-11.0.20-cp39-abi3-manylinux_2_29_aarch64.whl", hash = "sha256:e84f80dcdd1eaa7eb64fedd91877d17fbb40e90034f4d2656b493eaefb9264a7", size = 10618981 }, + { url = "https://files.pythonhosted.org/packages/36/9a/4f99f1c6ff395a9517f38efcbf72702019bf11ec0c951974cf3ffe266a43/mosek-11.0.20-cp39-abi3-win_amd64.whl", hash = "sha256:971b668dade7653b2abb45e0b42ef3a8ab18d693c9db7d73d8b5f2086a12ac9e", size = 10649438 }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +] + +[[package]] +name = "osqp" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/13/c74c50bab7477029daf9fc99455cc9c8b25e50843e44cf096b237d3ac6b2/osqp-1.0.4.tar.gz", hash = "sha256:0877552e325ff4cc1c676796ba482904eb4b66e750eff5b91df3273201f5ed00", size = 56640 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/31/f7814d83a7eff5264e96d062500eb7cade7ff17867dc9934561a0ab52644/osqp-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f4f06e1a67c272b8a4a0741020e859a8c67802466597397da2869802eb6a345", size = 311159 }, + { url = "https://files.pythonhosted.org/packages/d3/cd/033889ada7047b920206920aec62db104772ee13a9fb338148ead61c3d21/osqp-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:738d0b03d350b97c2cfeecc41620082a913e1efd06d2d58e4cc1d54d00ebfac5", size = 290328 }, + { url = "https://files.pythonhosted.org/packages/fe/e6/061f1747de56fa24aeb7f4db5da1a5604847fb72c6d1ea410bb46a73f7d6/osqp-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9ce84caf57a0166123c923b5ca4d8f126799351f8454e96e25c339240e2b4db", size = 344566 }, + { url = "https://files.pythonhosted.org/packages/84/f2/97bb67a653eb1a1a5e333caf0fb8e71890e56c7f32b7cf0a24927b54adbb/osqp-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:67a7af7779be5036d1f1729489781628feaa1dffa33ae44feefa172b6b9e173e", size = 301742 }, + { url = "https://files.pythonhosted.org/packages/14/b3/ecaa1638cf8c177f1fe8b79d7c715a2693c61158b47bf3a5269453d0eefe/osqp-1.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:819603a8b6d84e17bbe0e17558235e5dbe3d6b95021550d7da4942aa35b31b60", size = 312670 }, + { url = "https://files.pythonhosted.org/packages/b2/37/6e52dbbfedaad4534148853496741739a3430d52231638c84d65dc1a58e7/osqp-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e76644654e30edf97eb5d400ffa57f3541b551842921ed9ac16db8f307883d9", size = 291655 }, + { url = "https://files.pythonhosted.org/packages/3a/3b/a3332b0f79375bff9a14863f9b32d98428f9300fc171e29b6e4df4850986/osqp-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddd7fc52e01c602f3482878b334eed1b366a9940b2053059968d3d47688de9ef", size = 345271 }, + { url = "https://files.pythonhosted.org/packages/a1/85/19e2b49117f7ea20d94129d67c9b982e788ded90c561e0029c42fc7eb0ea/osqp-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:c2a2ac69feb009ac43f80f33dfe5b32baaccefd22c6fcb9176b48f755506f120", size = 302883 }, + { url = "https://files.pythonhosted.org/packages/f6/0f/e5896586d495c82a37f68e4ba26c10c7969858f6991584f4ddb477e83be4/osqp-1.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6432c8ea5db1b334eb20abce2c0eca081edc070ea86ec634bb62e8ca1a014e21", size = 313468 }, + { url = "https://files.pythonhosted.org/packages/4b/79/a1e26d05e2026489c0ea76503d8ae614af0cc436d4c9cdedae0f17da6fc3/osqp-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8510861708fe664a0942bccfea4b3ea566b12927e54b415f539c24b8cad095c0", size = 291828 }, + { url = "https://files.pythonhosted.org/packages/17/f3/428837a4fee6080bda8017367fc1bef1f61a80144576978cb404be5b6f97/osqp-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a3e3321b11426fa7a33b84f87f8d8608fdcd56c3992739012370f4d475b6a8f", size = 345079 }, + { url = "https://files.pythonhosted.org/packages/3c/17/46343994f07d0b629a661274b40632499e4920ee355b67717458d3d2b47e/osqp-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:73357ceb0cee581a8a18f32a50dda8954c80374bc94e77c06d8ececb402e2a22", size = 303076 }, + { url = "https://files.pythonhosted.org/packages/d9/ea/9a1b52ebe4a50baed976c42903b0164d3d4911d7bf80956400e0802362a9/osqp-1.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae336207b52bbab6d663e7fb77beaffc53279e91a14e3db76ad2726b0e935d6", size = 313562 }, + { url = "https://files.pythonhosted.org/packages/db/70/92232c9af70e05c0f5c99544ea97826cad49f8e3415f4106ca46ff124e07/osqp-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06dc3c6d7a0e2d4552dbc98a127e6d9a70d57ab856c8343c83aca42817ee8da3", size = 291900 }, + { url = "https://files.pythonhosted.org/packages/af/58/5eb3f6125ec82c7a22453b4a96d5a47d52a54fa0221b687c612558880faa/osqp-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11e4b50f66795d12e63b25b02d0261973b36ee8612ef8c949c124c74231da183", size = 344964 }, + { url = "https://files.pythonhosted.org/packages/47/09/0156d737b2dc1978e51a25540d29cd0a1597b81f0dadf2b563bd55d8702c/osqp-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:14334e9473d0dd2fde3465b0d4c7a7de827798426ae2b40cb26504b376c48413", size = 303053 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/8b/b158ad57ed44d3cc54db8d68ad7c0a58b8fc0e4c7a3f995f9d62d5b464a1/pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", size = 3198442 }, + { url = "https://files.pythonhosted.org/packages/b1/f8/bb5d956142f86c2d6cc36704943fa761f2d2e4c48b7436fd0a85c20f1713/pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", size = 3030553 }, + { url = "https://files.pythonhosted.org/packages/22/7f/0e413bb3e2aa797b9ca2c5c38cb2e2e45d88654e5b12da91ad446964cfae/pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", size = 4405503 }, + { url = "https://files.pythonhosted.org/packages/f3/b4/cc647f4d13f3eb837d3065824aa58b9bcf10821f029dc79955ee43f793bd/pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", size = 4490648 }, + { url = "https://files.pythonhosted.org/packages/c2/6f/240b772a3b35cdd7384166461567aa6713799b4e78d180c555bd284844ea/pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", size = 4508937 }, + { url = "https://files.pythonhosted.org/packages/f3/5e/7ca9c815ade5fdca18853db86d812f2f188212792780208bdb37a0a6aef4/pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", size = 4599802 }, + { url = "https://files.pythonhosted.org/packages/02/81/c3d9d38ce0c4878a77245d4cf2c46d45a4ad0f93000227910a46caff52f3/pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", size = 4576717 }, + { url = "https://files.pythonhosted.org/packages/42/49/52b719b89ac7da3185b8d29c94d0e6aec8140059e3d8adcaa46da3751180/pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", size = 4654874 }, + { url = "https://files.pythonhosted.org/packages/5b/0b/ede75063ba6023798267023dc0d0401f13695d228194d2242d5a7ba2f964/pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", size = 2331717 }, + { url = "https://files.pythonhosted.org/packages/ed/3c/9831da3edea527c2ed9a09f31a2c04e77cd705847f13b69ca60269eec370/pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", size = 2676204 }, + { url = "https://files.pythonhosted.org/packages/01/97/1f66ff8a1503d8cbfc5bae4dc99d54c6ec1e22ad2b946241365320caabc2/pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", size = 2414767 }, + { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450 }, + { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550 }, + { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018 }, + { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006 }, + { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773 }, + { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069 }, + { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460 }, + { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304 }, + { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809 }, + { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338 }, + { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918 }, + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, + { url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727 }, + { url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833 }, + { url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472 }, + { url = "https://files.pythonhosted.org/packages/b2/1b/e35d8a158e21372ecc48aac9c453518cfe23907bb82f950d6e1c72811eb0/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", size = 3459976 }, + { url = "https://files.pythonhosted.org/packages/26/da/2c11d03b765efff0ccc473f1c4186dc2770110464f2177efaed9cf6fae01/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", size = 3527133 }, + { url = "https://files.pythonhosted.org/packages/79/1a/4e85bd7cadf78412c2a3069249a09c32ef3323650fd3005c97cca7aa21df/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", size = 3571555 }, + { url = "https://files.pythonhosted.org/packages/69/03/239939915216de1e95e0ce2334bf17a7870ae185eb390fab6d706aadbfc0/pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", size = 2674713 }, + { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734 }, + { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841 }, + { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470 }, + { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013 }, + { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165 }, + { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586 }, + { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751 }, +] + +[[package]] +name = "pip" +version = "25.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/de/241caa0ca606f2ec5fe0c1f4261b0465df78d786a38da693864a116c37f4/pip-25.1.1.tar.gz", hash = "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077", size = 1940155 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "scikit-learn" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702 }, + { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765 }, + { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991 }, + { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182 }, + { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517 }, + { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620 }, + { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234 }, + { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155 }, + { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069 }, + { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809 }, + { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516 }, + { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837 }, + { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728 }, + { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700 }, + { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613 }, + { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001 }, + { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360 }, + { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004 }, + { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776 }, + { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865 }, + { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804 }, + { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530 }, + { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852 }, + { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256 }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511 }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151 }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732 }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964 }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749 }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383 }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201 }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255 }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035 }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499 }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602 }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415 }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622 }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796 }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684 }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504 }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735 }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284 }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958 }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454 }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199 }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455 }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140 }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549 }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184 }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256 }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540 }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115 }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884 }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018 }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716 }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342 }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869 }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851 }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011 }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407 }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030 }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709 }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045 }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062 }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132 }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503 }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097 }, +] + +[[package]] +name = "scs" +version = "3.2.7.post2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/2f/3f15676b0f4cc73879400d94f2c5c64130cad0bbca266aff1365dc643e79/scs-3.2.7.post2.tar.gz", hash = "sha256:4245a4f76328cc73911f20e1414df68d41ead4bcc4a187503a9cd639b644014b", size = 1600725 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/ae/4763d0461f2fb424b276c891156520d8cb76166d98a4ddbb0aa0631a708d/scs-3.2.7.post2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b7271ff566ac9df929c8cf7d1b024b89c3882b541c21a7a6d9aa94480822bccb", size = 105856 }, + { url = "https://files.pythonhosted.org/packages/49/e1/c9246d57588d8376d131a1e7055496b0a1d0239c5a949f9067df904044ea/scs-3.2.7.post2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb2997f53ef3426934599517c6e0e77f4f05cc23c3aa2380fd176c7fd22bc0c8", size = 93595 }, + { url = "https://files.pythonhosted.org/packages/58/df/3c88703fe7c592b1184704c668d796045be57ee94d0d29599423628dea77/scs-3.2.7.post2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8307b7302f8105148478a5723a2f7d5a3cbf86ef3cc6f27567203addfa3b10", size = 10443087 }, + { url = "https://files.pythonhosted.org/packages/1e/18/cfae3a8809fec2a13abfa79e7d32aac1884a4e03642ad39665cfc960f59d/scs-3.2.7.post2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:f34cc43c9eb1092423b55f01430ad99b4e5825a6595ead8e081f985032685e8c", size = 5066485 }, + { url = "https://files.pythonhosted.org/packages/85/53/0da457be1dcf3f0c20dcf21ef96610638a143c073c53f5b82842d1d9636d/scs-3.2.7.post2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f645f3789bc4659de2a468c2e4db552f6656bcb286e81f3cb42d5a607028627b", size = 11612087 }, + { url = "https://files.pythonhosted.org/packages/2c/84/55f5f5fb95e4a38f2501a7bd6840a91524b44c4aea2f05975c2ddf101e03/scs-3.2.7.post2-cp310-cp310-win_amd64.whl", hash = "sha256:e5f90940c383b68dd7960b734105cd1dd6c11c80275321de3a6388f563a1ff19", size = 7432199 }, + { url = "https://files.pythonhosted.org/packages/2b/a8/75e215fb61f65c7dee0a5d2c8e2b9043967fd332b70a4d47478bca45ec10/scs-3.2.7.post2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d551b90d9e2c0497ee17d8c3db325d6fcefa4419057954e68709da8b9184d4f", size = 105857 }, + { url = "https://files.pythonhosted.org/packages/1f/e4/b757b7926cc3355ba41ba747afb5e4d6c553d05be840ea25dc47b47b47b1/scs-3.2.7.post2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c15d035dda04a6626d3cd9b68d3bf814d2e0eb3cb372021775bd358fd8c7405", size = 93594 }, + { url = "https://files.pythonhosted.org/packages/e7/34/bf8e999e13e00946660a1e9009e67d5718356c06b9a2b2905f10829a7c45/scs-3.2.7.post2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6da6add18f039e7e08f0ebc13cb1f853ec4c96ae81d7a578f46e0f9f0e5bf4b5", size = 10443087 }, + { url = "https://files.pythonhosted.org/packages/d3/03/d41749f5c680241345669da533bc7ce6f5f1fac6d88fb255792fdd187e3f/scs-3.2.7.post2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d6c965f026e56c92b59a9c96744eb90178982c270ab196f58a0260ac392785aa", size = 5066487 }, + { url = "https://files.pythonhosted.org/packages/ac/b1/26db804cdc4009745f4bc4be2a478ac2c29f017672747a3ce64d46bccc7f/scs-3.2.7.post2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0427a5bf9aa43eb2a22083e1a43412db5054a88d695fdaa6018cd6fb3a9f0203", size = 11612089 }, + { url = "https://files.pythonhosted.org/packages/bf/34/a42b90bff9330ac57e1dcc8c35b978cee47d6e9eee14cb71981b801fc7cf/scs-3.2.7.post2-cp311-cp311-win_amd64.whl", hash = "sha256:4d05ec092c891eb842630f343ebc0c46d2ef6047f325a835771b13f9804d6b3b", size = 7432186 }, + { url = "https://files.pythonhosted.org/packages/0b/ef/982d35cadee11137a27c80404155265bb2c4e5899551436ef5e6cc28a0bc/scs-3.2.7.post2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e4af2968b046ee55fa0dc89dcd3bfba771f1027d9224cb6efa10008d8bfee1", size = 107289 }, + { url = "https://files.pythonhosted.org/packages/33/2a/f807b0f9dd108c9c75c4d12692803d687be7bd32c91dbfd7213837b3b6ed/scs-3.2.7.post2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc46fef9743d4629337382f034fda92dfce338659e8377afae674517b7d8345f", size = 93544 }, + { url = "https://files.pythonhosted.org/packages/82/0e/f56426e3b3d9ac12dac252c1c4a0e65a530d460b9448e3fc2e20ac8e6bed/scs-3.2.7.post2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f92e925d89004276a449850926a45536f75c03cab701b5e758b1a7efa119ba08", size = 10443128 }, + { url = "https://files.pythonhosted.org/packages/e4/f3/343803e20415bf604e4b237fdce4203f51c35e89707d18eafa7e3fe172d7/scs-3.2.7.post2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:640faf61f85b933fdfc3d33d7ce4f0049b082b245e82d2d6a8c2c54aa0b7f540", size = 5066484 }, + { url = "https://files.pythonhosted.org/packages/33/9a/5b06bc2ba789aa2ce5ba57be503f2563bbc772c0e7b4249e646e44fdcd2b/scs-3.2.7.post2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a520c9bef84eee734df0da3e5e06aa9192d3be34cd5e6d4221cc01f4d09b20c0", size = 11612180 }, + { url = "https://files.pythonhosted.org/packages/30/49/1645fa1219493ac94475ab8f48a2520d2fc27f486327f2b0f167440a8188/scs-3.2.7.post2-cp312-cp312-win_amd64.whl", hash = "sha256:2995d4099943c3fd754b3e39fe178a9c03dcb9c7d84b40f64ac5eb26d8d6085a", size = 7432205 }, + { url = "https://files.pythonhosted.org/packages/af/ac/239489abd708d9411a27d3f4d9e6047cad1cc35aff34566e5bae354b1725/scs-3.2.7.post2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aaa3753e82250913e17c792e7ca7eb0bde03ac41200923f3dfd3f6bd5ec6f308", size = 107292 }, + { url = "https://files.pythonhosted.org/packages/89/72/ea3b94f16b1e9aeb1b8f6ecf70ef6e6f1f3074ea4a9adb240167674d6e9b/scs-3.2.7.post2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:08e9c20b482c03f292b3da7ce4cbddb2697508ffd747304564868e87da7cb4b2", size = 93548 }, + { url = "https://files.pythonhosted.org/packages/da/3f/6e94437915d54ea5d366fa8f585454fc1c2e65a35587a811d2594d91d369/scs-3.2.7.post2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56af9310bd2d000c45d7829e2935b5445480ea6bcc6091c58d4e3ab2a94125be", size = 10443131 }, + { url = "https://files.pythonhosted.org/packages/da/94/fabedf8acabc1bb679e906ed1747ee00d0683a054369cde8c5b74a0d1efd/scs-3.2.7.post2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ec6e7bdb18d4b84c7f56f94db445ec0c43deec5aa659201467aa85b2f64b8123", size = 5066485 }, + { url = "https://files.pythonhosted.org/packages/6c/a8/c440fb404d64b016fbfeaf80f4aac3ba817e1c65bdf1072311e17dd0281c/scs-3.2.7.post2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:91316903b2ea625990abe18bb92e8ce63536e5eafb9623ecf1cb199fb05ea574", size = 11612182 }, + { url = "https://files.pythonhosted.org/packages/9d/f9/840ef19a298ef7099f4a692772001f2f552b0917a3fb230f872a1c40ba11/scs-3.2.7.post2-cp313-cp313-win_amd64.whl", hash = "sha256:a2c48cd19e39bf87dae0b20a289fff44930458fc2ca2afa0f899058dc41e5545", size = 7432191 }, +] + +[[package]] +name = "setuptools" +version = "80.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "slycot" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/18/96acf7dcdc931d0c9a165a8aa3a6f3e6b145d0d69aea43ecef82b8611888/slycot-0.6.0.tar.gz", hash = "sha256:98ecf19cad0c6ce0d6f4db95169efc14f89a285ac24a4a8da1ca5b96b9da3c23", size = 3084062 } + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +]