korba 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8a9dc1d59d577592a4ef91601bb34a4921e2bb1eb731c0fe866698759717d09
4
- data.tar.gz: bb572412946fd8f08a00ab844c4a4975d08953871984a142225ffe09b786417d
3
+ metadata.gz: 5dc3a4abbdd189f4388673e3960a615940a4feb68c359cce37a29ff47fa7efcb
4
+ data.tar.gz: c312b2be706e321242d0f4c762134ad82ea681348e5631ec36a13ed91c36db11
5
5
  SHA512:
6
- metadata.gz: a6f13b11b06a7dc3159b5219204a3c83791a5708f7fc8d64c0d72734bd751c655b8438ef7d29fbb2599f94dfa6657e9c282235dbf86239fab49ad12a2eafff9d
7
- data.tar.gz: bcc2a2d8b20d9accbe8b6079334ce6c4610a22e78363d7c9e6243f39a1608c46a90adc0b7d0e52c53adb9fb7c81b7a4dc60f1a291e523e3166bc54b68f9a6897
6
+ metadata.gz: f0cc15b9fa16cec125d3902e3b52e1336acf4246c72860049955c32456d1eaa0e56a2fd99953477d36141da23b3b8696ed0b8a611c80bc51e1249368c7f07f84
7
+ data.tar.gz: ffcbf5ed21c86f069d83276227c9db29e5b59d257d1630f5a1fb15eaff0550b881b1e7753fcc551ce6c647c49faf2bc84ee52efb015fbd9ad8efa9355b2da6ad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.8.0] - 2026-04-19
2
+
3
+ - Enable orbit propagation using 4th-order Runge-Kutta
4
+ - Enable conversion from Cartesian to Keplerian elements
5
+
6
+ ## [0.7.1] - 2026-04-05
7
+
8
+ - FIX: Fixed calculation when orbital inclination and eccentricity are 0
9
+
1
10
  ## [0.7.0] - 2026-04-01
2
11
 
3
12
  - FIX: Use SGP4 for TLE to Cartesian conversion
data/lib/korba/car.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Korba
4
4
  class Car
5
+ include OrbitUtils
6
+
5
7
  attr_reader :object_name, :epoch, :x, :y, :z, :vx, :vy, :vz
6
8
 
7
9
  def initialize(object_name:, epoch:, x:, y:, z:, vx:, vy:, vz:)
@@ -14,5 +16,85 @@ module Korba
14
16
  @vy = vy
15
17
  @vz = vz
16
18
  end
19
+
20
+ def to_kep
21
+ inclination = rad_to_deg(Math.atan2(Math.sqrt(specific_angular_momentum_vector[0] ** 2 + specific_angular_momentum_vector[1] ** 2), specific_angular_momentum_vector[2]))
22
+ ra_of_asc_node = rad_to_deg(Math.atan2(specific_angular_momentum_vector[0], -specific_angular_momentum_vector[1]))
23
+
24
+ Kep.new(object_name:,
25
+ epoch:,
26
+ semi_major_axis:,
27
+ eccentricity:,
28
+ inclination:,
29
+ ra_of_asc_node:,
30
+ arg_of_pericenter: normalize_deg(arg_of_latitude - true_anomaly),
31
+ mean_anomaly: rad_to_deg(deg_to_rad(eccentric_anomaly) - eccentricity * Math.sin(deg_to_rad(eccentric_anomaly))))
32
+ end
33
+
34
+ private
35
+
36
+ def position_vector
37
+ @position_vector ||= Vector[x, y, z]
38
+ end
39
+
40
+ def velocity_vector
41
+ @velocity_vector ||= Vector[vx, vy, vz]
42
+ end
43
+
44
+ def distance
45
+ @distance ||= position_vector.norm
46
+ end
47
+
48
+ def velocity
49
+ @velocity ||= velocity_vector.norm
50
+ end
51
+
52
+ def semi_major_axis
53
+ @semi_major_axis ||= Constant::GME * distance / (2 * Constant::GME - distance * velocity ** 2)
54
+ end
55
+
56
+ def eccentricity
57
+ @eccentricity ||= Math.sqrt(((semi_major_axis - distance) / semi_major_axis) ** 2 + (position_vector.dot(velocity_vector) ** 2) / (Constant::GME * semi_major_axis))
58
+ end
59
+
60
+ def eccentric_anomaly
61
+ @eccentric_anomaly ||= rad_to_deg(Math.atan2(Math.sqrt(semi_major_axis / Constant::GME) * position_vector.dot(velocity_vector), semi_major_axis - distance))
62
+ end
63
+
64
+ def true_anomaly
65
+ factor = (Math.cos(deg_to_rad(eccentric_anomaly)) - eccentricity) / (1 - eccentricity * Math.cos(deg_to_rad(eccentric_anomaly)))
66
+ rad_to_deg(Math.atan2(Math.sqrt(1 - factor ** 2), factor))
67
+ end
68
+
69
+ def angular_momentum_vector
70
+ @angular_momentum_vector ||= position_vector.cross(velocity_vector)
71
+ end
72
+
73
+ def raan_vector
74
+ @raan_vector ||= Vector[0, 0, 1].cross(angular_momentum_vector)
75
+ end
76
+
77
+ def eccentricity_vector
78
+ @eccentricity_vector ||= ((velocity ** 2 - Constant::GME / distance) * position_vector - (position_vector * velocity_vector) * velocity_vector) / Constant::GME
79
+ end
80
+
81
+ def specific_angular_momentum_vector
82
+ @specific_angular_momentum_vector ||= position_vector.cross(velocity_vector)
83
+ end
84
+
85
+ def raan_vector
86
+ @raan_vector ||= Vector[0, 0, 1].cross(specific_angular_momentum_vector)
87
+ end
88
+
89
+ def arg_of_latitude
90
+ y = position_vector.dot(raan_vector)
91
+ x = position_vector.norm * raan_vector.norm
92
+ arg_of_latitude = (Math.atan2(
93
+ Math.sqrt(1 - (y / x) ** 2),
94
+ y / x
95
+ ))
96
+ arg_of_latitude = 2 * Math::PI - arg_of_latitude if z < 0
97
+ rad_to_deg(arg_of_latitude)
98
+ end
17
99
  end
18
100
  end
@@ -5,6 +5,7 @@ module Korba
5
5
  # geocentric gravitational constant in m3/s2
6
6
  GME = 3.9860044188e14
7
7
  # nominal equatorial Earth radius in m
8
- EARTH_RADIUS = 6378137
8
+ EARTH_RADIUS = 6378137.0
9
+ J2 = 1.08262668e-3
9
10
  end
10
11
  end
data/lib/korba/kep.rb CHANGED
@@ -19,19 +19,37 @@ module Korba
19
19
  end
20
20
 
21
21
  def to_car
22
- vector_n = Vector[
23
- Math.sin(deg_to_rad(inclination)) * Math.sin(deg_to_rad(ra_of_asc_node)),
24
- -Math.sin(deg_to_rad(inclination)) * Math.cos(deg_to_rad(ra_of_asc_node)),
25
- Math.cos(deg_to_rad(inclination))
26
- ]
27
- vector_omega = Vector[Math.cos(deg_to_rad(ra_of_asc_node)), Math.sin(deg_to_rad(ra_of_asc_node)), 0]
28
- vector_m = vector_n.cross(vector_omega)
22
+ # 特異点対応: inclinationが0(または極めて小さい)場合の処理
23
+ is_equatorial = inclination.abs < 1e-10
24
+
25
+ if is_equatorial
26
+ # 赤道面内の場合、x軸を基準にする
27
+ vector_omega = Vector[1.0, 0.0, 0.0]
28
+ vector_m = Vector[0.0, 1.0, 0.0]
29
+ vector_n = Vector[0.0, 0.0, 1.0]
30
+ else
31
+ vector_n = Vector[
32
+ Math.sin(deg_to_rad(inclination)) * Math.sin(deg_to_rad(ra_of_asc_node)),
33
+ -Math.sin(deg_to_rad(inclination)) * Math.cos(deg_to_rad(ra_of_asc_node)),
34
+ Math.cos(deg_to_rad(inclination))
35
+ ]
36
+ vector_omega = Vector[Math.cos(deg_to_rad(ra_of_asc_node)), Math.sin(deg_to_rad(ra_of_asc_node)), 0.0]
37
+ vector_m = vector_n.cross(vector_omega)
38
+ end
29
39
 
30
40
  r_angle_factor = deg_to_rad(arg_of_pericenter + true_anomaly)
31
41
  vector_r = distance * (Math.cos(r_angle_factor) * vector_omega + Math.sin(r_angle_factor) * vector_m)
32
42
  v_angle_factor = deg_to_rad(arg_of_pericenter + true_anomaly - path_angle)
33
43
  vector_v = velocity * (-Math.sin(v_angle_factor) * vector_omega + (Math.cos(v_angle_factor)) * vector_m)
34
44
 
45
+ # NOTE: こっちでもいい
46
+ # factor_matrix = Matrix[[Math.cos(deg_to_rad(ra_of_asc_node)), -Math.sin(deg_to_rad(ra_of_asc_node)) * Math.cos(deg_to_rad(inclination))],
47
+ # [Math.sin(deg_to_rad(ra_of_asc_node)), Math.cos(deg_to_rad(ra_of_asc_node)) * Math.cos(deg_to_rad(inclination))],
48
+ # [0, Math.sin(deg_to_rad(inclination))]] *
49
+ # Matrix[[Math.cos(deg_to_rad(arg_of_pericenter)), -Math.sin(deg_to_rad(arg_of_pericenter))], [Math.sin(deg_to_rad(arg_of_pericenter)), Math.cos(deg_to_rad(arg_of_pericenter))]]
50
+ # vector_r = Vector[*(factor_matrix * Matrix[[Math.cos(deg_to_rad(true_anomaly)) - eccentricity], [Math.sqrt(1 - eccentricity ** 2) * Math.sin(deg_to_rad(true_anomaly))]] * semi_major_axis).to_a.flatten]
51
+ # vector_v = Vector[*(factor_matrix * Matrix[[-Math.sin(deg_to_rad(true_anomaly))], [Math.sqrt(1 - eccentricity ** 2) * Math.cos(deg_to_rad(true_anomaly))]] * Math.sqrt(Constant::GME * semi_major_axis) / vector_r.norm).to_a.flatten]
52
+
35
53
  Car.new(object_name:, epoch:, x: vector_r[0], y: vector_r[1], z: vector_r[2], vx: vector_v[0], vy: vector_v[1], vz: vector_v[2])
36
54
  end
37
55
  end
@@ -4,7 +4,7 @@ module Korba
4
4
  module OrbitUtils
5
5
  def semi_major_axis
6
6
  # a = (μ / n^2)^(1/3) m
7
- (Constant::GME / (mean_motion * 2 * Math::PI / 86400.0) ** 2.0) ** (1.0 / 3.0)
7
+ (Constant::GME / (mean_motion * 2.0 * Math::PI / 86400.0) ** 2.0) ** (1.0 / 3.0)
8
8
  end
9
9
 
10
10
  def height_at_perigee
@@ -21,8 +21,12 @@ module Korba
21
21
  end
22
22
 
23
23
  def true_anomaly
24
- factor = (Math.cos(deg_to_rad(eccentric_anomaly)) - eccentricity) / (1 - eccentricity * Math.cos(deg_to_rad(eccentric_anomaly)))
25
- rad_to_deg(Math.acos(factor))
24
+ e_rad = deg_to_rad(eccentric_anomaly)
25
+ y = Math.sqrt(1 - eccentricity ** 2) * Math.sin(e_rad)
26
+ x = Math.cos(e_rad) - eccentricity
27
+
28
+ # atan2(y, x) で正確な位相(-pi から pi)を求める
29
+ rad_to_deg(Math.atan2(y, x))
26
30
  end
27
31
 
28
32
  def distance
@@ -30,10 +34,12 @@ module Korba
30
34
  end
31
35
 
32
36
  def velocity
33
- Math.sqrt(Constant::GME * (2 / distance - 1 / semi_major_axis))
37
+ Math.sqrt(Constant::GME * (2.0 / distance - 1.0 / semi_major_axis))
34
38
  end
35
39
 
36
40
  def path_angle
41
+ return 0.0 if eccentricity < 1e-15
42
+
37
43
  factor = Math.sqrt(Constant::GME * semi_major_axis * (1 - eccentricity ** 2)) / (distance * velocity)
38
44
  rad_to_deg(Math.acos(factor))
39
45
  end
@@ -44,8 +50,8 @@ module Korba
44
50
  end
45
51
 
46
52
  def normalize_rad(rad)
47
- rad = rad + 2 * Math::PI if rad < 0
48
- normalize_rad = rad > 2 * Math::PI ? rad - 2 * Math::PI : rad
53
+ rad = rad + 2.0 * Math::PI if rad < 0
54
+ normalize_rad = rad > 2.0 * Math::PI ? rad - 2.0 * Math::PI : rad
49
55
  normalize_rad(normalize_rad) if normalize_rad != rad
50
56
  normalize_rad
51
57
  end
@@ -56,8 +62,8 @@ module Korba
56
62
  end
57
63
 
58
64
  def normalize_deg(deg)
59
- deg = deg + 360 if deg < 0
60
- normalized_deg = deg > 360 ? deg - 360 : deg
65
+ deg = deg + 360.0 if deg < 0
66
+ normalized_deg = deg > 360.0 ? deg - 360.0 : deg
61
67
  normalize_deg(normalized_deg) if normalized_deg != deg
62
68
  normalized_deg
63
69
  end
@@ -0,0 +1,63 @@
1
+ module Korba
2
+ module Propagator
3
+ class Rk4
4
+ def initialize(initial_car, disable_j2: false)
5
+ @initial_car = initial_car
6
+ @disable_j2 = disable_j2
7
+ end
8
+
9
+ def propagate(seconds_after_epoch, dt = 1.0)
10
+ steps = (seconds_after_epoch / dt).to_i
11
+ t = 0.0
12
+ y = Vector[@initial_car.x, @initial_car.y, @initial_car.z, @initial_car.vx, @initial_car.vy, @initial_car.vz]
13
+
14
+ steps.times do
15
+ y = step(t, y, dt)
16
+ t += dt
17
+ end
18
+
19
+ Car.new(
20
+ object_name: @initial_car.object_name,
21
+ epoch: @initial_car.epoch + seconds_after_epoch,
22
+ x: y[0],
23
+ y: y[1],
24
+ z: y[2],
25
+ vx: y[3],
26
+ vy: y[4],
27
+ vz: y[5],
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def step(t, y, dt)
34
+ k1 = f(t, y)
35
+ k2 = f(t + dt / 2.0, y + k1 * (dt / 2.0))
36
+ k3 = f(t + dt / 2.0, y + k2 * (dt / 2.0))
37
+ k4 = f(t + dt, y + k3 * dt)
38
+
39
+ y + (k1 + 2 * k2 + 2 * k3 + k4) * (dt / 6.0)
40
+ end
41
+
42
+ def f(t, y)
43
+ position_vector = Vector[y[0], y[1], y[2]]
44
+ velocity_vector = Vector[y[3], y[4], y[5]]
45
+ r = position_vector.magnitude
46
+ base_acceleration_vector = -Constant::GME * position_vector / (r ** 3)
47
+ acceleration_vector = @disable_j2 ? base_acceleration_vector : add_j2_effect(base_acceleration_vector, r, position_vector[2])
48
+
49
+ Vector[velocity_vector[0], velocity_vector[1], velocity_vector[2], acceleration_vector[0], acceleration_vector[1], acceleration_vector[2]]
50
+ end
51
+
52
+ def add_j2_effect(base_acceleration_vector, r, z)
53
+ j2_factor = (3.0 / 2.0) * Constant::J2 * (Constant::EARTH_RADIUS / r) ** 2
54
+ factor = 5 * (z / r) ** 2
55
+ [
56
+ base_acceleration_vector[0] * (1 + j2_factor * (1 - factor)),
57
+ base_acceleration_vector[1] * (1 + j2_factor * (1 - factor)),
58
+ base_acceleration_vector[2] * (1 + j2_factor * (3 - factor)),
59
+ ]
60
+ end
61
+ end
62
+ end
63
+ end