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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deep_atomic
3
- Version: 0.2.1
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 upon numpy only. Mainly for practice and learning.
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 deeplearning operations are implemented. For those that also exist in NumPy, we follow NumPy's API conventions.
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, create a boolean tensor
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. same for da.fmin / da.minimum
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.all(axis=-1, keepdims=False) # reduction of and operation. axis=None, keepdims=False by default
89
- c = d.any(axis=-1, keepdims=False) # reduction of or operation. axis=None, keepdims=False by default
90
- c = da.where(d, a, b) # return elements chosen from a or b depending on condition
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 support scalar source points._
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 get!
130
+ print(res.grad) # gradient computed!
127
131
  ```
128
132
 
129
- ## Todo
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
- Recommend manage dependencies using [uv](https://github.com/astral-sh/uv).
155
- We use [pre-commit](https://github.com/pre-commit/pre-commit) to manage hooks which help to lint and format our code.
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 upon numpy only. Mainly for practice and learning.
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 deeplearning operations are implemented. For those that also exist in NumPy, we follow NumPy's API conventions.
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, create a boolean tensor
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. same for da.fmin / da.minimum
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.all(axis=-1, keepdims=False) # reduction of and operation. axis=None, keepdims=False by default
68
- c = d.any(axis=-1, keepdims=False) # reduction of or operation. axis=None, keepdims=False by default
69
- c = da.where(d, a, b) # return elements chosen from a or b depending on condition
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 support scalar source points._
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 get!
109
+ print(res.grad) # gradient computed!
106
110
  ```
107
111
 
108
- ## Todo
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
- Recommend manage dependencies using [uv](https://github.com/astral-sh/uv).
134
- We use [pre-commit](https://github.com/pre-commit/pre-commit) to manage hooks which help to lint and format our code.
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "deep_atomic"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "A simple deep learning framework built upon numpy only"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  """deep-atomic - A simple deep learning framework built upon numpy only"""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
4
4
  __author__ = "elliot_zh@proton.me"
5
5
 
6
6
  from .op import *
@@ -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(recurse=recurse):
79
+ for _name, module in self.named_modules():
80
80
  yield module
81
81
 
82
- def named_module(self, prefix="", remove_duplicate=True, memo=None):
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 .graph import *
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
- "repeats_axis",
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, repeats_axis):
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, repeats_axis):
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.1"
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