File size: 10,537 Bytes
fbd9366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
"""
Functions that work on nested structures of torch.Tensor or numpy array
"""

from typing import Any, Dict, List, Optional, Union

import numpy as np
import torch
import tree

from ..data_structure.tree_utils import (
    copy_non_leaf,
    is_sequence,
    tree_assign_at_path,
    tree_value_at_path,
)
from .functional_utils import make_recursive_func


def is_array_tensor(obj):
    return isinstance(obj, (np.ndarray, torch.Tensor))


def is_numpy(obj):
    return isinstance(obj, np.ndarray)


def is_tensor(obj):
    return torch.is_tensor(obj)


def any_stack(xs: List, *, dim: int = 0):
    """
    Works for both torch Tensor and numpy array
    """

    def _any_stack_helper(*xs):
        x = xs[0]
        if isinstance(x, np.ndarray):
            return np.stack(xs, axis=dim)
        elif torch.is_tensor(x):
            return torch.stack(xs, dim=dim)
        elif isinstance(x, float):
            # special treatment for float, defaults to float32
            return np.array(xs, dtype=np.float32)
        else:
            return np.array(xs)

    return tree.map_structure(_any_stack_helper, *xs)


def any_concat(xs: List, *, dim: int = 0):
    """
    Works for both torch Tensor and numpy array
    """

    def _any_concat_helper(*xs):
        x = xs[0]
        if isinstance(x, np.ndarray):
            return np.concatenate(xs, axis=dim)
        elif torch.is_tensor(x):
            return torch.cat(xs, dim=dim)
        elif isinstance(x, float):
            # special treatment for float, defaults to float32
            return np.array(xs, dtype=np.float32)
        else:
            return np.array(xs)

    return tree.map_structure(_any_concat_helper, *xs)


def any_chunk(x, chunks: int, *, dim: int = 0, strict: bool = True) -> List[Any]:
    """
    Works for both torch Tensor and numpy array

    Returns:
        list of chunked nested structures
    """
    assert chunks >= 1

    x_copies = [copy_non_leaf(x) for _ in range(chunks)]

    def _any_chunk_helper(path, x):
        if is_array_tensor(x):
            if isinstance(x, np.ndarray):
                chunked_values = np.split(x, chunks, axis=dim)
            else:
                chunked_values = torch.chunk(x, chunks, dim=dim)

            if path:
                for xc, chunked in zip(x_copies, chunked_values):
                    tree_assign_at_path(xc, path, chunked)
            else:  # top-level, no nested path
                for i, chunked in enumerate(chunked_values):
                    x_copies[i] = chunked
        else:
            if strict:
                raise NotImplementedError(f"Cannot chunk type {type(x)}")
            else:
                return

    tree.map_structure_with_path(_any_chunk_helper, x)
    return x_copies


def chunk_seq(arr, chunks: int, check_divide=True):
    """
    Args:
        check_divide: True to force arr must divide n
    """
    k, m = divmod(len(arr), chunks)
    if check_divide and m != 0:
        raise ValueError(f"Array len {len(arr)} does not divide chunks {chunks}")
    return (arr[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(chunks))


@make_recursive_func
def any_zeros_like(x: Union[Dict, np.ndarray, torch.Tensor, int, float, np.number]):
    """Returns a zero-filled object of the same (d)type and shape as the input.

    The difference between this and `np.zeros_like()` is that this works well
    with `np.number`, `int`, `float`, and `jax.numpy.DeviceArray` objects without
    converting them to `np.ndarray`s.

    Args:
      x: The object to replace with 0s.

    Returns:
      A zero-filed object of the same (d)type and shape as the input.
    """
    if isinstance(x, (int, float, np.number)):
        return type(x)(0)
    elif is_tensor(x):
        return torch.zeros_like(x)
    elif is_numpy(x):
        return np.zeros_like(x)
    else:
        raise ValueError(
            f"Input ({type(x)}) must be either a numpy array, a tensor, an int, or a float."
        )


@make_recursive_func
def any_ones_like(x: Union[Dict, np.ndarray, torch.Tensor, int, float, np.number]):
    """Returns a one-filled object of the same (d)type and shape as the input.
    The difference between this and `np.ones_like()` is that this works well
    with `np.number`, `int`, `float`, and `jax.numpy.DeviceArray` objects without
    converting them to `np.ndarray`s.
    Args:
      x: The object to replace with 1s.
    Returns:
      A one-filed object of the same (d)type and shape as the input.
    """
    if isinstance(x, (int, float, np.number)):
        return type(x)(1)
    elif is_tensor(x):
        return torch.ones_like(x)
    elif is_numpy(x):
        return np.ones_like(x)
    else:
        raise ValueError(
            f"Input ({type(x)}) must be either a numpy array, a tensor, an int, or a float."
        )


@make_recursive_func
def any_zero_(x: Union[Dict, np.ndarray, torch.Tensor]):
    """
    Apply in-place zero-out to a tensor, i.e. x.zero_()
    """
    if is_tensor(x):
        x.zero_()
    elif is_numpy(x):
        x.fill(0)
    else:
        raise ValueError(f"Input ({type(x)}) must be either a numpy array or a tensor")


@make_recursive_func
def any_fill_(x: Union[Dict, np.ndarray, torch.Tensor], value):
    """
    Apply in-place zero-out to a tensor, i.e. x.zero_()
    """
    if is_tensor(x):
        x.fill_(value)
    elif is_numpy(x):
        x.fill(value)
    else:
        raise ValueError(f"Input ({type(x)}) must be either a numpy array or a tensor")


def get_batch_size(x, strict: bool = False) -> int:
    """
    Args:
        x: can be any arbitrary nested structure of np array and torch tensor
        strict: True to check all batch sizes are the same
    """

    def _get_batch_size(x):
        if isinstance(x, np.ndarray):
            return x.shape[0]
        elif torch.is_tensor(x):
            return x.size(0)
        else:
            return len(x)

    xs = tree.flatten(x)

    if strict:
        batch_sizes = [_get_batch_size(x) for x in xs]
        assert all(
            b == batch_sizes[0] for b in batch_sizes
        ), f"batch sizes must all be the same in nested structure: {batch_sizes}"
        return batch_sizes[0]
    else:
        return _get_batch_size(xs[0])


@make_recursive_func
def add_batch_dim(x):
    if is_numpy(x):
        return np.expand_dims(x, axis=0)
    elif is_tensor(x):
        return x.unsqueeze(0)
    else:
        raise NotImplementedError(f"Unsupported data structure: {type(x)}")


@make_recursive_func
def remove_batch_dim(x):
    if is_numpy(x):
        return np.squeeze(x, axis=0)
    elif is_tensor(x):
        return x.squeeze(0)
    else:
        raise NotImplementedError(f"Unsupported data structure: {type(x)}")


@make_recursive_func
def any_to_primitive(x):
    if isinstance(x, (np.ndarray, np.number, torch.Tensor)):
        return x.tolist()
    else:
        return x


@make_recursive_func
def any_get_shape(x):
    if is_numpy(x):
        return tuple(x.shape)
    elif is_tensor(x):
        return tuple(x.size())
    else:
        raise NotImplementedError(f"Unsupported data structure: {type(x)}")


@make_recursive_func
def any_mean(x, dim: Optional[int] = None, keepdim: bool = False):
    if is_numpy(x):
        return np.mean(x, axis=dim, keepdims=keepdim)
    elif is_tensor(x):
        return torch.mean(x, dim=dim, keepdim=keepdim)
    else:
        raise NotImplementedError(f"Unsupported data structure: {type(x)}")


@make_recursive_func
def any_variance(x, dim: Optional[int] = None, keepdim: bool = False, unbiased: bool = False):
    if is_numpy(x):
        return np.var(x, axis=dim, keepdims=keepdim, ddof=1 if unbiased else 0)
    elif is_tensor(x):
        return torch.var(x, dim=dim, keepdim=keepdim, unbiased=unbiased)
    else:
        raise NotImplementedError(f"Unsupported data structure: {type(x)}")


@make_recursive_func
def any_describe_str(x, shape_only=False):
    """
    Describe type, shape, device, data type (of np array/tensor)
    Very useful for debugging
    """
    t = type(x)
    tname = type(x).__name__
    if is_numpy(x):
        shape = list(x.shape)
        if x.size == 1:
            if shape_only:
                return f"np scalar: {x.item()} {shape}"
            else:
                return f"np scalar: {x.item()} {shape} {x.dtype}"
        else:
            if shape_only:
                return f"np: {shape}"
            else:
                return f"np: {shape} {x.dtype}"
    elif is_tensor(x):
        shape = list(x.size())
        if x.numel() == 1:
            if shape_only:
                return f"torch scalar: {x.item()} {shape}"
            else:
                return f"torch scalar: {x.item()} {shape} {x.dtype} {x.device}"
        else:
            if shape_only:
                return f"torch: {shape}"
            else:
                return f"torch: {shape} {x.dtype} {x.device}"
    elif is_sequence(x):
        return f"{tname}[{len(x)}]"
    elif isinstance(x, str):
        return x
    elif x is None:
        return "None"
    elif np.issubdtype(t, np.number) or np.issubdtype(t, np.bool_):
        return f"{tname}: {x}"
    else:
        return f"{tname}"


def any_describe(x, msg="", *, shape_only=False):
    # from omlet.utils import yaml_dumps
    from pprint import pprint

    if isinstance(x, str) and msg != "":
        x, msg = msg, x

    if msg:
        msg += ": "
    print(msg, end="")
    pprint(any_describe_str(x, shape_only=shape_only))


@make_recursive_func
def any_slice(x, slice):
    """
    Args:
        slice: you can use np.s_[...] to return the slice object
    """
    if is_array_tensor(x):
        return x[slice]
    else:
        return x


def any_assign(x, assign_value, slice):
    """
    Recursive version of x[slice] = assign_value
    If structures of x and assign_value do not match, we will respect `assign_value`
    E.g. x = {'a': ..., 'b': ...}, assign_value = {'a': ...}, then 'b' will not change

    Use np.s_[...] to get advanced slicing
    """

    def _any_assign_helper(path, v):
        y = tree_value_at_path(x, path)
        y[slice] = v

    tree.map_structure_with_path(_any_assign_helper, assign_value)


@make_recursive_func
def any_transpose_first_two_axes(x):
    """
    util to convert between (L, B, ...) and (B, L, ...)
    """
    if is_numpy(x):
        return np.swapaxes(x, 0, 1)
    elif is_tensor(x):
        return torch.swapaxes(x, 0, 1)
    else:
        raise ValueError(f"Input ({type(x)}) must be either a numpy array or a tensor.")