pyotc 0.2.2__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.
Files changed (34) hide show
  1. pyotc/__init__.py +5 -0
  2. pyotc/examples/__init__.py +0 -0
  3. pyotc/examples/edge_awareness.py +86 -0
  4. pyotc/examples/lollipops.py +54 -0
  5. pyotc/examples/stochastic_block_model.py +57 -0
  6. pyotc/examples/wheel.py +127 -0
  7. pyotc/otc.py +5 -0
  8. pyotc/otc_backend/__init__.py +0 -0
  9. pyotc/otc_backend/graph/__init__.py +3 -0
  10. pyotc/otc_backend/graph/utils.py +109 -0
  11. pyotc/otc_backend/optimal_transport/__init__.py +0 -0
  12. pyotc/otc_backend/optimal_transport/logsinkhorn.py +78 -0
  13. pyotc/otc_backend/optimal_transport/native.py +49 -0
  14. pyotc/otc_backend/optimal_transport/native_refactor.py +51 -0
  15. pyotc/otc_backend/optimal_transport/pot.py +38 -0
  16. pyotc/otc_backend/policy_iteration/__init__.py +0 -0
  17. pyotc/otc_backend/policy_iteration/dense/__init__.py +0 -0
  18. pyotc/otc_backend/policy_iteration/dense/approx_tce.py +42 -0
  19. pyotc/otc_backend/policy_iteration/dense/entropic.py +161 -0
  20. pyotc/otc_backend/policy_iteration/dense/entropic_tci.py +49 -0
  21. pyotc/otc_backend/policy_iteration/dense/exact.py +127 -0
  22. pyotc/otc_backend/policy_iteration/dense/exact_tce.py +56 -0
  23. pyotc/otc_backend/policy_iteration/dense/exact_tci_lp.py +65 -0
  24. pyotc/otc_backend/policy_iteration/dense/exact_tci_pot.py +90 -0
  25. pyotc/otc_backend/policy_iteration/sparse/__init__.py +0 -0
  26. pyotc/otc_backend/policy_iteration/sparse/exact.py +89 -0
  27. pyotc/otc_backend/policy_iteration/sparse/exact_tce.py +78 -0
  28. pyotc/otc_backend/policy_iteration/sparse/exact_tci.py +88 -0
  29. pyotc/otc_backend/policy_iteration/utils.py +112 -0
  30. pyotc-0.2.2.dist-info/METADATA +38 -0
  31. pyotc-0.2.2.dist-info/RECORD +34 -0
  32. pyotc-0.2.2.dist-info/WHEEL +4 -0
  33. pyotc-0.2.2.dist-info/licenses/AUTHORS.rst +12 -0
  34. pyotc-0.2.2.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,78 @@
1
+ """
2
+ Original Transition Coupling Evaluation (TCE) method from:
3
+ https://www.jmlr.org/papers/volume23/21-0519/21-0519.pdf
4
+ """
5
+
6
+ import numpy as np
7
+ import scipy.sparse as sp
8
+
9
+
10
+ def exact_tce(R_sparse, c):
11
+ """
12
+ Computes the exact Transition Coupling Evaluation (TCE) vectors g and h for a given sparse transition matrix R_sparse and cost vector c.
13
+
14
+ Specifically, solves the block linear system outlined in Algorithm 1a of the paper:
15
+ "Optimal Transport for Stationary Markov Chains via Policy Iteration"
16
+ (https://www.jmlr.org/papers/volume23/21-0519/21-0519.pdf).
17
+
18
+ Due to memory constraints associated with direct solvers (e.g., sp.linalg.spsolve),
19
+ an iterative solver (scipy.sparse.linalg.lsmr) is employed to efficiently handle large-scale sparse systems.
20
+
21
+ Notes:
22
+ 1. When A in Ax = b is close to singular, we have observed few cases that both SciPy functions (scipy.sparse.linalg.spsolve, scipy.sparse.linalg.lsmr)
23
+ can produce results that differ from NumPy's solver, leading to different results with dense implementation and non-convergence.
24
+ This is an issue with SciPy solvers and remains an unresolved issue. The best approach in such cases is to fall back to the dense implementation.
25
+
26
+ 2. Solving Ax = b using a direct solver (scipy.sparse.linalg.spsolve) on large networks resulted in:
27
+ "Not enough memory to perform factorization."
28
+ This is likely due to excessive fill-in during LU factorization of the large sparse matrix.
29
+ To address this, we switch to an iterative solver (scipy.sparse.linalg.lsmr),
30
+ which is more memory-efficient and better suited for large-scale sparse systems.
31
+
32
+ 3. To leave open the possibility of switching from 'lsmr' to 'spsolve', the corresponding 'spsolve' code has been retained as a commented-out block.
33
+
34
+ Args:
35
+ R_sparse (scipy.sparse.csr_matrix): Sparse transition matrix of shape (dx*dy, dx*dy).
36
+ c (np.ndarray): Cost vector of shape (dx, dy).
37
+
38
+ Returns:
39
+ g (np.ndarray): Average cost (gain) vector of shape (dx*dy,).
40
+ h (np.ndarray): Total extra cost (bias) vector of shape (dx*dy,).
41
+ """
42
+ n = R_sparse.shape[0]
43
+ c = c.reshape(n)
44
+
45
+ # Construct the block matrix A and right-hand side vector b
46
+ I = sp.eye(n, format="csr")
47
+ zero = sp.csr_matrix((n, n))
48
+ A = sp.bmat(
49
+ [[I - R_sparse, zero, zero], [I, I - R_sparse, zero], [zero, I, I - R_sparse]],
50
+ format="csr",
51
+ )
52
+ b = np.concatenate([np.zeros(n), c, np.zeros(n)])
53
+
54
+ # print("Solving sparse linear system in exact tce...")
55
+ # permc_specs = ['COLAMD', 'MMD_ATA', 'MMD_AT_PLUS_A', 'NATURAL']
56
+ # solution = None
57
+ # for spec in permc_specs:
58
+ # try:
59
+ # current_solution = sp.linalg.spsolve(A, rhs, permc_spec=spec)
60
+ # if not np.any(np.abs(current_solution) > 1e15):
61
+ # print("spsolve successful with spec:", spec)
62
+ # solution = current_solution
63
+ # break
64
+ # else:
65
+ # print(f"Solution with {spec} contains large values, trying next spec.")
66
+ # except ValueError as e:
67
+ # print(f"spsolve with {spec} encountered an error: trying next spec.")
68
+ # if solution is None:
69
+ # raise RuntimeError("Failed to find a stable solution with any of the provided permc_specs for sp.linalg.spsolve solver.")
70
+
71
+ # Solve the linear system using an iterative solver (lsmr)
72
+ solution = sp.linalg.lsmr(A, b, atol=1e-10, btol=1e-10)[0]
73
+
74
+ # Extract vectors g and h from the solution
75
+ g = solution[:n]
76
+ h = solution[n : 2 * n]
77
+
78
+ return g, h
@@ -0,0 +1,88 @@
1
+ """
2
+ Original Transition Coupling Improvements (TCI) method from:
3
+ https://www.jmlr.org/papers/volume23/21-0519/21-0519.pdf
4
+ """
5
+
6
+ import numpy as np
7
+ import scipy.sparse as sp
8
+ from pyotc.otc_backend.optimal_transport.pot import computeot_pot
9
+
10
+
11
+ def setup_ot(f, Px, Py, R):
12
+ """
13
+ This improvement step updates the transition coupling matrix R that minimizes the product Rf element-wise.
14
+ In more detail, we may select a transition coupling R such that for each state pair (x, y),
15
+ the corresponding row r = R((x, y), ·) minimizes rf over couplings r in Pi(Px(x, ·), Py(y, ·)).
16
+ This is done by solving the optimal transport problem for each state pair (x, y) in the source
17
+ and target Markov chains. The resulting transition coupling matrix R is updated accordingly.
18
+
19
+ This function uses the POT (Python Optimal Transport) library to solve the optimal transport problem
20
+ for each (x, y) state pair and updates the transition coupling matrix.
21
+
22
+ Args:
23
+ f (np.ndarray): Cost function reshaped as of shape (dx*dy,).
24
+ Px (np.ndarray): Transition matrix of the source Markov chain of shape (dx, dx).
25
+ Py (np.ndarray): Transition matrix of the target Markov chain of shape (dy, dy).
26
+ R (np.ndarray): Transition coupling matrix to update of shape (dx*dy, dx*dy).
27
+
28
+ Returns:
29
+ R (np.ndarray): Updated transition coupling matrix of shape (dx*dy, dx*dy).
30
+ """
31
+
32
+ dx, dy = Px.shape[0], Py.shape[0]
33
+ f_mat = np.reshape(f, (dx, dy))
34
+
35
+ for x_row in range(dx):
36
+ for y_row in range(dy):
37
+ dist_x = Px[x_row, :]
38
+ dist_y = Py[y_row, :]
39
+ # Check if either distribution is degenerate.
40
+ if np.any(dist_x == 1) or np.any(dist_y == 1):
41
+ sol = np.outer(dist_x, dist_y)
42
+ else:
43
+ sol, _ = computeot_pot(f_mat, dist_x, dist_y)
44
+ idx = dy * x_row + y_row
45
+ sol_flat = sol.flatten()
46
+ for j in np.nonzero(sol_flat)[0]:
47
+ R[idx, j] = sol_flat[j]
48
+
49
+ return R
50
+
51
+
52
+ def exact_tci(g, h, R0, Px, Py):
53
+ """
54
+ Performs the Transition Coupling Improvement (TCI) step in the OTC algorithm.
55
+
56
+ This function attempts to update the current coupling transition matrix R0
57
+ based on the evaluation vectors g and h obtained from the Transition Coupling Evaluation (TCE).
58
+
59
+ Args:
60
+ g (np.ndarray): Gain vector from TCE of shape (dx*dy,).
61
+ h (np.ndarray): Bias vector from TCE of shape (dx*dy,).
62
+ R0 (scipy.sparse.csr_matrix or lil_matrix): Current transition coupling matrix of shape (dx*dy, dx*dy).
63
+ Px (np.ndarray): Transition matrix of the source Markov chain of shape (dx, dx).
64
+ Py (np.ndarray): Transition matrix of the target Markov chain of shape (dy, dy).
65
+
66
+ Returns:
67
+ R (scipy.sparse.lil_matrix): Improved transition coupling matrix of shape (dx*dy, dx*dy).
68
+ """
69
+
70
+ # Check if g is constant.
71
+ dx, dy = Px.shape[0], Py.shape[0]
72
+ R = sp.lil_matrix((dx * dy, dx * dy))
73
+ g_const = np.max(g) - np.min(g) <= 1e-3
74
+
75
+ # If g is not constant, improve transition coupling against g.
76
+ if not g_const:
77
+ R = setup_ot(g, Px, Py, R)
78
+ if np.max(np.abs(R0.dot(g) - R.dot(g))) <= 1e-7:
79
+ R = R0.copy()
80
+ else:
81
+ return R
82
+
83
+ # Try to improve with respect to h.
84
+ R = setup_ot(h, Px, Py, R)
85
+ if np.max(np.abs(R0.dot(h) - R.dot(h))) <= 1e-4:
86
+ R = R0.copy()
87
+
88
+ return R
@@ -0,0 +1,112 @@
1
+ import numpy as np
2
+ from scipy.optimize import linprog
3
+
4
+
5
+ def get_best_stat_dist(P, c):
6
+ """
7
+ Given a transition matrix P and a cost vector c,
8
+ this function computes the stationary distribution that minimizes the expected cost
9
+ via linear programming.
10
+
11
+ Args:
12
+ P (np.ndarray): Transition matrix.
13
+ c (np.ndarray): Cost vector.
14
+
15
+ Returns:
16
+ stat_dist (np.ndarray): Best stationary distribution.
17
+ exp_cost (float): Corresponding expected cost.
18
+ """
19
+
20
+ # Set up constraints.
21
+ n = P.shape[0]
22
+ c = np.reshape(c, (n, -1))
23
+ Aeq = np.concatenate((P.T - np.eye(n), np.ones((1, n))), axis=0)
24
+ beq = np.concatenate((np.zeros((n, 1)), 1), axis=None)
25
+ beq = beq.reshape(-1, 1)
26
+ bound = [[0, None]] * n
27
+
28
+ # Solve linear program.
29
+ res = linprog(c, A_eq=Aeq, b_eq=beq, bounds=bound)
30
+ stat_dist = res.x
31
+ exp_cost = res.fun
32
+
33
+ return stat_dist, exp_cost
34
+
35
+
36
+ def get_stat_dist(P, method="best", c=None):
37
+ """
38
+ Computes the stationary distribution of a Markov chain given its transition matrix P.
39
+
40
+ Supports multiple methods:
41
+ - 'best': Solves a linear program that minimizes cost under stationarity constraints.
42
+ - 'eigen': Solves for the stationary distribution using the eigenvalue method.
43
+ - 'iterative': Uses power iteration for large or sparse matrices.
44
+
45
+ Args:
46
+ P (np.ndarray): Transition matrix of the Markov chain, shape (n, n).
47
+ method (str): Method used to compute the stationary distribution.
48
+ One of 'eigen', 'iterative', or 'best'. Defaults to 'best'.
49
+ c (np.ndarray, optional): Cost vector of shape (n,) used only when method='best'.
50
+
51
+ Returns:
52
+ pi (np.ndarray): Stationary distribution vector of shape (n,), summing to 1.
53
+
54
+ Raises:
55
+ ValueError: If method is 'best' but cost vector `c` is not provided,
56
+ or if an invalid method name is given.
57
+ """
58
+ if method == "best":
59
+ # 'best' method minimizes expected cost under stationary constraints
60
+ if c is None:
61
+ raise ValueError("Cost function 'c' is required when method='best'.")
62
+
63
+ n = P.shape[0]
64
+ c = np.reshape(c, (n, -1))
65
+
66
+ # Stationarity constraint: π^T P = π^T ⇨ (P^T - I)^T π = 0
67
+ # Add additional constraint: sum(π) = 1
68
+ Aeq = np.concatenate((P.T - np.eye(n), np.ones((1, n))), axis=0)
69
+ beq = np.concatenate((np.zeros((n, 1)), 1), axis=None)
70
+ beq = beq.reshape(-1, 1)
71
+
72
+ bound = [[0, None]] * n
73
+
74
+ # Solve the linear program: minimize c^T π s.t. Aeq π = beq
75
+ res = linprog(c, A_eq=Aeq, b_eq=beq, bounds=bound)
76
+ pi = res.x
77
+ return pi
78
+
79
+ elif method == "eigen":
80
+ # Computes the stationary distribution using eigenvalue decomposition
81
+ # Calculate the eigenvalues and eigenvectors
82
+ eigenvalues, eigenvectors = np.linalg.eig(P.T)
83
+
84
+ # Identify the eigenvector associated with eigenvalue closest to 1 and normalize to obtain a valid distribution
85
+ idx = np.argmin(np.abs(eigenvalues - 1))
86
+ pi = np.real(eigenvectors[:, idx])
87
+ pi /= np.sum(pi)
88
+ return pi
89
+
90
+ elif method == "iterative":
91
+ # Computes the stationary distribution using power iteration
92
+ max_iter = 10000
93
+ tol = 1e-10
94
+ n = P.shape[0]
95
+
96
+ # Start from uniform distribution
97
+ pi = np.ones(n) / n
98
+
99
+ for _ in range(max_iter):
100
+ pi_new = pi @ P
101
+ if np.linalg.norm(pi_new - pi, ord=1) < tol:
102
+ break
103
+ pi = pi_new
104
+
105
+ # Normalize the resulting distribution
106
+ pi /= np.sum(pi)
107
+ return pi
108
+
109
+ else:
110
+ raise ValueError(
111
+ f"Invalid method '{method}'. Must be one of 'best', 'eigen', or 'iterative'."
112
+ )
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyotc
3
+ Version: 0.2.2
4
+ Summary: Perform optimal transition coupling (OTC) in python.
5
+ Author: Jay Hineman, Yuning Pan, Bongsoo Yi
6
+ License: MIT license
7
+ License-File: AUTHORS.rst
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: matplotlib>=3.10.0
11
+ Requires-Dist: networkx>=3.4.2
12
+ Requires-Dist: numpy>=2.2.1
13
+ Requires-Dist: pot>=0.9.5
14
+ Description-Content-Type: text/markdown
15
+
16
+ # pyotc
17
+ [![codecov](https://codecov.io/github/pyotc/pyotc/graph/badge.svg?token=52QPNW0AP7)](https://codecov.io/github/pyotc/pyotc)
18
+
19
+ A python implementations of optimal transport coupling algorithms.
20
+
21
+ ## Documentation
22
+ Find sphinx documentation [here](https://pyotc.github.io/pyotc/).
23
+
24
+ ## Install
25
+ See [install instructions](INSTALL.md)
26
+
27
+ ## Run Tests
28
+ With a `uv` setup one can simply
29
+ ```bash
30
+ uv run pytest
31
+ ```
32
+ Otherwise, in `pip` installed context with deps met, `pytest` should behave as expected.
33
+
34
+ ## Contributing
35
+ Guidelines for contribution to `pyotc` are provided in [CONTRIBUTING.md](./CONTRIBUTING.md).
36
+
37
+ ## Changelog
38
+ A summary of changes and guide to versioning are recoreded in [CHANGELOG.md](./CHANGELOG.md).
@@ -0,0 +1,34 @@
1
+ pyotc/__init__.py,sha256=Od-nJxr4TP_mgmXMxpwaItNDISKzR0V6X8QakVNTmr8,125
2
+ pyotc/otc.py,sha256=_we15B7Gw-IgxVlXnXbWS_8d0LGwHbaZiSrHwBSGbM0,94
3
+ pyotc/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pyotc/examples/edge_awareness.py,sha256=k4pAs_UkDC2ZzgvkQZD9aY-gemjd8I9UAzWA1x8Z_3E,2513
5
+ pyotc/examples/lollipops.py,sha256=fmvPkdSL9T5ZjQU8SpFnIox5DDuiVVwikjt7w3ukxLc,1589
6
+ pyotc/examples/stochastic_block_model.py,sha256=aF-vZ9i3L75_rguTCmF3bD5qMI8T3QmlzdCMHivrVOw,1895
7
+ pyotc/examples/wheel.py,sha256=JkBngoyHnppNUS2uXZSyRQOBhUynPOuVcEsYQdEcBQM,4177
8
+ pyotc/otc_backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ pyotc/otc_backend/graph/__init__.py,sha256=n-t13QCEVXJ3naqd6gUnUAekIpUYwBjjO-f-WXWCbkg,58
10
+ pyotc/otc_backend/graph/utils.py,sha256=mnUfgsfSEV79ej7a-4NUcLl-b956E6zKse3aYD4hRUo,3160
11
+ pyotc/otc_backend/optimal_transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pyotc/otc_backend/optimal_transport/logsinkhorn.py,sha256=V8fwUw-8UHQAKaeBGYGyq5bXilaM9IUAv4b78FEG29g,2069
13
+ pyotc/otc_backend/optimal_transport/native.py,sha256=vxctK2knNoiyOGPV7oYMxGnh8S0iDyNW_D076dElNeo,1577
14
+ pyotc/otc_backend/optimal_transport/native_refactor.py,sha256=83Ev5YuzYSyUyZPJTfRDk1xtXdjpYCJvH1KzV8MX2D8,1271
15
+ pyotc/otc_backend/optimal_transport/pot.py,sha256=zeFEGJBQgVg2eFgAYHjd1ddbHQmKxrbUN1bQ4EmCZSc,1295
16
+ pyotc/otc_backend/policy_iteration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ pyotc/otc_backend/policy_iteration/utils.py,sha256=GCfgXhofn6ykqXkg0CJPm0VbO3mZr0CxgRwUGA2r9xc,3802
18
+ pyotc/otc_backend/policy_iteration/dense/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ pyotc/otc_backend/policy_iteration/dense/approx_tce.py,sha256=PA92ysxJgRFwUbHxGYx6nctxr8GkL7I3WvakWfvXdNc,1150
20
+ pyotc/otc_backend/policy_iteration/dense/entropic.py,sha256=uwdtxYOXipI3aXfmoY7U-55XumoH9gWGzWACSAGaDXY,6421
21
+ pyotc/otc_backend/policy_iteration/dense/entropic_tci.py,sha256=TXKcQmLhpr9vYnjhIGO8sqx5O_7jwkY0pQ3eiFthtRg,1798
22
+ pyotc/otc_backend/policy_iteration/dense/exact.py,sha256=hZqKq7zfZ5q7v3JnX37vCrpXJVDD84U496NDZA8OEvk,5001
23
+ pyotc/otc_backend/policy_iteration/dense/exact_tce.py,sha256=WI5I9btsnrBksYDQ9BvnHe42pBaegNNrGRdYMrEkJKw,1851
24
+ pyotc/otc_backend/policy_iteration/dense/exact_tci_lp.py,sha256=-mnwfJj_42V3DBZIjcfSFh9uwudEBm2gOOAwpiClRBM,1911
25
+ pyotc/otc_backend/policy_iteration/dense/exact_tci_pot.py,sha256=9lJCB3JtnabDVeYAx6Igv7AkkAqEGa99LJnOOGNpIzY,3494
26
+ pyotc/otc_backend/policy_iteration/sparse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ pyotc/otc_backend/policy_iteration/sparse/exact.py,sha256=2VskHrp1nDGPV2yEsD5MBagrZHkUDCdChMZ28WOb0uk,4400
28
+ pyotc/otc_backend/policy_iteration/sparse/exact_tce.py,sha256=oSymhuok3logzt0wYSYJeMkTls0zzC0kRiQw2nHXk7c,3626
29
+ pyotc/otc_backend/policy_iteration/sparse/exact_tci.py,sha256=mW90o-2ZVd9fjgKFDlJhR223G5JYxBSzsHBr7NdK6eg,3446
30
+ pyotc-0.2.2.dist-info/METADATA,sha256=yTtg2-uCXeJCSpSeh-NUWJ_i2NnxLzLjFuLcO46mexY,1122
31
+ pyotc-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
32
+ pyotc-0.2.2.dist-info/licenses/AUTHORS.rst,sha256=FtGxBvQjLU6Vdej_-zcsdWxTwnInCsjqejFRmaeC7TU,136
33
+ pyotc-0.2.2.dist-info/licenses/LICENSE,sha256=DfWCeVpuOnLG1KJZfBO24ry8u90eeQi7mTdjoo5pt8M,1070
34
+ pyotc-0.2.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,12 @@
1
+ =======
2
+ Credits
3
+ =======
4
+
5
+ * Jay Hineman, @jhineman
6
+ * Bongsoo Yi
7
+ * Yuning Pan
8
+
9
+ Contributors
10
+ ------------
11
+
12
+ None yet. Why not be the first?
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, Jay Hineman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+