RR-layer 0.1.1__py3-none-any.whl
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.
- RR_layer/__init__.py +1 -0
- RR_layer/rr_layer.py +375 -0
- RR_layer/utils.py +0 -0
- rr_layer-0.1.1.dist-info/METADATA +100 -0
- rr_layer-0.1.1.dist-info/RECORD +7 -0
- rr_layer-0.1.1.dist-info/WHEEL +4 -0
- rr_layer-0.1.1.dist-info/licenses/LICENSE +21 -0
RR_layer/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .rr_layer import RRLayer
|
RR_layer/rr_layer.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn as nn
|
|
4
|
+
from collections import deque
|
|
5
|
+
|
|
6
|
+
def stable_SVD(A):
|
|
7
|
+
""" Computes a numerically stable SVD.
|
|
8
|
+
|
|
9
|
+
A: input matrix (..., m, n)
|
|
10
|
+
"""
|
|
11
|
+
return StableSVD.apply(A)
|
|
12
|
+
|
|
13
|
+
class StableSVD(torch.autograd.Function):
|
|
14
|
+
@staticmethod
|
|
15
|
+
def forward(ctx, A):
|
|
16
|
+
# Just compute standard SVD
|
|
17
|
+
U, S, Vh = torch.linalg.svd(A, full_matrices=False)
|
|
18
|
+
|
|
19
|
+
# Save for backward
|
|
20
|
+
ctx.save_for_backward(U, S, Vh)
|
|
21
|
+
ctx.original_shape = A.shape
|
|
22
|
+
|
|
23
|
+
return U, S, Vh
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def backward(ctx, dU, dS, dVh):
|
|
27
|
+
"""
|
|
28
|
+
Backward pass for stable SVD.
|
|
29
|
+
Computes gradient w.r.t. input A given gradients on U, S, Vh.
|
|
30
|
+
"""
|
|
31
|
+
U, S, Vh = ctx.saved_tensors
|
|
32
|
+
m, n = ctx.original_shape[-2:]
|
|
33
|
+
|
|
34
|
+
dtype = U.dtype
|
|
35
|
+
device = U.device
|
|
36
|
+
|
|
37
|
+
H = lambda x: x.transpose(-2, -1).conj()
|
|
38
|
+
T = lambda x: x.transpose(-2, -1)
|
|
39
|
+
|
|
40
|
+
# Diagonal helpers
|
|
41
|
+
def diag_embed(x):
|
|
42
|
+
return torch.diag_embed(x)
|
|
43
|
+
|
|
44
|
+
# Singular value vector to diagonal for broadcasting
|
|
45
|
+
S_mat = S.unsqueeze(-2)
|
|
46
|
+
S_diff = S_mat - S_mat.transpose(-2, -1)
|
|
47
|
+
|
|
48
|
+
# F matrix for repeated singular values
|
|
49
|
+
eps = 1e-20
|
|
50
|
+
F = torch.where(S_diff.abs() > eps, 1.0 / S_diff, torch.zeros_like(S_diff))
|
|
51
|
+
|
|
52
|
+
# Gradient from singular values
|
|
53
|
+
dA = U @ diag_embed(dS) @ Vh
|
|
54
|
+
|
|
55
|
+
# Contributions from U
|
|
56
|
+
Ut_dU = H(U) @ dU
|
|
57
|
+
skew_U = F * (Ut_dU - Ut_dU.transpose(-2, -1))
|
|
58
|
+
dA += U @ skew_U @ diag_embed(S) @ Vh
|
|
59
|
+
|
|
60
|
+
# Contributions from V
|
|
61
|
+
V = H(Vh) # n x k
|
|
62
|
+
Vt_dV = H(V) @ H(dVh) # k x k
|
|
63
|
+
skew_V = F * (Vt_dV - Vt_dV.transpose(-2, -1))
|
|
64
|
+
dA += U @ diag_embed(S) @ skew_V @ Vh
|
|
65
|
+
|
|
66
|
+
# Rectangular adjustments (like JAX)
|
|
67
|
+
# s_inv = 1 / S with stable zero handling
|
|
68
|
+
s_zeros = (S == 0).to(dtype)
|
|
69
|
+
s_inv = 1.0 / (S + s_zeros) - s_zeros # shape: (k,)
|
|
70
|
+
|
|
71
|
+
if m > n:
|
|
72
|
+
dAV = dA @ V
|
|
73
|
+
dA += (dAV - U @ (H(U) @ dAV)) * s_inv
|
|
74
|
+
elif n > m:
|
|
75
|
+
dAHU = H(dA) @ U
|
|
76
|
+
print("first term shape: ", H(dAHU - V @ (Vh @ dAHU)).shape)
|
|
77
|
+
print("s_inv shape: ", s_inv.shape)
|
|
78
|
+
|
|
79
|
+
dA += H(dAHU - V @ (Vh @ dAHU)) * s_inv.unsqueeze(1)
|
|
80
|
+
|
|
81
|
+
return dA
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
import math
|
|
85
|
+
import warnings
|
|
86
|
+
from collections import deque
|
|
87
|
+
|
|
88
|
+
import torch
|
|
89
|
+
import torch.nn as nn
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class RRLayer(nn.Module):
|
|
93
|
+
r"""
|
|
94
|
+
Rank Reduction (RR) layer.
|
|
95
|
+
|
|
96
|
+
During training, the layer computes a truncated SVD across the batch
|
|
97
|
+
dimension and reconstructs the input using the top ``rank`` singular
|
|
98
|
+
components.
|
|
99
|
+
|
|
100
|
+
During evaluation, the layer projects the input onto a learned inference
|
|
101
|
+
basis obtained from the recent training bases.
|
|
102
|
+
|
|
103
|
+
A custom basis can always be supplied through the ``basis`` argument of
|
|
104
|
+
:meth:`forward`, in which case the train/eval behavior is bypassed.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
rank (int):
|
|
108
|
+
Number of singular values to retain.
|
|
109
|
+
|
|
110
|
+
basis_history_size (int, optional):
|
|
111
|
+
Number of recent batch bases to keep when estimating the inference
|
|
112
|
+
basis. Default: 20.
|
|
113
|
+
|
|
114
|
+
Shape:
|
|
115
|
+
- Input: ``(N, *)``
|
|
116
|
+
- Output: ``(N, *)``
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> rr = RRLayer(rank=8)
|
|
120
|
+
>>> rr.train()
|
|
121
|
+
>>> y = rr(torch.randn(32, 768))
|
|
122
|
+
|
|
123
|
+
>>> rr.eval()
|
|
124
|
+
>>> y = rr(torch.randn(32, 768))
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
rank: int,
|
|
130
|
+
basis_history_size: int = 20,
|
|
131
|
+
):
|
|
132
|
+
super().__init__()
|
|
133
|
+
|
|
134
|
+
if rank <= 0:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"rank must be positive, got {rank}."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if basis_history_size <= 0:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"basis_history_size must be positive, got "
|
|
142
|
+
f"{basis_history_size}."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
self.rank = rank
|
|
146
|
+
self.basis_history_size = basis_history_size
|
|
147
|
+
|
|
148
|
+
self.register_buffer(
|
|
149
|
+
"inference_basis",
|
|
150
|
+
None,
|
|
151
|
+
persistent=True,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self._basis_bank = deque(
|
|
155
|
+
maxlen=basis_history_size,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def extra_repr(self) -> str:
|
|
159
|
+
return (
|
|
160
|
+
f"rank={self.rank}, "
|
|
161
|
+
f"basis_history_size={self.basis_history_size}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def train(self, mode: bool = True):
|
|
165
|
+
"""
|
|
166
|
+
Switch between training and evaluation mode.
|
|
167
|
+
|
|
168
|
+
When switching from training to evaluation for the first time,
|
|
169
|
+
an inference basis is automatically computed from the stored
|
|
170
|
+
training bases.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
previous_mode = self.training
|
|
174
|
+
|
|
175
|
+
super().train(mode)
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
previous_mode
|
|
179
|
+
and not mode
|
|
180
|
+
and self.inference_basis is None
|
|
181
|
+
and len(self._basis_bank) > 0
|
|
182
|
+
):
|
|
183
|
+
self.finalize_basis()
|
|
184
|
+
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
@torch.no_grad()
|
|
188
|
+
def finalize_basis(self) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Build the inference basis from the stored training bases.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
if len(self._basis_bank) == 0:
|
|
194
|
+
raise RuntimeError(
|
|
195
|
+
"Cannot finalize basis: no stored bases available."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
device = next(self.parameters(), None)
|
|
199
|
+
|
|
200
|
+
if device is None:
|
|
201
|
+
device = self.inference_basis.device \
|
|
202
|
+
if self.inference_basis is not None \
|
|
203
|
+
else self._basis_bank[0].device
|
|
204
|
+
else:
|
|
205
|
+
device = device.device
|
|
206
|
+
|
|
207
|
+
W = torch.cat(
|
|
208
|
+
[
|
|
209
|
+
basis.to(device)
|
|
210
|
+
for basis in self._basis_bank
|
|
211
|
+
],
|
|
212
|
+
dim=1,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
U, _, _ = stable_SVD(W)
|
|
216
|
+
|
|
217
|
+
r = min(self.rank, U.shape[1])
|
|
218
|
+
|
|
219
|
+
self.inference_basis = U[:, :r]
|
|
220
|
+
|
|
221
|
+
def _validate_inputs(
|
|
222
|
+
self,
|
|
223
|
+
x: torch.Tensor,
|
|
224
|
+
basis: torch.Tensor | None,
|
|
225
|
+
) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Validate inputs for RRLayer.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if not isinstance(x, torch.Tensor):
|
|
231
|
+
raise TypeError(
|
|
232
|
+
f"x must be a torch.Tensor, got {type(x)}."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if x.ndim < 2:
|
|
236
|
+
raise ValueError(
|
|
237
|
+
"RRLayer expects input of shape (N, ...), "
|
|
238
|
+
f"got shape {tuple(x.shape)}."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if x.shape[0] == 0:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
"Batch size must be greater than zero."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if not torch.isfinite(x).all():
|
|
247
|
+
raise ValueError(
|
|
248
|
+
"Input tensor contains NaN or Inf values."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
M = math.prod(x.shape[1:])
|
|
252
|
+
N = x.shape[0]
|
|
253
|
+
|
|
254
|
+
rank_max = min(M, N)
|
|
255
|
+
|
|
256
|
+
if self.rank > rank_max:
|
|
257
|
+
warnings.warn(
|
|
258
|
+
f"Requested rank={self.rank}, but the maximum "
|
|
259
|
+
f"achievable rank is {rank_max}. "
|
|
260
|
+
f"Using rank={rank_max}.",
|
|
261
|
+
stacklevel=2,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if basis is not None:
|
|
265
|
+
|
|
266
|
+
if not isinstance(basis, torch.Tensor):
|
|
267
|
+
raise TypeError(
|
|
268
|
+
f"basis must be a torch.Tensor, got {type(basis)}."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if basis.ndim != 2:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"basis must have shape (M, r), "
|
|
274
|
+
f"got shape {tuple(basis.shape)}."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if basis.shape[0] != M:
|
|
278
|
+
raise ValueError(
|
|
279
|
+
f"basis has {basis.shape[0]} rows but "
|
|
280
|
+
f"expected {M}."
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if basis.shape[1] == 0:
|
|
284
|
+
raise ValueError(
|
|
285
|
+
"basis must contain at least one column."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if basis.device != x.device:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"basis is on {basis.device} while "
|
|
291
|
+
f"x is on {x.device}."
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if basis.dtype != x.dtype:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"basis dtype ({basis.dtype}) does not match "
|
|
297
|
+
f"x dtype ({x.dtype})."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if not torch.isfinite(basis).all():
|
|
301
|
+
raise ValueError(
|
|
302
|
+
"basis contains NaN or Inf values."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def forward(
|
|
306
|
+
self,
|
|
307
|
+
x: torch.Tensor,
|
|
308
|
+
basis: torch.Tensor | None = None,
|
|
309
|
+
return_factors: bool = False,
|
|
310
|
+
):
|
|
311
|
+
self._validate_inputs(x, basis)
|
|
312
|
+
|
|
313
|
+
original_shape = x.shape
|
|
314
|
+
batch_size = original_shape[0]
|
|
315
|
+
|
|
316
|
+
X = torch.movedim(x, 0, -1)
|
|
317
|
+
X = X.reshape(-1, batch_size)
|
|
318
|
+
|
|
319
|
+
if basis is not None:
|
|
320
|
+
|
|
321
|
+
basis_used = basis
|
|
322
|
+
|
|
323
|
+
coeffs = basis_used.T @ X
|
|
324
|
+
|
|
325
|
+
X_hat = basis_used @ coeffs
|
|
326
|
+
|
|
327
|
+
elif self.training:
|
|
328
|
+
|
|
329
|
+
U, S, Vh = stable_SVD(X)
|
|
330
|
+
|
|
331
|
+
r = min(self.rank, S.shape[0])
|
|
332
|
+
|
|
333
|
+
basis_used = U[:, :r]
|
|
334
|
+
|
|
335
|
+
coeffs = (
|
|
336
|
+
S[:r].unsqueeze(1)
|
|
337
|
+
* Vh[:r]
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
X_hat = basis_used @ coeffs
|
|
341
|
+
|
|
342
|
+
self._basis_bank.append(
|
|
343
|
+
basis_used.detach().cpu()
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
else:
|
|
347
|
+
|
|
348
|
+
if self.inference_basis is None:
|
|
349
|
+
raise RuntimeError(
|
|
350
|
+
"RRLayer has no inference basis. "
|
|
351
|
+
"Call finalize_basis() or run training "
|
|
352
|
+
"before evaluation."
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
basis_used = self.inference_basis
|
|
356
|
+
|
|
357
|
+
coeffs = basis_used.T @ X
|
|
358
|
+
|
|
359
|
+
X_hat = basis_used @ coeffs
|
|
360
|
+
|
|
361
|
+
output = X_hat.reshape(
|
|
362
|
+
*original_shape[1:],
|
|
363
|
+
batch_size,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
output = torch.movedim(
|
|
367
|
+
output,
|
|
368
|
+
-1,
|
|
369
|
+
0,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if return_factors:
|
|
373
|
+
return output, basis_used, coeffs
|
|
374
|
+
|
|
375
|
+
return output
|
RR_layer/utils.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: RR_layer
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A repo for RR layer in Pytorch.
|
|
5
|
+
Author-email: Jad Mounayer <jad.mounayer@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: matplotlib
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: torch
|
|
12
|
+
Requires-Dist: torchvision
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# RR_layer
|
|
16
|
+
|
|
17
|
+
`RR_layer` is a JAX / Equinox library implementing a **Rank-Reduction (RR) layer**: a layer that compresses its input by projecting onto a low-rank SVD basis.
|
|
18
|
+
|
|
19
|
+
The mechanics are different between training and evaluation:
|
|
20
|
+
|
|
21
|
+
- **During training**, the SVD basis is **recomputed from scratch for every batch**. For each batch, the layer computes the SVD of the batch, truncates to the top `r` components, and uses that truncated basis to project — so the basis is always data-dependent and up to date with the current batch, and gradients flow through the SVD/projection step. The model also automatically saves the last `N=basis_history_size` bases to be later used in evaluation.
|
|
22
|
+
- **During evaluation**, when layer.eval() is called, the layer will find a common basis to the whole dataset `Uf`, based on the last `N=basis_history_size` that were saved. When the layer is called in evaluation mode, the SVD is replaced by a projection on that basis `Uf`.
|
|
23
|
+
- In both cases, the shape of the input should be (N x ...), where N is the number samples in a batch. Note that the output will have the same shape is the input. This is because the layer performs the SVD, truncates, but then returns the reconstructed latent space. So the compression happens inside eventhough the output still has the same shape.
|
|
24
|
+
|
|
25
|
+
## What's inside
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
RR_layer/
|
|
29
|
+
├── RR_layer/
|
|
30
|
+
│ ├── __init__.py
|
|
31
|
+
│ └── rr_layer.py # the layer implementation(s)
|
|
32
|
+
├── examples/
|
|
33
|
+
| ├──── autoencoder
|
|
34
|
+
│ | ├── generate_data.ipynb
|
|
35
|
+
│ | ├── train_baseline.ipynb
|
|
36
|
+
│ | ├── train_RRAE.ipynb
|
|
37
|
+
│ | └── compare.ipynb
|
|
38
|
+
└── tests/
|
|
39
|
+
└── tests.py
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `RR_layer/rr_layer.py`
|
|
43
|
+
|
|
44
|
+
The library code. Contains the `RR_layer` Equinox module: a layer with two modes of operation —
|
|
45
|
+
|
|
46
|
+
- **train mode**: computes the SVD of the current batch's activations, truncates to rank `r`, and projects onto that basis (gradients flow through this).
|
|
47
|
+
- **eval mode**: projects onto a basis that was saved from training, rather than computing a new SVD.
|
|
48
|
+
|
|
49
|
+
### `Examples/`
|
|
50
|
+
|
|
51
|
+
The example notebooks are meant to be worked through **in order**:
|
|
52
|
+
|
|
53
|
+
1. **`generate_data.ipynb`** — generates the dataset used in the other notebooks. Run this first; it should leave behind whatever data artifacts the later notebooks load.
|
|
54
|
+
|
|
55
|
+
2. **`train_baseline.ipynb`** — trains a standard, non-RR autoencoder as a reference point. This gives you a baseline reconstruction quality and runtime to compare the RRAE against.
|
|
56
|
+
|
|
57
|
+
3. **`train_RRAE.ipynb`** — trains the Rank-Reduction Auto-Encoder, built with one or more `RR_layer` in the latent space. During training this recomputes the SVD basis per batch as described above; at the end of training it should save the basis to be reused at evaluation time.
|
|
58
|
+
|
|
59
|
+
4. **`compare.ipynb`** — loads the trained baseline and RRAE models (using the saved, frozen SVD basis for the RRAE) and compares them for tasks such as linear interpolation and random generation.
|
|
60
|
+
|
|
61
|
+
Run the notebooks in the order above — each later notebook generally depends on artifacts (data, images) produced by the earlier ones.
|
|
62
|
+
|
|
63
|
+
### `tests/tests.py`
|
|
64
|
+
|
|
65
|
+
Unit tests for the `RR_layer` module. Useful both to verify correctness after installing, and as additional usage examples of the layer API beyond the notebooks (e.g. shape checks, gradient flow through the SVD step in train mode, etc.)
|
|
66
|
+
|
|
67
|
+
## Running the tests
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m pytest tests/tests.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install RR_layer
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick start
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import jax
|
|
83
|
+
from RR_layer import RRLayer # replace with actual export
|
|
84
|
+
|
|
85
|
+
inp = torch.randn(32, 768)
|
|
86
|
+
rr = RRLayer(rank=8)
|
|
87
|
+
|
|
88
|
+
rr.train()
|
|
89
|
+
y = rr(inp) # will compute the SVD
|
|
90
|
+
|
|
91
|
+
rr.eval()
|
|
92
|
+
y = rr(inp) # will project onto a saved basis
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Background
|
|
96
|
+
|
|
97
|
+
For more information about Rank Reduction layers, please refer to:
|
|
98
|
+
|
|
99
|
+
- **Paper:** [Rank Reduction Autoencoders](https://arxiv.org/abs/2405.13980)
|
|
100
|
+
- **Related repo:** [JadM133/RRAEs](https://github.com/JadM133/RRAEs)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
RR_layer/__init__.py,sha256=OjDRIEzUoj_Jgnm-nZofAoJAZrioLMhuf16kYHWmgc8,31
|
|
2
|
+
RR_layer/rr_layer.py,sha256=QEboZdhPMDHZvULlRLJQnO2nHD4H1h5zbpfZcSxdNk4,10027
|
|
3
|
+
RR_layer/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
rr_layer-0.1.1.dist-info/METADATA,sha256=HJNXocCuUI5vbPAZSj2IJI-WibvTzWLeZn1HDiAc68Q,4369
|
|
5
|
+
rr_layer-0.1.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
rr_layer-0.1.1.dist-info/licenses/LICENSE,sha256=QQKXj7kEw2yUJxPDzHUnEPRO7YweIU6AAlwPLnhtinA,1090
|
|
7
|
+
rr_layer-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jad Mounayer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|