ephem 0.4.1 → 0.5.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.
data/benchmarks/run.rb ADDED
@@ -0,0 +1,431 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark/ips"
4
+ require "objspace"
5
+ require_relative "../lib/ephem"
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Configuration
9
+ # ---------------------------------------------------------------------------
10
+
11
+ ROOT = File.expand_path("..", __dir__)
12
+ SPK_FULL = File.join(ROOT, "spec", "support", "data", "de432s.bsp")
13
+ SPK_EXCERPT = File
14
+ .join(ROOT, "spec", "support", "data", "de421_2000_excerpt.bsp")
15
+ PCK_EXCERPT = File
16
+ .join(ROOT, "spec", "support", "data", "moon_pa_de440_excerpt.bpc")
17
+
18
+ JD_J2000 = Ephem::Core::Constants::Time::J2000_EPOCH # 2451545.0
19
+ JD_TEST = 2459000.0 # 2020-09-30, well within de432s range
20
+
21
+ SEQUENTIAL_STEPS = 1000
22
+
23
+ # Body pairs available in DE432s: [center, target]
24
+ BODY_PAIRS = {
25
+ "Sun" => [0, 10],
26
+ "Earth-Moon Bary" => [0, 3],
27
+ "Mars Bary" => [0, 4],
28
+ "Jupiter Bary" => [0, 5],
29
+ "Saturn Bary" => [0, 6]
30
+ }.freeze
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Helpers
34
+ # ---------------------------------------------------------------------------
35
+
36
+ def separator(title)
37
+ puts
38
+ puts "=" * 70
39
+ puts " #{title}"
40
+ puts "=" * 70
41
+ puts
42
+ end
43
+
44
+ def ensure_file!(path)
45
+ return if File.exist?(path)
46
+ abort "SPK file not found: #{path}\n" \
47
+ "Run specs first or place the file manually."
48
+ end
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Preflight
52
+ # ---------------------------------------------------------------------------
53
+
54
+ ensure_file!(SPK_FULL)
55
+ ensure_file!(SPK_EXCERPT)
56
+
57
+ puts "Ephem Benchmark Suite"
58
+ puts "-" * 70
59
+ puts "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})"
60
+ puts "Ephem: #{Ephem::VERSION}"
61
+ puts "SPK: #{File.basename(SPK_FULL)} (#{(File.size(SPK_FULL) / 1024.0 / 1024).round(2)} MB)"
62
+ puts "Excerpt: #{File.basename(SPK_EXCERPT)} (#{(File.size(SPK_EXCERPT) / 1024.0).round(1)} KB)"
63
+ puts "J2000: #{JD_J2000}"
64
+ puts "Test JD: #{JD_TEST}"
65
+ puts "-" * 70
66
+
67
+ # Pre-open the main SPK file and warm up data for hot-path benchmarks
68
+ spk = Ephem::SPK.open(SPK_FULL)
69
+ segment_emb = spk[0, 3] # Earth-Moon Barycenter
70
+ segment_sun = spk[0, 10] # Sun
71
+
72
+ # Trigger lazy data loading so hot-path benchmarks don't include I/O
73
+ segment_emb.compute(JD_TEST)
74
+ segment_sun.compute(JD_TEST)
75
+
76
+ # Pre-compute time arrays for sequential/random benchmarks
77
+ sequential_times = Array.new(SEQUENTIAL_STEPS) { |i| JD_TEST + i }
78
+ random_times = sequential_times.shuffle
79
+
80
+ # =========================================================================
81
+ # 1. SPK FILE OPENING
82
+ # =========================================================================
83
+
84
+ separator "1. SPK File Opening"
85
+
86
+ GC.start
87
+ Benchmark.ips do |x|
88
+ x.report("SPK.open (full 10MB)") do
89
+ s = Ephem::SPK.open(SPK_FULL)
90
+ s.close
91
+ end
92
+
93
+ x.report("SPK.open (excerpt 54KB)") do
94
+ s = Ephem::SPK.open(SPK_EXCERPT)
95
+ s.close
96
+ end
97
+
98
+ x.compare!
99
+ end
100
+
101
+ # =========================================================================
102
+ # 2. FIRST DATA LOAD (COLD START)
103
+ # =========================================================================
104
+
105
+ separator "2. First Data Load (Cold Start)"
106
+
107
+ puts "Measures the cost of the first compute() call on a segment,"
108
+ puts "which triggers lazy loading of coefficient data from disk."
109
+ puts
110
+
111
+ GC.start
112
+ Benchmark.ips do |x|
113
+ x.report("cold compute (load + eval)") do
114
+ segment_emb.clear_data
115
+ segment_emb.compute(JD_TEST)
116
+ end
117
+
118
+ x.report("warm compute (cached)") do
119
+ segment_emb.compute(JD_TEST)
120
+ end
121
+
122
+ x.compare!
123
+ end
124
+
125
+ # Ensure data is loaded again for subsequent benchmarks
126
+ segment_emb.compute(JD_TEST)
127
+
128
+ # =========================================================================
129
+ # 3. SINGLE POSITION COMPUTATION (HOT PATH)
130
+ # =========================================================================
131
+
132
+ separator "3. Single Position Computation (Hot Path)"
133
+
134
+ puts "segment.compute(time) — Chebyshev eval + Vector creation"
135
+ puts
136
+
137
+ GC.start
138
+ Benchmark.ips do |x|
139
+ x.report("compute (position)") { segment_emb.compute(JD_TEST) }
140
+ end
141
+
142
+ # =========================================================================
143
+ # 4. SINGLE STATE COMPUTATION
144
+ # =========================================================================
145
+
146
+ separator "4. Single State Computation"
147
+
148
+ puts "segment.compute_and_differentiate(time) — position + velocity"
149
+ puts
150
+
151
+ GC.start
152
+ Benchmark.ips do |x|
153
+ x.report("compute_and_differentiate (state)") do
154
+ segment_emb.compute_and_differentiate(JD_TEST)
155
+ end
156
+ end
157
+
158
+ # =========================================================================
159
+ # 5. POSITION vs STATE (COMPARISON)
160
+ # =========================================================================
161
+
162
+ separator "5. Position vs State (Direct Comparison)"
163
+
164
+ GC.start
165
+ Benchmark.ips do |x|
166
+ x.report("compute (position only)") { segment_emb.compute(JD_TEST) }
167
+ x.report("compute_and_differentiate") do
168
+ segment_emb.compute_and_differentiate(JD_TEST)
169
+ end
170
+
171
+ x.compare!
172
+ end
173
+
174
+ # =========================================================================
175
+ # 6. SEQUENTIAL TIME ACCESS
176
+ # =========================================================================
177
+
178
+ separator "6. Sequential Time Access (#{SEQUENTIAL_STEPS} days)"
179
+
180
+ puts "Tests interval-finding binary search with temporal locality."
181
+ puts "The @last_interval cache should accelerate sequential access."
182
+ puts
183
+
184
+ GC.start
185
+ Benchmark.ips do |x|
186
+ x.report("sequential (#{SEQUENTIAL_STEPS} days)") do
187
+ sequential_times.each { |t| segment_emb.compute(t) }
188
+ end
189
+ end
190
+
191
+ # =========================================================================
192
+ # 7. RANDOM TIME ACCESS
193
+ # =========================================================================
194
+
195
+ separator "7. Random Time Access (#{SEQUENTIAL_STEPS} days, shuffled)"
196
+
197
+ puts "Same times as above but shuffled — measures interval cache miss impact."
198
+ puts
199
+
200
+ GC.start
201
+ Benchmark.ips do |x|
202
+ x.report("sequential access") do
203
+ sequential_times.each { |t| segment_emb.compute(t) }
204
+ end
205
+
206
+ x.report("random access") do
207
+ random_times.each { |t| segment_emb.compute(t) }
208
+ end
209
+
210
+ x.compare!
211
+ end
212
+
213
+ # =========================================================================
214
+ # 8. BATCH COMPUTATION
215
+ # =========================================================================
216
+
217
+ separator "8. Batch vs Loop Computation"
218
+
219
+ batch_sizes = [10, 100, 1000]
220
+
221
+ batch_sizes.each do |n|
222
+ times = sequential_times.first(n)
223
+
224
+ puts "--- Batch size: #{n} ---"
225
+ GC.start
226
+ Benchmark.ips do |x|
227
+ x.report("loop (#{n} times)") do
228
+ times.each { |t| segment_emb.compute_and_differentiate(t) }
229
+ end
230
+
231
+ x.report("batch (#{n} times)") do
232
+ segment_emb.compute_and_differentiate(times)
233
+ end
234
+
235
+ x.compare!
236
+ end
237
+ puts
238
+ end
239
+
240
+ # =========================================================================
241
+ # 9. MULTIPLE BODIES
242
+ # =========================================================================
243
+
244
+ separator "9. Multiple Bodies (Computation Speed by Target)"
245
+
246
+ # Pre-load all segments
247
+ body_segments = {}
248
+ BODY_PAIRS.each do |name, (center, target)|
249
+ seg = spk[center, target]
250
+ seg.compute(JD_TEST) # warm up
251
+ body_segments[name] = seg
252
+ end
253
+
254
+ GC.start
255
+ Benchmark.ips do |x|
256
+ body_segments.each do |name, seg|
257
+ x.report(name) { seg.compute(JD_TEST) }
258
+ end
259
+
260
+ x.compare!
261
+ end
262
+
263
+ # =========================================================================
264
+ # 10. CHEBYSHEV POLYNOMIAL (MICRO-BENCHMARK)
265
+ # =========================================================================
266
+
267
+ separator "10. Chebyshev Polynomial Evaluation (Micro)"
268
+
269
+ puts "Direct ChebyshevPolynomial.evaluate / evaluate_derivative calls"
270
+ puts "with real coefficients extracted from a loaded segment."
271
+ puts
272
+
273
+ # Extract real coefficient data from the loaded segment
274
+ coefficients = segment_emb.instance_variable_get(:@coefficients)
275
+ radii = segment_emb.instance_variable_get(:@radii)
276
+
277
+ # Pick the first interval's coefficients
278
+ test_coeffs = coefficients[0]
279
+ test_radius = radii[0]
280
+ test_t = 0.0 # midpoint of the interval (normalized)
281
+
282
+ puts "Polynomial degree: #{test_coeffs.size} terms"
283
+ puts "Components per term: #{test_coeffs.first.size}"
284
+ puts
285
+
286
+ GC.start
287
+ Benchmark.ips do |x|
288
+ x.report("evaluate (position)") do
289
+ Ephem::Computation::ChebyshevPolynomial.evaluate(test_coeffs, test_t)
290
+ end
291
+
292
+ x.report("evaluate_derivative (velocity)") do
293
+ Ephem::Computation::ChebyshevPolynomial.evaluate_derivative(
294
+ test_coeffs, test_t, test_radius
295
+ )
296
+ end
297
+
298
+ x.report("evaluate_with_derivative (pos+vel, 1 pass)") do
299
+ Ephem::Computation::ChebyshevPolynomial.evaluate_with_derivative(
300
+ test_coeffs, test_t, test_radius
301
+ )
302
+ end
303
+
304
+ x.compare!
305
+ end
306
+
307
+ # =========================================================================
308
+ # 11. VECTOR OPERATIONS
309
+ # =========================================================================
310
+
311
+ separator "11. Vector Operations"
312
+
313
+ v1 = Ephem::Core::Vector.new(1.0, 2.0, 3.0)
314
+ v2 = Ephem::Core::Vector.new(4.0, 5.0, 6.0)
315
+
316
+ GC.start
317
+ Benchmark.ips do |x|
318
+ x.report("Vector.new") { Ephem::Core::Vector.new(1.0, 2.0, 3.0) }
319
+ x.report("Vector + Vector") { v1 + v2 }
320
+ x.report("Vector - Vector") { v1 - v2 }
321
+ x.report("Vector.dot") { v1.dot(v2) }
322
+ x.report("Vector.cross") { v1.cross(v2) }
323
+ x.report("Vector.magnitude") { v1.magnitude }
324
+ x.report("Vector.to_a") { v1.to_a }
325
+
326
+ x.compare!
327
+ end
328
+
329
+ # =========================================================================
330
+ # 12. MEMORY PROFILING
331
+ # =========================================================================
332
+
333
+ separator "12. Memory Profiling"
334
+
335
+ # --- 12a. Object allocations per compute call ---
336
+ puts "--- Object allocations per call ---"
337
+ puts
338
+
339
+ # Warm up
340
+ segment_emb.compute(JD_TEST)
341
+ segment_emb.compute_and_differentiate(JD_TEST)
342
+
343
+ # Measure compute
344
+ GC.start
345
+ GC.disable
346
+ before = GC.stat[:total_allocated_objects]
347
+ segment_emb.compute(JD_TEST)
348
+ after = GC.stat[:total_allocated_objects]
349
+ GC.enable
350
+ puts " compute (position): #{after - before} objects allocated"
351
+
352
+ # Measure compute_and_differentiate
353
+ GC.start
354
+ GC.disable
355
+ before = GC.stat[:total_allocated_objects]
356
+ segment_emb.compute_and_differentiate(JD_TEST)
357
+ after = GC.stat[:total_allocated_objects]
358
+ GC.enable
359
+ puts " compute_and_differentiate: #{after - before} objects allocated"
360
+
361
+ # Measure batch of 100
362
+ times_100 = sequential_times.first(100)
363
+ GC.start
364
+ GC.disable
365
+ before = GC.stat[:total_allocated_objects]
366
+ segment_emb.compute_and_differentiate(times_100)
367
+ after = GC.stat[:total_allocated_objects]
368
+ GC.enable
369
+ alloc_100 = after - before
370
+ puts " batch compute_and_diff (100): #{alloc_100} objects (#{(alloc_100 / 100.0).round(1)}/call)"
371
+
372
+ puts
373
+
374
+ # --- 12b. Segment data memory footprint ---
375
+ puts "--- Segment data memory footprint ---"
376
+ puts
377
+
378
+ # Force a fresh load and measure
379
+ segment_fresh = spk[0, 3]
380
+ segment_fresh.clear_data
381
+
382
+ GC.start
383
+ before_mem = ObjectSpace.memsize_of_all
384
+ segment_fresh.compute(JD_TEST) # triggers load
385
+ GC.start
386
+ after_mem = ObjectSpace.memsize_of_all
387
+
388
+ delta_kb = (after_mem - before_mem) / 1024.0
389
+ puts " Segment [0,3] data load: ~#{delta_kb.round(1)} KB"
390
+
391
+ # Report coefficient array size
392
+ coeffs = segment_fresh.instance_variable_get(:@coefficients)
393
+ if coeffs
394
+ puts " Coefficient records: #{coeffs.size}"
395
+ puts " Terms per record: #{coeffs.first&.size}"
396
+ puts " Components per term: #{coeffs.first&.first&.size}"
397
+ end
398
+
399
+ # =========================================================================
400
+ # 13. PCK ORIENTATION (BINARY PCK)
401
+ # =========================================================================
402
+
403
+ separator "13. PCK Orientation (Binary PCK)"
404
+
405
+ puts "angles_at (Euler angles) vs orientation_at (angles + rates)"
406
+ puts
407
+
408
+ pck = Ephem::PCK.open(PCK_EXCERPT)
409
+ moon = pck[31008] # MOON_PA_DE440 frame
410
+ moon.angles_at(JD_J2000) # warm up
411
+
412
+ orientation_times = Array.new(SEQUENTIAL_STEPS) { |index| JD_J2000 + index }
413
+
414
+ GC.start
415
+ Benchmark.ips do |x|
416
+ x.report("angles_at (scalar)") { moon.angles_at(JD_J2000) }
417
+ x.report("orientation_at (scalar)") { moon.orientation_at(JD_J2000) }
418
+ x.report("angles_at (batch 1000)") { moon.angles_at(orientation_times) }
419
+
420
+ x.compare!
421
+ end
422
+
423
+ pck.close
424
+
425
+ # =========================================================================
426
+ # Cleanup
427
+ # =========================================================================
428
+
429
+ spk.close
430
+
431
+ separator "Benchmark Complete"
data/lib/ephem/cli.rb CHANGED
@@ -47,22 +47,22 @@ module Ephem
47
47
  Ruby Ephem - A tool for working with JPL Ephemerides
48
48
 
49
49
  Commands:
50
- excerpt - Create an excerpt of an SPK file
50
+ excerpt - Create an excerpt of an SPK or binary PCK kernel
51
51
  help - Show this help message
52
52
 
53
53
  Excerpt command:
54
54
  ruby-ephem excerpt [options] START_DATE END_DATE INPUT_FILE OUTPUT_FILE
55
55
 
56
56
  Options:
57
- --targets TARGET_IDS - Comma-separated list of target IDs to include
58
- (default: all targets)
57
+ --targets TARGET_IDS - Comma-separated list of target/body IDs to
58
+ include (default: all)
59
59
 
60
- Example:
60
+ Examples:
61
61
  ruby-ephem excerpt --targets 3,10,399 2000-01-01 2030-01-01 de440s.bsp excerpt.bsp
62
+ ruby-ephem excerpt --targets 31008 2000-01-01 2030-01-01 moon_pa_de440.bpc moon_excerpt.bpc
62
63
 
63
- This will create an excerpt of de440s.bsp containing only the specified
64
- targets (Earth-Moon barycenter, Sun, Earth) for the period from
65
- 2000-01-01 to 2030-01-01.
64
+ The input kernel kind (SPK or binary PCK) is detected automatically and
65
+ the excerpt is written in the same format.
66
66
  HELP
67
67
  end
68
68
 
@@ -125,9 +125,9 @@ module Ephem
125
125
  puts "Including all targets"
126
126
  end
127
127
 
128
- spk = Ephem::SPK.open(input_file)
128
+ kernel = open_kernel(input_file)
129
129
 
130
- excerpt_spk = spk.excerpt(
130
+ excerpt_kernel = kernel.excerpt(
131
131
  output_path: output_file,
132
132
  start_jd: start_jd,
133
133
  end_jd: end_jd,
@@ -136,8 +136,8 @@ module Ephem
136
136
  )
137
137
 
138
138
  puts "Excerpt created successfully!"
139
- puts "Original segments: #{spk.segments.size}"
140
- puts "Excerpt segments: #{excerpt_spk.segments.size}"
139
+ puts "Original segments: #{kernel.segments.size}"
140
+ puts "Excerpt segments: #{excerpt_kernel.segments.size}"
141
141
 
142
142
  original_size = File.size(input_file)
143
143
  excerpt_size = File.size(output_file)
@@ -148,12 +148,25 @@ module Ephem
148
148
  puts "Original: #{original_size} bytes"
149
149
  puts "Excerpt: #{excerpt_size} bytes"
150
150
 
151
- spk.close
152
- excerpt_spk.close
151
+ kernel.close
152
+ excerpt_kernel.close
153
153
  rescue => e
154
154
  puts "Error creating excerpt: #{e.message}"
155
155
  puts e.backtrace if options[:debug]
156
156
  end
157
157
  end
158
+
159
+ def self.open_kernel(path)
160
+ daf = Ephem::IO::DAF.new(File.open(path, "rb"))
161
+
162
+ if daf.file_type == :pck
163
+ Ephem::PCK.new(daf: daf)
164
+ else
165
+ Ephem::SPK.new(daf: daf)
166
+ end
167
+ rescue
168
+ daf&.close
169
+ raise
170
+ end
158
171
  end
159
172
  end
@@ -22,13 +22,13 @@ module Ephem
22
22
  b1x = b1y = b1z = 0.0
23
23
  b2x = b2y = b2z = 0.0
24
24
 
25
+ t2 = 2.0 * t
25
26
  k = n - 1
26
27
  while k > 0
27
28
  c = coeffs[k]
28
29
  c0 = c[0]
29
30
  c1 = c[1]
30
31
  c2 = c[2]
31
- t2 = 2.0 * t
32
32
  tx = t2 * b1x - b2x + c0
33
33
  ty = t2 * b1y - b2y + c1
34
34
  tz = t2 * b1z - b2z + c2
@@ -52,9 +52,9 @@ module Ephem
52
52
  # @param coeffs [Array<Array<Float>>] Array of coefficients; shape is
53
53
  # [n_terms][3].
54
54
  # @param t [Float] The normalized independent variable (in [-1, 1]).
55
- # @param radius [Float] The half-length of the time interval (days).
55
+ # @param radius [Float] The half-length of the time interval (seconds).
56
56
  # @return [Array<Float>] The 3-vector derivative (velocity), in units per
57
- # second.
57
+ # day.
58
58
  def self.evaluate_derivative(coeffs, t, radius)
59
59
  n = coeffs.size
60
60
  return [0.0, 0.0, 0.0] if n < 2
@@ -62,13 +62,13 @@ module Ephem
62
62
  d1x = d1y = d1z = 0.0
63
63
  d2x = d2y = d2z = 0.0
64
64
 
65
+ t2 = 2.0 * t
65
66
  k = n - 1
66
67
  while k > 0
67
68
  c = coeffs[k]
68
69
  c0 = c[0]
69
70
  c1 = c[1]
70
71
  c2 = c[2]
71
- t2 = 2.0 * t
72
72
  k2 = 2 * k
73
73
  tx = t2 * d1x - d2x + k2 * c0
74
74
  ty = t2 * d1y - d2y + k2 * c1
@@ -85,6 +85,65 @@ module Ephem
85
85
  scale = Ephem::Core::Constants::Time::SECONDS_PER_DAY / (2.0 * radius)
86
86
  [d1x * scale, d1y * scale, d1z * scale]
87
87
  end
88
+
89
+ ##
90
+ # Evaluates a 3D Chebyshev polynomial and its time derivative in a single
91
+ # pass. It runs the same value and derivative recurrences as {evaluate}
92
+ # and {evaluate_derivative}, but fused into one loop so the coefficient
93
+ # fetch and loop control are shared. Results are bit-for-bit identical to
94
+ # calling the two methods separately.
95
+ #
96
+ # @param coeffs [Array<Array<Float>>] coefficients; shape [n_terms][3].
97
+ # @param t [Float] normalized independent variable, in [-1, 1].
98
+ # @param radius [Float] half-length of the time interval (seconds).
99
+ # @return [Array(Array<Float>, Array<Float>)] [position, velocity], with
100
+ # velocity in units per day.
101
+ def self.evaluate_with_derivative(coeffs, t, radius)
102
+ n = coeffs.size
103
+ b1x = b1y = b1z = 0.0
104
+ b2x = b2y = b2z = 0.0
105
+ d1x = d1y = d1z = 0.0
106
+ d2x = d2y = d2z = 0.0
107
+
108
+ t2 = 2.0 * t
109
+ k = n - 1
110
+ while k > 0
111
+ c = coeffs[k]
112
+ c0 = c[0]
113
+ c1 = c[1]
114
+ c2 = c[2]
115
+ k2 = 2 * k
116
+
117
+ bx = t2 * b1x - b2x + c0
118
+ by = t2 * b1y - b2y + c1
119
+ bz = t2 * b1z - b2z + c2
120
+ dx = t2 * d1x - d2x + k2 * c0
121
+ dy = t2 * d1y - d2y + k2 * c1
122
+ dz = t2 * d1z - d2z + k2 * c2
123
+
124
+ b2x = b1x
125
+ b2y = b1y
126
+ b2z = b1z
127
+ b1x = bx
128
+ b1y = by
129
+ b1z = bz
130
+ d2x = d1x
131
+ d2y = d1y
132
+ d2z = d1z
133
+ d1x = dx
134
+ d1y = dy
135
+ d1z = dz
136
+ k -= 1
137
+ end
138
+
139
+ c0, c1, c2 = coeffs[0]
140
+ position = [t * b1x - b2x + c0, t * b1y - b2y + c1, t * b1z - b2z + c2]
141
+
142
+ scale = Ephem::Core::Constants::Time::SECONDS_PER_DAY / (2.0 * radius)
143
+ velocity = [d1x * scale, d1y * scale, d1z * scale]
144
+
145
+ [position, velocity]
146
+ end
88
147
  end
89
148
  end
90
149
  end