Skip to content

Commit edd3cf8

Browse files
authored
IMCCE INPOP support (#20)
I can't hide this change makes me super happy. _INPOP_ is the ephemerides series from the Paris Observatory, more precisely from the IMCCE laboratory. The Paris Observatory has been a central place for astronomy for the last few centuries and the data produces today by the IMCCE is among the most accurate and precise we can produce today. I frequently use data from the IMCCE in the development and tests of Astronoby, main library using Ephem at the moment, so it feels right to support their ephemerides as well. I am extremely lucky that they offer a SPICE version of their ephemeris files for most of INPOP's versions. As you can see, this change doesn't implement any modification of the SPK parsing logic, it only adds the support for downloading files from somewhere else than the JPL. The choice between JPL and IMCCE is just a matter of preference, their data being extremely close. While you can witness differences of a few kilometers in positions, the difference becomes barely noticeable when used in libraries transforming the data into topocentric coordinates like Astronoby. The list of supported files is: * `inpop10b.bsp` * `inpop10b_large.bsp` * `inpop10e.bsp` * `inpop10e_large.bsp` * `inpop13c.bsp` * `inpop13c_large.bsp` * `inpop17a.bsp` * `inpop17a_large.bsp` * `inpop19a.bsp` * `inpop19a_large.bsp` * `inpop21a.bsp` * `inpop21a_large` The difference between ephemerides with or without `_large` is the time period: from 1900 to 2100 for regular ephemerides, from 1000 to 3000, Common Era.
1 parent e322110 commit edd3cf8

12 files changed

Lines changed: 216 additions & 14 deletions

Gemfile.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ PATH
22
remote: .
33
specs:
44
ephem (0.1.0)
5+
minitar (~> 0.12)
56
numo-narray (~> 0.9.2.1)
7+
zlib (~> 3.2)
68

79
GEM
810
remote: https://rubygems.org/
@@ -19,6 +21,7 @@ GEM
1921
json (2.10.2)
2022
language_server-protocol (3.17.0.4)
2123
lint_roller (1.1.0)
24+
minitar (0.12.1)
2225
numo-narray (0.9.2.1)
2326
parallel (1.26.3)
2427
parser (3.3.7.1)
@@ -85,6 +88,7 @@ GEM
8588
unicode-display_width (3.1.4)
8689
unicode-emoji (~> 4.0, >= 4.0.4)
8790
unicode-emoji (4.0.4)
91+
zlib (3.2.1)
8892

8993
PLATFORMS
9094
arm64-darwin-23

README.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
# Ephem
22

3-
Ephem is a Ruby gem that provides a simple interface to the JPL Development
4-
Ephemeris (DE) series as SPICE binary kernel files. The DE series is a
5-
collection of numerical integrations of the equations of motion of the solar
6-
system, used to calculate the positions of the planets, the Moon, and other
7-
celestial bodies with high precision.
3+
Ephem is a Ruby gem that provides a simple interface to the SPICE binary kernel
4+
files such as:
5+
* _JPL [Development Ephemeris]_ (DE)
6+
* _IMCCE [Intégrateur numérique planétaire de l'Observatoire de Paris]_ (INPOP)
87

9-
Ephem currently only support planetary ephemerides like DE405, DE421, DE430,
10-
etc.
8+
[Development Ephemeris]: https://ssd.jpl.nasa.gov/planets/eph_export.html
9+
[Intégrateur numérique planétaire de l'Observatoire de Paris]: https://www.imcce.fr/inpop
1110

12-
The library in high development mode and does not have a stable version yet.
11+
These files are a collection of numerical integrations of the equations of
12+
motion of the Solar System, used to calculate the positions of the planets,
13+
the Moon, and other celestial bodies with high precision.
14+
15+
Ephem currently only support planetary ephemerides like DE421, DE430,
16+
INPOP19A, etc.
17+
18+
The library is in high development mode and does not have a stable version yet.
1319
The API is subject to major changes at the moment, please keep that in mind if
1420
you consider adding this gem as a dependency.
1521

@@ -30,11 +36,13 @@ gem install ephem
3036

3137
## How to select the right kernel file
3238

33-
JPL produces many different kernels over the years, with different accuracy and
34-
ranges of supported years. Here are some that we would recommend to begin with:
39+
JPL and IMCCE produces many different kernels over the years, with different
40+
accuracy and ranges of supported years. Here are some that we would recommend to
41+
begin with:
3542

3643
* `de421.bsp`: from 1900 to 2050, 17 MB
3744
* `de440s.bsp`: from 1849 to 2150, 32 MB
45+
* `inpop19a.bsp`: from 1900 to 2100, 22 MB
3846

3947
## Usage
4048

ephem.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ Gem::Specification.new do |spec|
3434
spec.executables = ["ruby-ephem"]
3535
spec.require_paths = ["lib"]
3636

37+
spec.add_dependency "minitar", "~> 0.12"
3738
spec.add_dependency "numo-narray", "~> 0.9.2.1"
39+
spec.add_dependency "zlib", "~> 3.2"
3840

3941
spec.add_development_dependency "csv", "~> 3.3"
4042
spec.add_development_dependency "irb", "~> 1.15"

lib/ephem/download.rb

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
# frozen_string_literal: true
22

3+
require "minitar"
34
require "net/http"
5+
require "tempfile"
6+
require "zlib"
47

58
module Ephem
69
class Download
7-
BASE_URL = "https://ssd.jpl.nasa.gov/ftp/eph/planets/bsp/"
10+
JPL_BASE_URL = "https://ssd.jpl.nasa.gov/ftp/eph/planets/bsp/"
11+
IMCCE_BASE_URL = "https://ftp.imcce.fr/pub/ephem/planets/"
812

9-
SUPPORTED_KERNELS = %w[
13+
JPL_KERNELS = %w[
1014
de102.bsp
1115
de200.bsp
1216
de202.bsp
@@ -53,6 +57,39 @@ class Download
5357
de441.bsp
5458
].freeze
5559

60+
IMCCE_KERNELS = {
61+
"inpop10b.bsp" => "inpop10b_TDB_m100_p100_spice.bsp",
62+
"inpop10b_large.bsp" => "inpop10b_TDB_m1000_p1000_spice.bsp",
63+
"inpop10e.bsp" => "inpop10e_TDB_m100_p100_spice.bsp",
64+
"inpop10e_large.bsp" => "inpop10e_TDB_m1000_p1000_spice.bsp",
65+
"inpop13c.bsp" => "inpop13c_TDB_m100_p100_spice.bsp",
66+
"inpop13c_large.bsp" => "inpop13c_TDB_m1000_p1000_spice.bsp",
67+
"inpop17a.bsp" => "inpop17a_TDB_m100_p100_spice.bsp",
68+
"inpop17a_large.bsp" => "inpop17a_TDB_m1000_p1000_spice.bsp",
69+
"inpop19a.bsp" => "inpop19a_TDB_m100_p100_spice.bsp",
70+
"inpop19a_large.bsp" => "inpop19a_TDB_m1000_p1000_spice.bsp",
71+
"inpop21a.bsp" => "inpop21a_TDB_m100_p100_spice.bsp",
72+
"inpop21a_large.bsp" => "inpop21a_TDB_m1000_p1000_spice.bsp"
73+
}.freeze
74+
75+
IMCCE_KERNELS_MATCHING = {
76+
"inpop10b.bsp" => "inpop10b/inpop10b_TDB_m100_p100_spice.tar.gz",
77+
"inpop10b_large.bsp" => "inpop10b/inpop10b_TDB_m1000_p1000_spice.tar.gz",
78+
"inpop10e.bsp" => "inpop10e/inpop10e_TDB_m100_p100_spice_release2.tar.gz",
79+
"inpop10e_large.bsp" =>
80+
"inpop10e/inpop10e_TDB_m1000_p1000_spice_release2.tar.gz",
81+
"inpop13c.bsp" => "inpop13c/inpop13c_TDB_m100_p100_spice.tar.gz",
82+
"inpop13c_large.bsp" => "inpop13c/inpop13c_TDB_m1000_p1000_spice.tar.gz",
83+
"inpop17a.bsp" => "inpop17a/inpop17a_TDB_m100_p100_spice.tar.gz",
84+
"inpop17a_large.bsp" => "inpop17a/inpop17a_TDB_m1000_p1000_spice.tar.gz",
85+
"inpop19a.bsp" => "inpop19a/inpop19a_TDB_m100_p100_spice.tar.gz",
86+
"inpop19a_large.bsp" => "inpop19a/inpop19a_TDB_m1000_p1000_spice.tar.gz",
87+
"inpop21a.bsp" => "inpop21a/inpop21a_TDB_m100_p100_spice.tar.gz",
88+
"inpop21a_large.bsp" => "inpop21a/inpop21a_TDB_m1000_p1000_spice.tar.gz"
89+
}.freeze
90+
91+
SUPPORTED_KERNELS = (JPL_KERNELS + IMCCE_KERNELS.keys).freeze
92+
5693
def self.call(name:, target:)
5794
new(name, target).call
5895
end
@@ -64,8 +101,7 @@ def initialize(name, local_path)
64101
end
65102

66103
def call
67-
uri = URI("#{BASE_URL}#{@name}")
68-
content = Net::HTTP.get(uri)
104+
content = jpl_kernel? ? download_from_jpl : download_from_imcce
69105
File.write(@local_path, content)
70106

71107
true
@@ -79,5 +115,33 @@ def validate_requested_kernel!
79115
"Kernel #{@name} is not supported by the library at the moment."
80116
end
81117
end
118+
119+
def jpl_kernel?
120+
JPL_KERNELS.include?(@name)
121+
end
122+
123+
def download_from_jpl
124+
uri = URI("#{JPL_BASE_URL}#{@name}")
125+
Net::HTTP.get(uri)
126+
end
127+
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]
139+
end
140+
end
141+
end
142+
ensure
143+
temp_file.close
144+
temp_file.unlink
145+
end
82146
end
83147
end

lib/ephem/segments/base_segment.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class BaseSegment
3636
attr_reader :target
3737
# @return [Integer] the center body ID
3838
attr_reader :center
39+
# @return [String] the source of the segment
40+
attr_reader :source
3941

4042
# Initialize a new segment
4143
#

lib/ephem/spk.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ module Ephem
1515
# spk.close
1616
#
1717
class SPK
18+
TYPES = [
19+
INPOP = "IMCCE INPOP",
20+
JPL_DE = "JPL DE"
21+
].freeze
22+
23+
INPOP_REGEXP = /^\s+\d{4}\.\d{5}0+$/
24+
DE_REGEXP = /^[A-Z]E-(\d{4})LE-\1$/
25+
DE_FILENAME = "NIO2SPK"
26+
1827
DATA_TYPE_IDENTIFIER = 5
1928
SEGMENT_CLASSES = {}
2029

@@ -78,6 +87,18 @@ def [](center, target)
7887
end
7988
end
8089

90+
# Type of SPK file to make the difference between JPL DE and IMCCE INPOP
91+
#
92+
# @return [String, nil] The type of the SPK file
93+
def type
94+
@type ||= if @daf.record_data.internal_filename.match?(INPOP_REGEXP)
95+
INPOP
96+
elsif @daf.record_data.internal_filename == DE_FILENAME ||
97+
segments.first&.source&.match?(DE_REGEXP)
98+
JPL_DE
99+
end
100+
end
101+
81102
# Returns the comments stored in the SPK file.
82103
#
83104
# @return [String] The comments from the DAF file

spec/ephem/download_spec.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,35 @@
22

33
RSpec.describe Ephem::Download do
44
describe ".call" do
5+
context "when downloading a JPL kernel" do
6+
it "downloads and writes the JPL kernel file" do
7+
name = "de440.bsp"
8+
target_path = "tmp/kernel.bsp"
9+
mock_content = "large-binary-data"
10+
allow(Net::HTTP).to receive(:get).and_return(mock_content)
11+
allow(File).to receive(:write)
12+
13+
described_class.call(name: name, target: target_path)
14+
15+
expect(File).to have_received(:write).with(target_path, mock_content)
16+
end
17+
end
18+
19+
context "when downloading an IMCCE kernel" do
20+
it "downloads, extracts, and writes the IMCCE kernel file" do
21+
name = "inpop19a.bsp"
22+
target_path = "tmp/kernel.bsp"
23+
mock_content = "large-binary-data"
24+
tar_gz_file = create_temp_tar_gz_with(name => mock_content)
25+
allow(Net::HTTP).to receive(:get).and_return(tar_gz_file.read)
26+
allow(File).to receive(:write)
27+
28+
described_class.call(name: name, target: target_path)
29+
30+
expect(File).to have_received(:write).with(target_path, mock_content)
31+
end
32+
end
33+
534
context "when the kernel is not supported" do
635
it "raises an UnsupportedError" do
736
expect { described_class.call(name: "unsupported", target: "path") }.to(
@@ -13,4 +42,25 @@
1342
end
1443
end
1544
end
45+
46+
def create_temp_tar_gz_with(files)
47+
Tempfile.new.tap do |tempfile|
48+
Zlib::GzipWriter.open(tempfile) do |gz|
49+
Minitar::Writer.open(gz) do |tar|
50+
files.each do |filename, content|
51+
io = StringIO.new(content)
52+
tar.add_file_simple(
53+
described_class::IMCCE_KERNELS[filename],
54+
size: content.bytesize,
55+
mode: 0o644,
56+
mtime: Time.now.to_i
57+
) do |out|
58+
IO.copy_stream(io, out)
59+
end
60+
end
61+
end
62+
end
63+
tempfile.rewind
64+
end
65+
end
1666
end

spec/ephem/spk_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
RSpec.describe Ephem::SPK do
4+
include TestSpkHelper
5+
46
describe ".open" do
57
it "creates a new SPK instance with a DAF from the given path" do
68
file_double = instance_double(File)
@@ -73,6 +75,43 @@
7375
end
7476
end
7577

78+
describe "#type" do
79+
context "when record data has INPOP-like filename" do
80+
it "returns the INPOP identifier" do
81+
spk = described_class.open(inpop21a_2000_excerpt)
82+
83+
type = spk.type
84+
85+
expect(type).to eq described_class::INPOP
86+
end
87+
end
88+
89+
context "when record data has DE-like filename" do
90+
it "returns the JPL DE identifier" do
91+
spk = described_class.open(de421_2000_excerpt)
92+
93+
type = spk.type
94+
95+
expect(type).to eq described_class::JPL_DE
96+
end
97+
end
98+
99+
context "when the first segment has a DE-like source" do
100+
it "returns the JPL DE identifier" do
101+
spk = described_class.open(de405_2000_excerpt)
102+
103+
type = spk.type
104+
105+
expect(type).to eq described_class::JPL_DE
106+
end
107+
end
108+
109+
context "when there is no information available" do
110+
it "returns nil" do
111+
end
112+
end
113+
end
114+
76115
describe "#to_s" do
77116
it "returns a formatted description of the SPK file and its segments" do
78117
daf = instance_double(Ephem::IO::DAF)
57.9 KB
Binary file not shown.
53.9 KB
Binary file not shown.

0 commit comments

Comments
 (0)