my_math_gem 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.
- checksums.yaml +7 -0
- data/LICENSE +11 -0
- data/README.md +0 -0
- data/lib/my_math_gem/calculus.rb +113 -0
- data/lib/my_math_gem/complex_number.rb +85 -0
- data/lib/my_math_gem/data_processing.rb +176 -0
- data/lib/my_math_gem/fourier.rb +115 -0
- data/lib/my_math_gem/linear_algebra.rb +126 -0
- data/lib/my_math_gem/ode_solver.rb +142 -0
- data/lib/my_math_gem/optimization.rb +162 -0
- data/lib/my_math_gem/statistics.rb +298 -0
- data/lib/my_math_gem/version.rb +3 -0
- data/lib/my_math_gem.rb +23 -0
- data/my_math_gem.gemspec +30 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2e0a3e729a12b6f393f4e517d1aa7ea6fd1b323f17ce0d24323eb3e7e8994cb0
|
|
4
|
+
data.tar.gz: 64db298a15dda3aa7a95abf877568d3d18b58846830062ca13e01b3791153b59
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: becf314c18f3ac4f52b4eab65b1d1380fff5e6a985c34e40b2689e0203d8f4e7aec5d7b6cb7c3011ab0ee812ed7fa1a45b2b201dbe3163550e9f98e7413ba67b
|
|
7
|
+
data.tar.gz: 7dfffd172439642ab57bcab54e50d0e5fd2e6403dd89e1ea9f884399e1b997e34e176bb31667f194f89ee9debd9aaae3d12dc944f2f978238aaf87b704559184
|
data/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andri
|
|
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
|
+
...
|
data/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module MyMathGem
|
|
2
|
+
module Calculus
|
|
3
|
+
# Turunan orde n menggunakan finite difference (central difference)
|
|
4
|
+
def self.derivative_n(f, x, n=1, h=1e-5)
|
|
5
|
+
raise ArgumentError, "n harus positif integer" unless n.is_a?(Integer) && n > 0
|
|
6
|
+
raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
|
|
7
|
+
|
|
8
|
+
# Turunan orde 1
|
|
9
|
+
if n == 1
|
|
10
|
+
return derivative(f, x, h, :central)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Rekursif turunan orde lebih tinggi
|
|
14
|
+
lower_order = ->(t) { derivative_n(f, t, n - 1, h) }
|
|
15
|
+
derivative(lower_order, x, h, :central)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Turunan parsial fungsi multivariabel f, terhadap variabel index var_idx (0-based)
|
|
19
|
+
# args = array nilai input [x,y,z,...]
|
|
20
|
+
def self.partial_derivative(f, args, var_idx, h=1e-5)
|
|
21
|
+
raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
|
|
22
|
+
raise ArgumentError, "args harus array" unless args.is_a?(Array)
|
|
23
|
+
raise ArgumentError, "var_idx di luar jangkauan" unless var_idx.between?(0, args.length-1)
|
|
24
|
+
|
|
25
|
+
args_forward = args.dup
|
|
26
|
+
args_backward = args.dup
|
|
27
|
+
args_forward[var_idx] += h
|
|
28
|
+
args_backward[var_idx] -= h
|
|
29
|
+
(f.call(*args_forward) - f.call(*args_backward)) / (2 * h)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Integral adaptif sederhana menggunakan recursive Simpson's rule
|
|
33
|
+
def self.adaptive_integral(f, a, b, tol=1e-6, max_recursion=20)
|
|
34
|
+
raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
|
|
35
|
+
|
|
36
|
+
simpson = ->(fa, fm, fb, h) { h * (fa + 4*fm + fb) / 6.0 }
|
|
37
|
+
|
|
38
|
+
recursive_adaptive = lambda do |f, a, b, fa, fm, fb, integral, tol, depth|
|
|
39
|
+
mid = (a + b) / 2.0
|
|
40
|
+
f1 = f.call((a + mid) / 2.0)
|
|
41
|
+
f2 = f.call((mid + b) / 2.0)
|
|
42
|
+
left = simpson.call(fa, f1, fm, (mid - a))
|
|
43
|
+
right = simpson.call(fm, f2, fb, (b - mid))
|
|
44
|
+
if depth >= max_recursion || (left + right - integral).abs < 15 * tol
|
|
45
|
+
return left + right + (left + right - integral) / 15.0
|
|
46
|
+
else
|
|
47
|
+
left_int = recursive_adaptive.call(f, a, mid, fa, f1, fm, left, tol/2, depth + 1)
|
|
48
|
+
right_int = recursive_adaptive.call(f, mid, b, fm, f2, fb, right, tol/2, depth + 1)
|
|
49
|
+
left_int + right_int
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
fa = f.call(a)
|
|
54
|
+
fb = f.call(b)
|
|
55
|
+
mid = (a + b) / 2.0
|
|
56
|
+
fm = f.call(mid)
|
|
57
|
+
initial = simpson.call(fa, fm, fb, (b - a))
|
|
58
|
+
|
|
59
|
+
recursive_adaptive.call(f, a, b, fa, fm, fb, initial, tol, 0)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Turunan numerik standar fungsi f di titik x
|
|
63
|
+
# Metode: :central (default), :forward, :backward
|
|
64
|
+
def self.derivative(f, x, h=1e-5, method=:central)
|
|
65
|
+
raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
|
|
66
|
+
|
|
67
|
+
case method
|
|
68
|
+
when :central
|
|
69
|
+
(f.call(x + h) - f.call(x - h)) / (2 * h)
|
|
70
|
+
when :forward
|
|
71
|
+
(f.call(x + h) - f.call(x)) / h
|
|
72
|
+
when :backward
|
|
73
|
+
(f.call(x) - f.call(x - h)) / h
|
|
74
|
+
else
|
|
75
|
+
raise ArgumentError, "Method tidak valid. Gunakan :central, :forward, atau :backward"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Integral numerik fungsi f dari a ke b
|
|
80
|
+
# Metode: :trapezoid (default) atau :simpson
|
|
81
|
+
# n = jumlah segmen, genap jika pakai Simpson
|
|
82
|
+
def self.integral(f, a, b, n=1000, method=:trapezoid)
|
|
83
|
+
raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
|
|
84
|
+
raise ArgumentError, "n harus positif integer" unless n.is_a?(Integer) && n > 0
|
|
85
|
+
raise ArgumentError, "Untuk Simpson, n harus genap" if method == :simpson && n.odd?
|
|
86
|
+
|
|
87
|
+
case method
|
|
88
|
+
when :trapezoid
|
|
89
|
+
dx = (b - a).to_f / n
|
|
90
|
+
total = 0.0
|
|
91
|
+
n.times do |i|
|
|
92
|
+
x0 = a + i * dx
|
|
93
|
+
x1 = x0 + dx
|
|
94
|
+
total += 0.5 * (f.call(x0) + f.call(x1)) * dx
|
|
95
|
+
end
|
|
96
|
+
total
|
|
97
|
+
|
|
98
|
+
when :simpson
|
|
99
|
+
dx = (b - a).to_f / n
|
|
100
|
+
sum = f.call(a) + f.call(b)
|
|
101
|
+
(1...n).each do |i|
|
|
102
|
+
x = a + i * dx
|
|
103
|
+
weight = i.odd? ? 4 : 2
|
|
104
|
+
sum += weight * f.call(x)
|
|
105
|
+
end
|
|
106
|
+
(dx / 3) * sum
|
|
107
|
+
|
|
108
|
+
else
|
|
109
|
+
raise ArgumentError, "Method tidak valid. Gunakan :trapezoid atau :simpson"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module MyMathGem
|
|
2
|
+
module ComplexNumber
|
|
3
|
+
Complex = Struct.new(:re, :im) do
|
|
4
|
+
TOLERANCE = 1e-12
|
|
5
|
+
|
|
6
|
+
def +(other)
|
|
7
|
+
Complex.new(self.re + other.re, self.im + other.im)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def -(other)
|
|
11
|
+
Complex.new(self.re - other.re, self.im - other.im)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def -@
|
|
15
|
+
Complex.new(-self.re, -self.im)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def *(other)
|
|
19
|
+
Complex.new(
|
|
20
|
+
self.re * other.re - self.im * other.im,
|
|
21
|
+
self.re * other.im + self.im * other.re
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def /(other)
|
|
26
|
+
denom = other.re**2 + other.im**2
|
|
27
|
+
raise ZeroDivisionError, "Pembagian dengan nol" if denom == 0
|
|
28
|
+
Complex.new(
|
|
29
|
+
(self.re * other.re + self.im * other.im) / denom.to_f,
|
|
30
|
+
(self.im * other.re - self.re * other.im) / denom.to_f
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def modulus
|
|
35
|
+
Math.sqrt(re**2 + im**2)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Argumen (fase) bilangan kompleks, hasil dalam radian (-π sampai π)
|
|
39
|
+
def arg
|
|
40
|
+
Math.atan2(im, re)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def conjugate
|
|
44
|
+
Complex.new(re, -im)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Buat bilangan kompleks dari polar form: magnitude r, angle theta (radian)
|
|
48
|
+
def self.from_polar(r, theta)
|
|
49
|
+
new(r * Math.cos(theta), r * Math.sin(theta))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
if im >= 0
|
|
54
|
+
"#{re} + #{im}i"
|
|
55
|
+
else
|
|
56
|
+
"#{re} - #{-im}i"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_a
|
|
61
|
+
[re, im]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Cek kesamaan dengan toleransi floating point
|
|
65
|
+
def ==(other)
|
|
66
|
+
return false unless other.is_a?(Complex)
|
|
67
|
+
(self.re - other.re).abs < TOLERANCE && (self.im - other.im).abs < TOLERANCE
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Eksponensial bilangan kompleks (e^(a+bi))
|
|
71
|
+
def exp
|
|
72
|
+
e_to_re = Math.exp(re)
|
|
73
|
+
Complex.new(
|
|
74
|
+
e_to_re * Math.cos(im),
|
|
75
|
+
e_to_re * Math.sin(im)
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Logaritma bilangan kompleks (natural log)
|
|
80
|
+
def log
|
|
81
|
+
Complex.new(Math.log(modulus), arg)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
module MyMathGem
|
|
2
|
+
module DataProcessing
|
|
3
|
+
# Buang nil atau NaN dari data
|
|
4
|
+
def self.clean_data(data)
|
|
5
|
+
raise ArgumentError, "Data harus array" unless data.is_a?(Array)
|
|
6
|
+
data.compact.reject { |x| x.respond_to?(:nan?) && x.nan? }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.mean(data)
|
|
10
|
+
data = clean_data(data)
|
|
11
|
+
raise ArgumentError, "Data harus tidak kosong" if data.empty?
|
|
12
|
+
data.sum.to_f / data.size
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.weighted_mean(data, weights)
|
|
16
|
+
data = clean_data(data)
|
|
17
|
+
raise ArgumentError, "Data dan weights harus array sama panjang dan tidak kosong" if data.size == 0 || data.size != weights.size
|
|
18
|
+
total_weight = weights.sum.to_f
|
|
19
|
+
raise ArgumentError, "Total bobot tidak boleh nol" if total_weight == 0
|
|
20
|
+
weighted_sum = data.zip(weights).sum { |v, w| v * w }
|
|
21
|
+
weighted_sum / total_weight
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.median(data)
|
|
25
|
+
data = clean_data(data)
|
|
26
|
+
raise ArgumentError, "Data harus tidak kosong" if data.empty?
|
|
27
|
+
sorted = data.sort
|
|
28
|
+
mid = sorted.size / 2
|
|
29
|
+
sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]).to_f / 2
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.trimmed_mean(data, trim_ratio=0.1)
|
|
33
|
+
data = clean_data(data)
|
|
34
|
+
raise ArgumentError, "trim_ratio harus antara 0 dan 0.5" unless trim_ratio.is_a?(Numeric) && trim_ratio >= 0 && trim_ratio <= 0.5
|
|
35
|
+
raise ArgumentError, "Data harus cukup besar untuk trimming" if data.size < 2
|
|
36
|
+
sorted = data.sort
|
|
37
|
+
trim_count = (trim_ratio * sorted.size).floor
|
|
38
|
+
trimmed = sorted[trim_count...-trim_count] || []
|
|
39
|
+
raise ArgumentError, "Trimmed data kosong" if trimmed.empty?
|
|
40
|
+
trimmed.sum.to_f / trimmed.size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.mode(data)
|
|
44
|
+
data = clean_data(data)
|
|
45
|
+
raise ArgumentError, "Data harus tidak kosong" if data.empty?
|
|
46
|
+
freq = data.each_with_object(Hash.new(0)) { |v, h| h[v] += 1 }
|
|
47
|
+
max_freq = freq.values.max
|
|
48
|
+
freq.select { |_, v| v == max_freq }.keys
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.variance(data)
|
|
52
|
+
data = clean_data(data)
|
|
53
|
+
raise ArgumentError, "Data harus minimal 2 elemen" if data.size < 2
|
|
54
|
+
m = mean(data)
|
|
55
|
+
sum_sq = data.sum { |x| (x - m)**2 }
|
|
56
|
+
sum_sq.to_f / (data.size - 1)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.standard_deviation(data)
|
|
60
|
+
Math.sqrt(variance(data))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.standard_error_mean(data)
|
|
64
|
+
sd = standard_deviation(data)
|
|
65
|
+
Math.sqrt(sd.to_f / clean_data(data).size)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.min_max_normalize(data)
|
|
69
|
+
data = clean_data(data)
|
|
70
|
+
raise ArgumentError, "Data harus tidak kosong" if data.empty?
|
|
71
|
+
min = data.min
|
|
72
|
+
max = data.max
|
|
73
|
+
range = max - min
|
|
74
|
+
raise ArgumentError, "Range data 0, normalisasi gagal" if range == 0
|
|
75
|
+
data.map { |x| (x - min).to_f / range }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.z_score_normalize(data)
|
|
79
|
+
data = clean_data(data)
|
|
80
|
+
raise ArgumentError, "Data harus minimal 2 elemen" if data.size < 2
|
|
81
|
+
m = mean(data)
|
|
82
|
+
sd = standard_deviation(data)
|
|
83
|
+
raise ArgumentError, "Standar deviasi 0, normalisasi gagal" if sd == 0
|
|
84
|
+
data.map { |x| (x - m) / sd.to_f }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.robust_scale(data)
|
|
88
|
+
# Skala menggunakan median dan IQR
|
|
89
|
+
data = clean_data(data)
|
|
90
|
+
raise ArgumentError, "Data harus minimal 4 elemen" if data.size < 4
|
|
91
|
+
med = median(data)
|
|
92
|
+
q1 = percentile(data.sort, 25)
|
|
93
|
+
q3 = percentile(data.sort, 75)
|
|
94
|
+
iqr = q3 - q1
|
|
95
|
+
raise ArgumentError, "IQR 0, robust scaling gagal" if iqr == 0
|
|
96
|
+
data.map { |x| (x - med).to_f / iqr }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.moving_average(data, w=3)
|
|
100
|
+
data = clean_data(data)
|
|
101
|
+
raise ArgumentError, "Window harus integer > 0" unless w.is_a?(Integer) && w > 0
|
|
102
|
+
return data if w == 1
|
|
103
|
+
smoothed = []
|
|
104
|
+
data.each_cons(w) { |window| smoothed << (window.sum.to_f / w) }
|
|
105
|
+
smoothed
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.filter_outliers(data)
|
|
109
|
+
data = clean_data(data)
|
|
110
|
+
raise ArgumentError, "Data harus minimal 4 elemen" if data.size < 4
|
|
111
|
+
sorted = data.sort
|
|
112
|
+
q1 = percentile(sorted, 25)
|
|
113
|
+
q3 = percentile(sorted, 75)
|
|
114
|
+
iqr = q3 - q1
|
|
115
|
+
lower_bound = q1 - 1.5 * iqr
|
|
116
|
+
upper_bound = q3 + 1.5 * iqr
|
|
117
|
+
data.select { |x| x >= lower_bound && x <= upper_bound }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.skewness(data)
|
|
121
|
+
data = clean_data(data)
|
|
122
|
+
n = data.size
|
|
123
|
+
raise ArgumentError, "Data minimal 3 elemen" if n < 3
|
|
124
|
+
m = mean(data)
|
|
125
|
+
sd = standard_deviation(data)
|
|
126
|
+
return 0 if sd == 0
|
|
127
|
+
sum_cubed = data.sum { |x| (x - m)**3 }
|
|
128
|
+
(n.to_f / ((n-1)*(n-2))) * (sum_cubed / (sd**3))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.kurtosis(data)
|
|
132
|
+
data = clean_data(data)
|
|
133
|
+
n = data.size
|
|
134
|
+
raise ArgumentError, "Data minimal 4 elemen" if n < 4
|
|
135
|
+
m = mean(data)
|
|
136
|
+
sd = standard_deviation(data)
|
|
137
|
+
return 0 if sd == 0
|
|
138
|
+
sum_quad = data.sum { |x| (x - m)**4 }
|
|
139
|
+
numerator = (n*(n+1)*sum_quad) / ((n-1)*(n-2)*(n-3)*(sd**4))
|
|
140
|
+
denominator = (3*((n-1)**2)) / ((n-2)*(n-3))
|
|
141
|
+
numerator - denominator
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.percentile(sorted_data, p)
|
|
145
|
+
raise ArgumentError, "p harus antara 0 dan 100" unless (0..100).include?(p)
|
|
146
|
+
return sorted_data.first if p == 0
|
|
147
|
+
return sorted_data.last if p == 100
|
|
148
|
+
|
|
149
|
+
rank = (p.to_f / 100) * (sorted_data.size - 1)
|
|
150
|
+
lower_idx = rank.floor
|
|
151
|
+
upper_idx = rank.ceil
|
|
152
|
+
if lower_idx == upper_idx
|
|
153
|
+
sorted_data[lower_idx]
|
|
154
|
+
else
|
|
155
|
+
lower_value = sorted_data[lower_idx]
|
|
156
|
+
upper_value = sorted_data[upper_idx]
|
|
157
|
+
fraction = rank - lower_idx
|
|
158
|
+
lower_value + fraction * (upper_value - lower_value)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.pearson_correlation(x, y)
|
|
163
|
+
x = clean_data(x)
|
|
164
|
+
y = clean_data(y)
|
|
165
|
+
raise ArgumentError, "x dan y harus array sama panjang dan > 1" if x.size <= 1 || x.size != y.size
|
|
166
|
+
mx = mean(x)
|
|
167
|
+
my = mean(y)
|
|
168
|
+
numerator = x.zip(y).sum { |xi, yi| (xi - mx) * (yi - my) }
|
|
169
|
+
denom_x = Math.sqrt(x.sum { |xi| (xi - mx)**2 })
|
|
170
|
+
denom_y = Math.sqrt(y.sum { |yi| (yi - my)**2 })
|
|
171
|
+
denom = denom_x * denom_y
|
|
172
|
+
return 0 if denom == 0
|
|
173
|
+
numerator.to_f / denom
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module MyMathGem
|
|
2
|
+
module Fourier
|
|
3
|
+
PI2 = 2 * Math::PI
|
|
4
|
+
|
|
5
|
+
# Window functions
|
|
6
|
+
module Window
|
|
7
|
+
def self.hamming(n)
|
|
8
|
+
(0...n).map { |i| 0.54 - 0.46 * Math.cos(PI2 * i / (n - 1)) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.hanning(n)
|
|
12
|
+
(0...n).map { |i| 0.5 * (1 - Math.cos(PI2 * i / (n - 1))) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.blackman(n)
|
|
16
|
+
(0...n).map { |i| 0.42 - 0.5 * Math.cos(PI2 * i / (n - 1)) + 0.08 * Math.cos(4 * Math::PI * i / (n - 1)) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Zero-pad input to next power of two length
|
|
21
|
+
def self.zero_pad_to_power_of_two(signal)
|
|
22
|
+
n = signal.size
|
|
23
|
+
target = 1
|
|
24
|
+
target <<= 1 while target < n
|
|
25
|
+
padded = signal.dup
|
|
26
|
+
padded += Array.new(target - n, 0)
|
|
27
|
+
padded
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# FFT with zero-padding if input size not power of two
|
|
31
|
+
def self.fft(signal)
|
|
32
|
+
n = signal.size
|
|
33
|
+
padded_signal = n & (n - 1) == 0 ? signal : zero_pad_to_power_of_two(signal)
|
|
34
|
+
fft_recursive(padded_signal)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.fft_recursive(signal)
|
|
38
|
+
n = signal.size
|
|
39
|
+
return [signal[0].is_a?(Complex) ? signal[0] : Complex(signal[0], 0)] if n == 1
|
|
40
|
+
|
|
41
|
+
even = fft_recursive(signal.each_slice(2).map(&:first))
|
|
42
|
+
odd = fft_recursive(signal.each_slice(2).map(&:last))
|
|
43
|
+
|
|
44
|
+
(0...n/2).flat_map do |k|
|
|
45
|
+
t = Complex.polar(1, -PI2 * k / n) * odd[k]
|
|
46
|
+
[even[k] + t, even[k] - t]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# IFFT with zero-padding support
|
|
51
|
+
def self.ifft(spectrum)
|
|
52
|
+
n = spectrum.size
|
|
53
|
+
raise ArgumentError, "Spektrum harus array" unless spectrum.is_a?(Array)
|
|
54
|
+
conjugated = spectrum.map(&:conj)
|
|
55
|
+
fft_res = fft_recursive(conjugated)
|
|
56
|
+
fft_res.map { |c| c.conj / n }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Magnitude dari spektrum
|
|
60
|
+
def self.magnitude(spectrum)
|
|
61
|
+
spectrum.map(&:abs)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Fase spektrum (radian)
|
|
65
|
+
def self.phase(spectrum)
|
|
66
|
+
spectrum.map(&:phase)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Power Spectral Density (PSD)
|
|
70
|
+
def self.power_spectral_density(spectrum)
|
|
71
|
+
magnitude(spectrum).map { |mag| mag**2 / spectrum.size }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Filter frekuensi range (indeks)
|
|
75
|
+
def self.filter_frequency(spectrum, low_idx, high_idx)
|
|
76
|
+
n = spectrum.size
|
|
77
|
+
raise ArgumentError, "Indeks frekuensi tidak valid" if low_idx < 0 || high_idx >= n || low_idx > high_idx
|
|
78
|
+
spectrum.each_with_index.map { |v, i| (i >= low_idx && i <= high_idx) ? v : Complex(0, 0) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Rekonstruksi sinyal real dari spectrum
|
|
82
|
+
def self.reconstruct_signal(spectrum)
|
|
83
|
+
idft_res = ifft(spectrum)
|
|
84
|
+
idft_res.map(&:real)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Spectrogram: bagi sinyal jadi jendela, hitung FFT tiap jendela
|
|
88
|
+
# window_size dan hop_size dalam sample count
|
|
89
|
+
# window_func: symbol :hamming, :hanning, :blackman atau nil (no window)
|
|
90
|
+
# Return array of spectra arrays
|
|
91
|
+
def self.spectrogram(signal, window_size:, hop_size:, window_func: nil)
|
|
92
|
+
window = case window_func
|
|
93
|
+
when :hamming then Window.hamming(window_size)
|
|
94
|
+
when :hanning then Window.hanning(window_size)
|
|
95
|
+
when :blackman then Window.blackman(window_size)
|
|
96
|
+
else Array.new(window_size, 1.0) # no window
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
spectrogram = []
|
|
100
|
+
(0..signal.size - window_size).step(hop_size) do |start_idx|
|
|
101
|
+
segment = signal[start_idx, window_size]
|
|
102
|
+
windowed = segment.zip(window).map { |v, w| v * w }
|
|
103
|
+
spectrum = fft(windowed)
|
|
104
|
+
spectrogram << spectrum
|
|
105
|
+
end
|
|
106
|
+
spectrogram
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Konversi indeks spektrum ke frekuensi (Hz)
|
|
110
|
+
# sample_rate dalam Hz
|
|
111
|
+
def self.index_to_frequency(index, spectrum_size, sample_rate)
|
|
112
|
+
index * sample_rate.to_f / spectrum_size
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module MyMathGem
|
|
2
|
+
module LinearAlgebra
|
|
3
|
+
# Validasi matriks (array of arrays) persegi
|
|
4
|
+
def self.square_matrix?(m)
|
|
5
|
+
return false unless m.is_a?(Array) && !m.empty?
|
|
6
|
+
n = m.length
|
|
7
|
+
m.all? { |row| row.is_a?(Array) && row.length == n }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Dot product dua vektor (array angka)
|
|
11
|
+
def self.dot(v1, v2)
|
|
12
|
+
raise ArgumentError, "Vektor harus sama panjang" unless v1.length == v2.length
|
|
13
|
+
v1.zip(v2).map { |a, b| a * b }.sum
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Norm (magnitude) vektor
|
|
17
|
+
def self.norm(v)
|
|
18
|
+
Math.sqrt(v.map { |x| x**2 }.sum)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Normalisasi vektor (unit vector)
|
|
22
|
+
def self.normalize(v)
|
|
23
|
+
n = norm(v)
|
|
24
|
+
raise ArgumentError, "Vektor nol tidak dapat dinormalisasi" if n == 0
|
|
25
|
+
v.map { |x| x.to_f / n }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Cross product dua vektor 3D
|
|
29
|
+
def self.cross(v1, v2)
|
|
30
|
+
raise ArgumentError, "Cross product hanya untuk vektor 3 dimensi" unless v1.length == 3 && v2.length == 3
|
|
31
|
+
[
|
|
32
|
+
v1[1]*v2[2] - v1[2]*v2[1],
|
|
33
|
+
v1[2]*v2[0] - v1[0]*v2[2],
|
|
34
|
+
v1[0]*v2[1] - v1[1]*v2[0]
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Perkalian matriks (mxn) dengan matriks (nxp) atau vektor (n)
|
|
39
|
+
def self.matrix_multiply(m1, m2)
|
|
40
|
+
# Jika m2 adalah vektor
|
|
41
|
+
if m2.is_a?(Array) && !m2.empty? && !m2[0].is_a?(Array)
|
|
42
|
+
raise ArgumentError, "Jumlah kolom m1 harus sama dengan panjang vektor m2" unless m1[0].length == m2.length
|
|
43
|
+
|
|
44
|
+
m1.map do |row|
|
|
45
|
+
dot(row, m2)
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
raise ArgumentError, "Jumlah kolom m1 harus sama dengan jumlah baris m2" unless m1[0].length == m2.length
|
|
49
|
+
|
|
50
|
+
result = Array.new(m1.length) { Array.new(m2[0].length, 0) }
|
|
51
|
+
m1.length.times do |i|
|
|
52
|
+
m2[0].length.times do |j|
|
|
53
|
+
sum = 0
|
|
54
|
+
m2.length.times do |k|
|
|
55
|
+
sum += m1[i][k] * m2[k][j]
|
|
56
|
+
end
|
|
57
|
+
result[i][j] = sum
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Transpose matriks
|
|
65
|
+
def self.transpose(m)
|
|
66
|
+
raise ArgumentError, "Input harus matriks (array of arrays)" unless m.is_a?(Array) && m.all? { |row| row.is_a?(Array) }
|
|
67
|
+
m[0].length.times.map { |i| m.map { |row| row[i] } }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Determinan matriks 2x2
|
|
71
|
+
def self.determinant_2x2(m)
|
|
72
|
+
raise ArgumentError, "Matriks harus 2x2" unless m.length == 2 && m.all? { |row| row.length == 2 }
|
|
73
|
+
m[0][0]*m[1][1] - m[0][1]*m[1][0]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Determinan matriks 3x3
|
|
77
|
+
def self.determinant_3x3(m)
|
|
78
|
+
raise ArgumentError, "Matriks harus 3x3" unless m.length == 3 && m.all? { |row| row.length == 3 }
|
|
79
|
+
a, b, c = m[0]
|
|
80
|
+
d, e, f = m[1]
|
|
81
|
+
g, h, i = m[2]
|
|
82
|
+
a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Invers matriks 2x2
|
|
86
|
+
def self.inverse_2x2(m)
|
|
87
|
+
det = determinant_2x2(m)
|
|
88
|
+
raise ArgumentError, "Matriks singular, determinan = 0" if det == 0
|
|
89
|
+
inv_det = 1.0 / det
|
|
90
|
+
[
|
|
91
|
+
[ m[1][1]*inv_det, -m[0][1]*inv_det ],
|
|
92
|
+
[ -m[1][0]*inv_det, m[0][0]*inv_det ]
|
|
93
|
+
]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Invers matriks 3x3 (menggunakan metode kofaktor)
|
|
97
|
+
def self.inverse_3x3(m)
|
|
98
|
+
det = determinant_3x3(m)
|
|
99
|
+
raise ArgumentError, "Matriks singular, determinan = 0" if det == 0
|
|
100
|
+
inv_det = 1.0 / det
|
|
101
|
+
|
|
102
|
+
cofactors = Array.new(3) { Array.new(3, 0) }
|
|
103
|
+
|
|
104
|
+
# Hitung kofaktor untuk tiap elemen
|
|
105
|
+
(0..2).each do |row|
|
|
106
|
+
(0..2).each do |col|
|
|
107
|
+
minor = minor_2x2(m, row, col)
|
|
108
|
+
cofactors[row][col] = ((row + col).even? ? 1 : -1) * determinant_2x2(minor)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Transpose kofaktor untuk dapat adjoin matrix
|
|
113
|
+
adjugate = transpose(cofactors)
|
|
114
|
+
|
|
115
|
+
# Kalikan dengan 1/det
|
|
116
|
+
adjugate.map { |row| row.map { |val| val * inv_det } }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Helper: matriks minor 2x2 dengan menghilangkan baris row_to_remove dan kolom col_to_remove
|
|
120
|
+
def self.minor_2x2(m, row_to_remove, col_to_remove)
|
|
121
|
+
m.each_with_index
|
|
122
|
+
.reject { |_, r| r == row_to_remove }
|
|
123
|
+
.map { |row, _| row.each_with_index.reject { |_, c| c == col_to_remove }.map(&:first) }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
module MyMathGem
|
|
2
|
+
module ODESolver
|
|
3
|
+
# Metode Euler untuk ODE y' = f(t, y)
|
|
4
|
+
def self.euler(t0:, y0:, t_end:, steps:, f:, callback: nil, return_separate: false)
|
|
5
|
+
validate_params!(t0, y0, t_end, steps, f)
|
|
6
|
+
|
|
7
|
+
dt = (t_end - t0).to_f / steps
|
|
8
|
+
ts = [t0]
|
|
9
|
+
ys = [deep_copy(y0)]
|
|
10
|
+
|
|
11
|
+
steps.times do |i|
|
|
12
|
+
t, y = ts[-1], ys[-1]
|
|
13
|
+
dy = f.call(t, y)
|
|
14
|
+
y_next = vector_add(y, scalar_multiply(dy, dt))
|
|
15
|
+
t_next = t + dt
|
|
16
|
+
|
|
17
|
+
ts << t_next
|
|
18
|
+
ys << y_next
|
|
19
|
+
|
|
20
|
+
callback.call(t_next, y_next) if callback.is_a?(Proc)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return_separate ? { times: ts, values: ys } : ts.zip(ys)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Metode Runge-Kutta orde 4 (RK4)
|
|
27
|
+
def self.runge_kutta4(t0:, y0:, t_end:, steps:, f:, callback: nil, return_separate: false)
|
|
28
|
+
validate_params!(t0, y0, t_end, steps, f)
|
|
29
|
+
|
|
30
|
+
dt = (t_end - t0).to_f / steps
|
|
31
|
+
ts = [t0]
|
|
32
|
+
ys = [deep_copy(y0)]
|
|
33
|
+
|
|
34
|
+
steps.times do
|
|
35
|
+
t, y = ts[-1], ys[-1]
|
|
36
|
+
|
|
37
|
+
k1 = f.call(t, y)
|
|
38
|
+
k2 = f.call(t + dt/2.0, vector_add(y, scalar_multiply(k1, dt/2.0)))
|
|
39
|
+
k3 = f.call(t + dt/2.0, vector_add(y, scalar_multiply(k2, dt/2.0)))
|
|
40
|
+
k4 = f.call(t + dt, vector_add(y, scalar_multiply(k3, dt)))
|
|
41
|
+
|
|
42
|
+
increment = scalar_multiply(
|
|
43
|
+
vector_add(
|
|
44
|
+
vector_add(k1, scalar_multiply(k2, 2)),
|
|
45
|
+
vector_add(scalar_multiply(k3, 2), k4)
|
|
46
|
+
),
|
|
47
|
+
dt / 6.0
|
|
48
|
+
)
|
|
49
|
+
y_next = vector_add(y, increment)
|
|
50
|
+
t_next = t + dt
|
|
51
|
+
|
|
52
|
+
ts << t_next
|
|
53
|
+
ys << y_next
|
|
54
|
+
|
|
55
|
+
callback.call(t_next, y_next) if callback.is_a?(Proc)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return_separate ? { times: ts, values: ys } : ts.zip(ys)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Metode Heun (Improved Euler)
|
|
62
|
+
def self.heun(t0:, y0:, t_end:, steps:, f:, callback: nil, return_separate: false)
|
|
63
|
+
validate_params!(t0, y0, t_end, steps, f)
|
|
64
|
+
|
|
65
|
+
dt = (t_end - t0).to_f / steps
|
|
66
|
+
ts = [t0]
|
|
67
|
+
ys = [deep_copy(y0)]
|
|
68
|
+
|
|
69
|
+
steps.times do
|
|
70
|
+
t, y = ts[-1], ys[-1]
|
|
71
|
+
|
|
72
|
+
k1 = f.call(t, y)
|
|
73
|
+
y_predict = vector_add(y, scalar_multiply(k1, dt))
|
|
74
|
+
k2 = f.call(t + dt, y_predict)
|
|
75
|
+
increment = scalar_multiply(vector_add(k1, k2), dt / 2.0)
|
|
76
|
+
y_next = vector_add(y, increment)
|
|
77
|
+
t_next = t + dt
|
|
78
|
+
|
|
79
|
+
ts << t_next
|
|
80
|
+
ys << y_next
|
|
81
|
+
|
|
82
|
+
callback.call(t_next, y_next) if callback.is_a?(Proc)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return_separate ? { times: ts, values: ys } : ts.zip(ys)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def self.validate_params!(t0, y0, t_end, steps, f)
|
|
91
|
+
raise ArgumentError, "steps harus integer positif" unless steps.is_a?(Integer) && steps > 0
|
|
92
|
+
raise ArgumentError, "t_end harus > t0" unless t_end > t0
|
|
93
|
+
raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
|
|
94
|
+
raise ArgumentError, "y0 harus scalar, array, atau nested array" unless valid_y0?(y0)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.valid_y0?(y0)
|
|
98
|
+
scalar?(y0) || vector?(y0) || matrix?(y0)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.scalar?(x)
|
|
102
|
+
x.is_a?(Numeric)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.vector?(v)
|
|
106
|
+
v.is_a?(Array) && v.all? { |e| scalar?(e) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.matrix?(m)
|
|
110
|
+
m.is_a?(Array) && m.all? { |row| vector?(row) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.vector_add(v1, v2)
|
|
114
|
+
if scalar?(v1) && scalar?(v2)
|
|
115
|
+
v1 + v2
|
|
116
|
+
elsif vector?(v1) && vector?(v2) && v1.length == v2.length
|
|
117
|
+
v1.zip(v2).map { |a,b| a + b }
|
|
118
|
+
elsif matrix?(v1) && matrix?(v2) && v1.length == v2.length
|
|
119
|
+
v1.zip(v2).map { |row1, row2| vector_add(row1, row2) }
|
|
120
|
+
else
|
|
121
|
+
raise ArgumentError, "v1 dan v2 harus tipe yang sama dan ukuran cocok"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.scalar_multiply(v, scalar)
|
|
126
|
+
if scalar?(v)
|
|
127
|
+
v * scalar
|
|
128
|
+
elsif vector?(v)
|
|
129
|
+
v.map { |x| x * scalar }
|
|
130
|
+
elsif matrix?(v)
|
|
131
|
+
v.map { |row| scalar_multiply(row, scalar) }
|
|
132
|
+
else
|
|
133
|
+
raise ArgumentError, "v harus scalar, vector, atau matrix"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Deep copy nilai y0 supaya aman dari modifikasi luar
|
|
138
|
+
def self.deep_copy(obj)
|
|
139
|
+
Marshal.load(Marshal.dump(obj))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
module MyMathGem
|
|
2
|
+
module Optimization
|
|
3
|
+
class Optimizer
|
|
4
|
+
attr_reader :x, :fx, :history
|
|
5
|
+
|
|
6
|
+
def initialize(f:, grad: nil, x_init:, learning_rate: 0.01, max_iter: 1000, tol: 1e-6,
|
|
7
|
+
method: :gd, momentum: 0.9, beta1: 0.9, beta2: 0.999, epsilon: 1e-8,
|
|
8
|
+
lr_decay: nil, decay_rate: 0.99, verbose: false, early_stopping: true)
|
|
9
|
+
@f = f
|
|
10
|
+
@grad = grad
|
|
11
|
+
@x = x_init.dup
|
|
12
|
+
@learning_rate = learning_rate.to_f
|
|
13
|
+
@max_iter = max_iter
|
|
14
|
+
@tol = tol
|
|
15
|
+
@method = method
|
|
16
|
+
@momentum = momentum
|
|
17
|
+
@beta1 = beta1
|
|
18
|
+
@beta2 = beta2
|
|
19
|
+
@epsilon = epsilon
|
|
20
|
+
@lr_decay = lr_decay # :linear, :exponential, or nil
|
|
21
|
+
@decay_rate = decay_rate
|
|
22
|
+
@verbose = verbose
|
|
23
|
+
@early_stopping = early_stopping
|
|
24
|
+
|
|
25
|
+
@v = Array.new(@x.size, 0.0) # for momentum
|
|
26
|
+
@m = Array.new(@x.size, 0.0) # for Adam
|
|
27
|
+
@s = Array.new(@x.size, 0.0) # for Adam
|
|
28
|
+
@t = 0 # timestep for Adam
|
|
29
|
+
|
|
30
|
+
@grad ||= ->(x) { numerical_gradient(@f, x) }
|
|
31
|
+
|
|
32
|
+
@history = []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def optimize
|
|
36
|
+
prev_fx = nil
|
|
37
|
+
|
|
38
|
+
@max_iter.times do |i|
|
|
39
|
+
@t += 1
|
|
40
|
+
g = @grad.call(@x)
|
|
41
|
+
raise ArgumentError, "Gradien harus array dengan panjang sama seperti x" unless valid_gradient?(g)
|
|
42
|
+
|
|
43
|
+
case @method
|
|
44
|
+
when :gd
|
|
45
|
+
step = scalar_multiply(g, @learning_rate)
|
|
46
|
+
@x = vector_subtract(@x, step)
|
|
47
|
+
when :momentum
|
|
48
|
+
@v = vector_add(scalar_multiply(@v, @momentum), scalar_multiply(g, 1 - @momentum))
|
|
49
|
+
step = scalar_multiply(@v, @learning_rate)
|
|
50
|
+
@x = vector_subtract(@x, step)
|
|
51
|
+
when :adam
|
|
52
|
+
update_adam(g)
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError, "Metode optimasi tidak dikenal: #{@method}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@fx = @f.call(@x)
|
|
58
|
+
@history << { iteration: i + 1, x: @x.dup, fx: @fx }
|
|
59
|
+
|
|
60
|
+
if @verbose
|
|
61
|
+
puts "Iterasi #{i+1}: f(x) = #{@fx}, ||grad|| = #{vector_norm(g)}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if prev_fx && @early_stopping
|
|
65
|
+
change = (prev_fx - @fx).abs
|
|
66
|
+
break if change < @tol
|
|
67
|
+
end
|
|
68
|
+
prev_fx = @fx
|
|
69
|
+
|
|
70
|
+
apply_lr_decay(i)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@x
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def update_adam(g)
|
|
79
|
+
@m = vector_add(scalar_multiply(@m, @beta1), scalar_multiply(g, 1 - @beta1))
|
|
80
|
+
@s = vector_add(scalar_multiply(@s, @beta2), scalar_multiply(elementwise_square(g), 1 - @beta2))
|
|
81
|
+
|
|
82
|
+
m_hat = scalar_multiply(@m, 1.0 / (1 - @beta1**@t))
|
|
83
|
+
s_hat = scalar_multiply(@s, 1.0 / (1 - @beta2**@t))
|
|
84
|
+
|
|
85
|
+
update = m_hat.zip(s_hat).map do |mi, si|
|
|
86
|
+
mi / (Math.sqrt(si) + @epsilon)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
step = scalar_multiply(update, @learning_rate)
|
|
90
|
+
@x = vector_subtract(@x, step)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def apply_lr_decay(iter)
|
|
94
|
+
return unless @lr_decay
|
|
95
|
+
case @lr_decay
|
|
96
|
+
when :linear
|
|
97
|
+
@learning_rate = [@learning_rate - @decay_rate * iter, 0].max
|
|
98
|
+
when :exponential
|
|
99
|
+
@learning_rate *= @decay_rate
|
|
100
|
+
else
|
|
101
|
+
# no decay
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def numerical_gradient(f, x, h=1e-8)
|
|
106
|
+
grad = Array.new(x.size, 0.0)
|
|
107
|
+
x_copy = x.dup
|
|
108
|
+
x.size.times do |i|
|
|
109
|
+
x_copy[i] = x[i] + h
|
|
110
|
+
fxh1 = f.call(x_copy)
|
|
111
|
+
x_copy[i] = x[i] - h
|
|
112
|
+
fxh2 = f.call(x_copy)
|
|
113
|
+
grad[i] = (fxh1 - fxh2) / (2 * h)
|
|
114
|
+
x_copy[i] = x[i]
|
|
115
|
+
end
|
|
116
|
+
grad
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def vector_subtract(v1, v2)
|
|
120
|
+
raise ArgumentError, "Vektor harus sama panjang" unless v1.size == v2.size
|
|
121
|
+
v1.zip(v2).map { |a, b| a - b }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def vector_add(v1, v2)
|
|
125
|
+
raise ArgumentError, "Vektor harus sama panjang" unless v1.size == v2.size
|
|
126
|
+
v1.zip(v2).map { |a, b| a + b }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def scalar_multiply(v, scalar)
|
|
130
|
+
v.map { |x| x * scalar }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def elementwise_square(v)
|
|
134
|
+
v.map { |x| x**2 }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def vector_norm(v)
|
|
138
|
+
Math.sqrt(v.map { |x| x**2 }.sum)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def valid_gradient?(g)
|
|
142
|
+
g.is_a?(Array) && g.size == @x.size && g.all? { |v| v.is_a?(Numeric) }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Helper method singkat untuk pengguna yg ingin cepat pakai GD tanpa class
|
|
147
|
+
def self.gradient_descent(**kwargs)
|
|
148
|
+
opt = Optimizer.new(**kwargs.merge(method: :gd))
|
|
149
|
+
opt.optimize
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.gradient_descent_momentum(**kwargs)
|
|
153
|
+
opt = Optimizer.new(**kwargs.merge(method: :momentum))
|
|
154
|
+
opt.optimize
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.adam(**kwargs)
|
|
158
|
+
opt = Optimizer.new(**kwargs.merge(method: :adam))
|
|
159
|
+
opt.optimize
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
module MyMathGem
|
|
2
|
+
module Statistics
|
|
3
|
+
# Mean (rata-rata)
|
|
4
|
+
def self.mean(data)
|
|
5
|
+
data = validate_numeric_array(data)
|
|
6
|
+
data.sum.to_f / data.size
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Median
|
|
10
|
+
def self.median(data)
|
|
11
|
+
data = validate_numeric_array(data)
|
|
12
|
+
sorted = data.sort
|
|
13
|
+
mid = sorted.size / 2
|
|
14
|
+
sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]).to_f / 2
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Mode (bisa banyak)
|
|
18
|
+
def self.mode(data)
|
|
19
|
+
data = validate_numeric_array(data)
|
|
20
|
+
freq = data.each_with_object(Hash.new(0)) { |v, h| h[v] += 1 }
|
|
21
|
+
max_freq = freq.values.max
|
|
22
|
+
freq.select { |_, v| v == max_freq }.keys
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Varians sampel
|
|
26
|
+
def self.variance(data)
|
|
27
|
+
data = validate_numeric_array(data)
|
|
28
|
+
raise ArgumentError, "Data harus >= 2 elemen" if data.size < 2
|
|
29
|
+
m = mean(data)
|
|
30
|
+
sum_sq = data.sum { |x| (x - m)**2 }
|
|
31
|
+
sum_sq.to_f / (data.size - 1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Standar deviasi sampel
|
|
35
|
+
def self.standard_deviation(data)
|
|
36
|
+
Math.sqrt(variance(data))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Quantile (persentil p 0..1)
|
|
40
|
+
def self.quantile(data, p)
|
|
41
|
+
data = validate_numeric_array(data)
|
|
42
|
+
raise ArgumentError, "p harus antara 0 dan 1" unless p.is_a?(Numeric) && p.between?(0, 1)
|
|
43
|
+
sorted = data.sort
|
|
44
|
+
rank = p * (sorted.size - 1)
|
|
45
|
+
lower_idx = rank.floor
|
|
46
|
+
upper_idx = rank.ceil
|
|
47
|
+
if lower_idx == upper_idx
|
|
48
|
+
sorted[lower_idx]
|
|
49
|
+
else
|
|
50
|
+
lower_value = sorted[lower_idx]
|
|
51
|
+
upper_value = sorted[upper_idx]
|
|
52
|
+
lower_value + (rank - lower_idx) * (upper_value - lower_value)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Skewness (kemiringan)
|
|
57
|
+
def self.skewness(data)
|
|
58
|
+
data = validate_numeric_array(data)
|
|
59
|
+
raise ArgumentError, "Data harus >= 3 elemen" if data.size < 3
|
|
60
|
+
m = mean(data)
|
|
61
|
+
sd = standard_deviation(data)
|
|
62
|
+
return 0 if sd == 0
|
|
63
|
+
n = data.size
|
|
64
|
+
sum_cubed = data.sum { |x| (x - m)**3 }
|
|
65
|
+
(n.to_f / ((n-1)*(n-2))) * (sum_cubed / (sd**3))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Kurtosis (keruncingan)
|
|
69
|
+
def self.kurtosis(data)
|
|
70
|
+
data = validate_numeric_array(data)
|
|
71
|
+
raise ArgumentError, "Data harus >= 4 elemen" if data.size < 4
|
|
72
|
+
m = mean(data)
|
|
73
|
+
sd = standard_deviation(data)
|
|
74
|
+
return 0 if sd == 0
|
|
75
|
+
n = data.size
|
|
76
|
+
sum_quad = data.sum { |x| (x - m)**4 }
|
|
77
|
+
numerator = (n*(n+1)*sum_quad) / ((n-1)*(n-2)*(n-3)*(sd**4))
|
|
78
|
+
denominator = (3*((n-1)**2)) / ((n-2)*(n-3))
|
|
79
|
+
numerator - denominator
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Pearson correlation
|
|
83
|
+
def self.pearson_correlation(x, y)
|
|
84
|
+
x, y = validate_numeric_array(x), validate_numeric_array(y)
|
|
85
|
+
raise ArgumentError, "x dan y harus sama panjang >= 2" unless x.size == y.size && x.size >= 2
|
|
86
|
+
mx, my = mean(x), mean(y)
|
|
87
|
+
numerator = x.zip(y).sum { |xi, yi| (xi - mx) * (yi - my) }
|
|
88
|
+
denom_x = Math.sqrt(x.sum { |xi| (xi - mx)**2 })
|
|
89
|
+
denom_y = Math.sqrt(y.sum { |yi| (yi - my)**2 })
|
|
90
|
+
denom = denom_x * denom_y
|
|
91
|
+
return 0 if denom == 0
|
|
92
|
+
numerator.to_f / denom
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Spearman rank correlation
|
|
96
|
+
def self.spearman_correlation(x, y)
|
|
97
|
+
x, y = validate_numeric_array(x), validate_numeric_array(y)
|
|
98
|
+
raise ArgumentError, "x dan y harus sama panjang >= 2" unless x.size == y.size && x.size >= 2
|
|
99
|
+
rank_x = rank_array(x)
|
|
100
|
+
rank_y = rank_array(y)
|
|
101
|
+
pearson_correlation(rank_x, rank_y)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Covariance sample
|
|
105
|
+
def self.covariance(x, y)
|
|
106
|
+
x, y = validate_numeric_array(x), validate_numeric_array(y)
|
|
107
|
+
raise ArgumentError, "x dan y harus sama panjang >= 2" unless x.size == y.size && x.size >= 2
|
|
108
|
+
mx, my = mean(x), mean(y)
|
|
109
|
+
sum = x.zip(y).sum { |xi, yi| (xi - mx) * (yi - my) }
|
|
110
|
+
sum.to_f / (x.size - 1)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Z-score untuk data
|
|
114
|
+
def self.z_scores(data)
|
|
115
|
+
data = validate_numeric_array(data)
|
|
116
|
+
m = mean(data)
|
|
117
|
+
sd = standard_deviation(data)
|
|
118
|
+
raise ArgumentError, "Stddev 0, z-score tidak dapat dihitung" if sd == 0
|
|
119
|
+
data.map { |x| (x - m) / sd }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Confidence interval untuk mean (default distribusi normal)
|
|
123
|
+
# level contoh 0.95 untuk 95% CI
|
|
124
|
+
def self.confidence_interval_mean(data, level = 0.95)
|
|
125
|
+
data = validate_numeric_array(data)
|
|
126
|
+
n = data.size
|
|
127
|
+
raise ArgumentError, "Data harus >= 2 elemen" if n < 2
|
|
128
|
+
m = mean(data)
|
|
129
|
+
sd = standard_deviation(data)
|
|
130
|
+
alpha = 1 - level
|
|
131
|
+
# t-distribution critical value (approximation)
|
|
132
|
+
t_crit = t_distribution_critical_value(n - 1, alpha / 2)
|
|
133
|
+
margin = t_crit * sd / Math.sqrt(n)
|
|
134
|
+
[m - margin, m + margin]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# t-distribution critical value (two-tailed), menggunakan inverse CDF aproksimasi
|
|
138
|
+
# Implementasi sederhana menggunakan inverse normal approximation (baik untuk n>30)
|
|
139
|
+
def self.t_distribution_critical_value(df, alpha)
|
|
140
|
+
# gunakan distribusi normal z untuk aproksimasi sederhana (lebih kompleks bisa pakai gem statistik)
|
|
141
|
+
# untuk df>30 pendekatan z cukup baik
|
|
142
|
+
z = inverse_normal_cdf(1 - alpha)
|
|
143
|
+
z
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Inverse normal CDF aproksimasi (probit), menggunakan algoritma Peter John Acklam
|
|
147
|
+
def self.inverse_normal_cdf(p)
|
|
148
|
+
raise ArgumentError, "p harus di (0,1)" unless p > 0 && p < 1
|
|
149
|
+
# koefisien dan algoritma Acklam (singkat)
|
|
150
|
+
a1 = -39.6968302866538; a2 = 220.946098424521; a3 = -275.928510446969
|
|
151
|
+
a4 = 138.357751867269; a5 = -30.6647980661472; a6 = 2.50662827745924
|
|
152
|
+
|
|
153
|
+
b1 = -54.4760987982241; b2 = 161.585836858041; b3 = -155.698979859887
|
|
154
|
+
b4 = 66.8013118877197; b5 = -13.2806815528857
|
|
155
|
+
|
|
156
|
+
c1 = -0.00778489400243029; c2 = -0.322396458041136; c3 = -2.40075827716184
|
|
157
|
+
c4 = -2.54973253934373; c5 = 4.37466414146497; c6 = 2.93816398269878
|
|
158
|
+
|
|
159
|
+
d1 = 0.00778469570904146; d2 = 0.32246712907004; d3 = 2.445134137143
|
|
160
|
+
d4 = 3.75440866190742
|
|
161
|
+
|
|
162
|
+
plow = 0.02425
|
|
163
|
+
phigh = 1 - plow
|
|
164
|
+
|
|
165
|
+
if p < plow
|
|
166
|
+
q = Math.sqrt(-2 * Math.log(p))
|
|
167
|
+
(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
|
|
168
|
+
((((d1 * q + d2) * q + d3) * q + d4) * q + 1)
|
|
169
|
+
elsif p > phigh
|
|
170
|
+
q = Math.sqrt(-2 * Math.log(1 - p))
|
|
171
|
+
-(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
|
|
172
|
+
((((d1 * q + d2) * q + d3) * q + d4) * q + 1)
|
|
173
|
+
else
|
|
174
|
+
q = p - 0.5
|
|
175
|
+
r = q * q
|
|
176
|
+
(((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * q /
|
|
177
|
+
(((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# One sample t-test: apakah mean data berbeda signifikan dari nilai mu0
|
|
182
|
+
# Return nilai t-statistik dan p-value (two-tailed)
|
|
183
|
+
def self.t_test_one_sample(data, mu0)
|
|
184
|
+
data = validate_numeric_array(data)
|
|
185
|
+
n = data.size
|
|
186
|
+
raise ArgumentError, "Data harus >= 2 elemen" if n < 2
|
|
187
|
+
m = mean(data)
|
|
188
|
+
sd = standard_deviation(data)
|
|
189
|
+
t_stat = (m - mu0) / (sd / Math.sqrt(n))
|
|
190
|
+
p_val = 2 * (1 - t_cdf(t_stat.abs, n - 1))
|
|
191
|
+
[t_stat, p_val]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Two sample t-test (independen), equal variance assumed
|
|
195
|
+
def self.t_test_two_sample(x, y)
|
|
196
|
+
x, y = validate_numeric_array(x), validate_numeric_array(y)
|
|
197
|
+
nx, ny = x.size, y.size
|
|
198
|
+
raise ArgumentError, "Data harus >= 2 elemen" if nx < 2 || ny < 2
|
|
199
|
+
mx, my = mean(x), mean(y)
|
|
200
|
+
varx, vary = variance(x), variance(y)
|
|
201
|
+
pooled_var = ((nx -1) * varx + (ny - 1) * vary) / (nx + ny - 2)
|
|
202
|
+
se = Math.sqrt(pooled_var * (1.0/nx + 1.0/ny))
|
|
203
|
+
t_stat = (mx - my) / se
|
|
204
|
+
df = nx + ny - 2
|
|
205
|
+
p_val = 2 * (1 - t_cdf(t_stat.abs, df))
|
|
206
|
+
[t_stat, p_val]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Mann-Whitney U test (Wilcoxon rank sum) non-parametrik dua sampel
|
|
210
|
+
def self.mann_whitney_u_test(x, y)
|
|
211
|
+
x, y = validate_numeric_array(x), validate_numeric_array(y)
|
|
212
|
+
nx, ny = x.size, y.size
|
|
213
|
+
combined = x.zip(Array.new(nx, :x)) + y.zip(Array.new(ny, :y))
|
|
214
|
+
sorted = combined.sort_by(&:first)
|
|
215
|
+
ranks = rank_array(sorted.map(&:first))
|
|
216
|
+
rank_x = []
|
|
217
|
+
rank_y = []
|
|
218
|
+
sorted.each_with_index do |(_, label), i|
|
|
219
|
+
(label == :x ? rank_x : rank_y) << ranks[i]
|
|
220
|
+
end
|
|
221
|
+
ux = nx * ny + (nx * (nx + 1)) / 2.0 - rank_x.sum
|
|
222
|
+
uy = nx * ny - ux
|
|
223
|
+
u = [ux, uy].min
|
|
224
|
+
# Approximate p-value for large samples (normal approx)
|
|
225
|
+
mu = nx * ny / 2.0
|
|
226
|
+
sigma = Math.sqrt(nx * ny * (nx + ny + 1) / 12.0)
|
|
227
|
+
z = (u - mu) / sigma
|
|
228
|
+
p_val = 2 * (1 - normal_cdf(z.abs))
|
|
229
|
+
[u, p_val]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Simple linear regression y = a + bx
|
|
233
|
+
# Return hash {intercept: a, slope: b, r_squared: r2}
|
|
234
|
+
def self.linear_regression(x, y)
|
|
235
|
+
x, y = validate_numeric_array(x), validate_numeric_array(y)
|
|
236
|
+
raise ArgumentError, "x dan y harus sama panjang >= 2" unless x.size == y.size && x.size >= 2
|
|
237
|
+
mx, my = mean(x), mean(y)
|
|
238
|
+
cov = covariance(x, y)
|
|
239
|
+
varx = variance(x)
|
|
240
|
+
slope = cov / varx
|
|
241
|
+
intercept = my - slope * mx
|
|
242
|
+
|
|
243
|
+
# R squared
|
|
244
|
+
ss_tot = y.sum { |yi| (yi - my)**2 }
|
|
245
|
+
ss_res = y.zip(x).sum { |yi, xi| (yi - (intercept + slope * xi))**2 }
|
|
246
|
+
r_squared = 1 - ss_res.to_f / ss_tot
|
|
247
|
+
|
|
248
|
+
{ intercept: intercept, slope: slope, r_squared: r_squared }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
def self.validate_numeric_array(data)
|
|
254
|
+
raise ArgumentError, "Data harus array tidak kosong" unless data.is_a?(Array) && !data.empty?
|
|
255
|
+
numeric_data = data.map do |v|
|
|
256
|
+
if v.is_a?(Numeric)
|
|
257
|
+
v.to_f
|
|
258
|
+
else
|
|
259
|
+
raise ArgumentError, "Semua elemen data harus numerik"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
numeric_data
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def self.rank_array(data)
|
|
266
|
+
sorted = data.each_with_index.sort_by(&:first)
|
|
267
|
+
ranks = Array.new(data.size)
|
|
268
|
+
i = 0
|
|
269
|
+
while i < sorted.size
|
|
270
|
+
j = i
|
|
271
|
+
while j + 1 < sorted.size && sorted[j+1][0] == sorted[i][0]
|
|
272
|
+
j += 1
|
|
273
|
+
end
|
|
274
|
+
avg_rank = (i + j + 2) / 2.0
|
|
275
|
+
(i..j).each { |k| ranks[sorted[k][1]] = avg_rank }
|
|
276
|
+
i = j + 1
|
|
277
|
+
end
|
|
278
|
+
ranks
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def self.normal_cdf(z)
|
|
282
|
+
0.5 * (1 + erf(z / Math.sqrt(2)))
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def self.erf(x)
|
|
286
|
+
t = 1.0 / (1.0 + 0.3275911 * x.abs)
|
|
287
|
+
a1, a2, a3, a4, a5 = 0.254829592, -0.284496736, 1.421413741, -1.453152027, 1.061405429
|
|
288
|
+
ans = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x*x)
|
|
289
|
+
x >= 0 ? ans : -ans
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# t distribution CDF (gunakan normal approx untuk simplifikasi)
|
|
293
|
+
def self.t_cdf(t, df)
|
|
294
|
+
# bisa diganti dengan implementasi distribusi t yg lebih akurat
|
|
295
|
+
normal_cdf(t)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
data/lib/my_math_gem.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# lib/my_math_gem.rb
|
|
2
|
+
|
|
3
|
+
# Versi gem
|
|
4
|
+
require_relative 'my_math_gem/version'
|
|
5
|
+
|
|
6
|
+
module MyMathGem
|
|
7
|
+
# Autoload modul-modul agar dimuat saat pertama kali digunakan (lazy loading)
|
|
8
|
+
autoload :Calculus, 'my_math_gem/calculus'
|
|
9
|
+
autoload :LinearAlgebra, 'my_math_gem/linear_algebra'
|
|
10
|
+
autoload :Statistics, 'my_math_gem/statistics'
|
|
11
|
+
autoload :OdeSolver, 'my_math_gem/ode_solver'
|
|
12
|
+
autoload :Optimization, 'my_math_gem/optimization'
|
|
13
|
+
autoload :ComplexNumber, 'my_math_gem/complex_number'
|
|
14
|
+
autoload :Fourier, 'my_math_gem/fourier'
|
|
15
|
+
autoload :DataProcessing, 'my_math_gem/data_processing'
|
|
16
|
+
|
|
17
|
+
# Modul utama yang berisi kumpulan fungsi analisa matematika
|
|
18
|
+
#
|
|
19
|
+
# Contoh pemakaian:
|
|
20
|
+
# MyMathGem::Calculus.integrate(...)
|
|
21
|
+
# MyMathGem::Statistics.mean([...])
|
|
22
|
+
# MyMathGem::Fourier.fft([...])
|
|
23
|
+
end
|
data/my_math_gem.gemspec
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# my_math_gem.gemspec
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/my_math_gem/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "my_math_gem"
|
|
7
|
+
spec.version = MyMathGem::VERSION
|
|
8
|
+
spec.authors = ["Andri"]
|
|
9
|
+
spec.email = ["ikuide@gmail.com"]
|
|
10
|
+
spec.summary = "Gem untuk analisa matematika lengkap"
|
|
11
|
+
spec.description = <<-DESC
|
|
12
|
+
MyMathGem adalah kumpulan modul Ruby untuk analisa matematika,
|
|
13
|
+
termasuk kalkulus, aljabar linier, statistik, pemrosesan data,
|
|
14
|
+
solusi ODE, optimasi, bilangan kompleks, dan transformasi Fourier.
|
|
15
|
+
DESC
|
|
16
|
+
|
|
17
|
+
spec.homepage = "" # Kosongkan jika tidak punya homepage
|
|
18
|
+
spec.license = "MIT"
|
|
19
|
+
|
|
20
|
+
# File-file yang disertakan di dalam gem
|
|
21
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
22
|
+
Dir.glob("lib/**/*.rb") + ["README.md", "LICENSE", "my_math_gem.gemspec"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
spec.require_paths = ["lib"]
|
|
26
|
+
spec.required_ruby_version = ">= 2.5"
|
|
27
|
+
|
|
28
|
+
# Metadata bisa dikosongkan jika tidak punya homepage
|
|
29
|
+
spec.metadata = {}
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: my_math_gem
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andri
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
MyMathGem adalah kumpulan modul Ruby untuk analisa matematika,
|
|
14
|
+
termasuk kalkulus, aljabar linier, statistik, pemrosesan data,
|
|
15
|
+
solusi ODE, optimasi, bilangan kompleks, dan transformasi Fourier.
|
|
16
|
+
email:
|
|
17
|
+
- ikuide@gmail.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/my_math_gem.rb
|
|
25
|
+
- lib/my_math_gem/calculus.rb
|
|
26
|
+
- lib/my_math_gem/complex_number.rb
|
|
27
|
+
- lib/my_math_gem/data_processing.rb
|
|
28
|
+
- lib/my_math_gem/fourier.rb
|
|
29
|
+
- lib/my_math_gem/linear_algebra.rb
|
|
30
|
+
- lib/my_math_gem/ode_solver.rb
|
|
31
|
+
- lib/my_math_gem/optimization.rb
|
|
32
|
+
- lib/my_math_gem/statistics.rb
|
|
33
|
+
- lib/my_math_gem/version.rb
|
|
34
|
+
- my_math_gem.gemspec
|
|
35
|
+
homepage: ''
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata: {}
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.5'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.6.7
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Gem untuk analisa matematika lengkap
|
|
56
|
+
test_files: []
|