risk-network 0.0.9b35__tar.gz → 0.0.9b38__tar.gz

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.
Files changed (48) hide show
  1. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/PKG-INFO +1 -1
  2. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/__init__.py +1 -1
  3. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/neighborhoods/domains.py +138 -89
  4. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/graph/api.py +9 -2
  5. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk_network.egg-info/PKG-INFO +1 -1
  6. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/LICENSE +0 -0
  7. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/MANIFEST.in +0 -0
  8. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/README.md +0 -0
  9. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/pyproject.toml +0 -0
  10. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/annotations/__init__.py +0 -0
  11. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/annotations/annotations.py +0 -0
  12. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/annotations/io.py +0 -0
  13. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/constants.py +0 -0
  14. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/log/__init__.py +0 -0
  15. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/log/console.py +0 -0
  16. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/log/parameters.py +0 -0
  17. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/neighborhoods/__init__.py +0 -0
  18. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/neighborhoods/api.py +0 -0
  19. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/neighborhoods/community.py +0 -0
  20. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/neighborhoods/neighborhoods.py +0 -0
  21. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/__init__.py +0 -0
  22. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/geometry.py +0 -0
  23. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/graph/__init__.py +0 -0
  24. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/graph/graph.py +0 -0
  25. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/graph/summary.py +0 -0
  26. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/io.py +0 -0
  27. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/__init__.py +0 -0
  28. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/api.py +0 -0
  29. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/canvas.py +0 -0
  30. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/contour.py +0 -0
  31. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/labels.py +0 -0
  32. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/network.py +0 -0
  33. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/plotter.py +0 -0
  34. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/utils/colors.py +0 -0
  35. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/network/plotter/utils/layout.py +0 -0
  36. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/risk.py +0 -0
  37. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/stats/__init__.py +0 -0
  38. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/stats/permutation/__init__.py +0 -0
  39. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/stats/permutation/permutation.py +0 -0
  40. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/stats/permutation/test_functions.py +0 -0
  41. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/stats/significance.py +0 -0
  42. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk/stats/stat_tests.py +0 -0
  43. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk_network.egg-info/SOURCES.txt +0 -0
  44. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk_network.egg-info/dependency_links.txt +0 -0
  45. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk_network.egg-info/requires.txt +0 -0
  46. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/risk_network.egg-info/top_level.txt +0 -0
  47. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/setup.cfg +0 -0
  48. {risk_network-0.0.9b35 → risk_network-0.0.9b38}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: risk-network
3
- Version: 0.0.9b35
3
+ Version: 0.0.9b38
4
4
  Summary: A Python package for biological network analysis
5
5
  Author: Ira Horecka
6
6
  Author-email: Ira Horecka <ira89@icloud.com>
@@ -7,4 +7,4 @@ RISK: Regional Inference of Significant Kinships
7
7
 
8
8
  from risk.risk import RISK
9
9
 
10
- __version__ = "0.0.9-beta.35"
10
+ __version__ = "0.0.9-beta.38"
@@ -5,12 +5,13 @@ risk/neighborhoods/domains
5
5
 
6
6
  from contextlib import suppress
7
7
  from itertools import product
8
- from typing import Tuple
8
+ from typing import Tuple, Union
9
9
 
10
10
  import numpy as np
11
11
  import pandas as pd
12
12
  from scipy.cluster.hierarchy import linkage, fcluster
13
- from sklearn.metrics import silhouette_score
13
+ from scipy.optimize import minimize_scalar
14
+ from sklearn.metrics import calinski_harabasz_score, davies_bouldin_score, silhouette_score
14
15
  from tqdm import tqdm
15
16
 
16
17
  from risk.annotations import get_weighted_description
@@ -18,12 +19,19 @@ from risk.constants import GROUP_LINKAGE_METHODS, GROUP_DISTANCE_METRICS
18
19
  from risk.log import logger
19
20
 
20
21
 
22
+ class LinkageThresholdError(Exception):
23
+ """Exception raised for errors in the linkage threshold optimization process."""
24
+
25
+ pass
26
+
27
+
21
28
  def define_domains(
22
29
  top_annotations: pd.DataFrame,
23
30
  significant_neighborhoods_significance: np.ndarray,
24
31
  linkage_criterion: str,
25
32
  linkage_method: str,
26
33
  linkage_metric: str,
34
+ linkage_threshold: Union[str, float],
27
35
  ) -> pd.DataFrame:
28
36
  """Define domains and assign nodes to these domains based on their significance scores and clustering,
29
37
  handling errors by assigning unique domains when clustering fails.
@@ -31,12 +39,19 @@ def define_domains(
31
39
  Args:
32
40
  top_annotations (pd.DataFrame): DataFrame of top annotations data for the network nodes.
33
41
  significant_neighborhoods_significance (np.ndarray): The binary significance matrix below alpha.
34
- linkage_criterion (str): The clustering criterion for defining groups.
35
- linkage_method (str): The linkage method for clustering.
36
- linkage_metric (str): The linkage metric for clustering.
42
+ linkage_criterion (str): The clustering criterion for defining groups. Use "distance" for distance-based
43
+ clustering or "maxclust" for a fixed number of clusters. Use "off" to skip clustering.
44
+ linkage_method (str): The linkage method for clustering. Use "auto" to try multiple methods.
45
+ linkage_metric (str): The linkage metric for clustering. Use "auto" to try multiple metrics.
46
+ linkage_threshold (str, float): The linkage threshold for clustering, or one of "silhouette",
47
+ "calinski_harabasz", or "davies_bouldin" to optimize the threshold.
37
48
 
38
49
  Returns:
39
50
  pd.DataFrame: DataFrame with the primary domain for each node.
51
+
52
+ Raises:
53
+ ValueError: If an improper value is passed for linkage_threshold. Acceptable values are "silhouette",
54
+ "calinski_harabasz", "davies_bouldin", or a float value.
40
55
  """
41
56
  try:
42
57
  if linkage_criterion == "off":
@@ -47,8 +62,10 @@ def define_domains(
47
62
  # Safeguard the matrix by replacing NaN, Inf, and -Inf values
48
63
  m = _safeguard_matrix(m)
49
64
  # Optimize silhouette score across different linkage methods and distance metrics
50
- best_linkage, best_metric, best_threshold = _optimize_silhouette_across_linkage_and_metrics(
51
- m, linkage_criterion, linkage_method, linkage_metric
65
+ best_linkage, best_metric, best_threshold = (
66
+ _optimize_linkage_threshold_across_methods_and_metrics(
67
+ m, linkage_criterion, linkage_method, linkage_metric, linkage_threshold
68
+ )
52
69
  )
53
70
  # Perform hierarchical clustering
54
71
  Z = linkage(m, method=best_linkage, metric=best_metric)
@@ -74,6 +91,9 @@ def define_domains(
74
91
  f"Error encountered. Skipping clustering and assigning {n_rows} unique domains."
75
92
  )
76
93
  top_annotations["domain"] = range(1, n_rows + 1) # Assign unique domains
94
+ except LinkageThresholdError as e:
95
+ # If a LinkageThresholdError is encountered, raise a ValueError with the original exception
96
+ raise ValueError(e) from e
77
97
 
78
98
  # Create DataFrames to store domain information
79
99
  node_to_significance = pd.DataFrame(
@@ -195,125 +215,154 @@ def _safeguard_matrix(matrix: np.ndarray) -> np.ndarray:
195
215
  return matrix
196
216
 
197
217
 
198
- def _optimize_silhouette_across_linkage_and_metrics(
199
- m: np.ndarray, linkage_criterion: str, linkage_method: str, linkage_metric: str
218
+ def _optimize_linkage_threshold_across_methods_and_metrics(
219
+ m: np.ndarray,
220
+ linkage_criterion: str,
221
+ linkage_method: str,
222
+ linkage_metric: str,
223
+ linkage_threshold: Union[str, float],
200
224
  ) -> Tuple[str, str, float]:
201
- """Optimize silhouette score across different linkage methods and distance metrics.
225
+ """Optimize the linkage method, metric, and threshold for hierarchical clustering. If the threshold is
226
+ a string, optimize the threshold using the specified metric; otherwise, use the provided threshold.
202
227
 
203
228
  Args:
204
229
  m (np.ndarray): Data matrix.
205
- linkage_criterion (str): Clustering criterion.
206
- linkage_method (str): Linkage method for clustering.
207
- linkage_metric (str): Linkage metric for clustering.
230
+ linkage_criterion (str): Criterion for fcluster (typically "distance").
231
+ linkage_method (str): Linkage method for clustering, or "auto" to try multiple methods.
232
+ linkage_metric (str): Distance metric for clustering, or "auto" to try multiple metrics.
233
+ linkage_threshold (str, float): Either a numeric threshold or one of the following keywords:
234
+ "silhouette", "calinski_harabasz", or "davies_bouldin" to trigger optimization.
208
235
 
209
236
  Returns:
210
237
  Tuple[str, str, float]:
211
- - Best linkage method (str)
212
- - Best linkage metric (str)
213
- - Best threshold (float)
238
+ - The chosen linkage method.
239
+ - The chosen linkage metric.
240
+ - The optimized threshold (a float).
241
+
242
+ Raises:
243
+ ValueError: If linkage_threshold is neither one of the supported keywords nor convertible to float.
214
244
  """
215
- best_overall_method = linkage_method
216
- best_overall_metric = linkage_metric
245
+ # Supported linkage threshold metrics
246
+ supported_linkage_thresholds = {"silhouette", "calinski_harabasz", "davies_bouldin"}
247
+
248
+ # If linkage_threshold is a string:
249
+ if isinstance(linkage_threshold, str):
250
+ if linkage_threshold in supported_linkage_thresholds:
251
+ opt_metric = linkage_threshold
252
+ else:
253
+ try:
254
+ threshold_float = float(linkage_threshold)
255
+ except (TypeError, ValueError):
256
+ raise LinkageThresholdError(
257
+ f"linkage_threshold must be one of {', '.join(supported_linkage_thresholds)} or a float value."
258
+ )
259
+ return linkage_method, linkage_metric, threshold_float
260
+ else:
261
+ # If not a string, try to convert it to float.
262
+ try:
263
+ threshold_float = float(linkage_threshold)
264
+ except (TypeError, ValueError):
265
+ raise LinkageThresholdError(
266
+ f"linkage_threshold must be one of {', '.join(supported_linkage_thresholds)} or a float value."
267
+ )
268
+ return linkage_method, linkage_metric, threshold_float
269
+
270
+ # Otherwise, perform optimization using the specified metric (opt_metric).
271
+ best_overall_method = None
272
+ best_overall_metric = None
273
+ best_overall_threshold = None
217
274
  best_overall_score = -np.inf
218
- best_overall_threshold = 1
219
275
 
220
- linkage_methods = GROUP_LINKAGE_METHODS if linkage_method == "auto" else [linkage_method]
221
- linkage_metrics = GROUP_DISTANCE_METRICS if linkage_metric == "auto" else [linkage_metric]
222
- total_combinations = len(linkage_methods) * len(linkage_metrics)
276
+ # Use the provided lists if "auto" is specified.
277
+ methods = GROUP_LINKAGE_METHODS if linkage_method == "auto" else [linkage_method]
278
+ metrics = GROUP_DISTANCE_METRICS if linkage_metric == "auto" else [linkage_metric]
279
+ total_combinations = len(methods) * len(metrics)
223
280
 
224
- # Evaluating optimal linkage method and metric
225
281
  for method, metric in tqdm(
226
- product(linkage_methods, linkage_metrics),
282
+ product(methods, metrics),
227
283
  desc="Evaluating optimal linkage method and metric",
228
284
  total=total_combinations,
229
285
  bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
230
286
  ):
231
- # Some linkage methods and metrics may not work with certain data
232
287
  with suppress(ValueError):
233
288
  Z = linkage(m, method=method, metric=metric)
234
- threshold, score = _find_best_silhouette_score(Z, m, metric, linkage_criterion)
289
+ threshold, score = _find_optimal_linkage_threshold(
290
+ Z, m, metric, linkage_criterion, opt_metric=opt_metric
291
+ )
235
292
  if score > best_overall_score:
236
293
  best_overall_score = score
237
294
  best_overall_threshold = threshold
238
295
  best_overall_method = method
239
296
  best_overall_metric = metric
240
297
 
298
+ if best_overall_method is None or best_overall_metric is None or best_overall_threshold is None:
299
+ raise ValueError("Optimization failed to determine an optimal threshold.")
241
300
  return best_overall_method, best_overall_metric, best_overall_threshold
242
301
 
243
302
 
244
- def _find_best_silhouette_score(
303
+ def _find_optimal_linkage_threshold(
245
304
  Z: np.ndarray,
246
305
  m: np.ndarray,
247
306
  linkage_metric: str,
248
307
  linkage_criterion: str,
249
- lower_bound: float = 0.001,
250
- upper_bound: float = 1.0,
251
- resolution: float = 0.001,
308
+ opt_metric: str = "silhouette",
252
309
  ) -> Tuple[float, float]:
253
- """Find the best silhouette score using binary search.
310
+ """Find the optimal linkage threshold coefficient for hierarchical clustering. The function optimizes
311
+ the threshold value using the specified metric (opt_metric).
254
312
 
255
313
  Args:
256
- Z (np.ndarray): Linkage matrix.
257
- m (np.ndarray): Data matrix.
258
- linkage_metric (str): Linkage metric for silhouette score calculation.
259
- linkage_criterion (str): Clustering criterion.
260
- lower_bound (float, optional): Lower bound for search. Defaults to 0.001.
261
- upper_bound (float, optional): Upper bound for search. Defaults to 1.0.
262
- resolution (float, optional): Desired resolution for the best threshold. Defaults to 0.001.
314
+ Z (np.ndarray): Linkage matrix generated by a hierarchical clustering algorithm.
315
+ m (np.ndarray): Data matrix used for clustering.
316
+ linkage_metric (str): Metric used to calculate distances between data points
317
+ (e.g., "euclidean" or "cosine").
318
+ linkage_criterion (str): Criterion to pass to `fcluster`, typically "distance".
319
+ opt_metric (str, optional): Metric to optimize clustering quality. Options are:
320
+ "silhouette", "calinski_harabasz", or "davies_bouldin". Defaults to "silhouette".
263
321
 
264
322
  Returns:
265
323
  Tuple[float, float]:
266
- - Best threshold (float): The threshold that yields the best silhouette score.
267
- - Best silhouette score (float): The highest silhouette score achieved.
268
- """
269
- best_score = -np.inf
270
- best_threshold = None
324
+ - best_threshold (float): The optimal linkage threshold coefficient.
325
+ - best_metric_value (float): The value of the clustering quality metric achieved
326
+ at the optimal threshold (higher for "silhouette" and "calinski_harabasz",
327
+ lower for "davies_bouldin").
271
328
 
272
- # Test lower bound
273
- max_d_lower = np.max(Z[:, 2]) * lower_bound
274
- clusters_lower = fcluster(Z, max_d_lower, criterion=linkage_criterion)
275
- try:
276
- score_lower = silhouette_score(m, clusters_lower, metric=linkage_metric)
277
- except ValueError:
278
- score_lower = -np.inf
279
-
280
- # Test upper bound
281
- max_d_upper = np.max(Z[:, 2]) * upper_bound
282
- clusters_upper = fcluster(Z, max_d_upper, criterion=linkage_criterion)
283
- try:
284
- score_upper = silhouette_score(m, clusters_upper, metric=linkage_metric)
285
- except ValueError:
286
- score_upper = -np.inf
287
-
288
- # Determine initial bounds for binary search
289
- if score_lower > score_upper:
290
- best_score = score_lower
291
- best_threshold = lower_bound
292
- upper_bound = (lower_bound + upper_bound) / 2
293
- else:
294
- best_score = score_upper
295
- best_threshold = upper_bound
296
- lower_bound = (lower_bound + upper_bound) / 2
297
-
298
- # Binary search loop
299
- while upper_bound - lower_bound > resolution:
300
- mid_threshold = (upper_bound + lower_bound) / 2
301
- max_d_mid = np.max(Z[:, 2]) * mid_threshold
302
- clusters_mid = fcluster(Z, max_d_mid, criterion=linkage_criterion)
329
+ Raises:
330
+ ValueError: If the `opt_metric` argument is not one of the supported metrics.
331
+ """
332
+ # Get the maximum distance in the linkage matrix
333
+ max_d = np.max(Z[:, 2])
334
+ resolution = 1e-6
335
+
336
+ def compute_objective(coefficient: float) -> float:
337
+ """Compute the objective function for optimization."""
338
+ threshold_val = coefficient * max_d
339
+ clusters = fcluster(Z, threshold_val, criterion=linkage_criterion)
340
+ unique_clusters = np.unique(clusters)
341
+ if len(unique_clusters) <= 1 or len(unique_clusters) == m.shape[0]:
342
+ return 1e6
303
343
  try:
304
- score_mid = silhouette_score(m, clusters_mid, metric=linkage_metric)
305
- except ValueError:
306
- score_mid = -np.inf
307
-
308
- # Update best score and threshold if mid-point is better
309
- if score_mid > best_score:
310
- best_score = score_mid
311
- best_threshold = mid_threshold
312
-
313
- # Adjust bounds based on the scores
314
- if score_lower > score_upper:
315
- upper_bound = mid_threshold
316
- else:
317
- lower_bound = mid_threshold
344
+ if opt_metric == "silhouette":
345
+ score = silhouette_score(m, clusters, metric=linkage_metric)
346
+ return -score # We want to maximize the score.
347
+ elif opt_metric == "calinski_harabasz":
348
+ score = calinski_harabasz_score(m, clusters)
349
+ return -score
350
+ elif opt_metric == "davies_bouldin":
351
+ score = davies_bouldin_score(m, clusters)
352
+ return score
353
+ else:
354
+ raise ValueError(f"Unknown optimization metric: {opt_metric}.")
355
+ except Exception:
356
+ return 1e6
357
+
358
+ # Optimize the threshold using the specified metric
359
+ res = minimize_scalar(
360
+ compute_objective, bounds=(0.0, 1.0), method="bounded", options={"xatol": resolution}
361
+ )
362
+
363
+ best_threshold = res.x
364
+ best_obj = res.fun
365
+ # For silhouette and calinski_harabasz, the objective was negative.
366
+ best_metric_value = -best_obj if opt_metric in ["silhouette", "calinski_harabasz"] else best_obj
318
367
 
319
- return best_threshold, float(best_score)
368
+ return best_threshold, float(best_metric_value)
@@ -42,6 +42,7 @@ class GraphAPI:
42
42
  linkage_criterion: str = "distance",
43
43
  linkage_method: str = "average",
44
44
  linkage_metric: str = "yule",
45
+ linkage_threshold: float = 0.2,
45
46
  min_cluster_size: int = 5,
46
47
  max_cluster_size: int = 1000,
47
48
  ) -> Graph:
@@ -57,8 +58,12 @@ class GraphAPI:
57
58
  impute_depth (int, optional): Depth for imputing neighbors. Defaults to 0.
58
59
  prune_threshold (float, optional): Distance threshold for pruning neighbors. Defaults to 0.0.
59
60
  linkage_criterion (str, optional): Clustering criterion for defining domains. Defaults to "distance".
60
- linkage_method (str, optional): Clustering method to use. Defaults to "average".
61
- linkage_metric (str, optional): Metric to use for calculating distances. Defaults to "yule".
61
+ linkage_method (str, optional): Clustering method to use. Defaults to "average". Choose "auto"
62
+ to automatically select the best linkage method.
63
+ linkage_metric (str, optional): Metric to use for calculating distances. Defaults to "yule". Choose "auto"
64
+ to automatically select the best linkage metric.
65
+ linkage_threshold (str, float, optional): Threshold for clustering. Choose "silhouette", "calinski_harabasz",
66
+ or "davies_bouldin" to automatically select the best threshold. Defaults to 0.2.
62
67
  min_cluster_size (int, optional): Minimum size for clusters. Defaults to 5.
63
68
  max_cluster_size (int, optional): Maximum size for clusters. Defaults to 1000.
64
69
 
@@ -76,6 +81,7 @@ class GraphAPI:
76
81
  linkage_criterion=linkage_criterion,
77
82
  linkage_method=linkage_method,
78
83
  linkage_metric=linkage_metric,
84
+ linkage_threshold=linkage_threshold,
79
85
  min_cluster_size=min_cluster_size,
80
86
  max_cluster_size=max_cluster_size,
81
87
  )
@@ -130,6 +136,7 @@ class GraphAPI:
130
136
  linkage_criterion=linkage_criterion,
131
137
  linkage_method=linkage_method,
132
138
  linkage_metric=linkage_metric,
139
+ linkage_threshold=linkage_threshold,
133
140
  )
134
141
  # Trim domains and top annotations based on cluster size constraints
135
142
  domains, trimmed_domains = trim_domains(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: risk-network
3
- Version: 0.0.9b35
3
+ Version: 0.0.9b38
4
4
  Summary: A Python package for biological network analysis
5
5
  Author: Ira Horecka
6
6
  Author-email: Ira Horecka <ira89@icloud.com>
File without changes