ts2net 0.7.1__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.
- ts2net-0.7.1/LICENSE +21 -0
- ts2net-0.7.1/PKG-INFO +403 -0
- ts2net-0.7.1/README.md +342 -0
- ts2net-0.7.1/pyproject.toml +82 -0
- ts2net-0.7.1/ts2net_rs/Cargo.lock +1013 -0
- ts2net-0.7.1/ts2net_rs/Cargo.toml +38 -0
- ts2net-0.7.1/ts2net_rs/src/distance/mod.rs +3 -0
- ts2net-0.7.1/ts2net_rs/src/embedding/mod.rs +3 -0
- ts2net-0.7.1/ts2net_rs/src/graphs/mod.rs +3 -0
- ts2net-0.7.1/ts2net_rs/src/graphs/visibility.rs +87 -0
- ts2net-0.7.1/ts2net_rs/src/lib.rs +931 -0
- ts2net-0.7.1/ts2net_rs/src/python/mod.rs +34 -0
- ts2net-0.7.1/ts2net_rs/src/utils/mod.rs +3 -0
ts2net-0.7.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kyle T. Jones
|
|
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.
|
ts2net-0.7.1/PKG-INFO
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ts2net
|
|
3
|
+
Version: 0.7.1
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Requires-Dist: numpy>=1.23
|
|
13
|
+
Requires-Dist: networkx>=3.0
|
|
14
|
+
Requires-Dist: pandas>=1.5
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: matplotlib>=3.8
|
|
17
|
+
Requires-Dist: scipy>=1.9
|
|
18
|
+
Requires-Dist: joblib>=1.0
|
|
19
|
+
Requires-Dist: click>=8.0
|
|
20
|
+
Requires-Dist: scikit-learn>=1.0
|
|
21
|
+
Requires-Dist: statsmodels>=0.14
|
|
22
|
+
Requires-Dist: pyarrow>=12.0
|
|
23
|
+
Requires-Dist: fastparquet>=2023.0.0
|
|
24
|
+
Requires-Dist: polars>=0.20
|
|
25
|
+
Requires-Dist: pynndescent ; extra == 'approx'
|
|
26
|
+
Requires-Dist: torch>=2.0 ; extra == 'cnn'
|
|
27
|
+
Requires-Dist: pytest>=7.0 ; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
|
|
29
|
+
Requires-Dist: black>=23.0 ; extra == 'dev'
|
|
30
|
+
Requires-Dist: flake8>=6.0 ; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.0 ; extra == 'dev'
|
|
32
|
+
Requires-Dist: isort>=5.12 ; extra == 'dev'
|
|
33
|
+
Requires-Dist: pyyaml>=6.0 ; extra == 'dev'
|
|
34
|
+
Requires-Dist: torch>=2.0 ; extra == 'dev'
|
|
35
|
+
Requires-Dist: tslearn ; extra == 'dtw'
|
|
36
|
+
Requires-Dist: pandas-datareader ; extra == 'examples'
|
|
37
|
+
Requires-Dist: signalplot ; extra == 'examples'
|
|
38
|
+
Requires-Dist: cudtw ; extra == 'gpu'
|
|
39
|
+
Requires-Dist: minepy ; extra == 'mic'
|
|
40
|
+
Requires-Dist: pyreadr ; extra == 'rdata'
|
|
41
|
+
Requires-Dist: numba ; extra == 'speed'
|
|
42
|
+
Provides-Extra: approx
|
|
43
|
+
Provides-Extra: cnn
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Provides-Extra: dtw
|
|
46
|
+
Provides-Extra: examples
|
|
47
|
+
Provides-Extra: gpu
|
|
48
|
+
Provides-Extra: mic
|
|
49
|
+
Provides-Extra: rdata
|
|
50
|
+
Provides-Extra: speed
|
|
51
|
+
License-File: LICENSE
|
|
52
|
+
Summary: Time series to network conversion and analysis
|
|
53
|
+
Author-email: "Kyle T. Jones" <kyle@kyletjones.com>
|
|
54
|
+
Requires-Python: >=3.12
|
|
55
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
56
|
+
Project-URL: Documentation, https://ts2net.readthedocs.io
|
|
57
|
+
Project-URL: Homepage, https://github.com/kylejones200/ts2net
|
|
58
|
+
Project-URL: Issues, https://github.com/kylejones200/ts2net/issues
|
|
59
|
+
Project-URL: Repository, https://github.com/kylejones200/ts2net
|
|
60
|
+
|
|
61
|
+
# ts2net
|
|
62
|
+
|
|
63
|
+
[](https://badge.fury.io/py/ts2net)
|
|
64
|
+
[](https://ts2net.readthedocs.io/en/latest/?badge=latest)
|
|
65
|
+
[](https://github.com/kylejones200/ts2net/actions)
|
|
66
|
+
[](https://opensource.org/licenses/MIT)
|
|
67
|
+
|
|
68
|
+
Time series to networks. Clean API for visibility graphs, recurrence networks, and transition networks.
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
### Basic Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install ts2net
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Optional Dependencies
|
|
79
|
+
|
|
80
|
+
Install with optional features:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Performance acceleration (Numba)
|
|
84
|
+
pip install ts2net[speed]
|
|
85
|
+
|
|
86
|
+
# BSTS decomposition (statsmodels)
|
|
87
|
+
pip install ts2net[bsts]
|
|
88
|
+
|
|
89
|
+
# Temporal CNN embeddings (PyTorch)
|
|
90
|
+
pip install ts2net[cnn]
|
|
91
|
+
|
|
92
|
+
# All optional features
|
|
93
|
+
pip install ts2net[all]
|
|
94
|
+
|
|
95
|
+
# Development dependencies
|
|
96
|
+
pip install ts2net[dev]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Verify Installation
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import ts2net
|
|
103
|
+
print(ts2net.__version__)
|
|
104
|
+
|
|
105
|
+
from ts2net import HVG
|
|
106
|
+
import numpy as np
|
|
107
|
+
x = np.random.randn(100)
|
|
108
|
+
hvg = HVG()
|
|
109
|
+
hvg.build(x)
|
|
110
|
+
print(f"✓ Installation successful: {hvg.n_nodes} nodes, {hvg.n_edges} edges")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Quick Start
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import numpy as np
|
|
117
|
+
from ts2net import HVG
|
|
118
|
+
|
|
119
|
+
x = np.random.randn(1000)
|
|
120
|
+
|
|
121
|
+
hvg = HVG()
|
|
122
|
+
hvg.build(x)
|
|
123
|
+
|
|
124
|
+
print(hvg.n_nodes, hvg.n_edges)
|
|
125
|
+
print(hvg.degree_sequence())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Adjacency Matrix
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
A = hvg.adjacency_matrix()
|
|
132
|
+
print(A.shape) # (1000, 1000)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## NetworkX (Optional)
|
|
136
|
+
|
|
137
|
+
NetworkX is optional. Convert only if needed:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
G = hvg.as_networkx()
|
|
141
|
+
import networkx as nx
|
|
142
|
+
print(nx.average_clustering(G))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Structural Decomposition and Residual Topology
|
|
146
|
+
|
|
147
|
+
For time series with predictable structure (seasonality, trends), decompose first, then analyze the residual:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from ts2net.bsts import features, BSTSSpec
|
|
151
|
+
|
|
152
|
+
# Decompose and analyze residual in one pass
|
|
153
|
+
spec = BSTSSpec(
|
|
154
|
+
level=True,
|
|
155
|
+
trend=False,
|
|
156
|
+
seasonal_periods=[24, 168] # Daily and weekly for hourly data
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
result = features(x, methods=['hvg', 'transition'], bsts=spec)
|
|
160
|
+
|
|
161
|
+
# Access three feature blocks
|
|
162
|
+
raw_stats = result.raw_stats # Basic series statistics
|
|
163
|
+
structural_stats = result.structural_stats # Component variances, seasonal strength
|
|
164
|
+
residual_network_stats = result.residual_network_stats # Network features from residual
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Use cases:**
|
|
168
|
+
- Compare meters/wells without seasonal confounds
|
|
169
|
+
- Flag series where structural model fails (high residual complexity)
|
|
170
|
+
- Separate predictable structure from irregular dynamics
|
|
171
|
+
|
|
172
|
+
**Installation:**
|
|
173
|
+
```bash
|
|
174
|
+
pip install ts2net[bsts] # Installs statsmodels
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
See `examples/bsts_features.py` for complete examples.
|
|
178
|
+
|
|
179
|
+
## Large Series
|
|
180
|
+
|
|
181
|
+
For large series, use output modes to control memory usage:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# Degrees only (most memory efficient)
|
|
185
|
+
hvg = HVG(output="degrees")
|
|
186
|
+
hvg.build(x)
|
|
187
|
+
degrees = hvg.degree_sequence() # Fast, no edge storage
|
|
188
|
+
|
|
189
|
+
# Stats only (summary statistics without edges)
|
|
190
|
+
hvg = HVG(output="stats")
|
|
191
|
+
hvg.build(x)
|
|
192
|
+
stats = hvg.stats() # n_nodes, n_edges, avg_degree, etc.
|
|
193
|
+
|
|
194
|
+
# Full edges (default, use for small-medium series)
|
|
195
|
+
hvg = HVG(output="edges")
|
|
196
|
+
hvg.build(x)
|
|
197
|
+
edges = hvg.edges # Full edge list
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Scale Guidelines
|
|
201
|
+
|
|
202
|
+
| Series Length | Method | Recommended Settings | Memory Risk |
|
|
203
|
+
|--------------|--------|---------------------|-------------|
|
|
204
|
+
| n < 10k | All methods | `output="edges"` | Safe |
|
|
205
|
+
| 10k < n < 100k | HVG | `output="edges"` or `output="degrees"` | Safe with sparse |
|
|
206
|
+
| 10k < n < 100k | NVG | `limit=2000-5000`, `output="degrees"` | **Use horizon limit** |
|
|
207
|
+
| 10k < n < 100k | Recurrence | `rule='knn'`, `k=10-30` | **Avoid exact all-pairs** |
|
|
208
|
+
| n > 100k | HVG | `output="degrees"` or `output="stats"` | Safe |
|
|
209
|
+
| n > 100k | NVG | `limit=2000-5000`, `max_edges=1e6`, `output="degrees"` | **Required limits** |
|
|
210
|
+
| n > 100k | Recurrence | `rule='knn'`, `k=10-30`, `output="degrees"` | **kNN only** |
|
|
211
|
+
|
|
212
|
+
**Critical Warnings:**
|
|
213
|
+
- **Dense adjacency matrices are disabled by default** for n > 50k (prevents 63GB+ memory blowup)
|
|
214
|
+
- **NVG without `limit` can create millions of edges** for smooth series
|
|
215
|
+
- **Recurrence exact all-pairs is O(n²) memory** - use kNN for large n
|
|
216
|
+
- **NetworkX conversion refused** for n > 200k (use `force=True` to override)
|
|
217
|
+
|
|
218
|
+
**Memory Estimates:**
|
|
219
|
+
- Dense adjacency: ~8 * n² bytes (e.g., 90k nodes = 63 GB)
|
|
220
|
+
- Sparse adjacency: ~16 * m bytes where m = edges (e.g., 100k edges = 1.6 MB)
|
|
221
|
+
- Edge list: ~16 * m bytes (similar to sparse)
|
|
222
|
+
- Degrees only: ~8 * n bytes (e.g., 90k nodes = 720 KB)
|
|
223
|
+
|
|
224
|
+
## Methods
|
|
225
|
+
|
|
226
|
+
### Visibility Graphs
|
|
227
|
+
|
|
228
|
+
**HVG** - Horizontal Visibility Graph
|
|
229
|
+
```python
|
|
230
|
+
from ts2net import HVG
|
|
231
|
+
|
|
232
|
+
hvg = HVG(weighted=False, limit=None)
|
|
233
|
+
hvg.build(x)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**NVG** - Natural Visibility Graph
|
|
237
|
+
```python
|
|
238
|
+
from ts2net import NVG
|
|
239
|
+
|
|
240
|
+
# For large series, use horizon limit and bounded work
|
|
241
|
+
nvg = NVG(weighted=False, limit=5000, max_edges=1_000_000, output="degrees")
|
|
242
|
+
nvg.build(x)
|
|
243
|
+
|
|
244
|
+
# Or with memory limit
|
|
245
|
+
nvg = NVG(limit=2000, max_memory_mb=100) # Caps at ~100MB
|
|
246
|
+
nvg.build(x)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Recurrence Networks
|
|
250
|
+
|
|
251
|
+
Phase space recurrence:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from ts2net import RecurrenceNetwork
|
|
255
|
+
|
|
256
|
+
rn = RecurrenceNetwork(m=3, tau=1, rule='knn', k=5)
|
|
257
|
+
rn.build(x)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Parameters:
|
|
261
|
+
- `m`: embedding dimension (None = auto via FNN)
|
|
262
|
+
- `tau`: time delay
|
|
263
|
+
- `rule`: 'knn', 'epsilon', 'radius'
|
|
264
|
+
- `k`: neighbors for k-NN
|
|
265
|
+
- `epsilon`: threshold for epsilon-recurrence
|
|
266
|
+
|
|
267
|
+
### Transition Networks
|
|
268
|
+
|
|
269
|
+
Symbolic dynamics:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from ts2net import TransitionNetwork
|
|
273
|
+
|
|
274
|
+
tn = TransitionNetwork(symbolizer='ordinal', order=3)
|
|
275
|
+
tn.build(x)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Symbolizers:
|
|
279
|
+
- `'ordinal'`: ordinal patterns
|
|
280
|
+
- `'equal_width'`: equal-width bins
|
|
281
|
+
- `'equal_freq'`: equal-frequency bins (quantiles)
|
|
282
|
+
- `'kmeans'`: k-means clustering
|
|
283
|
+
|
|
284
|
+
## Compare Methods
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from ts2net import build_network
|
|
288
|
+
|
|
289
|
+
x = np.random.randn(1000)
|
|
290
|
+
|
|
291
|
+
for method in ['hvg', 'nvg', 'recurrence', 'transition']:
|
|
292
|
+
if method == 'recurrence':
|
|
293
|
+
g = build_network(x, method, m=3, rule='knn', k=5)
|
|
294
|
+
elif method == 'transition':
|
|
295
|
+
g = build_network(x, method, symbolizer='ordinal', order=3)
|
|
296
|
+
else:
|
|
297
|
+
g = build_network(x, method)
|
|
298
|
+
|
|
299
|
+
print(f"{method}: {g.n_edges} edges")
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Output:
|
|
303
|
+
```
|
|
304
|
+
hvg: 1979 edges
|
|
305
|
+
nvg: 2931 edges
|
|
306
|
+
recurrence: 3159 edges
|
|
307
|
+
transition: 18 edges
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Multivariate
|
|
311
|
+
|
|
312
|
+
Multiple time series → network where nodes = time series:
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from ts2net.multivariate import ts_dist, net_knn
|
|
316
|
+
|
|
317
|
+
X = np.random.randn(30, 1000) # 30 series, 1000 points each
|
|
318
|
+
|
|
319
|
+
D = ts_dist(X, method='dtw', n_jobs=-1)
|
|
320
|
+
G = net_knn(D, k=5)
|
|
321
|
+
|
|
322
|
+
print(G.n_nodes, G.n_edges)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Distance methods: `'correlation'`, `'dtw'`, `'nmi'`, `'voi'`, `'es'`, `'vr'`
|
|
326
|
+
|
|
327
|
+
Network builders: `net_knn`, `net_enn`, `net_weighted`
|
|
328
|
+
|
|
329
|
+
## Performance
|
|
330
|
+
|
|
331
|
+
With Numba (recommended):
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
pip install numba
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Speedups:
|
|
338
|
+
- HVG: 100x faster
|
|
339
|
+
- NVG: 180x faster
|
|
340
|
+
- Recurrence: 10x faster
|
|
341
|
+
|
|
342
|
+
## API
|
|
343
|
+
|
|
344
|
+
All methods follow the same pattern:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
builder = Method(**params)
|
|
348
|
+
builder.build(x)
|
|
349
|
+
|
|
350
|
+
# Access results
|
|
351
|
+
builder.n_nodes
|
|
352
|
+
builder.n_edges
|
|
353
|
+
builder.edges # list of tuples
|
|
354
|
+
builder.degree_sequence() # numpy array
|
|
355
|
+
builder.adjacency_matrix() # numpy array
|
|
356
|
+
builder.as_networkx() # optional conversion
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Troubleshooting
|
|
360
|
+
|
|
361
|
+
### Common Issues
|
|
362
|
+
|
|
363
|
+
**Memory errors with large time series:**
|
|
364
|
+
- Use `output="degrees"` or `output="stats"` instead of `output="edges"`
|
|
365
|
+
- For NVG, always set `limit` parameter (e.g., `limit=5000`)
|
|
366
|
+
- For recurrence networks, use `rule='knn'` with small `k` (10-30) instead of exact all-pairs
|
|
367
|
+
|
|
368
|
+
**Slow performance:**
|
|
369
|
+
- Install Numba: `pip install numba` (100-180x speedup for visibility graphs)
|
|
370
|
+
- Use `output="degrees"` if you don't need full edge lists
|
|
371
|
+
- For multivariate, use `n_jobs=-1` for parallel distance computation
|
|
372
|
+
|
|
373
|
+
**Import errors:**
|
|
374
|
+
- Ensure you're using Python 3.12+
|
|
375
|
+
- If Rust extension fails to build, install Rust: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
|
|
376
|
+
- For development, run `maturin develop --release` after installing Rust
|
|
377
|
+
|
|
378
|
+
**NetworkX conversion refused:**
|
|
379
|
+
- For very large graphs (n > 200k), NetworkX conversion is disabled by default
|
|
380
|
+
- Use `force=True` to override: `hvg.as_networkx(force=True)`
|
|
381
|
+
- Consider using `output="degrees"` or `output="stats"` instead
|
|
382
|
+
|
|
383
|
+
### Getting Help
|
|
384
|
+
|
|
385
|
+
- 📖 [Full Documentation](https://ts2net.readthedocs.io)
|
|
386
|
+
- 💬 [Open an Issue](https://github.com/kylejones200/ts2net/issues)
|
|
387
|
+
- 📝 [Examples](https://github.com/kylejones200/ts2net/tree/main/examples)
|
|
388
|
+
|
|
389
|
+
## Citation
|
|
390
|
+
|
|
391
|
+
Multivariate methods based on:
|
|
392
|
+
|
|
393
|
+
Ferreira, L.N. (2024). From time series to networks in R with the ts2net package. *Applied Network Science*, 9(1), 32. https://doi.org/10.1007/s41109-024-00642-2
|
|
394
|
+
|
|
395
|
+
## Contributing
|
|
396
|
+
|
|
397
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
398
|
+
|
|
399
|
+
## License
|
|
400
|
+
|
|
401
|
+
MIT
|
|
402
|
+
|
|
403
|
+
|