grx-tensor 0.1.0

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.
@@ -0,0 +1,50 @@
1
+ # =============================================================
2
+ # Makefile.mingw — Windows (MinGW-w64 / MSYS2)
3
+ #
4
+ # Compila grx_core.c DIRECTAMENTE en lib/grx/ (sin archivo intermedio).
5
+ # El único .dll vive en lib/grx/grx_core.dll
6
+ #
7
+ # Requisitos:
8
+ # MSYS2: pacman -S mingw-w64-x86_64-gcc
9
+ #
10
+ # Uso:
11
+ # make -f Makefile.mingw → compila
12
+ # make -f Makefile.mingw clean → elimina el .dll de lib/grx/
13
+ # =============================================================
14
+
15
+ CC = x86_64-w64-mingw32-gcc
16
+ CFLAGS = -O3 -march=native -ffast-math -funroll-loops \
17
+ -Wall -Wextra -std=c11
18
+ LDFLAGS = -lm
19
+ SRC_DIR = ../grx
20
+ SRC = $(SRC_DIR)/grx_core.c
21
+ HEADER = $(SRC_DIR)/grx_core.h
22
+
23
+ # Destino final — directamente en lib/grx/
24
+ OUT_DIR = ../../lib/grx
25
+ LIB = grx_core.dll
26
+ TARGET = $(OUT_DIR)/$(LIB)
27
+
28
+ # __declspec(dllexport) ya está en el header vía GRX_API
29
+ SHARED = -shared -Wl,--out-implib,$(OUT_DIR)/libgrx_core.a
30
+
31
+ AVX2_TEST := $(shell echo 'int main(){}' | $(CC) -mavx2 -mfma -x c - -o NUL 2>&1)
32
+ ifeq ($(AVX2_TEST),)
33
+ CFLAGS += -mavx2 -mfma
34
+ $(info [GRX] AVX2 + FMA habilitados)
35
+ else
36
+ $(info [GRX] Sin AVX2 — modo escalar)
37
+ endif
38
+
39
+ .PHONY: all clean
40
+
41
+ all: $(TARGET)
42
+
43
+ $(TARGET): $(SRC) $(HEADER)
44
+ mkdir -p $(OUT_DIR)
45
+ $(CC) $(CFLAGS) $(SHARED) $(SRC) -o $(TARGET) $(LDFLAGS)
46
+ @echo [GRX] Compilado → $(TARGET)
47
+
48
+ clean:
49
+ rm -f "$(TARGET)" "$(OUT_DIR)/libgrx_core.a" 2>/dev/null || true
50
+ @echo [GRX] Limpiado: $(TARGET)
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/grx/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "grx-tensor"
7
+ spec.version = GRX::VERSION
8
+ spec.authors = ["Angel Gabriel Garcia Razo"]
9
+ spec.email = ["garabatoangelopolis@gmail.com"]
10
+
11
+ spec.summary = "Tensor framework for Ruby with autograd and a C+SIMD compute core"
12
+ spec.description = <<~DESC
13
+ GRX brings PyTorch-style tensor operations to Ruby. Every arithmetic op,
14
+ activation, and optimizer step runs through a native C library compiled
15
+ with AVX2+FMA SIMD. Ruby is the interface — C does the work.
16
+
17
+ Features: autograd, SGD/Adam optimizers, Linear/Sequential/Dropout/BatchNorm
18
+ layers, MSE/BCE/CrossEntropy loss functions, Xavier and He weight init.
19
+ Cross-platform: .so on Linux, .dylib on macOS, .dll on Windows.
20
+ DESC
21
+
22
+ spec.homepage = "https://github.com/Gabo-Razo/grx-tensor"
23
+ spec.license = "MIT"
24
+
25
+ spec.required_ruby_version = ">= 3.0.0"
26
+
27
+ spec.metadata = {
28
+ "homepage_uri" => spec.homepage,
29
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
30
+ "bug_tracker_uri" => "#{spec.homepage}/issues"
31
+ }
32
+
33
+ spec.files = Dir[
34
+ "lib/**/*.rb",
35
+ "ext/grx/**/*.{c,h,rb}",
36
+ "ext/unix/Makefile",
37
+ "ext/windows/Makefile.mingw",
38
+ "*.gemspec",
39
+ "README.md",
40
+ "LICENSE.txt",
41
+ "CHANGELOG.md"
42
+ ]
43
+
44
+ spec.require_paths = ["lib"]
45
+
46
+ # rake-compiler compiles ext/grx/extconf.rb on `gem install`
47
+ spec.extensions = ["ext/grx/extconf.rb"]
48
+
49
+ spec.post_install_message = <<~MSG
50
+
51
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52
+ GRX-Tensor #{GRX::VERSION} installed
53
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54
+
55
+ Compile the C extension to enable AVX2+FMA SIMD:
56
+
57
+ Linux / macOS: make -C ext/unix
58
+ Windows: make -C ext/windows -f Makefile.mingw
59
+
60
+ Without it, GRX runs in pure Ruby fallback mode (slower but correct).
61
+
62
+ Quick start:
63
+
64
+ require "grx"
65
+
66
+ a = GRX.tensor([1.0, 2.0, 3.0], [3], requires_grad: true)
67
+ b = GRX.tensor([4.0, 5.0, 6.0], [3], requires_grad: true)
68
+ c = a + b
69
+ c.backward
70
+ puts a.grad.to_a # [1.0, 1.0, 1.0]
71
+
72
+ net = GRX::NN::Sequential.new(
73
+ GRX::NN::Linear.new(4, 16),
74
+ GRX::NN::ReLU.new,
75
+ GRX::NN::Linear.new(16, 1)
76
+ )
77
+ opt = GRX::Optim::Adam.new(net.parameters, lr: 0.001)
78
+
79
+ Docs: https://github.com/Gabo-Razo/grx-tensor
80
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81
+
82
+ MSG
83
+
84
+ spec.add_development_dependency "rake", "~> 13.0"
85
+ spec.add_development_dependency "rake-compiler", "~> 1.2"
86
+ spec.add_development_dependency "minitest", "~> 5.0"
87
+ spec.add_development_dependency "bundler", "~> 2.0"
88
+ end
data/lib/grx/c_api.rb ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+ require "fiddle/import"
5
+
6
+ module GRX
7
+ module CAPI
8
+ extend Fiddle::Importer
9
+
10
+ LIB_NAME = case RUBY_PLATFORM
11
+ when /mingw|mswin|windows/i then "grx_core.dll"
12
+ when /darwin/i then "libgrx_core.dylib"
13
+ else "libgrx_core.so"
14
+ end
15
+
16
+ # rake-compiler siempre genera el archivo como "grx_core.so" / "grx_core.bundle" / "grx_core.dll"
17
+ # (sin el prefijo "lib"), y lo pone un nivel arriba de lib/grx/
18
+ RAKE_COMPILER_NAME = case RUBY_PLATFORM
19
+ when /mingw|mswin|windows/i then "grx_core.dll"
20
+ when /darwin/i then "grx_core.bundle"
21
+ else "grx_core.so"
22
+ end
23
+
24
+ LIB_PATHS = [
25
+ # 1. make -C ext/unix → lib/grx/libgrx_core.so
26
+ File.expand_path(LIB_NAME, __dir__),
27
+ # 2. gem install (rake-compiler) → lib/grx_core.so (un nivel arriba)
28
+ File.expand_path("../#{RAKE_COMPILER_NAME}", __dir__),
29
+ # 3. gem install en Ruby versioned path → lib/ruby/X.X.X/grx_core.so
30
+ File.expand_path("../../#{RAKE_COMPILER_NAME}", __dir__),
31
+ # 4. desarrollo local sin instalar
32
+ File.expand_path("../../ext/grx/#{LIB_NAME}", __dir__),
33
+ ].freeze
34
+
35
+ LOADED = begin
36
+ path = LIB_PATHS.find { |p| File.exist?(p) }
37
+ raise Fiddle::DLError, "No se encontró #{LIB_NAME} en #{LIB_PATHS.inspect}" unless path
38
+ dlload path
39
+ true
40
+ rescue Fiddle::DLError => e
41
+ warn "[GRX] Extensión C no disponible: #{e.message}\n" \
42
+ " → Ejecuta: make -C ext/unix install\n" \
43
+ " → Corriendo en modo Ruby puro (sin SIMD)."
44
+ false
45
+ end
46
+
47
+ if LOADED
48
+ # Memoria
49
+ extern "double* grx_alloc(size_t)"
50
+ extern "void grx_free(double*)"
51
+
52
+ # Aritmética element-wise
53
+ extern "void grx_add (double*, double*, double*, size_t)"
54
+ extern "void grx_sub (double*, double*, double*, size_t)"
55
+ extern "void grx_mul (double*, double*, double*, size_t)"
56
+ extern "void grx_div (double*, double*, double*, size_t)"
57
+ extern "void grx_scale (double*, double, double*, size_t)"
58
+ extern "void grx_add_scalar(double*, double, double*, size_t)"
59
+ extern "void grx_negate (double*, double*, size_t)"
60
+
61
+ # Matemáticas element-wise
62
+ extern "void grx_abs (double*, double*, size_t)"
63
+ extern "void grx_sqrt (double*, double*, size_t)"
64
+ extern "void grx_square(double*, double*, size_t)"
65
+ extern "void grx_log (double*, double*, size_t)"
66
+ extern "void grx_exp (double*, double*, size_t)"
67
+ extern "void grx_pow (double*, double, double*, size_t)"
68
+ extern "void grx_clip (double*, double, double, double*, size_t)"
69
+
70
+ # Reducciones
71
+ extern "double grx_sum (double*, size_t)"
72
+ extern "double grx_mean(double*, size_t)"
73
+ extern "double grx_max (double*, size_t)"
74
+ extern "double grx_min (double*, size_t)"
75
+
76
+ # Álgebra lineal
77
+ extern "double grx_dot (double*, double*, size_t)"
78
+ extern "void grx_matmul (double*, double*, double*, size_t, size_t, size_t)"
79
+
80
+ # Activaciones
81
+ extern "void grx_relu (double*, double*, size_t)"
82
+ extern "void grx_leaky_relu (double*, double, double*, size_t)"
83
+ extern "void grx_tanh_act (double*, double*, size_t)"
84
+ extern "void grx_sigmoid (double*, double*, size_t)"
85
+ extern "void grx_softmax (double*, double*, size_t)"
86
+
87
+ # Optimizadores
88
+ extern "void grx_sgd_step (double*, double*, double, size_t)"
89
+ extern "void grx_adam_step(double*, double*, double*, double*, double, double, double, double, double, double, size_t)"
90
+
91
+ # Inicialización de pesos
92
+ extern "void grx_init_xavier_uniform(double*, size_t, size_t, size_t)"
93
+ extern "void grx_init_he_normal (double*, size_t, size_t)"
94
+ end
95
+ end
96
+ end
data/lib/grx/errors.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRX
4
+ class Error < StandardError; end
5
+ class ShapeError < Error; end
6
+ class DimensionError < Error; end
7
+ class StorageError < Error; end
8
+ end
data/lib/grx/loss.rb ADDED
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRX
4
+ module Loss
5
+ # ================================================================
6
+ # MSELoss — Mean Squared Error
7
+ # L = mean((pred - target)^2) → retorna Float
8
+ # ================================================================
9
+ class MSELoss
10
+ def call(pred, target)
11
+ raise ShapeError, "Shapes incompatibles" if pred.shape != target.shape
12
+ (pred - target).square.mean
13
+ end
14
+ end
15
+
16
+ # ================================================================
17
+ # MAELoss — Mean Absolute Error
18
+ # L = mean(|pred - target|)
19
+ # ================================================================
20
+ class MAELoss
21
+ def call(pred, target)
22
+ raise ShapeError, "Shapes incompatibles" if pred.shape != target.shape
23
+ (pred - target).abs.mean
24
+ end
25
+ end
26
+
27
+ # ================================================================
28
+ # BCELoss — Binary Cross-Entropy
29
+ # L = -mean(t*log(p) + (1-t)*log(1-p))
30
+ # pred debe estar en (0,1) — aplica sigmoid antes si usas logits.
31
+ # ================================================================
32
+ class BCELoss
33
+ EPS = 1e-7
34
+
35
+ def call(pred, target)
36
+ raise ShapeError, "Shapes incompatibles" if pred.shape != target.shape
37
+ p_data = pred.to_a.map { |v| v < EPS ? EPS : (v > 1-EPS ? 1-EPS : v) }
38
+ t_data = target.to_a
39
+ total = p_data.size.to_f
40
+ loss = p_data.each_with_index.sum do |p, i|
41
+ t = t_data[i]
42
+ -(t * Math.log(p) + (1 - t) * Math.log(1 - p))
43
+ end
44
+ loss / total
45
+ end
46
+ end
47
+
48
+ # ================================================================
49
+ # CrossEntropyLoss — Softmax + NLL
50
+ # L = -mean(sum(target * log(softmax(logits))))
51
+ # ================================================================
52
+ class CrossEntropyLoss
53
+ EPS = 1e-7
54
+
55
+ def call(logits, target)
56
+ raise ShapeError, "Shapes incompatibles" if logits.shape != target.shape
57
+ probs = logits.softmax.to_a.map { |v| v < EPS ? EPS : v }
58
+ t_data = target.to_a
59
+ loss = probs.each_with_index.sum { |p, i| -t_data[i] * Math.log(p) }
60
+ loss / probs.size.to_f
61
+ end
62
+ end
63
+
64
+ # ================================================================
65
+ # HuberLoss — Smooth L1
66
+ # ================================================================
67
+ class HuberLoss
68
+ def initialize(delta: 1.0)
69
+ @delta = delta
70
+ end
71
+
72
+ def call(pred, target)
73
+ raise ShapeError, "Shapes incompatibles" if pred.shape != target.shape
74
+ d = @delta
75
+ diffs = (pred - target).abs.to_a
76
+ loss = diffs.sum { |v| v <= d ? 0.5 * v * v : d * (v - 0.5 * d) }
77
+ loss / diffs.size.to_f
78
+ end
79
+ end
80
+ end
81
+ end
data/lib/grx/nn.rb ADDED
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRX
4
+ module NN
5
+ # ================================================================
6
+ # Module — clase base para todas las capas
7
+ # ================================================================
8
+ class Module
9
+ # Retorna todos los parámetros entrenables (para pasarlos al optimizador)
10
+ def parameters
11
+ instance_variables.flat_map do |var|
12
+ val = instance_variable_get(var)
13
+ case val
14
+ when Tensor then val.requires_grad ? [val] : []
15
+ when Module then val.parameters
16
+ when Array then val.flat_map { |v|
17
+ case v
18
+ when Tensor then v.requires_grad ? [v] : []
19
+ when Module then v.parameters
20
+ else []
21
+ end
22
+ }
23
+ else []
24
+ end
25
+ end
26
+ end
27
+
28
+ def zero_grad
29
+ parameters.each(&:zero_grad!)
30
+ end
31
+
32
+ # Subclases implementan forward
33
+ def call(*args)
34
+ forward(*args)
35
+ end
36
+ end
37
+
38
+ # ================================================================
39
+ # Linear — Capa densa (fully connected)
40
+ # y = x @ W^T + b
41
+ # ================================================================
42
+ class Linear < Module
43
+ attr_reader :weight, :bias
44
+
45
+ def initialize(in_features, out_features, bias: true)
46
+ @in_features = in_features
47
+ @out_features = out_features
48
+ @use_bias = bias
49
+
50
+ # Pesos: Xavier uniform (bueno para tanh/sigmoid)
51
+ @weight = Tensor.xavier_uniform([out_features, in_features], requires_grad: true)
52
+
53
+ # Bias: ceros
54
+ @bias = bias ? Tensor.zeros([out_features], requires_grad: true) : nil
55
+ end
56
+
57
+ def forward(x)
58
+ # x: [batch, in_features] → out: [batch, out_features]
59
+ # out = x @ W^T
60
+ out = x.matmul(@weight.transpose)
61
+
62
+ if @use_bias
63
+ # Sumamos bias fila por fila.
64
+ # Repetimos @bias batch_size veces para crear un tensor [batch, out_features]
65
+ # que comparte el grafo con @bias original.
66
+ batch_size = x.shape[0]
67
+ # Tile del bias: concatenamos el mismo tensor bias_size veces
68
+ # usando operaciones que mantienen el grafo conectado
69
+ bias_tiled = _tile_bias(@bias, batch_size, @out_features)
70
+ out + bias_tiled
71
+ else
72
+ out
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ # Crea un tensor [batch, out_features] repitiendo @bias batch veces.
79
+ # Usa add_scalar(0) para crear un nuevo nodo conectado al bias en el grafo.
80
+ def _tile_bias(bias, batch_size, out_features)
81
+ # Construimos el tensor tileado sumando el bias a un tensor de ceros
82
+ # de la forma correcta. Esto conecta el grafo al bias original.
83
+ data = Array.new(batch_size) { bias.to_a }.flatten
84
+ tiled = GRX::Tensor.create(data, [batch_size, out_features])
85
+ # Conectamos al bias original via suma con ceros — mantiene el grafo
86
+ zero_row = GRX::Tensor.zeros([batch_size, out_features])
87
+ result = zero_row + tiled
88
+ # Registramos manualmente la conexión al bias para backprop
89
+ if bias.requires_grad
90
+ result.requires_grad = true
91
+ result._grafo_hijos << bias
92
+ b = bias
93
+ bf = result.backward_fn
94
+ result.backward_fn = ->(g) {
95
+ bf&.call(g)
96
+ # Acumulamos gradiente en bias: suma sobre el batch
97
+ grad_data = g.to_a.each_slice(out_features).reduce([0.0]*out_features) { |acc, row|
98
+ acc.zip(row).map { |a, r| a + r }
99
+ }
100
+ b.agregar_gradiente(GRX::Tensor.create(grad_data, [out_features]))
101
+ }
102
+ end
103
+ result
104
+ end
105
+
106
+ public
107
+
108
+ def to_s
109
+ "Linear(#{@in_features} → #{@out_features}, bias: #{@use_bias})"
110
+ end
111
+ end
112
+
113
+ # ================================================================
114
+ # Sequential — Contenedor de capas en secuencia
115
+ # ================================================================
116
+ class Sequential < Module
117
+ def initialize(*layers)
118
+ @layers = layers
119
+ end
120
+
121
+ def forward(x)
122
+ @layers.reduce(x) { |input, layer| layer.call(input) }
123
+ end
124
+
125
+ def parameters
126
+ @layers.flat_map(&:parameters)
127
+ end
128
+
129
+ def to_s
130
+ layers_str = @layers.each_with_index.map { |l, i| " (#{i}): #{l}" }.join("\n")
131
+ "Sequential(\n#{layers_str}\n)"
132
+ end
133
+ end
134
+
135
+ # ================================================================
136
+ # Activaciones como capas (para usar en Sequential)
137
+ # ================================================================
138
+ class ReLU < Module
139
+ def forward(x) = x.relu
140
+ def to_s = "ReLU()"
141
+ end
142
+
143
+ class LeakyReLU < Module
144
+ def initialize(alpha = 0.01)
145
+ @alpha = alpha
146
+ end
147
+ def forward(x) = x.leaky_relu(@alpha)
148
+ def to_s = "LeakyReLU(alpha=#{@alpha})"
149
+ end
150
+
151
+ class Tanh < Module
152
+ def forward(x) = x.tanh
153
+ def to_s = "Tanh()"
154
+ end
155
+
156
+ class Sigmoid < Module
157
+ def forward(x) = x.sigmoid
158
+ def to_s = "Sigmoid()"
159
+ end
160
+
161
+ class Softmax < Module
162
+ def forward(x) = x.softmax
163
+ def to_s = "Softmax()"
164
+ end
165
+
166
+ # ================================================================
167
+ # Dropout — regularización durante entrenamiento
168
+ # ================================================================
169
+ class Dropout < Module
170
+ def initialize(p = 0.5)
171
+ @p = p
172
+ @training = true
173
+ end
174
+
175
+ def train!; @training = true; self; end
176
+ def eval!; @training = false; self; end
177
+
178
+ def forward(x)
179
+ return x unless @training && @p > 0
180
+
181
+ # Máscara binaria: 1 con prob (1-p), 0 con prob p
182
+ # Escalamos por 1/(1-p) para mantener la esperanza (inverted dropout)
183
+ scale = 1.0 / (1.0 - @p)
184
+ mask_data = x.to_a.map { rand > @p ? scale : 0.0 }
185
+ mask = Tensor.create(mask_data, x.shape)
186
+ x * mask
187
+ end
188
+
189
+ def to_s = "Dropout(p=#{@p})"
190
+ end
191
+
192
+ # ================================================================
193
+ # BatchNorm1d — Normalización por batch
194
+ # Estabiliza el entrenamiento de redes profundas.
195
+ # ================================================================
196
+ class BatchNorm1d < Module
197
+ def initialize(num_features, epsilon: 1e-5, momentum: 0.1)
198
+ @num_features = num_features
199
+ @epsilon = epsilon
200
+ @momentum = momentum
201
+ @training = true
202
+
203
+ # Parámetros entrenables
204
+ @gamma = Tensor.ones([num_features], requires_grad: true)
205
+ @beta = Tensor.zeros([num_features], requires_grad: true)
206
+
207
+ # Estadísticas corrientes (no entrenables, para inferencia)
208
+ @running_mean = Tensor.zeros([num_features])
209
+ @running_var = Tensor.ones([num_features])
210
+ end
211
+
212
+ def train!; @training = true; self; end
213
+ def eval!; @training = false; self; end
214
+
215
+ def forward(x)
216
+ # x: [batch, num_features]
217
+ batch_size = x.shape[0]
218
+
219
+ if @training
220
+ # Calculamos media y varianza del batch
221
+ batch_data = x.to_a
222
+ means = Array.new(@num_features) do |j|
223
+ batch_data.each_slice(@num_features).map { |row| row[j] }.sum / batch_size
224
+ end
225
+ vars = Array.new(@num_features) do |j|
226
+ col = batch_data.each_slice(@num_features).map { |row| row[j] }
227
+ col.sum { |v| (v - means[j]) ** 2 } / batch_size
228
+ end
229
+
230
+ # Actualizamos estadísticas corrientes
231
+ means.each_with_index do |m, j|
232
+ rm = @running_mean.to_a; rm[j] = (1 - @momentum) * rm[j] + @momentum * m
233
+ @running_mean = Tensor.create(rm, [@num_features])
234
+ end
235
+ vars.each_with_index do |v, j|
236
+ rv = @running_var.to_a; rv[j] = (1 - @momentum) * rv[j] + @momentum * v
237
+ @running_var = Tensor.create(rv, [@num_features])
238
+ end
239
+
240
+ mean_t = Tensor.create(means, [@num_features])
241
+ var_t = Tensor.create(vars, [@num_features])
242
+ else
243
+ mean_t = @running_mean
244
+ var_t = @running_var
245
+ end
246
+
247
+ # Normalizamos: x_hat = (x - mean) / sqrt(var + eps)
248
+ # Luego escalamos: y = gamma * x_hat + beta
249
+ norm_data = x.to_a.each_slice(@num_features).flat_map do |row|
250
+ row.each_with_index.map do |v, j|
251
+ x_hat = (v - mean_t.to_a[j]) / Math.sqrt(var_t.to_a[j] + @epsilon)
252
+ @gamma.to_a[j] * x_hat + @beta.to_a[j]
253
+ end
254
+ end
255
+
256
+ Tensor.create(norm_data, x.shape)
257
+ end
258
+
259
+ def to_s = "BatchNorm1d(#{@num_features})"
260
+ end
261
+ end
262
+ end