tmapper-py 0.1.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,63 @@
1
+ """Port of tmapper_tools/TCMdistance.m from the MATLAB toolbox."""
2
+
3
+ import numpy as np
4
+ import networkx as nx
5
+
6
+ from ._shortest_path import all_pairs_distance
7
+
8
+
9
+ def tcm_distance(g, nodet, weighted=False):
10
+ """Compute a temporal connectivity matrix: for every pair of original
11
+ time points, the shortest path length between the nodes (of a
12
+ simplified graph ``g``) that contain them.
13
+
14
+ Port of MATLAB's ``TCMdistance.m``. Sizes its output by the full range
15
+ covered by ``nodet`` (``max - min + 1``), not just the count of
16
+ distinct points referenced -- so time points genuinely not covered by
17
+ any node in ``nodet`` correctly stay ``NaN`` rather than being silently
18
+ zero-filled.
19
+
20
+ Parameters
21
+ ----------
22
+ g : networkx.Graph or networkx.DiGraph
23
+ The simplified graph.
24
+ nodet : sequence of sequence of int
25
+ ``nodet[i]`` gives the original time-point indices belonging to
26
+ node i of ``g`` (in the same node order as ``g.nodes()``).
27
+ weighted : bool, default False
28
+ Whether to use ``g``'s edge weights. If False, every edge is
29
+ treated as weight 1 (hop count).
30
+
31
+ Returns
32
+ -------
33
+ numpy.ndarray, shape (Nt, Nt)
34
+ The temporal connectivity matrix, where Nt = max(nodet) - min(nodet) + 1.
35
+ Entries for time points never covered by any node stay NaN.
36
+ """
37
+ nodelist = list(g.nodes())
38
+ all_t = np.concatenate([np.asarray(list(nt), dtype=int) for nt in nodet]) if nodet else np.array([], dtype=int)
39
+ if all_t.size == 0:
40
+ return np.zeros((0, 0))
41
+ t_0 = all_t.min()
42
+ Nt = all_t.max() - t_0 + 1
43
+
44
+ distmat = all_pairs_distance(g, nodelist, weight="weight" if weighted else None)
45
+
46
+ tcm = np.full((Nt, Nt), np.nan)
47
+
48
+ n_nodes = len(nodelist)
49
+ idx_lists = [np.asarray(list(nt), dtype=int) - t_0 for nt in nodet]
50
+
51
+ for i in range(n_nodes):
52
+ ii = idx_lists[i]
53
+ tcm[np.ix_(ii, ii)] = 0
54
+
55
+ for i in range(n_nodes):
56
+ for j in range(i + 1, n_nodes):
57
+ ii, jj = idx_lists[i], idx_lists[j]
58
+ block_ij = tcm[np.ix_(ii, jj)]
59
+ tcm[np.ix_(ii, jj)] = np.fmin(block_ij, distmat[i, j])
60
+ block_ji = tcm[np.ix_(jj, ii)]
61
+ tcm[np.ix_(jj, ii)] = np.fmin(block_ji, distmat[j, i])
62
+
63
+ return tcm
tmapper/tknndigraph.py ADDED
@@ -0,0 +1,190 @@
1
+ """Port of tmapper_tools/tknndigraph.m from the MATLAB toolbox."""
2
+
3
+ import numpy as np
4
+ import networkx as nx
5
+ from scipy.spatial.distance import cdist
6
+
7
+
8
+ def _percentile_with_inf(values, prct):
9
+ """Like ``np.percentile`` (linear interpolation method), but robust to
10
+ ``inf`` entries. ``np.percentile`` can return NaN when the interpolation
11
+ boundary falls between two ``inf`` values, because its formula computes
12
+ ``a + weight * (b - a)`` and ``inf - inf`` is NaN even when ``weight``
13
+ is (mathematically) zero -- 0 * NaN is still NaN, not 0. This matters
14
+ here since D routinely contains inf entries (masked self-loops/temporal
15
+ exclusions) and MATLAB's prctile does not have this problem.
16
+ """
17
+ sorted_vals = np.sort(np.asarray(values).ravel())
18
+ n = sorted_vals.size
19
+ if n == 0:
20
+ return np.inf
21
+ virtual_idx = (prct / 100.0) * (n - 1)
22
+ lo = int(np.floor(virtual_idx))
23
+ hi = int(np.ceil(virtual_idx))
24
+ if lo == hi:
25
+ return sorted_vals[lo]
26
+ a, b = sorted_vals[lo], sorted_vals[hi]
27
+ if np.isinf(a) and np.isinf(b) and a == b:
28
+ return a
29
+ frac = virtual_idx - lo
30
+ return a + frac * (b - a)
31
+
32
+
33
+ def tknndigraph(
34
+ X_or_D,
35
+ k,
36
+ tidx,
37
+ *,
38
+ reciprocal=True,
39
+ time_exclude_space=True,
40
+ time_exclude_range=1,
41
+ max_neighbor_dist=np.inf,
42
+ max_neighbor_dist_prct=100.0,
43
+ ):
44
+ """Construct a directed graph based on k-nearest neighbors that also
45
+ includes temporal neighbors (t -> t+1 links).
46
+
47
+ Port of MATLAB's ``tknndigraph.m``. Node i of the returned graph
48
+ corresponds to row i of ``X_or_D`` / element i of ``tidx`` (0-indexed,
49
+ unlike the 1-indexed MATLAB original).
50
+
51
+ Parameters
52
+ ----------
53
+ X_or_D : array_like, shape (N, d) or (N, N)
54
+ Either raw coordinates (N points in d-dim space) or a precomputed
55
+ (N, N) distance matrix. Auto-detected: treated as a distance
56
+ matrix only if square *and* symmetric; otherwise treated as raw
57
+ coordinates and converted to a Euclidean distance matrix.
58
+ k : int
59
+ Max number of spatial neighbors per point. Must be a positive
60
+ integer strictly less than N.
61
+ tidx : array_like of int, shape (N,)
62
+ Time index per point. Two points x, y are temporal neighbors iff
63
+ ``tidx[y] == tidx[x] + 1``.
64
+ reciprocal : bool, default True
65
+ Whether to require spatial k-NN links to be mutual (both
66
+ directions) to be kept.
67
+ time_exclude_space : bool, default True
68
+ Whether temporal neighbors are barred from also counting as
69
+ spatial neighbors.
70
+ time_exclude_range : int, default 1
71
+ How many following time points are excluded from spatial-kNN
72
+ consideration.
73
+ max_neighbor_dist : float, default inf
74
+ Absolute distance cutoff for spatial neighbors.
75
+ max_neighbor_dist_prct : float, default 100
76
+ Percentile distance cutoff for spatial neighbors. The stricter
77
+ (smaller) of this and ``max_neighbor_dist`` is applied.
78
+
79
+ Returns
80
+ -------
81
+ g : networkx.DiGraph
82
+ Unweighted directed graph, one node per input point.
83
+ par : dict
84
+ Parameters actually used, including the resolved
85
+ ``max_neighbor_dist``.
86
+ """
87
+ X_or_D = np.asarray(X_or_D, dtype=float)
88
+ if X_or_D.ndim != 2:
89
+ raise ValueError("X_or_D must be a 2D array.")
90
+ if not (np.isscalar(k) and float(k) == round(float(k)) and k >= 1):
91
+ raise ValueError("k must be a positive integer scalar.")
92
+ k = int(k)
93
+
94
+ tidx = np.asarray(tidx, dtype=float).ravel()
95
+ if tidx.shape[0] != X_or_D.shape[0]:
96
+ raise ValueError(
97
+ "tidx must have the same number of elements as rows of X_or_D."
98
+ )
99
+
100
+ par = {
101
+ "reciprocal": reciprocal,
102
+ "time_exclude_space": time_exclude_space,
103
+ "time_exclude_range": time_exclude_range,
104
+ "max_neighbor_dist": max_neighbor_dist,
105
+ "max_neighbor_dist_prct": max_neighbor_dist_prct,
106
+ "k": k,
107
+ }
108
+
109
+ # -- check input and obtain distance matrix D
110
+ nr, nc = X_or_D.shape
111
+ if nr != nc or not np.allclose(X_or_D, X_or_D.T):
112
+ D = cdist(X_or_D, X_or_D)
113
+ else:
114
+ D = X_or_D.copy()
115
+ Nn = D.shape[0]
116
+
117
+ if k >= Nn:
118
+ raise ValueError(f"k must be smaller than the number of points ({Nn}).")
119
+
120
+ np.fill_diagonal(D, np.inf) # exclude self-loops
121
+
122
+ # -- find indices for temporal links D[i(t), i(t+1)]
123
+ # t_wafter[i] is True iff point i has a point immediately after it in time
124
+ # (mirrors MATLAB's circshift(tidx,-1,1) - 1 == tidx, including its
125
+ # wraparound behavior at the last index, which is virtually always False
126
+ # for non-cyclic tidx and is harmless here for the same reason it is in
127
+ # the original).
128
+ tidx_next = np.roll(tidx, -1)
129
+ t_wafter = tidx_next - 1 == tidx
130
+
131
+ # t_after_idx1: unconditional immediate temporal edges i -> i+1
132
+ t_after_idx1 = np.zeros((Nn, Nn), dtype=bool)
133
+ for i in range(Nn):
134
+ if t_wafter[i]:
135
+ t_after_idx1[i, (i + 1) % Nn] = True
136
+
137
+ # t_after_idx: temporal-exclusion mask, extended up to time_exclude_range
138
+ # steps, restricted to strictly-forward (i < j) positions
139
+ t_after_idx = np.zeros((Nn, Nn), dtype=bool)
140
+ for n in range(1, time_exclude_range + 1):
141
+ for i in range(Nn):
142
+ if t_wafter[i]:
143
+ t_after_idx[i, (i + n) % Nn] = True
144
+ upper = np.triu(np.ones((Nn, Nn), dtype=bool), k=1)
145
+ t_after_idx = t_after_idx & upper
146
+
147
+ if time_exclude_space:
148
+ D[t_after_idx] = np.inf
149
+
150
+ # -- compute adjacency matrix (k nearest neighbors per row)
151
+ order = np.argsort(D, axis=1, kind="stable")
152
+ D_sorted = np.take_along_axis(D, order, axis=1)
153
+ A = np.zeros((Nn, Nn), dtype=bool)
154
+ rows = np.repeat(np.arange(Nn), k)
155
+ cols = order[:, :k].ravel()
156
+ A[rows, cols] = True
157
+
158
+ # -- check for duplicate points: any other point tied with the k-th
159
+ # nearest neighbor's distance is also included
160
+ dmax = D_sorted[:, k - 1][:, None]
161
+ A |= D <= dmax
162
+
163
+ # -- get distance threshold (the stricter of the absolute and percentile caps).
164
+ # NOTE: matches MATLAB's prctile(D(:), Prct) exactly by computing the
165
+ # percentile over the *full* D, Inf entries included (the masked
166
+ # diagonal/temporal-exclusion entries) -- not just the finite values.
167
+ prct_dist = _percentile_with_inf(D, max_neighbor_dist_prct)
168
+ resolved_max_dist = min(prct_dist, max_neighbor_dist)
169
+ par["max_neighbor_dist"] = resolved_max_dist
170
+
171
+ # -- remove neighbors that exceed max distance
172
+ A[D > resolved_max_dist] = False
173
+
174
+ # -- exclude or retain temporal links as spatial links
175
+ if time_exclude_space:
176
+ A_space = A & ~t_after_idx
177
+ else:
178
+ A_space = A.copy()
179
+
180
+ # -- enforce symmetry of spatial links
181
+ if reciprocal:
182
+ A_space = A_space & A_space.T
183
+ else:
184
+ A_space = A_space | A_space.T
185
+
186
+ # -- (re-)incorporate temporal links
187
+ A_final = t_after_idx1 | A_space
188
+
189
+ g = nx.from_numpy_array(A_final, create_using=nx.DiGraph)
190
+ return g, par
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: tmapper-py
3
+ Version: 0.1.0
4
+ Summary: Python port of Temporal Mapper 2 -- build attractor transition networks from time-series data
5
+ Author: Mengsen Zhang
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/Multiscale-Complex-Systems-Lab/tmapper-py
8
+ Project-URL: MATLAB original, https://github.com/Multiscale-Complex-Systems-Lab/tmapper2
9
+ Keywords: temporal mapper,topological data analysis,mapper,dynamical systems,time series,attractor,recurrence
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
18
+ Classifier: Topic :: Scientific/Engineering :: Visualization
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy>=1.24
23
+ Requires-Dist: scipy>=1.10
24
+ Requires-Dist: networkx>=3.0
25
+ Provides-Extra: plot
26
+ Requires-Dist: matplotlib>=3.7; extra == "plot"
27
+ Requires-Dist: python-igraph>=1.0; extra == "plot"
28
+ Requires-Dist: pyvis>=0.3; extra == "plot"
29
+ Provides-Extra: app
30
+ Requires-Dist: streamlit>=1.30; extra == "app"
31
+ Requires-Dist: pandas>=1.5; extra == "app"
32
+ Requires-Dist: matplotlib>=3.7; extra == "app"
33
+ Requires-Dist: python-igraph>=1.0; extra == "app"
34
+ Requires-Dist: pyvis>=0.3; extra == "app"
35
+ Provides-Extra: test
36
+ Requires-Dist: pytest>=7.0; extra == "test"
37
+ Requires-Dist: pandas>=1.5; extra == "test"
38
+ Provides-Extra: docs
39
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
40
+ Requires-Dist: mkdocstrings[python]>=0.25; extra == "docs"
41
+ Dynamic: license-file
42
+
43
+ # tmapper (Python)
44
+
45
+ A Python port of [Temporal Mapper 2](https://github.com/Multiscale-Complex-Systems-Lab/tmapper2), a toolbox for building **attractor transition networks** from time-series data.
46
+
47
+ **Status: feature-complete port.** Both the core two-step pipeline (`tknndigraph` + `filtergraph` + plotting) and the secondary cycle/path analysis toolkit (cycle counting/clustering, path decomposition, traffic, modularity) are ported and tested. `tknngraph.m` (a legacy, unused MATLAB function) was intentionally not ported.
48
+
49
+ For the full background, citation, and the original MATLAB implementation, see [tmapper2](https://github.com/Multiscale-Complex-Systems-Lab/tmapper2).
50
+
51
+ **Documentation, quickstart, and full API reference:** https://multiscale-complex-systems-lab.github.io/tmapper-py/
52
+
53
+ ## What's included
54
+
55
+ - **Core pipeline**: `tknndigraph`, `filtergraph`, `find_node_label`, `tcm_distance`, `plot_tmgraph`, `plot_tmgraph_tcm`, `plot_tmgraph_interactive` (draggable/zoomable/hoverable HTML via pyvis, no MATLAB equivalent)
56
+ - **Standalone graph builders**: `knngraph`, `cknngraph`
57
+ - **Graph/data utilities**: `node_size`, `node_measure`, `normalize_geodesic`, `normalize_tcm`, `members_to_tidx`, `subgraph_from_members`, `sym_dyn_to_digraph`, `digraph_to_graph`, `find_blocks`
58
+ - **Cycle/path analysis toolkit**: `cycle_count` (Giscard/Kriege/Wilson combinatorial-sieve counter, third-party algorithm carrying its own BSD license -- see `cycle_count.py`), `cycle_count2p`, `reorg_cycles`, `cycle_path_overlap`, `cycle_cluster`, `cycle_cluster_conn`, `cycle_cutter`, `cycles_to_paths`, `cycle_path_decomp`, `path_traffic`, `qasym`, `cal_mod`
59
+ - **Interactive app**: `tmapper-app`, a browser front end for the whole pipeline -- see below
60
+
61
+ All ported functions were cross-checked node-for-node and edge-for-edge against real MATLAB output on deterministic test graphs (see the test suite for the same hand-derived oracle values used in the MATLAB toolbox's own `tests/`).
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install tmapper-py # library
67
+ pip install "tmapper-py[app]" # library + the interactive app
68
+ ```
69
+
70
+ (The distribution is `tmapper-py` -- `tmapper` alone isn't available on PyPI,
71
+ see [Installation](https://multiscale-complex-systems-lab.github.io/tmapper-py/installation/).
72
+ Everything imports as `import tmapper` either way.)
73
+
74
+ For development, from a clone:
75
+
76
+ ```bash
77
+ git clone https://github.com/Multiscale-Complex-Systems-Lab/tmapper-py.git
78
+ cd tmapper-py
79
+ pip install -e ".[app,test]"
80
+ ```
81
+
82
+ ## Interactive app
83
+
84
+ Prefer point-and-click? `tmapper-app` runs the same pipeline in your browser -- load data, pick variables, turn the parameters, and explore the network without writing any code:
85
+
86
+ ```bash
87
+ pip install "tmapper-py[app]"
88
+ tmapper-app
89
+ ```
90
+
91
+ It handles missing data and downsampling for you (dropping incomplete rows before an anti-aliasing lowpass), exports both figures and analysis-ready data (interactive HTML, PNGs, a per-timepoint `timeline.csv`, GraphML, and a provenance JSON), and shows a runnable script reproducing the current build.
92
+
93
+ See the [Interactive App guide](https://multiscale-complex-systems-lab.github.io/tmapper-py/app/) for a full walkthrough.
94
+
95
+ ## Running tests
96
+
97
+ ```bash
98
+ pytest
99
+ ```
100
+
101
+ ## Notes for users porting workflows from MATLAB
102
+
103
+ - Node labels are 0-indexed (Pythonic), unlike the MATLAB original's 1-indexed convention.
104
+ - If you z-score your data before calling `tknndigraph` (as `tmapper_demo.m` does), note that MATLAB's `zscore` divides by the sample standard deviation (`N-1`), while `scipy.stats.zscore` defaults to the population standard deviation (`N`). Pass `ddof=1` to `scipy.stats.zscore` to match MATLAB's convention -- otherwise results can subtly diverge near k-NN/threshold boundaries. (Verified: with `ddof=1`, the Python and MATLAB pipelines produce identical output down to floating-point precision on the sample dataset.)
105
+
106
+ ## License
107
+
108
+ BSD 3-Clause License -- see [LICENSE](LICENSE).
@@ -0,0 +1,31 @@
1
+ tmapper/__init__.py,sha256=-2Jqu7rvO_C-WX4zo0wfmEXZSOx2RSUpDnYy-dtJFa8,1676
2
+ tmapper/_shortest_path.py,sha256=TRyUOHOuizrMYWnsoy_SVCU2aL4Q1hcp41EO1KiuhcU,1036
3
+ tmapper/cknngraph.py,sha256=Wamujr4i6h7fiCvm23yPL9whXXs5FbFEMghXmdjjxp8,2412
4
+ tmapper/cycle_cluster.py,sha256=5EbTgUKEXnVexRf5g9LK3uSSkJXxmRbN2MdfMPk8TrA,4089
5
+ tmapper/cycle_cluster_conn.py,sha256=1yvj7ksr2OODQZaVTyZI5DO11mQ-1ahvPIcOtPCmw3I,4263
6
+ tmapper/cycle_count.py,sha256=ZMs4pT7xZnmKmQpE-to9UL0NIiwam53j1I27935TlUI,5640
7
+ tmapper/cycle_count2p.py,sha256=TujH-xuqt4NHu-p36x1BQUOP7VkUmsWTKb-38fXfWww,2570
8
+ tmapper/cycle_cutter.py,sha256=AQtHRYTACyXK9muW_NINiH_CSIAia4MmvvSaB2KuVmg,1341
9
+ tmapper/cycle_overlap.py,sha256=dVOB952P2CX5wadm0rKqR-Nvc5kNhxcDxqVMfCa2BEo,2180
10
+ tmapper/cycle_path_decomp.py,sha256=QUYcOFZ3x-KOErxtn24dVhkR0NmkUteYvy6LGXZKU6A,4446
11
+ tmapper/cycles_to_paths.py,sha256=VNj1ipbd6OSRv2GK1Y4kBE4fyMj1RU_qu39pESaNF0I,1072
12
+ tmapper/filtergraph.py,sha256=L17Bp6OU3MdcFm6ObUP9E726sT8qZZ-2i9LdlCyM79Y,4408
13
+ tmapper/graph_utils.py,sha256=KY5ULQdk_l1GHfU18ilwdhgDZZZN0Ce9qUpJ47qcGZ4,9294
14
+ tmapper/knngraph.py,sha256=HxEyl2xU8LVg25i1jE0A3LFTSSUgF1OoyoTQRggqoo4,1941
15
+ tmapper/labeling.py,sha256=f1dZr-9yZcYtga0ZGC0NypQ1KiSgJm6EXxEZ4oCsLAI,1696
16
+ tmapper/modularity.py,sha256=HMlALb9QNXwpgPu4UYvF8KAdIvfnzIYj9Uscd4wWuFA,2029
17
+ tmapper/path_traffic.py,sha256=KnVKwirP3hCZKxCKqnNeqN2w5L7qnU80ZeiBhRg_QUU,1262
18
+ tmapper/plotting.py,sha256=e_wZ3xND_KLEMV9hP4uWHb_hJaGe6gHtGZyHrAu_uDU,17669
19
+ tmapper/sample_data.py,sha256=ODY_TKb95N6Bagaw75YMPIwbFjD9GZA5-MXdkFN1WmQ,715
20
+ tmapper/tcm_distance.py,sha256=yvp99IRaBzXy1v_7PFd2ZsuWBQhkkpHZdsn3Nzkad1Q,2268
21
+ tmapper/tknndigraph.py,sha256=M3aASlUW3K3_rE5OsTJ2KfhQQ0vpGAvg0Xz8ph-D9vE,7020
22
+ tmapper/app/__init__.py,sha256=5APA90YXLvorLyJayo6wQ2XsroXfbuQX-Q6B5EsoEBw,305
23
+ tmapper/app/launcher.py,sha256=3JT3uw1AzL8Ob6tJ01ogDGUh5hUWUNWbCLko0mi2_zQ,1127
24
+ tmapper/app/streamlit_app.py,sha256=9dSPvVjSi6xdOYXr1pOlGWJmUAOQWvYHM8nfldFYl5M,55962
25
+ tmapper/sampledata/EL_temp.csv,sha256=x8d1psZtkBhCagScJ6tdXZn_RxNyku5For6vwo6j2mA,1605205
26
+ tmapper_py-0.1.0.dist-info/licenses/LICENSE,sha256=PrN1LOSoJpxEvEWu6ZqsD_haOJDbD7oY-U23XdiAVw4,1517
27
+ tmapper_py-0.1.0.dist-info/METADATA,sha256=pR7DSIcXc66jZj_mDdNXhdpxMxc10RpKUdD6FRZbrVI,5769
28
+ tmapper_py-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
29
+ tmapper_py-0.1.0.dist-info/entry_points.txt,sha256=37FDwhSH65AcTarmiBtrHV1ftPXMLnKzE4V7HSamDmA,58
30
+ tmapper_py-0.1.0.dist-info/top_level.txt,sha256=rCHRsCknm1Ae25rp11L0fu_mQP7qcF7hUhgOZtNW5PM,8
31
+ tmapper_py-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tmapper-app = tmapper.app.launcher:main
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Multiscale Complex Systems Lab
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ tmapper