ephem 0.3.1 → 0.4.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: e8bc9fc6e66dd3aa9d61960cb3b28d43f4c90df7a70eedc1a6460f0dde73b25a
4
- data.tar.gz: a39afb7d865bf976cff25c0839557c22c8aede2ba29bb9ce5b2fa4fef9c17aef
3
+ metadata.gz: 87a0725b85c4ffbedd33b54f3780202c5e43aef7e7a710658df52a2e187f2dcb
4
+ data.tar.gz: f0d1aeccf6731b4d34344c80dd652e7cf012719728a97d3e86463fe5e98dbea7
5
5
  SHA512:
6
- metadata.gz: b832de3f8565111c21e55f8040fb7b8d337cce7d569ccd67a4499949176467bc7eff3f8fa27edabb5af07019637b2f93951cbddbe36ee91e7080b1c11a045144
7
- data.tar.gz: 48a6ef091dd882b9c480e50146bb4038bce15af264ad1e6b8f52a27f21c3796a6c6500a3fe6ee0229d9d534a611daf80f25cd2c0fccf00ee67202699785e033b
6
+ metadata.gz: 2ecffd718100afae3bb1c69e545f36c25c71aad4e9a75d766189a411afa92c1e3f77eda60a02ea17102e57a7d3a9c730b0728e5189ed9cbf2981201cff8bb3e4
7
+ data.tar.gz: e7ff31012dc8f1ffdf38118fc195ffc8849146c690132e9b6b48766456848cda001d11825c687fe0e58fac38a14ca93dcaee6c6ad4af75c1caf8cec9329704d1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2025-06-09
4
+
5
+ ### Improvements
6
+
7
+ * Improve Chebyshev polynomial performance ([#33])
8
+ * Improve download file management ([#34])
9
+ * Validate against all kernels and date ranges ([#36])
10
+ * Add supported Ruby versions ([#35])
11
+ * Bump rspec from 3.13.0 to 3.13.1 by @dependabot ([#38])
12
+ * Bump rake from 13.2.1 to 13.3.0 by @dependabot ([#39])
13
+ * Bump csv from 3.3.4 to 3.3.5 by @dependabot ([#40])
14
+
15
+ [#33]: https://github.com/rhannequin/ruby-ephem/pull/33
16
+ [#34]: https://github.com/rhannequin/ruby-ephem/pull/34
17
+ [#35]: https://github.com/rhannequin/ruby-ephem/pull/35
18
+ [#36]: https://github.com/rhannequin/ruby-ephem/pull/36
19
+ [#38]: https://github.com/rhannequin/ruby-ephem/pull/38
20
+ [#39]: https://github.com/rhannequin/ruby-ephem/pull/39
21
+ [#40]: https://github.com/rhannequin/ruby-ephem/pull/40
22
+
23
+ **Full Changelog**: https://github.com/rhannequin/ruby-ephem/compare/v0.3.1...v0.4.0
24
+
3
25
  ## [0.3.1] - 2025-05-16
4
26
 
5
27
  ### Bug fixes
@@ -1,192 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "numo/narray"
4
-
5
3
  module Ephem
6
4
  module Computation
7
- # Implements Chebyshev polynomial evaluation and differentiation for
8
- # astronomical calculations.
9
- #
10
- # Chebyshev polynomials are mathematical functions that can be used to
11
- # approximate other functions with high accuracy. In astronomical
12
- # calculations, they are used to approximate positions and velocities of
13
- # celestial bodies.
14
- #
15
- # The polynomial evaluation is done using the Clenshaw algorithm, which is
16
- # numerically stable and efficient. For performance optimization, polynomial
17
- # values are cached when they need to be used both for position and velocity
18
- # calculations.
19
- #
20
- # @example Calculating position in 3D space
21
- # # coefficients is a 2D array where:
22
- # # - First dimension is the polynomial degree (n terms)
23
- # # - Second dimension is the spatial dimension (3 for x,y,z)
24
- # coefficients = Numo::DFloat.cast([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
25
- #
26
- # # normalized_time must be between -1 and 1
27
- # polynomial = ChebyshevPolynomial.new(
28
- # coefficients: coefficients,
29
- # normalized_time: 0.5
30
- # )
31
- #
32
- # position = polynomial.evaluate
5
+ ##
6
+ # High-performance, three-dimensional Clenshaw evaluation and derivative
7
+ # evaluation for Chebyshev polynomials, as used in SPK ephemerides.
33
8
  #
34
- # @example Calculating velocity with time scaling
35
- # # radius is the time span in days
36
- # polynomial = ChebyshevPolynomial.new(
37
- # coefficients: coefficients,
38
- # normalized_time: 0.5,
39
- # radius: 32.0 # 32-day time span
40
- # )
9
+ # @see https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/cheby.html
10
+ # @see https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/spicelib/spkche.html
41
11
  #
42
- # velocity = polynomial.evaluate_derivative
43
- class ChebyshevPolynomial
44
- include Core::Constants::Time
45
-
46
- # Initializes a new Chebyshev polynomial calculator
47
- #
48
- # @param coefficients [Numo::NArray] 2D array of Chebyshev polynomial
49
- # coefficients. First dimension represents polynomial terms
50
- # (degree + 1). Second dimension represents spatial components
51
- # (usually 3 for x,y,z)
52
- # @param normalized_time [Float] Time parameter normalized to [-1, 1]
53
- # interval
54
- # @param radius [Float, nil] Optional scaling factor for derivative
55
- # calculations, usually represents the time span of the interval in days
56
- #
57
- # @raise [Ephem::InvalidInputError] if coefficients are not a 2D
58
- # Numo::NArray
59
- # @raise [Ephem::InvalidInputError] if normalized_time is outside [-1, 1]
60
- def initialize(coefficients:, normalized_time:, radius: nil)
61
- validate_inputs(coefficients, normalized_time)
62
-
63
- @coefficients = coefficients
64
- @normalized_time = normalized_time
65
- @radius = radius
66
- @degree = @coefficients.shape[0]
67
- @dimension = @coefficients.shape[1]
68
- @two_times_normalized_time = 2.0 * @normalized_time
69
- @polynomials = nil # Cache for polynomial values
70
- end
71
-
72
- # Evaluates the Chebyshev polynomial at the normalized time point
73
- #
74
- # Uses the Clenshaw algorithm for numerical stability. The algorithm
75
- # evaluates the polynomial using a recurrence relation, which is more
76
- # stable than direct power series evaluation.
77
- #
78
- # @return [Numo::DFloat] Evaluation result, array of size dimension
79
- # (usually 3)
80
- def evaluate
81
- @polynomials ||= generate_polynomials(@degree, @dimension)
82
- combine_polynomials(@polynomials)
83
- end
84
-
85
- # Calculates the derivative of the Chebyshev polynomial
86
- #
87
- # For astronomical calculations, this typically represents velocity.
88
- # For polynomials of degree < 2, returns zero array since the derivative
89
- # of constants and linear terms are constant or zero.
12
+ module ChebyshevPolynomial
13
+ ##
14
+ # Evaluates a 3D Chebyshev polynomial at a given normalized time.
90
15
  #
91
- # If radius is provided, scales the result to convert from normalized time
92
- # units to physical units (usually km/sec in astronomical calculations)
93
- #
94
- # @return [Numo::DFloat] Derivative values, array of size dimension
95
- # (usually 3)
96
- def evaluate_derivative
97
- return Numo::DFloat.zeros(@dimension) if @degree < 2
98
-
99
- derivative = calculate_derivative(@degree, @dimension)
100
- scale_derivative(derivative)
101
- end
102
-
103
- private
104
-
105
- def validate_inputs(coefficients, normalized_time)
106
- unless coefficients.is_a?(Numo::NArray) && coefficients.ndim == 2
107
- raise InvalidInputError, "Coefficients must be a 2D Numo::NArray"
16
+ # @param coeffs [Array<Array<Float>>] Array of coefficients; shape is
17
+ # [n_terms][3].
18
+ # @param t [Float] The normalized independent variable, in [-1, 1].
19
+ # @return [Array<Float>] The 3-vector result at t: [x, y, z]
20
+ def self.evaluate(coeffs, t)
21
+ n = coeffs.size
22
+ b1x = b1y = b1z = 0.0
23
+ b2x = b2y = b2z = 0.0
24
+
25
+ k = n - 1
26
+ while k > 0
27
+ c = coeffs[k]
28
+ c0 = c[0]
29
+ c1 = c[1]
30
+ c2 = c[2]
31
+ t2 = 2.0 * t
32
+ tx = t2 * b1x - b2x + c0
33
+ ty = t2 * b1y - b2y + c1
34
+ tz = t2 * b1z - b2z + c2
35
+ b2x = b1x
36
+ b2y = b1y
37
+ b2z = b1z
38
+ b1x = tx
39
+ b1y = ty
40
+ b1z = tz
41
+ k -= 1
108
42
  end
109
43
 
110
- unless (-1.0..1.0).cover?(normalized_time)
111
- raise InvalidInputError, "Normalized time must be in range [-1, 1]"
112
- end
113
- end
114
-
115
- # Generates the sequence of Chebyshev polynomials
116
- # Uses the recurrence relation for Chebyshev polynomials:
117
- # T₀(x) = 1
118
- # T₁(x) = x
119
- # Tₙ₊₁(x) = 2xTₙ(x) - Tₙ₋₁(x)
120
- def generate_polynomials(degree, dimension)
121
- polynomials = initialize_base_polynomials(dimension)
122
- return polynomials if degree <= 2
123
-
124
- build_polynomial_sequence(polynomials, degree)
125
- polynomials
126
- end
127
-
128
- # Initializes T₀(x) = 1 and T₁(x) = x polynomials
129
- def initialize_base_polynomials(dimension)
130
- [
131
- Numo::DFloat.ones(dimension),
132
- @normalized_time * Numo::DFloat.ones(dimension)
133
- ]
134
- end
135
-
136
- def build_polynomial_sequence(polynomials, degree)
137
- (2...degree).each do |i|
138
- polynomials << compute_next_polynomial(
139
- polynomials[i - 2],
140
- polynomials[i - 1]
141
- )
142
- end
143
- end
144
-
145
- def compute_next_polynomial(prev_prev, prev)
146
- @two_times_normalized_time * prev - prev_prev
44
+ c0, c1, c2 = coeffs[0]
45
+ [t * b1x - b2x + c0, t * b1y - b2y + c1, t * b1z - b2z + c2]
147
46
  end
148
47
 
149
- # Combines polynomials with their coefficients
150
- # Uses vectorized operations for efficiency. The final result is:
151
- # Σ(coefficients[i] * polynomials[i]) for i from 0 to degree-1
152
- def combine_polynomials(polynomials)
153
- (0...@degree).inject(Numo::DFloat.zeros(@dimension)) do |result, i|
154
- result + @coefficients[i, true] * polynomials[i]
155
- end
156
- end
157
-
158
- # Calculates derivative using the modified Clenshaw algorithm
159
- # The algorithm is adapted to compute derivatives of Chebyshev polynomials
160
- # while maintaining numerical stability
161
- def calculate_derivative(degree, dimension)
162
- @polynomials ||= generate_polynomials(degree, dimension)
163
-
164
- derivative_prev = Numo::DFloat.zeros(dimension)
165
- derivative = derivative_prev.clone
166
-
167
- (degree - 1).downto(1) do |i|
168
- derivative_next = derivative.clone
169
- derivative = derivative_prev
170
- derivative_prev = calculate_derivative_term(
171
- i,
172
- derivative,
173
- derivative_next
174
- )
48
+ ##
49
+ # Evaluates the time derivative of a 3D Chebyshev polynomial at a given
50
+ # normalized time.
51
+ #
52
+ # @param coeffs [Array<Array<Float>>] Array of coefficients; shape is
53
+ # [n_terms][3].
54
+ # @param t [Float] The normalized independent variable (in [-1, 1]).
55
+ # @param radius [Float] The half-length of the time interval (days).
56
+ # @return [Array<Float>] The 3-vector derivative (velocity), in units per
57
+ # second.
58
+ def self.evaluate_derivative(coeffs, t, radius)
59
+ n = coeffs.size
60
+ return [0.0, 0.0, 0.0] if n < 2
61
+
62
+ d1x = d1y = d1z = 0.0
63
+ d2x = d2y = d2z = 0.0
64
+
65
+ k = n - 1
66
+ while k > 0
67
+ c = coeffs[k]
68
+ c0 = c[0]
69
+ c1 = c[1]
70
+ c2 = c[2]
71
+ t2 = 2.0 * t
72
+ k2 = 2 * k
73
+ tx = t2 * d1x - d2x + k2 * c0
74
+ ty = t2 * d1y - d2y + k2 * c1
75
+ tz = t2 * d1z - d2z + k2 * c2
76
+ d2x = d1x
77
+ d2y = d1y
78
+ d2z = d1z
79
+ d1x = tx
80
+ d1y = ty
81
+ d1z = tz
82
+ k -= 1
175
83
  end
176
84
 
177
- derivative_prev
178
- end
179
-
180
- def calculate_derivative_term(index, deriv, deriv_next)
181
- @two_times_normalized_time * deriv -
182
- deriv_next +
183
- @coefficients[index, true] * 2 * index
184
- end
185
-
186
- def scale_derivative(derivative)
187
- return derivative unless @radius
188
-
189
- derivative * (SECONDS_PER_DAY / (2.0 * @radius))
85
+ scale = Ephem::Core::Constants::Time::SECONDS_PER_DAY / (2.0 * radius)
86
+ [d1x * scale, d1y * scale, d1z * scale]
190
87
  end
191
88
  end
192
89
  end
@@ -2,8 +2,10 @@
2
2
 
3
3
  require "minitar"
4
4
  require "net/http"
5
+ require "pathname"
5
6
  require "tempfile"
6
7
  require "zlib"
8
+ require "fileutils"
7
9
 
8
10
  module Ephem
9
11
  class Download
@@ -94,16 +96,15 @@ module Ephem
94
96
  new(name, target).call
95
97
  end
96
98
 
97
- def initialize(name, local_path)
99
+ def initialize(name, target_path)
98
100
  @name = name
99
- @local_path = local_path
101
+ @target_path = Pathname.new(target_path)
100
102
  validate_requested_kernel!
101
103
  end
102
104
 
103
105
  def call
104
- content = jpl_kernel? ? download_from_jpl : download_from_imcce
105
- File.binwrite(@local_path, content)
106
-
106
+ FileUtils.mkdir_p(@target_path.dirname)
107
+ jpl_kernel? ? download_jpl : download_imcce
107
108
  true
108
109
  end
109
110
 
@@ -120,28 +121,45 @@ module Ephem
120
121
  JPL_KERNELS.include?(@name)
121
122
  end
122
123
 
123
- def download_from_jpl
124
- uri = URI("#{JPL_BASE_URL}#{@name}")
125
- Net::HTTP.get(uri)
124
+ def download_jpl
125
+ uri = URI.join(JPL_BASE_URL, @name)
126
+ @target_path.open("wb") do |file|
127
+ stream_http_to_file(uri, file)
128
+ end
126
129
  end
127
130
 
128
- def download_from_imcce
129
- temp_file = Tempfile.new(%w[archive .tar.gz])
130
- uri = URI("#{IMCCE_BASE_URL}#{IMCCE_KERNELS_MATCHING[@name]}")
131
- content = Net::HTTP.get(uri)
132
- temp_file.write(content)
133
- temp_file.rewind
134
-
135
- Zlib::GzipReader.open(temp_file.path) do |gz|
136
- Minitar::Reader.open(gz) do |tar|
137
- tar.each_entry do |entry|
138
- return entry.read if entry.full_name == IMCCE_KERNELS[@name]
131
+ def download_imcce
132
+ tar_gz_uri = URI.join(IMCCE_BASE_URL, IMCCE_KERNELS_MATCHING[@name])
133
+ Tempfile.create(%w[archive .tar.gz]) do |temp_file|
134
+ stream_http_to_file(tar_gz_uri, temp_file)
135
+ temp_file.flush
136
+ temp_file.rewind
137
+
138
+ Zlib::GzipReader.open(temp_file.path) do |gz|
139
+ Minitar::Reader.open(gz) do |tar|
140
+ tar.each_entry do |entry|
141
+ next unless entry.full_name == IMCCE_KERNELS[@name]
142
+
143
+ @target_path.open("wb") { |file| file.write(entry.read) }
144
+
145
+ break
146
+ end
139
147
  end
140
148
  end
141
149
  end
142
- ensure
143
- temp_file.close
144
- temp_file.unlink
150
+ end
151
+
152
+ def stream_http_to_file(uri, file)
153
+ Net::HTTP.start(
154
+ uri.host,
155
+ uri.port,
156
+ use_ssl: uri.scheme == "https"
157
+ ) do |http|
158
+ request = Net::HTTP::Get.new(uri)
159
+ http.request(request) do |response|
160
+ response.read_body { |chunk| file.write(chunk) }
161
+ end
162
+ end
145
163
  end
146
164
  end
147
165
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "numo/narray"
4
+
3
5
  module Ephem
4
6
  module Segments
5
7
  # Manages data segments within SPICE kernel (SPK) files, providing methods
@@ -133,7 +135,7 @@ module Ephem
133
135
  # Synchronize access to data loading using a mutex lock
134
136
  # to prevent race conditions in multithreaded environments
135
137
  @data_lock.synchronize do
136
- return if @data_loaded # Return early if data is already loaded
138
+ return if @data_loaded
137
139
 
138
140
  component_count = determine_component_count
139
141
  coefficients_data = load_coefficient_data
@@ -180,27 +182,27 @@ module Ephem
180
182
  coefficients = Numo::DFloat.cast(coefficients_raw)
181
183
  coefficients = coefficients.reshape(segment_count, record_size)
182
184
 
183
- # Extract midpoints and radii from coefficient data
184
- # midpoints: array of times at middle of each record's interval
185
- # radii: array of half the interval length for each record
186
- @midpoints = coefficients[0...segment_count, 0]
187
- @radii = coefficients[0...segment_count, 1]
188
-
189
- # Extract Chebyshev polynomial coefficients and reshape to 3D array
190
- # dimensions: (coefficient_index, component_index, segment_index)
191
- coeffs = coefficients[0...segment_count, 2..-1]
192
- @coefficients = coeffs.reshape(
193
- segment_count,
194
- component_count,
195
- coefficient_count
196
- )
197
- .transpose(2, 1, 0)
185
+ @midpoints = coefficients[0...segment_count, 0].to_a
186
+ @radii = coefficients[0...segment_count, 1].to_a
187
+ n_terms = coefficient_count
188
+ n_components = component_count
189
+
190
+ @coefficients = Array.new(segment_count) do |i|
191
+ row = coefficients[i, 2..-1].to_a
192
+ Array.new(n_terms) do |k|
193
+ Array.new(n_components) do |j|
194
+ row[k + j * n_terms]
195
+ end
196
+ end
197
+ end
198
198
  end
199
199
 
200
200
  def convert_to_seconds(tdb, tdb2)
201
201
  case tdb
202
- when Array, Numo::NArray
202
+ when Array
203
203
  tdb.map { |t| time_to_seconds(t, tdb2) }
204
+ when Numo::NArray
205
+ tdb.to_a.map { |t| time_to_seconds(t, tdb2) }
204
206
  else
205
207
  time_to_seconds(tdb, tdb2)
206
208
  end
@@ -228,20 +230,18 @@ module Ephem
228
230
  def generate_single(tdb_seconds)
229
231
  interval = find_interval(tdb_seconds)
230
232
  normalized_time = compute_normalized_time(tdb_seconds, interval)
231
- coeffs = @coefficients[true, true, interval]
232
233
 
233
- position = Computation::ChebyshevPolynomial.new(
234
- coefficients: coeffs,
235
- normalized_time: normalized_time
236
- ).evaluate
237
-
238
- velocity = Computation::ChebyshevPolynomial.new(
239
- coefficients: coeffs,
240
- normalized_time: normalized_time,
241
- radius: @radii[interval]
242
- ).evaluate_derivative
243
-
244
- [position.to_a, velocity.to_a]
234
+ coeffs = @coefficients[interval] # already [n_terms][3]
235
+ position = Computation::ChebyshevPolynomial.evaluate(
236
+ coeffs,
237
+ normalized_time
238
+ )
239
+ velocity = Computation::ChebyshevPolynomial.evaluate_derivative(
240
+ coeffs,
241
+ normalized_time,
242
+ @radii[interval]
243
+ )
244
+ [position, velocity]
245
245
  end
246
246
 
247
247
  def generate_multiple(tdb_seconds)
data/lib/ephem/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ephem
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ephem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rémy Hannequin
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2025-06-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: minitar
@@ -244,7 +244,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
244
244
  - !ruby/object:Gem::Version
245
245
  version: '0'
246
246
  requirements: []
247
- rubygems_version: 3.6.7
247
+ rubygems_version: 3.6.2
248
248
  specification_version: 4
249
249
  summary: Compute astronomical ephemerides from NASA/JPL DE and IMCCE INPOP
250
250
  test_files: []