deep-atomic 0.2.1__tar.gz → 0.2.2__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.
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/PKG-INFO +18 -14
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/README.md +17 -13
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/pyproject.toml +1 -1
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/src/deep_atomic/__init__.py +1 -1
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/src/deep_atomic/nn.py +8 -8
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/src/deep_atomic/op.py +8 -8
- deep_atomic-0.2.2/tests/nn_test.py +583 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/tests/op_test.py +3 -5
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/uv.lock +1 -1
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/.github/workflows/publish-pypi.yml +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/.gitignore +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/.pre-commit-config.yaml +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/.vscode/settings.json +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/LICENSE +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/src/deep_atomic/graph.py +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/src/deep_atomic/optimizer.py +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/src/deep_atomic/tensor.py +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/src/deep_atomic/utils.py +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/tests/__init__.py +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/tests/conftest.py +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/tests/init_test.py +0 -0
- {deep_atomic-0.2.1 → deep_atomic-0.2.2}/tests/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deep_atomic
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A simple deep learning framework built upon numpy only
|
|
5
5
|
Author-email: Elliot Zhang <elliot_zh@proton.me>
|
|
6
6
|
License: MIT
|
|
@@ -21,7 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
# Deep Atomic
|
|
23
23
|
|
|
24
|
-
A simple deep learning framework built
|
|
24
|
+
A simple deep learning framework built on NumPy only. Mainly for practice and learning.
|
|
25
25
|
|
|
26
26
|
## Usage
|
|
27
27
|
|
|
@@ -53,7 +53,7 @@ a = da.Tensor(
|
|
|
53
53
|
|
|
54
54
|
### Supported Operations
|
|
55
55
|
|
|
56
|
-
Most essential deep
|
|
56
|
+
Most essential deep-learning operations are implemented. For those that also exist in NumPy, we follow NumPy's API conventions.
|
|
57
57
|
|
|
58
58
|
```python
|
|
59
59
|
a, b = da.Tensor(np.random.rand(3, 4)), da.Tensor(np.random.rand(3, 4))
|
|
@@ -70,24 +70,28 @@ c = da.log(a)
|
|
|
70
70
|
c = da.sin(a)
|
|
71
71
|
# the same for cos, tan, arcsin, arccos, arctan, sinh, cosh, tanh, arcsinh, arccosh, arctanh
|
|
72
72
|
|
|
73
|
-
d = a < b # element-wise comparison,
|
|
73
|
+
d = a < b # element-wise comparison, creates a boolean tensor
|
|
74
74
|
e = a <= b
|
|
75
75
|
c = a > b
|
|
76
76
|
c = a >= b
|
|
77
77
|
c = a == b
|
|
78
78
|
c = a != b
|
|
79
|
-
c = da.fmax(a, b) # IMPORTANT: here da.fmax is identical to da.maximum, for simplicity.
|
|
79
|
+
c = da.fmax(a, b) # IMPORTANT: here da.fmax is identical to da.maximum, for simplicity. Same for da.fmin / da.minimum
|
|
80
80
|
c = da.maximum(a, b)
|
|
81
81
|
c = da.fmin(a, b)
|
|
82
82
|
c = da.minimum(a, b)
|
|
83
83
|
|
|
84
84
|
c = da.logical_and(d, e) # element-wise and
|
|
85
|
+
c = d & e # equivalence
|
|
85
86
|
c = da.logical_or(d, e) # element-wise or
|
|
87
|
+
c = d | e
|
|
86
88
|
c = da.logical_xor(d, e) # element-wise xor
|
|
89
|
+
c = d ^ e
|
|
87
90
|
c = da.logical_not(d) # element-wise not
|
|
88
|
-
c = d
|
|
89
|
-
c = d.
|
|
90
|
-
c =
|
|
91
|
+
c = ~d
|
|
92
|
+
c = d.all(axis=-1, keepdims=False) # logical AND reduction. axis=None, keepdims=False by default
|
|
93
|
+
c = d.any(axis=-1, keepdims=False) # logical OR reduction. axis=None, keepdims=False by default
|
|
94
|
+
c = da.where(d, a, b) # returns elements chosen from a or b depending on condition
|
|
91
95
|
|
|
92
96
|
c = da.topk(a, 2, axis=-1, largest=True) # same as pytorch. axis=-1, largest=True by default
|
|
93
97
|
|
|
@@ -116,17 +120,17 @@ c = a.tile(2, 2) # method‑style alternative
|
|
|
116
120
|
|
|
117
121
|
### Autograd
|
|
118
122
|
|
|
119
|
-
Autograd is supported via computational graph.
|
|
120
|
-
_Currently only
|
|
123
|
+
Autograd is supported via a computational graph.
|
|
124
|
+
_Currently only supports scalar source points._
|
|
121
125
|
|
|
122
126
|
```python
|
|
123
127
|
x = Tensor(np.random.rand(3, 4)) # requires_grad == True by default
|
|
124
128
|
res = ... # some calculation related to x. res is a **scalar** result
|
|
125
129
|
res.backward()
|
|
126
|
-
print(res.grad) # gradient
|
|
130
|
+
print(res.grad) # gradient computed!
|
|
127
131
|
```
|
|
128
132
|
|
|
129
|
-
##
|
|
133
|
+
## To Do
|
|
130
134
|
|
|
131
135
|
- [ ] more basic operations and their autograd
|
|
132
136
|
- [x] topk
|
|
@@ -151,8 +155,8 @@ print(res.grad) # gradient get!
|
|
|
151
155
|
|
|
152
156
|
## Development
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
We use [pre-commit](https://github.com/pre-commit/pre-commit) to manage hooks
|
|
158
|
+
We recommend managing dependencies with [uv](https://github.com/astral-sh/uv).
|
|
159
|
+
We use [pre-commit](https://github.com/pre-commit/pre-commit) to manage hooks that help lint and format our code.
|
|
156
160
|
|
|
157
161
|
```bash
|
|
158
162
|
uv sync
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Deep Atomic
|
|
2
2
|
|
|
3
|
-
A simple deep learning framework built
|
|
3
|
+
A simple deep learning framework built on NumPy only. Mainly for practice and learning.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -32,7 +32,7 @@ a = da.Tensor(
|
|
|
32
32
|
|
|
33
33
|
### Supported Operations
|
|
34
34
|
|
|
35
|
-
Most essential deep
|
|
35
|
+
Most essential deep-learning operations are implemented. For those that also exist in NumPy, we follow NumPy's API conventions.
|
|
36
36
|
|
|
37
37
|
```python
|
|
38
38
|
a, b = da.Tensor(np.random.rand(3, 4)), da.Tensor(np.random.rand(3, 4))
|
|
@@ -49,24 +49,28 @@ c = da.log(a)
|
|
|
49
49
|
c = da.sin(a)
|
|
50
50
|
# the same for cos, tan, arcsin, arccos, arctan, sinh, cosh, tanh, arcsinh, arccosh, arctanh
|
|
51
51
|
|
|
52
|
-
d = a < b # element-wise comparison,
|
|
52
|
+
d = a < b # element-wise comparison, creates a boolean tensor
|
|
53
53
|
e = a <= b
|
|
54
54
|
c = a > b
|
|
55
55
|
c = a >= b
|
|
56
56
|
c = a == b
|
|
57
57
|
c = a != b
|
|
58
|
-
c = da.fmax(a, b) # IMPORTANT: here da.fmax is identical to da.maximum, for simplicity.
|
|
58
|
+
c = da.fmax(a, b) # IMPORTANT: here da.fmax is identical to da.maximum, for simplicity. Same for da.fmin / da.minimum
|
|
59
59
|
c = da.maximum(a, b)
|
|
60
60
|
c = da.fmin(a, b)
|
|
61
61
|
c = da.minimum(a, b)
|
|
62
62
|
|
|
63
63
|
c = da.logical_and(d, e) # element-wise and
|
|
64
|
+
c = d & e # equivalence
|
|
64
65
|
c = da.logical_or(d, e) # element-wise or
|
|
66
|
+
c = d | e
|
|
65
67
|
c = da.logical_xor(d, e) # element-wise xor
|
|
68
|
+
c = d ^ e
|
|
66
69
|
c = da.logical_not(d) # element-wise not
|
|
67
|
-
c = d
|
|
68
|
-
c = d.
|
|
69
|
-
c =
|
|
70
|
+
c = ~d
|
|
71
|
+
c = d.all(axis=-1, keepdims=False) # logical AND reduction. axis=None, keepdims=False by default
|
|
72
|
+
c = d.any(axis=-1, keepdims=False) # logical OR reduction. axis=None, keepdims=False by default
|
|
73
|
+
c = da.where(d, a, b) # returns elements chosen from a or b depending on condition
|
|
70
74
|
|
|
71
75
|
c = da.topk(a, 2, axis=-1, largest=True) # same as pytorch. axis=-1, largest=True by default
|
|
72
76
|
|
|
@@ -95,17 +99,17 @@ c = a.tile(2, 2) # method‑style alternative
|
|
|
95
99
|
|
|
96
100
|
### Autograd
|
|
97
101
|
|
|
98
|
-
Autograd is supported via computational graph.
|
|
99
|
-
_Currently only
|
|
102
|
+
Autograd is supported via a computational graph.
|
|
103
|
+
_Currently only supports scalar source points._
|
|
100
104
|
|
|
101
105
|
```python
|
|
102
106
|
x = Tensor(np.random.rand(3, 4)) # requires_grad == True by default
|
|
103
107
|
res = ... # some calculation related to x. res is a **scalar** result
|
|
104
108
|
res.backward()
|
|
105
|
-
print(res.grad) # gradient
|
|
109
|
+
print(res.grad) # gradient computed!
|
|
106
110
|
```
|
|
107
111
|
|
|
108
|
-
##
|
|
112
|
+
## To Do
|
|
109
113
|
|
|
110
114
|
- [ ] more basic operations and their autograd
|
|
111
115
|
- [x] topk
|
|
@@ -130,8 +134,8 @@ print(res.grad) # gradient get!
|
|
|
130
134
|
|
|
131
135
|
## Development
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
We use [pre-commit](https://github.com/pre-commit/pre-commit) to manage hooks
|
|
137
|
+
We recommend managing dependencies with [uv](https://github.com/astral-sh/uv).
|
|
138
|
+
We use [pre-commit](https://github.com/pre-commit/pre-commit) to manage hooks that help lint and format our code.
|
|
135
139
|
|
|
136
140
|
```bash
|
|
137
141
|
uv sync
|
|
@@ -46,7 +46,7 @@ class Module(ABC):
|
|
|
46
46
|
pass
|
|
47
47
|
|
|
48
48
|
def __call__(self, *args, **kwargs):
|
|
49
|
-
self.forward(*args, **kwargs)
|
|
49
|
+
return self.forward(*args, **kwargs)
|
|
50
50
|
|
|
51
51
|
def __init__(self):
|
|
52
52
|
# TODO: use OrderedDict?
|
|
@@ -72,19 +72,19 @@ class Module(ABC):
|
|
|
72
72
|
for name, buffer in self._buffers.items():
|
|
73
73
|
state_dict_[prefix + name] = buffer
|
|
74
74
|
for name, module in self._modules.items():
|
|
75
|
-
state_dict_ = {**state_dict_, **module.state_dict(prefix=name)}
|
|
75
|
+
state_dict_ = {**state_dict_, **module.state_dict(prefix=prefix + name)}
|
|
76
76
|
return state_dict_
|
|
77
77
|
|
|
78
78
|
def modules(self, recurse=True):
|
|
79
|
-
for _name, module in self.named_modules(
|
|
79
|
+
for _name, module in self.named_modules():
|
|
80
80
|
yield module
|
|
81
81
|
|
|
82
|
-
def
|
|
82
|
+
def named_modules(self, prefix="", remove_duplicate=True, memo=None):
|
|
83
83
|
if memo is None:
|
|
84
84
|
memo = set()
|
|
85
|
-
if self not in memo:
|
|
85
|
+
if id(self) not in memo:
|
|
86
86
|
if remove_duplicate:
|
|
87
|
-
memo.add(self)
|
|
87
|
+
memo.add(id(self))
|
|
88
88
|
yield prefix, self
|
|
89
89
|
if prefix:
|
|
90
90
|
prefix += "."
|
|
@@ -107,10 +107,10 @@ class Module(ABC):
|
|
|
107
107
|
if module_prefix:
|
|
108
108
|
module_prefix += "."
|
|
109
109
|
for k, v in members:
|
|
110
|
-
if v in memo:
|
|
110
|
+
if id(v) in memo:
|
|
111
111
|
continue
|
|
112
112
|
if remove_duplicate:
|
|
113
|
-
memo.add(v)
|
|
113
|
+
memo.add(id(v))
|
|
114
114
|
yield module_prefix + k, v
|
|
115
115
|
|
|
116
116
|
def parameters(self, recurse=True):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
|
|
3
|
-
from .
|
|
3
|
+
from . import graph as g
|
|
4
4
|
from .tensor import *
|
|
5
5
|
from .utils import *
|
|
6
6
|
|
|
@@ -89,10 +89,10 @@ def max(x: Tensor, axis=None, keepdims=False):
|
|
|
89
89
|
if x.requires_grad:
|
|
90
90
|
res.requires_grad = True
|
|
91
91
|
if axis == None:
|
|
92
|
-
res.dep = MinMax(x, None, keepdims, full_red_value=res.to_np())
|
|
92
|
+
res.dep = g.MinMax(x, None, keepdims, full_red_value=res.to_np())
|
|
93
93
|
else:
|
|
94
94
|
indices = np.argmax(x.to_np(), axis=axis, keepdims=True)
|
|
95
|
-
res.dep = MinMax(x, axis, keepdims, indices=indices)
|
|
95
|
+
res.dep = g.MinMax(x, axis, keepdims, indices=indices)
|
|
96
96
|
return res
|
|
97
97
|
|
|
98
98
|
|
|
@@ -101,10 +101,10 @@ def min(x: Tensor, axis=None, keepdims=False):
|
|
|
101
101
|
if x.requires_grad:
|
|
102
102
|
res.requires_grad = True
|
|
103
103
|
if axis == None:
|
|
104
|
-
res.dep = MinMax(x, None, keepdims, full_red_value=res.to_np())
|
|
104
|
+
res.dep = g.MinMax(x, None, keepdims, full_red_value=res.to_np())
|
|
105
105
|
else:
|
|
106
106
|
indices = np.argmin(x.to_np(), axis=axis, keepdims=True)
|
|
107
|
-
res.dep = MinMax(x, axis, keepdims, indices=indices)
|
|
107
|
+
res.dep = g.MinMax(x, axis, keepdims, indices=indices)
|
|
108
108
|
return res
|
|
109
109
|
|
|
110
110
|
|
|
@@ -155,7 +155,7 @@ def sigmoid(x: Tensor):
|
|
|
155
155
|
res[~pos] = exp(x_np[~pos]) / (1 + exp(x_np[~pos]))
|
|
156
156
|
res = Tensor(res, requires_grad=x.requires_grad)
|
|
157
157
|
if res.requires_grad:
|
|
158
|
-
res.dep = Sigmoid(x, res.to_np())
|
|
158
|
+
res.dep = g.Sigmoid(x, res.to_np())
|
|
159
159
|
return res
|
|
160
160
|
|
|
161
161
|
|
|
@@ -247,7 +247,7 @@ def where(condition, x1, x2):
|
|
|
247
247
|
requires_grad = True
|
|
248
248
|
res = Tensor(np.where(condition, x1, x2), requires_grad=requires_grad)
|
|
249
249
|
if res.requires_grad:
|
|
250
|
-
res.dep = Where(condition, x1, x2)
|
|
250
|
+
res.dep = g.Where(condition, x1, x2)
|
|
251
251
|
return res
|
|
252
252
|
|
|
253
253
|
|
|
@@ -257,7 +257,7 @@ def take_along_axis(x: Tensor, indices, axis=-1):
|
|
|
257
257
|
indices = indices.to_np() # cut off gradient
|
|
258
258
|
res = Tensor(np.take_along_axis(x, indices, axis), requires_grad=x.requires_grad)
|
|
259
259
|
if res.requires_grad:
|
|
260
|
-
res.dep = TakeAlongAxis(x, indices, axis)
|
|
260
|
+
res.dep = g.TakeAlongAxis(x, indices, axis)
|
|
261
261
|
return res
|
|
262
262
|
|
|
263
263
|
|
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
import deep_atomic as da
|
|
5
|
+
from deep_atomic.nn import (
|
|
6
|
+
Buffer,
|
|
7
|
+
BufferList,
|
|
8
|
+
Linear,
|
|
9
|
+
Module,
|
|
10
|
+
ModuleList,
|
|
11
|
+
Parameter,
|
|
12
|
+
ParameterList,
|
|
13
|
+
ReLU,
|
|
14
|
+
Sequential,
|
|
15
|
+
Softmax,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .utils import assert_close, numerical_grad
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _make_identity_module():
|
|
22
|
+
"""Factory for a minimal identity Module."""
|
|
23
|
+
|
|
24
|
+
class M(Module):
|
|
25
|
+
def forward(self, x):
|
|
26
|
+
return x
|
|
27
|
+
|
|
28
|
+
return M()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestBuffer:
|
|
32
|
+
def test_requires_grad_always_false(self):
|
|
33
|
+
"""Buffer.requires_grad is False even when passed requires_grad=True."""
|
|
34
|
+
buf = Buffer(np.array([1.0, 2.0, 3.0]), requires_grad=True)
|
|
35
|
+
assert buf.requires_grad is False
|
|
36
|
+
|
|
37
|
+
def test_is_buffer_and_tensor(self):
|
|
38
|
+
buf = Buffer(np.array([1, 2, 3]))
|
|
39
|
+
assert isinstance(buf, Buffer)
|
|
40
|
+
assert isinstance(buf, da.Tensor)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MEMBER_PARAMS = [
|
|
44
|
+
(Parameter, "named_parameters", "parameters"),
|
|
45
|
+
(Buffer, "named_buffers", "buffers"),
|
|
46
|
+
] # for TestModuleNamedMembers
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestModule:
|
|
50
|
+
class TestModuleSetAttr:
|
|
51
|
+
"""__setattr__ routes Parameter/Buffer/Module into their tracking dicts."""
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize(
|
|
54
|
+
"cls, dict_name",
|
|
55
|
+
[(Parameter, "_parameters"), (Buffer, "_buffers")],
|
|
56
|
+
)
|
|
57
|
+
def test_member_routing(self, cls, dict_name):
|
|
58
|
+
m = _make_identity_module()
|
|
59
|
+
v = cls(np.array([1.0]))
|
|
60
|
+
m.w = v
|
|
61
|
+
assert "w" in getattr(m, dict_name)
|
|
62
|
+
assert getattr(m, dict_name)["w"] is v
|
|
63
|
+
|
|
64
|
+
def test_module_routing(self):
|
|
65
|
+
child = _make_identity_module()
|
|
66
|
+
parent = _make_identity_module()
|
|
67
|
+
parent.child = child
|
|
68
|
+
assert "child" in parent._modules
|
|
69
|
+
assert parent._modules["child"] is child
|
|
70
|
+
|
|
71
|
+
def test_plain_value_not_tracked(self):
|
|
72
|
+
m = _make_identity_module()
|
|
73
|
+
m.x = 3
|
|
74
|
+
m.label = "hello"
|
|
75
|
+
assert "x" not in m._parameters
|
|
76
|
+
assert "x" not in m._buffers
|
|
77
|
+
assert "x" not in m._modules
|
|
78
|
+
assert "label" not in m._parameters
|
|
79
|
+
assert "label" not in m._buffers
|
|
80
|
+
assert "label" not in m._modules
|
|
81
|
+
|
|
82
|
+
class TestModuleStateDict:
|
|
83
|
+
def test_flat(self):
|
|
84
|
+
"""Flat module: state_dict contains every Parameter + Buffer keyed by name."""
|
|
85
|
+
|
|
86
|
+
class M(Module):
|
|
87
|
+
def __init__(self):
|
|
88
|
+
super().__init__()
|
|
89
|
+
self.w = Parameter(np.array([1.0, 2.0]))
|
|
90
|
+
self.buf = Buffer(np.array([3.0]))
|
|
91
|
+
|
|
92
|
+
def forward(self, x):
|
|
93
|
+
return x
|
|
94
|
+
|
|
95
|
+
m = M()
|
|
96
|
+
sd = m.state_dict()
|
|
97
|
+
assert set(sd.keys()) == {"w", "buf"}
|
|
98
|
+
assert sd["w"] is m.w
|
|
99
|
+
assert sd["buf"] is m.buf
|
|
100
|
+
|
|
101
|
+
def test_nested(self):
|
|
102
|
+
"""Nested: state_dict flattens recursively with dotted prefix."""
|
|
103
|
+
|
|
104
|
+
class Leaf(Module):
|
|
105
|
+
def __init__(self):
|
|
106
|
+
super().__init__()
|
|
107
|
+
self.w = Parameter(np.array([1.0]))
|
|
108
|
+
|
|
109
|
+
def forward(self, x):
|
|
110
|
+
return x
|
|
111
|
+
|
|
112
|
+
class Mid(Module):
|
|
113
|
+
def __init__(self):
|
|
114
|
+
super().__init__()
|
|
115
|
+
self.b = Leaf()
|
|
116
|
+
|
|
117
|
+
def forward(self, x):
|
|
118
|
+
return x
|
|
119
|
+
|
|
120
|
+
class Root(Module):
|
|
121
|
+
def __init__(self):
|
|
122
|
+
super().__init__()
|
|
123
|
+
self.a = Mid()
|
|
124
|
+
|
|
125
|
+
def forward(self, x):
|
|
126
|
+
return x
|
|
127
|
+
|
|
128
|
+
r = Root()
|
|
129
|
+
sd = r.state_dict()
|
|
130
|
+
assert sd == {"a.b.w": r.a.b.w}
|
|
131
|
+
|
|
132
|
+
class TestModuleNamedModules:
|
|
133
|
+
def test_no_submodules(self):
|
|
134
|
+
"""Module with no submodules yields only self."""
|
|
135
|
+
|
|
136
|
+
m = _make_identity_module()
|
|
137
|
+
result = list(m.named_modules())
|
|
138
|
+
assert len(result) == 1
|
|
139
|
+
assert result[0][0] == ""
|
|
140
|
+
assert result[0][1] is m
|
|
141
|
+
|
|
142
|
+
def test_recurse(self):
|
|
143
|
+
"""Yields self + all descendants with dotted prefixes."""
|
|
144
|
+
|
|
145
|
+
inner = _make_identity_module()
|
|
146
|
+
|
|
147
|
+
class Outer(Module):
|
|
148
|
+
def __init__(self):
|
|
149
|
+
super().__init__()
|
|
150
|
+
self.a = inner
|
|
151
|
+
|
|
152
|
+
def forward(self, x):
|
|
153
|
+
return x
|
|
154
|
+
|
|
155
|
+
outer = Outer()
|
|
156
|
+
result = list(outer.named_modules())
|
|
157
|
+
assert len(result) == 2
|
|
158
|
+
assert result[0][0] == ""
|
|
159
|
+
assert result[0][1] is outer
|
|
160
|
+
assert result[1][0] == "a"
|
|
161
|
+
assert result[1][1] is outer.a
|
|
162
|
+
|
|
163
|
+
def test_remove_duplicate(self):
|
|
164
|
+
"""remove_duplicate=True → shared submodule yielded once."""
|
|
165
|
+
|
|
166
|
+
shared = _make_identity_module()
|
|
167
|
+
|
|
168
|
+
class Parent(Module):
|
|
169
|
+
def __init__(self):
|
|
170
|
+
super().__init__()
|
|
171
|
+
self.left = shared
|
|
172
|
+
self.right = shared
|
|
173
|
+
|
|
174
|
+
def forward(self, x):
|
|
175
|
+
return x
|
|
176
|
+
|
|
177
|
+
p = Parent()
|
|
178
|
+
result = list(p.named_modules(remove_duplicate=True))
|
|
179
|
+
prefixes = [prefix for prefix, _ in result]
|
|
180
|
+
assert prefixes.count("") == 1 # self
|
|
181
|
+
assert prefixes.count("left") == 1
|
|
182
|
+
|
|
183
|
+
def test_modules_matches_named_modules(self):
|
|
184
|
+
"""modules() values match named_modules() values (names stripped)."""
|
|
185
|
+
|
|
186
|
+
inner = _make_identity_module()
|
|
187
|
+
|
|
188
|
+
class Outer(Module):
|
|
189
|
+
def __init__(self):
|
|
190
|
+
super().__init__()
|
|
191
|
+
self.a = inner
|
|
192
|
+
|
|
193
|
+
def forward(self, x):
|
|
194
|
+
return x
|
|
195
|
+
|
|
196
|
+
outer = Outer()
|
|
197
|
+
named_mods = [m for _, m in outer.named_modules()]
|
|
198
|
+
mods = list(outer.modules())
|
|
199
|
+
assert mods == named_mods
|
|
200
|
+
|
|
201
|
+
class TestModuleNamedMembers:
|
|
202
|
+
@pytest.mark.parametrize("cls, named, bare", MEMBER_PARAMS)
|
|
203
|
+
def test_flat(self, cls, named, bare):
|
|
204
|
+
class M(Module):
|
|
205
|
+
def __init__(self):
|
|
206
|
+
super().__init__()
|
|
207
|
+
self.a = cls(np.array([1.0]))
|
|
208
|
+
self.b = cls(np.array([2.0]))
|
|
209
|
+
|
|
210
|
+
def forward(self, x):
|
|
211
|
+
return x
|
|
212
|
+
|
|
213
|
+
m = M()
|
|
214
|
+
result = list(getattr(m, named)())
|
|
215
|
+
assert len(result) == 2
|
|
216
|
+
names = {name for name, _ in result}
|
|
217
|
+
assert names == {"a", "b"}
|
|
218
|
+
|
|
219
|
+
@pytest.mark.parametrize("cls, named, bare", MEMBER_PARAMS)
|
|
220
|
+
def test_nested_recurse(self, cls, named, bare):
|
|
221
|
+
class Child(Module):
|
|
222
|
+
def __init__(self):
|
|
223
|
+
super().__init__()
|
|
224
|
+
self.w = cls(np.array([1.0]))
|
|
225
|
+
|
|
226
|
+
def forward(self, x):
|
|
227
|
+
return x
|
|
228
|
+
|
|
229
|
+
class Parent(Module):
|
|
230
|
+
def __init__(self):
|
|
231
|
+
super().__init__()
|
|
232
|
+
self.child = Child()
|
|
233
|
+
self.b = cls(np.array([2.0]))
|
|
234
|
+
|
|
235
|
+
def forward(self, x):
|
|
236
|
+
return x
|
|
237
|
+
|
|
238
|
+
p = Parent()
|
|
239
|
+
result = list(getattr(p, named)())
|
|
240
|
+
names = {name for name, _ in result}
|
|
241
|
+
assert names == {"b", "child.w"}
|
|
242
|
+
|
|
243
|
+
@pytest.mark.parametrize("cls, named, bare", MEMBER_PARAMS)
|
|
244
|
+
def test_no_recurse(self, cls, named, bare):
|
|
245
|
+
class Child(Module):
|
|
246
|
+
def __init__(self):
|
|
247
|
+
super().__init__()
|
|
248
|
+
self.w = cls(np.array([1.0]))
|
|
249
|
+
|
|
250
|
+
def forward(self, x):
|
|
251
|
+
return x
|
|
252
|
+
|
|
253
|
+
class Parent(Module):
|
|
254
|
+
def __init__(self):
|
|
255
|
+
super().__init__()
|
|
256
|
+
self.child = Child()
|
|
257
|
+
self.b = cls(np.array([2.0]))
|
|
258
|
+
|
|
259
|
+
def forward(self, x):
|
|
260
|
+
return x
|
|
261
|
+
|
|
262
|
+
p = Parent()
|
|
263
|
+
result = list(getattr(p, named)(recurse=False))
|
|
264
|
+
names = {name for name, _ in result}
|
|
265
|
+
assert names == {"b"}
|
|
266
|
+
|
|
267
|
+
@pytest.mark.parametrize("cls, named, bare", MEMBER_PARAMS)
|
|
268
|
+
def test_remove_duplicate(self, cls, named, bare):
|
|
269
|
+
shared = cls(np.array([1.0]))
|
|
270
|
+
|
|
271
|
+
class Child(Module):
|
|
272
|
+
def __init__(self):
|
|
273
|
+
super().__init__()
|
|
274
|
+
self.m = shared
|
|
275
|
+
|
|
276
|
+
def forward(self, x):
|
|
277
|
+
return x
|
|
278
|
+
|
|
279
|
+
class Parent(Module):
|
|
280
|
+
def __init__(self):
|
|
281
|
+
super().__init__()
|
|
282
|
+
self.a = Child()
|
|
283
|
+
self.b = Child()
|
|
284
|
+
|
|
285
|
+
def forward(self, x):
|
|
286
|
+
return x
|
|
287
|
+
|
|
288
|
+
p = Parent()
|
|
289
|
+
result = list(getattr(p, named)(remove_duplicate=True))
|
|
290
|
+
names = [name for name, _ in result]
|
|
291
|
+
assert names.count("a.m") == 1
|
|
292
|
+
assert "b.m" not in names
|
|
293
|
+
|
|
294
|
+
@pytest.mark.parametrize("cls, named, bare", MEMBER_PARAMS)
|
|
295
|
+
def test_bare_matches_named(self, cls, named, bare):
|
|
296
|
+
class Child(Module):
|
|
297
|
+
def __init__(self):
|
|
298
|
+
super().__init__()
|
|
299
|
+
self.w = cls(np.array([1.0]))
|
|
300
|
+
|
|
301
|
+
def forward(self, x):
|
|
302
|
+
return x
|
|
303
|
+
|
|
304
|
+
class Parent(Module):
|
|
305
|
+
def __init__(self):
|
|
306
|
+
super().__init__()
|
|
307
|
+
self.child = Child()
|
|
308
|
+
self.b = cls(np.array([2.0]))
|
|
309
|
+
|
|
310
|
+
def forward(self, x):
|
|
311
|
+
return x
|
|
312
|
+
|
|
313
|
+
p = Parent()
|
|
314
|
+
named_vals = [v for _, v in getattr(p, named)()]
|
|
315
|
+
bare_vals = list(getattr(p, bare)())
|
|
316
|
+
assert bare_vals == named_vals
|
|
317
|
+
|
|
318
|
+
@pytest.mark.parametrize("cls, named, bare", MEMBER_PARAMS)
|
|
319
|
+
def test_empty(self, cls, named, bare):
|
|
320
|
+
m = _make_identity_module()
|
|
321
|
+
assert list(getattr(m, named)()) == []
|
|
322
|
+
assert list(getattr(m, bare)()) == []
|
|
323
|
+
|
|
324
|
+
class TestModuleTrainEval:
|
|
325
|
+
def test_train(self):
|
|
326
|
+
"""train() sets requires_grad=True on all params including nested."""
|
|
327
|
+
|
|
328
|
+
class Child(Module):
|
|
329
|
+
def __init__(self):
|
|
330
|
+
super().__init__()
|
|
331
|
+
self.w = Parameter(np.array([1.0]))
|
|
332
|
+
self.w.requires_grad = False
|
|
333
|
+
|
|
334
|
+
def forward(self, x):
|
|
335
|
+
return x
|
|
336
|
+
|
|
337
|
+
class Parent(Module):
|
|
338
|
+
def __init__(self):
|
|
339
|
+
super().__init__()
|
|
340
|
+
self.child = Child()
|
|
341
|
+
self.b = Parameter(np.array([2.0]))
|
|
342
|
+
self.b.requires_grad = False
|
|
343
|
+
|
|
344
|
+
def forward(self, x):
|
|
345
|
+
return x
|
|
346
|
+
|
|
347
|
+
p = Parent()
|
|
348
|
+
p.train()
|
|
349
|
+
params = list(p.parameters())
|
|
350
|
+
assert all(param.requires_grad for param in params)
|
|
351
|
+
|
|
352
|
+
def test_eval(self):
|
|
353
|
+
"""eval() sets requires_grad=False on all params including nested."""
|
|
354
|
+
|
|
355
|
+
class Child(Module):
|
|
356
|
+
def __init__(self):
|
|
357
|
+
super().__init__()
|
|
358
|
+
self.w = Parameter(np.array([1.0]))
|
|
359
|
+
|
|
360
|
+
def forward(self, x):
|
|
361
|
+
return x
|
|
362
|
+
|
|
363
|
+
class Parent(Module):
|
|
364
|
+
def __init__(self):
|
|
365
|
+
super().__init__()
|
|
366
|
+
self.child = Child()
|
|
367
|
+
self.b = Parameter(np.array([2.0]))
|
|
368
|
+
|
|
369
|
+
def forward(self, x):
|
|
370
|
+
return x
|
|
371
|
+
|
|
372
|
+
p = Parent()
|
|
373
|
+
p.eval()
|
|
374
|
+
params = list(p.parameters())
|
|
375
|
+
assert all(not param.requires_grad for param in params)
|
|
376
|
+
|
|
377
|
+
def test_buffers_unaffected(self):
|
|
378
|
+
"""Buffers keep requires_grad=False regardless of train/eval."""
|
|
379
|
+
|
|
380
|
+
class M(Module):
|
|
381
|
+
def __init__(self):
|
|
382
|
+
super().__init__()
|
|
383
|
+
self.buf = Buffer(np.array([0.0]))
|
|
384
|
+
|
|
385
|
+
def forward(self, x):
|
|
386
|
+
return x
|
|
387
|
+
|
|
388
|
+
m = M()
|
|
389
|
+
assert m.buf.requires_grad is False
|
|
390
|
+
m.train()
|
|
391
|
+
assert m.buf.requires_grad is False
|
|
392
|
+
m.eval()
|
|
393
|
+
assert m.buf.requires_grad is False
|
|
394
|
+
|
|
395
|
+
class TestModuleCall:
|
|
396
|
+
def test_call_returns_forward_result(self):
|
|
397
|
+
"""__call__ delegates to forward and returns its result."""
|
|
398
|
+
|
|
399
|
+
class M(Module):
|
|
400
|
+
def forward(self, x):
|
|
401
|
+
return x * 2
|
|
402
|
+
|
|
403
|
+
m = M()
|
|
404
|
+
result = m(da.Tensor(np.array([1.0, 2.0])))
|
|
405
|
+
expected = np.array([2.0, 4.0])
|
|
406
|
+
assert (result.to_np() == expected).all()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
LIST_PARAMS = [
|
|
410
|
+
(
|
|
411
|
+
ParameterList,
|
|
412
|
+
lambda: Parameter(np.array([1.0])),
|
|
413
|
+
"_parameters",
|
|
414
|
+
"named_parameters",
|
|
415
|
+
True,
|
|
416
|
+
),
|
|
417
|
+
(ModuleList, _make_identity_module, "_modules", "named_modules", False),
|
|
418
|
+
(BufferList, lambda: Buffer(np.array([1.0])), "_buffers", "named_buffers", True),
|
|
419
|
+
]
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class TestListContainers:
|
|
423
|
+
@pytest.mark.parametrize("list_cls, make_member, _d, _n, _c", LIST_PARAMS)
|
|
424
|
+
def test_len(self, list_cls, make_member, _d, _n, _c):
|
|
425
|
+
lst = list_cls([make_member(), make_member()])
|
|
426
|
+
assert len(lst) == 2
|
|
427
|
+
|
|
428
|
+
@pytest.mark.parametrize("list_cls, make_member, _d, _n, _c", LIST_PARAMS)
|
|
429
|
+
def test_getitem(self, list_cls, make_member, _d, _n, _c):
|
|
430
|
+
m0, m1 = make_member(), make_member()
|
|
431
|
+
lst = list_cls([m0, m1])
|
|
432
|
+
assert lst[0] is m0
|
|
433
|
+
assert lst[1] is m1
|
|
434
|
+
assert lst[-1] is m1
|
|
435
|
+
assert lst[-2] is m0
|
|
436
|
+
with pytest.raises(IndexError):
|
|
437
|
+
_ = lst[2]
|
|
438
|
+
with pytest.raises(IndexError):
|
|
439
|
+
_ = lst[-3]
|
|
440
|
+
|
|
441
|
+
@pytest.mark.parametrize("list_cls, make_member, _d, _n, _c", LIST_PARAMS)
|
|
442
|
+
def test_setitem(self, list_cls, make_member, _d, _n, _c):
|
|
443
|
+
m0, m1 = make_member(), make_member()
|
|
444
|
+
lst = list_cls([m0])
|
|
445
|
+
lst[0] = m1
|
|
446
|
+
assert lst[0] is m1
|
|
447
|
+
|
|
448
|
+
@pytest.mark.parametrize("list_cls, make_member, _d, _n, auto_convert", LIST_PARAMS)
|
|
449
|
+
def test_setitem_auto_converts(self, list_cls, make_member, _d, _n, auto_convert):
|
|
450
|
+
if not auto_convert:
|
|
451
|
+
pytest.skip("auto-convert only applies to ParameterList and BufferList")
|
|
452
|
+
t = da.Tensor(np.array([1.0]))
|
|
453
|
+
lst = list_cls([make_member()])
|
|
454
|
+
lst[0] = t
|
|
455
|
+
assert isinstance(lst[0], (Parameter, Buffer))
|
|
456
|
+
|
|
457
|
+
@pytest.mark.parametrize("list_cls, make_member, _d, _n, _c", LIST_PARAMS)
|
|
458
|
+
def test_iter(self, list_cls, make_member, _d, _n, _c):
|
|
459
|
+
members = [make_member(), make_member()]
|
|
460
|
+
lst = list_cls(members)
|
|
461
|
+
assert list(lst) == members
|
|
462
|
+
|
|
463
|
+
@pytest.mark.parametrize("list_cls, make_member, _d, _n, _c", LIST_PARAMS)
|
|
464
|
+
def test_append(self, list_cls, make_member, _d, _n, _c):
|
|
465
|
+
m = make_member()
|
|
466
|
+
lst = list_cls([make_member()])
|
|
467
|
+
initial_len = len(lst)
|
|
468
|
+
lst.append(m)
|
|
469
|
+
assert len(lst) == initial_len + 1
|
|
470
|
+
assert lst[-1] is m
|
|
471
|
+
|
|
472
|
+
@pytest.mark.parametrize("list_cls, make_member, _d, _n, _c", LIST_PARAMS)
|
|
473
|
+
def test_extend(self, list_cls, make_member, _d, _n, _c):
|
|
474
|
+
m0, m1 = make_member(), make_member()
|
|
475
|
+
lst = list_cls([make_member()])
|
|
476
|
+
lst.extend([m0, m1])
|
|
477
|
+
assert len(lst) == 3
|
|
478
|
+
assert lst[1] is m0
|
|
479
|
+
assert lst[2] is m1
|
|
480
|
+
|
|
481
|
+
@pytest.mark.parametrize("list_cls, make_member, dict_name, named, _c", LIST_PARAMS)
|
|
482
|
+
def test_parent_routing(self, list_cls, make_member, dict_name, named, _c):
|
|
483
|
+
m = make_member()
|
|
484
|
+
|
|
485
|
+
class Parent(Module):
|
|
486
|
+
def __init__(self):
|
|
487
|
+
super().__init__()
|
|
488
|
+
self.items = list_cls([m])
|
|
489
|
+
|
|
490
|
+
def forward(self, x):
|
|
491
|
+
return x
|
|
492
|
+
|
|
493
|
+
p = Parent()
|
|
494
|
+
entries = dict(getattr(p, named)())
|
|
495
|
+
assert "items.0" in entries
|
|
496
|
+
assert entries["items.0"] is m
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class TestLinear:
|
|
500
|
+
def test_weight_shape(self):
|
|
501
|
+
lin = Linear(3, 5)
|
|
502
|
+
assert lin.weight.shape == (3, 5)
|
|
503
|
+
|
|
504
|
+
def test_bias_shape(self):
|
|
505
|
+
lin = Linear(3, 5, bias=True)
|
|
506
|
+
assert lin.bias.shape == (5,)
|
|
507
|
+
|
|
508
|
+
def test_bias_is_none_when_disabled(self):
|
|
509
|
+
lin = Linear(3, 5, bias=False)
|
|
510
|
+
assert lin.bias is None
|
|
511
|
+
|
|
512
|
+
def test_forward_with_bias(self):
|
|
513
|
+
lin = Linear(3, 5, bias=True)
|
|
514
|
+
x_np = np.random.randn(2, 3).astype(np.float64)
|
|
515
|
+
x = da.Tensor(x_np)
|
|
516
|
+
out = lin(x)
|
|
517
|
+
expected = x_np @ lin.weight.to_np() + lin.bias.to_np()
|
|
518
|
+
assert out.shape == (2, 5)
|
|
519
|
+
assert (out.to_np() == expected).all()
|
|
520
|
+
|
|
521
|
+
def test_forward_without_bias(self):
|
|
522
|
+
lin = Linear(3, 5, bias=False)
|
|
523
|
+
x_np = np.random.randn(2, 3).astype(np.float64)
|
|
524
|
+
x = da.Tensor(x_np)
|
|
525
|
+
out = lin(x)
|
|
526
|
+
expected = x_np @ lin.weight.to_np()
|
|
527
|
+
assert out.shape == (2, 5)
|
|
528
|
+
assert (out.to_np() == expected).all()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# --- numpy equivalents for each activation ---
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _relu_np(z):
|
|
535
|
+
return np.maximum(z, 0)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _softmax_np(z, axis=-1):
|
|
539
|
+
z = z - z.max(axis=axis, keepdims=True)
|
|
540
|
+
e = np.exp(z)
|
|
541
|
+
return e / e.sum(axis=axis, keepdims=True)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
ACTIVATIONS = [
|
|
545
|
+
(ReLU, _relu_np),
|
|
546
|
+
(Softmax, _softmax_np),
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
class TestSequentialWithActivations:
|
|
551
|
+
@pytest.mark.parametrize("act_cls, act_np", ACTIVATIONS)
|
|
552
|
+
@pytest.mark.parametrize("bias", [True, False])
|
|
553
|
+
def test_forward(self, act_cls, act_np, bias):
|
|
554
|
+
lin = Linear(3, 5, bias=bias)
|
|
555
|
+
seq = Sequential([lin, act_cls()])
|
|
556
|
+
|
|
557
|
+
x_np = np.random.randn(2, 3).astype(np.float64)
|
|
558
|
+
x = da.Tensor(x_np)
|
|
559
|
+
|
|
560
|
+
out = seq(x)
|
|
561
|
+
|
|
562
|
+
z = x_np @ lin.weight.to_np()
|
|
563
|
+
if bias:
|
|
564
|
+
z = z + lin.bias.to_np()
|
|
565
|
+
expected = act_np(z)
|
|
566
|
+
assert_close(expected, out.to_np())
|
|
567
|
+
|
|
568
|
+
@pytest.mark.parametrize("act_cls, act_np", ACTIVATIONS)
|
|
569
|
+
@pytest.mark.parametrize("bias", [True, False])
|
|
570
|
+
def test_grad(self, act_cls, act_np, bias):
|
|
571
|
+
lin = Linear(3, 5, bias=bias)
|
|
572
|
+
seq = Sequential([lin, act_cls()])
|
|
573
|
+
|
|
574
|
+
x_np = np.random.randn(2, 3).astype(np.float64)
|
|
575
|
+
x = da.Tensor(x_np, requires_grad=True)
|
|
576
|
+
|
|
577
|
+
out = seq(x).sum()
|
|
578
|
+
out.backward()
|
|
579
|
+
|
|
580
|
+
def func(inp):
|
|
581
|
+
return seq(da.Tensor(inp) if not isinstance(inp, da.Tensor) else inp).sum()
|
|
582
|
+
|
|
583
|
+
assert_close(numerical_grad(func, x), x.grad)
|
|
@@ -285,7 +285,7 @@ class TestShapeOps:
|
|
|
285
285
|
_test_unary_grad(t, da.squeeze, axis=axis)
|
|
286
286
|
|
|
287
287
|
@pytest.mark.parametrize(
|
|
288
|
-
"
|
|
288
|
+
"repeats, axis",
|
|
289
289
|
[
|
|
290
290
|
(2, None),
|
|
291
291
|
(2, 0),
|
|
@@ -294,12 +294,10 @@ class TestShapeOps:
|
|
|
294
294
|
],
|
|
295
295
|
)
|
|
296
296
|
class TestRepeat:
|
|
297
|
-
def test_forward(self, unary,
|
|
298
|
-
repeats, axis = repeats_axis
|
|
297
|
+
def test_forward(self, unary, repeats, axis):
|
|
299
298
|
_test_unary_forward(unary, da.repeat, np.repeat, repeats=repeats, axis=axis)
|
|
300
299
|
|
|
301
|
-
def test_grad(self, unary,
|
|
302
|
-
repeats, axis = repeats_axis
|
|
300
|
+
def test_grad(self, unary, repeats, axis):
|
|
303
301
|
log_softmax_axis = -1 if axis is None else axis
|
|
304
302
|
_test_unary_grad(
|
|
305
303
|
unary,
|
|
@@ -186,7 +186,7 @@ toml = [
|
|
|
186
186
|
|
|
187
187
|
[[package]]
|
|
188
188
|
name = "deep-atomic"
|
|
189
|
-
version = "0.2.
|
|
189
|
+
version = "0.2.2"
|
|
190
190
|
source = { editable = "." }
|
|
191
191
|
dependencies = [
|
|
192
192
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.11'" },
|
|
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
|