binsparse 0.1.3__tar.gz → 0.1.4__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.
- {binsparse-0.1.3 → binsparse-0.1.4}/PKG-INFO +7 -1
- {binsparse-0.1.3 → binsparse-0.1.4}/README.md +6 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/pyproject.toml +1 -1
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/conversions/scipy.py +4 -1
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/conversions/sparse.py +17 -1
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/conversions/torch.py +15 -7
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_conversion_scipy.py +21 -4
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_conversion_sparse.py +33 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_conversion_torch.py +52 -11
- {binsparse-0.1.3 → binsparse-0.1.4}/.github/workflows/ci.yml +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/.github/workflows/publish.yml +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/.gitignore +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/bin/binsparse_to_binsparse +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/bin/binsparse_to_npy +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/bin/npy_to_binsparse +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/compliance-skips/numpy.txt +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/compliance-skips/reference.txt +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/compliance-skips/scipy.txt +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/compliance-skips/sparse.txt +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/pixi.lock +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/__init__.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/container.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/conversions/__init__.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/conversions/numpy.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/errors.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/io.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/tensor.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/binsparse/version.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/reference_cli/__init__.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/reference_cli/converters.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/reference_cli/framework.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/reference_cli/reformat.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_conversion_numpy.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_conversions.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_executables.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_io.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_reformat.py +0 -0
- {binsparse-0.1.3 → binsparse-0.1.4}/src/tests/test_tensor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: binsparse
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Python reference implementation of the Binsparse binary sparse format
|
|
5
5
|
Author-email: Willow Ahrens <ahrens@gatech.edu>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -75,6 +75,12 @@ require a zero-copy operation when the source and destination support it.
|
|
|
75
75
|
NumPy support is included by default; the other adapters require their
|
|
76
76
|
corresponding optional dependency.
|
|
77
77
|
|
|
78
|
+
SciPy, PyData/Sparse, and PyTorch COO imports sort indices and sum duplicate
|
|
79
|
+
entries before constructing a Binsparse tensor, without modifying the source.
|
|
80
|
+
`copy=False` requires canonical (sorted, duplicate-free) input; PyTorch COO
|
|
81
|
+
tensors must be coalesced first. PyTorch CSR/CSC imports rely on PyTorch's
|
|
82
|
+
sorted, unique index invariants.
|
|
83
|
+
|
|
78
84
|
## Source
|
|
79
85
|
The source code for `binsparse` is available on GitHub at [https://github.com/Binsparse/binsparse-reference-python](https://github.com/Binsparse/binsparse-reference-python)
|
|
80
86
|
|
|
@@ -54,6 +54,12 @@ require a zero-copy operation when the source and destination support it.
|
|
|
54
54
|
NumPy support is included by default; the other adapters require their
|
|
55
55
|
corresponding optional dependency.
|
|
56
56
|
|
|
57
|
+
SciPy, PyData/Sparse, and PyTorch COO imports sort indices and sum duplicate
|
|
58
|
+
entries before constructing a Binsparse tensor, without modifying the source.
|
|
59
|
+
`copy=False` requires canonical (sorted, duplicate-free) input; PyTorch COO
|
|
60
|
+
tensors must be coalesced first. PyTorch CSR/CSC imports rely on PyTorch's
|
|
61
|
+
sorted, unique index invariants.
|
|
62
|
+
|
|
57
63
|
## Source
|
|
58
64
|
The source code for `binsparse` is available on GitHub at [https://github.com/Binsparse/binsparse-reference-python](https://github.com/Binsparse/binsparse-reference-python)
|
|
59
65
|
|
|
@@ -26,7 +26,10 @@ def _prepare(value: Any, copy: bool | None) -> Any:
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def from_scipy(value: Any, *, copy: bool | None = None) -> BinsparseTensor:
|
|
29
|
-
"""Convert
|
|
29
|
+
"""Convert SciPy CSR, CSC, or COO to Binsparse, sorting and summing duplicates.
|
|
30
|
+
|
|
31
|
+
The input is not modified. ``copy=False`` requires canonical input.
|
|
32
|
+
"""
|
|
30
33
|
scipy_sparse = _scipy_sparse()
|
|
31
34
|
if not scipy_sparse.issparse(value) or value.ndim != 2:
|
|
32
35
|
raise TypeError("expected a two-dimensional SciPy sparse array or matrix")
|
|
@@ -25,12 +25,28 @@ def _sparse() -> Any:
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def from_sparse(value: Any, *, copy: bool | None = None) -> BinsparseTensor:
|
|
28
|
-
"""Convert
|
|
28
|
+
"""Convert PyData/Sparse COO to Binsparse, sorting and summing duplicates.
|
|
29
|
+
|
|
30
|
+
The input is not modified. ``copy=False`` requires canonical input.
|
|
31
|
+
"""
|
|
29
32
|
sparse = _sparse()
|
|
30
33
|
if not isinstance(value, sparse.COO) or value.ndim < 1:
|
|
31
34
|
raise TypeError("expected a non-scalar PyData/Sparse COO array")
|
|
32
35
|
if copy is True:
|
|
33
36
|
value = value.copy(deep=True)
|
|
37
|
+
if copy is False:
|
|
38
|
+
linear = value.linear_loc()
|
|
39
|
+
if not np.all(linear[1:] > linear[:-1]):
|
|
40
|
+
raise ValueError("copy=False cannot canonicalize a PyData/Sparse array")
|
|
41
|
+
else:
|
|
42
|
+
value = sparse.COO(
|
|
43
|
+
value.coords,
|
|
44
|
+
value.data,
|
|
45
|
+
shape=value.shape,
|
|
46
|
+
fill_value=value.fill_value,
|
|
47
|
+
sorted=False,
|
|
48
|
+
has_duplicates=True,
|
|
49
|
+
)
|
|
34
50
|
fill_value = np.asarray(value.fill_value).item()
|
|
35
51
|
shape = tuple(value.shape)
|
|
36
52
|
count = int(value.data.size)
|
|
@@ -51,7 +51,12 @@ def _numpy(value: Any, copy: bool | None) -> np.ndarray:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def from_torch(value: Any, *, copy: bool | None = None) -> BinsparseTensor:
|
|
54
|
-
"""Convert
|
|
54
|
+
"""Convert PyTorch dense, COO, CSR, or CSC to Binsparse.
|
|
55
|
+
|
|
56
|
+
COO entries are sorted and duplicates summed without modifying the input.
|
|
57
|
+
``copy=False`` requires coalesced COO input. CSR/CSC inputs are assumed to
|
|
58
|
+
satisfy PyTorch's sorted, unique index invariants.
|
|
59
|
+
"""
|
|
55
60
|
torch = _torch()
|
|
56
61
|
if not isinstance(value, torch.Tensor):
|
|
57
62
|
raise TypeError("expected a PyTorch tensor")
|
|
@@ -61,10 +66,16 @@ def from_torch(value: Any, *, copy: bool | None = None) -> BinsparseTensor:
|
|
|
61
66
|
return from_numpy(array, copy=False if copy is True else copy)
|
|
62
67
|
|
|
63
68
|
if value.layout == torch.sparse_coo:
|
|
64
|
-
indices = _numpy(value._indices(), copy)
|
|
65
|
-
values = _numpy(value._values(), copy)
|
|
66
69
|
if value.dense_dim() != 0:
|
|
67
70
|
raise TypeError("hybrid sparse COO tensors are not supported")
|
|
71
|
+
if not value.is_coalesced():
|
|
72
|
+
if copy is False:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"copy=False cannot canonicalize a PyTorch sparse tensor"
|
|
75
|
+
)
|
|
76
|
+
value = value.coalesce()
|
|
77
|
+
indices = _numpy(value._indices(), copy)
|
|
78
|
+
values = _numpy(value._values(), copy)
|
|
68
79
|
if value.sparse_dim() == 2:
|
|
69
80
|
return COORMatrix(
|
|
70
81
|
tuple(value.shape),
|
|
@@ -79,10 +90,7 @@ def from_torch(value: Any, *, copy: bool | None = None) -> BinsparseTensor:
|
|
|
79
90
|
level=SparseLevel(
|
|
80
91
|
value.sparse_dim(),
|
|
81
92
|
ElementLevel(values),
|
|
82
|
-
tuple(
|
|
83
|
-
indices[dimension, :]
|
|
84
|
-
for dimension in range(value.sparse_dim())
|
|
85
|
-
),
|
|
93
|
+
tuple(indices[dimension, :] for dimension in range(value.sparse_dim())),
|
|
86
94
|
),
|
|
87
95
|
)
|
|
88
96
|
|
|
@@ -28,22 +28,36 @@ def test_scipy_round_trip(constructor, tensor_type) -> None:
|
|
|
28
28
|
assert result.dtype == source.dtype
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
@pytest.mark.parametrize(
|
|
32
|
+
"constructor", [scipy_sparse.csr_array, scipy_sparse.csc_array]
|
|
33
|
+
)
|
|
34
|
+
def test_scipy_compressed_is_sorted_and_unique(constructor) -> None:
|
|
35
|
+
source = constructor(
|
|
33
36
|
(
|
|
34
37
|
np.array([1, 2, 3, 4, 5, 6]),
|
|
35
38
|
np.array([2, 1, 2, 1, 0, 1]),
|
|
36
39
|
np.array([0, 3, 6]),
|
|
37
40
|
),
|
|
38
|
-
shape=(2, 3),
|
|
41
|
+
shape=(2, 3) if constructor is scipy_sparse.csr_array else (3, 2),
|
|
39
42
|
)
|
|
43
|
+
original = (source.indptr.copy(), source.indices.copy(), source.data.copy())
|
|
40
44
|
tensor = from_scipy(source)
|
|
41
45
|
result = to_scipy(tensor)
|
|
42
46
|
|
|
47
|
+
assert isinstance(tensor, (CSRMatrix, CSCMatrix))
|
|
43
48
|
assert tensor.number_of_stored_values == 4
|
|
49
|
+
np.testing.assert_array_equal(tensor.pointers_to_1, [0, 2, 4])
|
|
50
|
+
np.testing.assert_array_equal(tensor.indices_1, [1, 2, 0, 1])
|
|
51
|
+
np.testing.assert_array_equal(tensor.values, [2, 4, 5, 10])
|
|
44
52
|
assert result.has_canonical_format
|
|
45
53
|
np.testing.assert_array_equal(result.toarray(), source.toarray())
|
|
46
54
|
assert not source.has_canonical_format
|
|
55
|
+
for actual, expected in zip(
|
|
56
|
+
(source.indptr, source.indices, source.data), original, strict=True
|
|
57
|
+
):
|
|
58
|
+
np.testing.assert_array_equal(actual, expected)
|
|
59
|
+
with pytest.raises(ValueError, match="canonicalize"):
|
|
60
|
+
from_scipy(source, copy=False)
|
|
47
61
|
|
|
48
62
|
|
|
49
63
|
def test_scipy_coo_is_sorted_and_unique() -> None:
|
|
@@ -57,7 +71,11 @@ def test_scipy_coo_is_sorted_and_unique() -> None:
|
|
|
57
71
|
tensor = from_scipy(source)
|
|
58
72
|
result = to_scipy(tensor)
|
|
59
73
|
|
|
74
|
+
assert isinstance(tensor, COORMatrix)
|
|
60
75
|
assert tensor.number_of_stored_values == 2
|
|
76
|
+
np.testing.assert_array_equal(tensor.indices_0, [0, 1])
|
|
77
|
+
np.testing.assert_array_equal(tensor.indices_1, [2, 0])
|
|
78
|
+
np.testing.assert_array_equal(tensor.values, [6, 4])
|
|
61
79
|
assert result.has_canonical_format
|
|
62
80
|
np.testing.assert_array_equal(result.toarray(), source.toarray())
|
|
63
81
|
assert not source.has_canonical_format
|
|
@@ -88,7 +106,6 @@ def test_scipy_copy_false_rejects_required_input_canonicalization() -> None:
|
|
|
88
106
|
from_scipy(source, copy=False)
|
|
89
107
|
|
|
90
108
|
|
|
91
|
-
|
|
92
109
|
def test_scipy_canonical_coo_copy_false_shares_buffers() -> None:
|
|
93
110
|
source = scipy_sparse.coo_array(
|
|
94
111
|
(np.array([3]), (np.array([0]), np.array([1]))),
|
|
@@ -9,6 +9,39 @@ from binsparse.tensor import COORMatrix, CustomTensor, ElementLevel, SparseLevel
|
|
|
9
9
|
sparse = pytest.importorskip("sparse")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
@pytest.mark.parametrize("ndim", [1, 2, 3])
|
|
13
|
+
@pytest.mark.parametrize("copy", [None, True, False])
|
|
14
|
+
@pytest.mark.parametrize("duplicates", [False, True])
|
|
15
|
+
def test_pydata_sparse_is_sorted_and_unique(ndim, copy, duplicates) -> None:
|
|
16
|
+
coordinates = [2, 0, 2, 1] if duplicates else [2, 0, 1]
|
|
17
|
+
coords = np.array([coordinates] * ndim)
|
|
18
|
+
data = np.arange(1, len(coordinates) + 1, dtype=np.float32)
|
|
19
|
+
# Bypass the constructor's normalization to exercise noncanonical buffers.
|
|
20
|
+
source = sparse.COO(
|
|
21
|
+
coords.copy(),
|
|
22
|
+
data.copy(),
|
|
23
|
+
shape=(3,) * ndim,
|
|
24
|
+
sorted=True,
|
|
25
|
+
has_duplicates=False,
|
|
26
|
+
fill_value=7,
|
|
27
|
+
)
|
|
28
|
+
if copy is False:
|
|
29
|
+
with pytest.raises(ValueError, match="canonicalize"):
|
|
30
|
+
from_sparse(source, copy=copy)
|
|
31
|
+
else:
|
|
32
|
+
tensor = from_sparse(source, copy=copy)
|
|
33
|
+
result = to_sparse(tensor)
|
|
34
|
+
assert tensor.number_of_stored_values == 3
|
|
35
|
+
assert tensor.fill_value == 7
|
|
36
|
+
assert result.dtype == data.dtype
|
|
37
|
+
np.testing.assert_array_equal(result.coords, [[0, 1, 2]] * ndim)
|
|
38
|
+
np.testing.assert_array_equal(
|
|
39
|
+
result.data, [2, 4, 4] if duplicates else [2, 3, 1]
|
|
40
|
+
)
|
|
41
|
+
np.testing.assert_array_equal(source.coords, coords)
|
|
42
|
+
np.testing.assert_array_equal(source.data, data)
|
|
43
|
+
|
|
44
|
+
|
|
12
45
|
def test_pydata_sparse_round_trip() -> None:
|
|
13
46
|
source = sparse.COO.from_numpy(np.array([[0, 2, 0], [3, 0, 4]], dtype=np.float32))
|
|
14
47
|
tensor = from_sparse(source)
|
|
@@ -6,6 +6,7 @@ import pytest
|
|
|
6
6
|
from binsparse.conversions import from_torch, to_torch
|
|
7
7
|
from binsparse.tensor import (
|
|
8
8
|
COORMatrix,
|
|
9
|
+
CSCMatrix,
|
|
9
10
|
CSRMatrix,
|
|
10
11
|
CustomTensor,
|
|
11
12
|
DenseLevel,
|
|
@@ -48,22 +49,63 @@ def test_torch_csr_round_trip() -> None:
|
|
|
48
49
|
assert torch.equal(result.to_dense(), source.to_dense())
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
@pytest.mark.parametrize("ndim", [1, 2, 3])
|
|
53
|
+
@pytest.mark.parametrize("copy", [None, True, False])
|
|
54
|
+
def test_torch_uncoalesced_coo_is_sorted_and_unique(ndim, copy) -> None:
|
|
55
|
+
coords = torch.tensor([[2, 0, 2, 1]] * ndim)
|
|
56
|
+
values = torch.tensor([1.0, 2.0, -1.0, 4.0])
|
|
52
57
|
source = torch.sparse_coo_tensor(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
(
|
|
58
|
+
coords.clone(),
|
|
59
|
+
values.clone(),
|
|
60
|
+
(3,) * ndim,
|
|
56
61
|
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
if copy is False:
|
|
63
|
+
with pytest.raises(ValueError, match="canonicalize"):
|
|
64
|
+
from_torch(source, copy=copy)
|
|
65
|
+
else:
|
|
66
|
+
tensor = from_torch(source, copy=copy)
|
|
67
|
+
result = to_torch(tensor)
|
|
68
|
+
assert tensor.number_of_stored_values == 3
|
|
69
|
+
assert result.dtype == source.dtype
|
|
70
|
+
assert torch.equal(result._indices(), torch.tensor([[0, 1, 2]] * ndim))
|
|
71
|
+
assert torch.equal(result._values(), torch.tensor([2.0, 4.0, 0.0]))
|
|
72
|
+
assert torch.equal(result.to_dense(), source.to_dense())
|
|
73
|
+
assert not source.is_coalesced()
|
|
74
|
+
assert torch.equal(source._indices(), coords)
|
|
75
|
+
assert torch.equal(source._values(), values)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize("layout", ["coo", "csr", "csc"])
|
|
79
|
+
@pytest.mark.parametrize("empty", [False, True])
|
|
80
|
+
def test_torch_canonical_sparse_copy_policy(layout, empty) -> None:
|
|
81
|
+
dense = torch.zeros((3, 3)) if empty else torch.eye(3)
|
|
82
|
+
source = dense.to_sparse(layout=getattr(torch, f"sparse_{layout}"))
|
|
83
|
+
shared = from_torch(source, copy=False)
|
|
84
|
+
copied = from_torch(source, copy=True)
|
|
85
|
+
assert isinstance(shared, (COORMatrix, CSRMatrix, CSCMatrix))
|
|
86
|
+
assert isinstance(copied, (COORMatrix, CSRMatrix, CSCMatrix))
|
|
87
|
+
original_values = source.values().numpy()
|
|
88
|
+
np.testing.assert_array_equal(copied.values, original_values)
|
|
89
|
+
if not empty:
|
|
90
|
+
assert np.shares_memory(shared.values, original_values)
|
|
91
|
+
assert not np.shares_memory(copied.values, original_values)
|
|
92
|
+
if layout == "coo":
|
|
93
|
+
assert isinstance(shared, COORMatrix)
|
|
94
|
+
assert isinstance(copied, COORMatrix)
|
|
95
|
+
assert np.shares_memory(shared.indices_0, source.indices().numpy())
|
|
96
|
+
assert not np.shares_memory(copied.indices_0, source.indices().numpy())
|
|
97
|
+
else:
|
|
98
|
+
indices = source.col_indices() if layout == "csr" else source.row_indices()
|
|
99
|
+
assert np.shares_memory(shared.indices_1, indices.numpy())
|
|
100
|
+
assert not np.shares_memory(copied.indices_1, indices.numpy())
|
|
61
101
|
|
|
62
102
|
|
|
63
103
|
@pytest.mark.parametrize("shape", [(5,), (2, 3, 4)])
|
|
64
104
|
def test_torch_nd_coo_round_trip(shape) -> None:
|
|
65
105
|
coordinates = torch.tensor([[0, size - 1] for size in shape])
|
|
66
|
-
source = torch.sparse_coo_tensor(
|
|
106
|
+
source = torch.sparse_coo_tensor(
|
|
107
|
+
coordinates, torch.tensor([2.0, 3.0]), shape
|
|
108
|
+
).coalesce()
|
|
67
109
|
tensor = from_torch(source, copy=False)
|
|
68
110
|
result = to_torch(tensor)
|
|
69
111
|
|
|
@@ -72,8 +114,7 @@ def test_torch_nd_coo_round_trip(shape) -> None:
|
|
|
72
114
|
assert tensor.level.rank == len(shape)
|
|
73
115
|
assert isinstance(tensor.level.level, ElementLevel)
|
|
74
116
|
assert all(
|
|
75
|
-
index.__array_interface__["data"][0]
|
|
76
|
-
== source._indices()[dimension].data_ptr()
|
|
117
|
+
index.__array_interface__["data"][0] == source._indices()[dimension].data_ptr()
|
|
77
118
|
for dimension, index in enumerate(tensor.level.indices)
|
|
78
119
|
)
|
|
79
120
|
assert torch.equal(result.to_dense(), source.to_dense())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|