cellmaps-utils 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+
2
+ import os
3
+ import time
4
+ import json
5
+ import getpass
6
+ import platform
7
+ import logging
8
+ import logging.config
9
+ import argparse
10
+ from cellmaps_utils import constants
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def setup_cmd_logging(args):
17
+ """
18
+ Sets up logging based on parsed command line arguments.
19
+ If **args.logconf** is set use that configuration otherwise look
20
+ at **args.verbose** and set logging for this module
21
+
22
+ This function assumes the following:
23
+
24
+ * **args.logconf** exists and is ``None`` or set to :py:class:`str`
25
+ containing path to logconf file
26
+
27
+ * **args.verbose** exists and is set to :py:class:`int` to one of
28
+ these values:
29
+
30
+ * ``0`` = no logging
31
+ * ``1`` = critical
32
+ * ``2`` = error
33
+ * ``3`` = warning
34
+ * ``4`` = info
35
+ * ``5`` = debug
36
+
37
+ :param args: parsed command line arguments from argparse
38
+ :type args: :py:class:`argparse.Namespace`
39
+ :raises AttributeError: If args is ``None`` or
40
+ if **args.logconf** is None or missing or
41
+ if **args.verbose** is None or missing
42
+ """
43
+
44
+ if args.logconf is None:
45
+ level = (50 - (10 * args.verbose))
46
+ logging.basicConfig(format=constants.LOG_FORMAT,
47
+ level=level)
48
+ logger.setLevel(level)
49
+ return
50
+
51
+ # logconf was set use that file
52
+ logging.config.fileConfig(args.logconf,
53
+ disable_existing_loggers=False)
54
+
55
+
56
+ def write_task_start_json(outdir=None, start_time=None,
57
+ data=None,
58
+ version=None):
59
+ """
60
+ Writes :py:const:`~cellmaps_utils.constants.TASK_FILE_PREFIX` **##** :py:const:`~cellmaps_utils.constants.TASK_START_FILE_SUFFIX`
61
+ file with information about
62
+ what is to be run. The **##** in name is value of **start_time**
63
+
64
+ .. code-block::
65
+
66
+ from cellmaps_utils import logutils
67
+ import time
68
+
69
+ logutils.write_task_start_json(outdir='./mydir', start_time=int(time.time()),
70
+ data={'someparam': 'some value'},
71
+ version='1.0.0')
72
+
73
+ :param outdir: directory to write file
74
+ :type outdir: str
75
+ :param start_time: time in seconds since epoch
76
+ If ``-1`` or ``None`` then value will
77
+ be set to current time
78
+ :type start_time: int
79
+ :param data: additional data to persist in
80
+ :type data: dict
81
+ :param version: Version of software
82
+ :type version: str
83
+ """
84
+ login = ''
85
+ try:
86
+ login = getpass.getuser()
87
+ except Exception as e:
88
+ logger.error('Unable to get login for user: ' + str(e))
89
+
90
+ if start_time is None or start_time == -1:
91
+ start_time = int(time.time())
92
+
93
+ task = {'start_time': start_time,
94
+ 'version': str(version),
95
+ 'pid': str(os.getpid()),
96
+ 'outdir': outdir,
97
+ 'login': login,
98
+ 'cwd': str(os.getcwd()),
99
+ 'platform': str(platform.platform()),
100
+ 'python': str(platform.python_version()),
101
+ 'system': str(platform.system()),
102
+ 'uname': str(platform.uname())
103
+ }
104
+ if data is not None:
105
+ task.update(data)
106
+
107
+ with open(os.path.join(outdir,
108
+ constants.TASK_FILE_PREFIX +
109
+ str(start_time) +
110
+ constants.TASK_START_FILE_SUFFIX), 'w') as f:
111
+ json.dump(task, f, indent=2)
112
+
113
+
114
+ def write_task_finish_json(outdir=None, start_time=None,
115
+ end_time=None, status=None):
116
+ """
117
+ Writes :py:const:`~cellmaps_utils.constants.TASK_FILE_PREFIX` **##** :py:const:`~cellmaps_utils.constants.TASK_FINISH_FILE_SUFFIX`
118
+ file in **outdir** directory where **##** is the **start_time** value
119
+
120
+ .. code-block::
121
+
122
+ from cellmaps_utils import logutils
123
+ import time
124
+
125
+ logutils.write_task_finish_json(outdir='./mydir', start_time=int(time.time())-10,
126
+ end_time=int(time.time()),
127
+ status=0)
128
+
129
+ :param outdir: directory to write file
130
+ :type outdir: str
131
+ :param start_time: time in seconds since epoch
132
+ if set to ``-1`` or ``None`` value will be set
133
+ to current time
134
+ :type start_time: int
135
+ :param end_time: time in seconds since epoch
136
+ if set to ``-1`` or ``None`` value will be set to
137
+ current time
138
+ :type end_time: int
139
+ :param status: status of task, ``0`` means success, otherwise error
140
+ :type status: int
141
+ """
142
+ if outdir is None or not os.path.isdir(outdir):
143
+ logger.error('Output directory is not set or not a '
144
+ 'directory, cannot write'
145
+ 'task finish json file')
146
+ return
147
+
148
+ if start_time is None or start_time == -1:
149
+ start_time = int(time.time())
150
+
151
+ if end_time is None or end_time == -1:
152
+ end_time = int(time.time())
153
+ task = {'end_time': end_time,
154
+ 'elapsed_time': int(end_time - start_time),
155
+ 'status': str(status)
156
+ }
157
+ with open(os.path.join(outdir,
158
+ constants.TASK_FILE_PREFIX +
159
+ str(start_time) +
160
+ constants.TASK_FINISH_FILE_SUFFIX), 'w') as f:
161
+ json.dump(task, f, indent=2)
162
+
163
+
164
+ def setup_filelogger(outdir=None, handlerprefix='cellmaps'):
165
+ """
166
+ Sets up a logger to write all debug and higher logs
167
+ to output **outdir**/:py:const:`~cellmaps_utils.constants.OUTPUT_LOG_FILE`
168
+ and all error level log messages and higher
169
+ to output **outdir**/:py:const:`~cellmaps_utils.constants.ERROR_LOG_FILE`
170
+
171
+ :param outdir: directory where to store :py:const:`~cellmaps_utils.constants.OUTPUT_LOG_FILE`
172
+ and :py:const:`~cellmaps_utils.constants.ERROR_LOG_FILE` files
173
+ :type outdir: str
174
+ :param handlerprefix: prefix of name to give to handlers and formatters,
175
+ if ``None`` code will set value to ``cellmaps``
176
+ :type handlerprefix: str
177
+ """
178
+ if handlerprefix is None:
179
+ handlerprefix = 'cellmaps'
180
+
181
+ err_log_file = os.path.join(outdir, constants.ERROR_LOG_FILE)
182
+ out_log_file = os.path.join(outdir, constants.OUTPUT_LOG_FILE)
183
+ logging.config.dictConfig({'version': 1,
184
+ 'propagate': True,
185
+ 'disable_existing_loggers': False,
186
+ 'loggers': {
187
+ '': {
188
+ 'level': 'NOTSET',
189
+ 'handlers': [handlerprefix + '_file_handler',
190
+ handlerprefix + '_error_file_handler']
191
+ }
192
+ },
193
+ 'handlers': {
194
+ handlerprefix + '_file_handler': {
195
+ 'level': 'DEBUG',
196
+ 'class': 'logging.FileHandler',
197
+ 'formatter': handlerprefix + '_formatter',
198
+ 'filename': out_log_file,
199
+ 'mode': 'a'
200
+ },
201
+ handlerprefix + '_error_file_handler': {
202
+ 'level': 'ERROR',
203
+ 'class': 'logging.FileHandler',
204
+ 'formatter': handlerprefix + '_formatter',
205
+ 'filename': err_log_file,
206
+ 'mode': 'a'
207
+ }
208
+ },
209
+ 'formatters': {
210
+ handlerprefix + '_formatter': {
211
+ 'format': constants.LOG_FORMAT
212
+ }
213
+ }
214
+ })
@@ -0,0 +1,246 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import pickle
4
+ import dill
5
+ from sklearn.metrics.pairwise import manhattan_distances, euclidean_distances, cosine_similarity
6
+ from scipy.spatial.distance import canberra
7
+
8
+
9
+ def upper_tri_values(df):
10
+ """
11
+ Return array with values of upper triangle of the DataFrame
12
+
13
+ :param df: Symmetric DataFrame
14
+ :type df: :py:class:`pandas.DataFrame`
15
+ :return:
16
+ :rtype: :py:func:`numpy.array`
17
+ """
18
+ m = df.values
19
+ return m[np.triu_indices(df.shape[0], k=1)]
20
+
21
+
22
+ def znorm(df):
23
+ """
24
+ Z-transform within each column.
25
+
26
+ :param df:
27
+ :return:
28
+ :rtype: :py:class:`pandas.DataFrame`
29
+ """
30
+ norm_df = pd.DataFrame(index=df.index, columns=df.columns)
31
+ for c in df.columns:
32
+ value = df[c]
33
+ norm_df[c] = (value - value.mean()) / value.std()
34
+ return norm_df
35
+
36
+
37
+ def cosine_similarity_scaled(df):
38
+ """
39
+ Calculate Cosine similarity between each pair of rows in a DataFrame.
40
+ Similarity scaled into [0, 1]
41
+
42
+ :param df:
43
+ :return:
44
+ :rtype: :py:class:`pandas.DataFrame`
45
+ """
46
+ sim = cosine_similarity(df)
47
+ shift = sim.min()
48
+ sim -= shift
49
+ scale = sim.max()
50
+ sim /= scale
51
+ return pd.DataFrame(sim, index=df.index.values, columns=df.index.values)
52
+
53
+
54
+ def manhattan_similarity(df):
55
+ """
56
+ Calculate Manhattan similarity between each pair of rows in a DataFrame.
57
+ Similarity scaled into [0, 1]
58
+
59
+ :param df:
60
+ :return:
61
+ :rtype: :py:class:`pandas.DataFrame`
62
+ """
63
+ # Get manhattan distance
64
+ dist = manhattan_distances(df)
65
+ # Convert distance to similarity by max-minus
66
+ sim = dist.max() - dist
67
+ # Scale into [0,1]
68
+ sim /= sim.max()
69
+ return pd.DataFrame(sim, index=df.index.values, columns=df.index.values)
70
+
71
+
72
+ def euclidean_similarity(df):
73
+ """
74
+ Calculate Euclidean similarity between each pair of rows in a DataFrame.
75
+ Similarity scaled into [0, 1]
76
+
77
+ :param df:
78
+ :return:
79
+ :rtype: :py:class:`pandas.DataFrame`
80
+ """
81
+ # Get euclidean distance
82
+ dist = euclidean_distances(df)
83
+ # Convert distance to similarity by max-minus
84
+ sim = dist.max() - dist
85
+ # Scale into [0,1]
86
+ sim /= sim.max()
87
+ return pd.DataFrame(sim, index=df.index.values, columns=df.index.values)
88
+
89
+
90
+ def canberra_similarity(df):
91
+ """
92
+ Calculate Canberra similarity between each pair of rows in a DataFrame.
93
+ Similarity scaled into [0, 1]
94
+
95
+ :param df:
96
+ :return:
97
+ :rtype: :py:class:`pandas.DataFrame`
98
+ """
99
+ index = df.index.values
100
+ dist = pd.DataFrame(0, index=index, columns=index, dtype=float)
101
+ for i in range(len(index) - 1):
102
+ a = df.loc[index[i]].values
103
+ for j in range(i + 1, len(index)):
104
+ b = df.loc[index[j]].values
105
+ d = canberra(a, b)
106
+ dist.at[index[i], index[j]] = d
107
+ dist.at[index[j], index[i]] = d
108
+ dist = dist.values
109
+ # Convert distance to similarity by max-minus
110
+ sim = dist.max() - dist
111
+ # Scale into [0,1]
112
+ sim /= sim.max()
113
+ return pd.DataFrame(sim, index=index, columns=index)
114
+
115
+
116
+ def pearson_scaled(df):
117
+ """
118
+ Calculate Pearson correlation between each pair of rows in a DataFrame.
119
+ Correlation scaled into [0, 1]
120
+
121
+ :param df:
122
+ :return:
123
+ :rtype: :py:class:`pandas.DataFrame`
124
+ """
125
+ corr = df.T.corr(method='pearson')
126
+ shift = corr.min().min()
127
+ corr -= shift
128
+ scale = corr.max().max()
129
+ corr /= scale
130
+ return corr
131
+
132
+
133
+ def spearman_scaled(df):
134
+ """
135
+ Calculate Spearman correlation between each pair of rows in a DataFrame.
136
+ Correlation scaled into [0, 1]
137
+
138
+ :param df:
139
+ :return:
140
+ """
141
+ corr = df.T.corr(method='spearman')
142
+ shift = corr.min().min()
143
+ corr -= shift
144
+ scale = corr.max().max()
145
+ corr /= scale
146
+ return corr
147
+
148
+
149
+ def kendall_scaled(df):
150
+ """
151
+ Calculate Kendall correlation between each pair of rows in a DataFrame.
152
+ Correlation scaled into [0, 1]
153
+
154
+ :param df:
155
+ :return:
156
+ """
157
+ corr = df.T.corr(method='kendall')
158
+ shift = corr.min().min()
159
+ corr -= shift
160
+ scale = corr.max().max()
161
+ corr /= scale
162
+ return corr
163
+
164
+
165
+ def check_symmetric(a, rtol=1e-05, atol=1e-08):
166
+ """
167
+ Check if the given numpy matrix is symmetric or not.
168
+
169
+ :param a:
170
+ :param rtol:
171
+ :param atol:
172
+ :return:
173
+ """
174
+ return np.allclose(a, a.T, rtol=rtol, atol=atol)
175
+
176
+
177
+ def save_obj(obj, fname, method='pickle'):
178
+ """
179
+
180
+ :param obj: object that want to be saved
181
+ :param fname: path to saved file
182
+ :type fname: str
183
+ :param method: {pickle, dill} specify package used for compressing
184
+ :type method: str
185
+ :raises ValueError: if **method** is not set to ``pickle`` or ``dill``
186
+ """
187
+ with open(fname, 'wb') as f:
188
+ if method == 'pickle':
189
+ pickle.dump(obj, f)
190
+ elif method == 'dill':
191
+ dill.dump(obj, f)
192
+ else:
193
+ raise ValueError('Please select method from {pickle, dill}!')
194
+ return
195
+
196
+
197
+ def load_obj(fname, method='pickle'):
198
+ """
199
+ Loading object that was saved in pickle format
200
+
201
+ :param fname: path to file
202
+ :type fname: str
203
+ :param method: {pickle, dill} specify package used for compressing
204
+ :type method: str
205
+ :raises ValueError: if **method** is not set to ``pickle`` or ``dill``
206
+ """
207
+ with open(fname, 'rb') as f:
208
+ if method == 'pickle':
209
+ return pickle.load(f)
210
+ elif method == 'dill':
211
+ return dill.load(f)
212
+ else:
213
+ raise ValueError('Please select method from {pickle, dill}!')
214
+ return
215
+
216
+
217
+ def jaccard(setA, setB):
218
+ """
219
+ Calculates jaccard
220
+
221
+ :param setA:
222
+ :param setB:
223
+ :return:
224
+ """
225
+ return len(setA.intersection(setB)) / len(setA.union(setB))
226
+
227
+
228
+ def scaled_P_to_nm(scaled_P):
229
+ """
230
+ TODO: Add doc here
231
+
232
+ :param scaled_P:
233
+ :return:
234
+ """
235
+ power = -3.968 * scaled_P + 4.326
236
+ return 10 ** power
237
+
238
+
239
+ def num_comb(x):
240
+ """
241
+ TODO: Add doc here
242
+
243
+ :param x:
244
+ :return:
245
+ """
246
+ return x * (x - 1) / 2
@@ -0,0 +1,193 @@
1
+ import os
2
+ import time
3
+ import uuid
4
+
5
+ import ndex2
6
+ import logging
7
+ import copy
8
+ from cellmaps_utils import constants
9
+
10
+ from ndex2.cx2 import RawCX2NetworkFactory, CX2Network
11
+ from requests import RequestException
12
+
13
+ from cellmaps_utils.exceptions import CellMapsError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class NDExHierarchyUploader(object):
19
+ """
20
+ Base class for uploading hierarchy and its parent network to NDEx.
21
+ """
22
+
23
+ def __init__(self, ndexserver, ndexuser, ndexpassword, visibility=None):
24
+ """
25
+ Constructor
26
+
27
+ :param ndexserver:
28
+ :type ndexserver: str
29
+ :param ndexuser:
30
+ :type ndexuser: str
31
+ :param ndexpassword:
32
+ :type ndexpassword: str
33
+ :param visibility: If set to ``public``, ``PUBLIC`` or ``True`` sets hierarchy and interactome to
34
+ publicly visibility on NDEx, otherwise they are left as private
35
+ :type visibility: str or bool
36
+ """
37
+ self._server = ndexserver
38
+ self._user = ndexuser
39
+ if ndexpassword is not None and os.path.isfile(ndexpassword):
40
+ with open(ndexpassword, 'r') as file:
41
+ self._password = file.readline().strip()
42
+ else:
43
+ self._password = ndexpassword
44
+ self._visibility = None
45
+ if visibility is not None:
46
+ if isinstance(visibility, bool):
47
+ if visibility is True:
48
+ self._visibility = 'PUBLIC'
49
+ elif isinstance(visibility, str):
50
+ if visibility.lower() == 'public':
51
+ self._visibility = 'PUBLIC'
52
+ self._ndexclient = None
53
+ self._initialize_ndex_client()
54
+
55
+ def _initialize_ndex_client(self):
56
+ """
57
+ Creates NDEx client
58
+ :raises CellmapsGenerateHierarchyError: If the NDEx server URL or user credentials are not specified.
59
+ """
60
+ logger.debug('Connecting to NDEx server: ' + str(self._server) +
61
+ ' with user: ' + str(self._user))
62
+ if self._server is None or self._user is None or self._password is None:
63
+ raise CellMapsError('NDEx server or credentials were not specified. '
64
+ 'NDEx client connection cannot be established.')
65
+ self._ndexclient = ndex2.client.Ndex2(host=self._server,
66
+ username=self._user,
67
+ password=self._password,
68
+ skip_version_check=True)
69
+
70
+ def _save_network(self, network, max_retries=3, retry_wait=10):
71
+ """
72
+ This method saves a network to the NDEx server and returns the unique NDEx UUID for the network
73
+ along with its URL. The visibility of the saved network is determined by the variable `self._visibility`.
74
+
75
+ :param network: Network to save
76
+ :type network: :py:class:`~ndex2.cx2.CX2Network`
77
+ :return: NDEX UUID of network
78
+ :rtype: str
79
+ """
80
+ logger.debug('Saving network named: ' + str(network.get_name()) +
81
+ ' to NDEx with visibility set to: ' + str(self._visibility))
82
+ retry_num = 1
83
+ while retry_num <= max_retries:
84
+ try:
85
+ res = self._ndexclient.save_new_cx2_network(network.to_cx2(),
86
+ visibility=self._visibility)
87
+ if not isinstance(res, str):
88
+ raise CellMapsError('Expected a str, but got this: ' + str(res))
89
+
90
+ ndexuuid = res[res.rfind('/') + 1:]
91
+
92
+ return ndexuuid, res.replace('v3', 'viewer')
93
+ except RequestException as re:
94
+ if retry_num == max_retries:
95
+ raise CellMapsError(str(max_retries) + 'attempts to save the network failed.')
96
+ logger.debug(str(re.response.text))
97
+ retry_num += 1
98
+ time.sleep(retry_wait)
99
+ except Exception as e:
100
+ raise CellMapsError('An error occurred while saving the network to NDEx: ' + str(e))
101
+
102
+ def get_cytoscape_url(self, ndexurl):
103
+ """
104
+ Generates a Cytoscape URL for a given NDEx network URL.
105
+
106
+ :param ndexurl: The URL of the NDEx network.
107
+ :type ndexurl: str
108
+ :return: The URL pointing to the network's view on the Cytoscape platform.
109
+ :rtype: str
110
+ """
111
+ ndexuuid = ndexurl[ndexurl.rfind('/') + 1:]
112
+ network_url = (f"https://{self._server.replace('https://', '').replace('http://', '')}"
113
+ f"/cytoscape/0/networks/{ndexuuid}")
114
+ return network_url
115
+
116
+ def _update_hcx_annotations(self, hierarchy, interactome_id):
117
+ """
118
+ This method updates the given network hierarchy with specific HCX annotations. These annotations
119
+ are associated with the interactome ID and the NDEx server where the interactome resides.
120
+
121
+ :param hierarchy: The network hierarchy that needs to be updated with HCX annotations.
122
+ :type hierarchy: `~ndex2.cx2.CX2Network`
123
+ :param interactome_id: The unique ID (UUID) of the interactome that is associated with the hierarchy.
124
+ :type interactome_id: str
125
+ :return: The updated hierarchy with the HCX annotations.
126
+ :rtype: `~ndex2.cx2.CX2Network`
127
+ """
128
+ hierarchy_copy = copy.deepcopy(hierarchy)
129
+ hierarchy_copy.add_network_attribute('HCX::interactionNetworkUUID', str(interactome_id))
130
+ hierarchy_copy.remove_network_attribute('HCX::interactionNetworkName')
131
+ return hierarchy_copy
132
+
133
+ def save_hierarchy_and_parent_network(self, hierarchy, parent_ppi):
134
+ """
135
+ Saves both the hierarchy and its parent network to the NDEx server. This method first saves the parent
136
+ network, then updates the hierarchy with HCX annotations based on the parent network's UUID, and
137
+ finally saves the updated hierarchy. It returns the UUIDs and URLs for both the hierarchy and
138
+ the parent network.
139
+
140
+ :param hierarchy: The hierarchy network to be saved.
141
+ :type hierarchy: :py:class:`~ndex2.cx2.CX2Network`
142
+ :param parent_ppi: The parent protein-protein interaction network associated with the hierarchy.
143
+ :type parent_ppi: :py:class:`~ndex2.cx2.CX2Network`
144
+ :return: UUIDs and URLs for both the parent network and the hierarchy.
145
+ :rtype: tuple
146
+ """
147
+ parent_url = None
148
+ if isinstance(parent_ppi, CX2Network):
149
+ parent_uuid, parent_url = self._save_network(parent_ppi)
150
+ else:
151
+ try:
152
+ _ = uuid.UUID(parent_ppi, version=4)
153
+ parent_uuid = parent_ppi
154
+ except ValueError:
155
+ raise CellMapsError(f'Invalid UUID format for parent_ppi: {parent_ppi}')
156
+
157
+ hierarchy_for_ndex = self._update_hcx_annotations(hierarchy, parent_uuid)
158
+ hierarchy_uuid, hierarchy_url = self._save_network(hierarchy_for_ndex)
159
+ return parent_uuid, parent_url, hierarchy_uuid, hierarchy_url
160
+
161
+ def upload_hierarchy_and_parent_network_from_files(self, hier_dir=None, hierarchy_path=None, parent_path=None):
162
+ """
163
+ Uploads hierarchy and parent network to NDEx from CX2 files.
164
+ It first checks if hierarchy_path and parent_path are provided.
165
+ If not provided, it tries to get them from hier_dir directory.
166
+ If none is specified or cannot find hierarchy and parent in hier_dir, it raises an exception.
167
+
168
+ :param hier_dir: The directory where the hierarchy and parent network files are located.
169
+ :type hier_dir: str
170
+ :param hierarchy_path: The path to the hierarchy network file.
171
+ :type hierarchy_path: str, optional
172
+ :param parent_path: The path to the parent network file.
173
+ :type parent_path: str, optional
174
+ :return: UUIDs and URLs for both the hierarchy and parent network.
175
+ :rtype: tuple
176
+ :raises CellMapsError: If the required hierarchy or parent network files do not exist.
177
+ """
178
+ if not hierarchy_path and hier_dir:
179
+ hierarchy_path = os.path.join(hier_dir, constants.HIERARCHY_NETWORK_PREFIX + constants.CX2_SUFFIX)
180
+ if not parent_path and hier_dir:
181
+ parent_path = os.path.join(hier_dir, constants.HIERARCHY_PARENT_NETWORK_PREFIX + constants.CX2_SUFFIX)
182
+
183
+ if not hierarchy_path or not os.path.exists(hierarchy_path):
184
+ raise CellMapsError(f'Hierarchy network file does not exist at {hierarchy_path}.')
185
+
186
+ if not parent_path or not os.path.exists(parent_path):
187
+ raise CellMapsError(f'Parent network file does not exist at {parent_path}.')
188
+
189
+ cx_factory = RawCX2NetworkFactory()
190
+ hierarchy_network = cx_factory.get_cx2network(hierarchy_path)
191
+ parent_network = cx_factory.get_cx2network(parent_path)
192
+
193
+ return self.save_hierarchy_and_parent_network(hierarchy_network, parent_network)