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.
- checksums.yaml +4 -4
- data/.tool-versions +1 -1
- data/CHANGELOG.md +121 -32
- data/README.md +81 -4
- data/benchmarks/run.rb +431 -0
- data/lib/ephem/cli.rb +26 -13
- data/lib/ephem/computation/chebyshev_polynomial.rb +63 -4
- data/lib/ephem/core/orientation.rb +118 -0
- data/lib/ephem/core/rotation.rb +82 -0
- data/lib/ephem/core/state.rb +5 -0
- data/lib/ephem/download.rb +33 -3
- data/lib/ephem/excerpt.rb +17 -12
- data/lib/ephem/io/binary_reader.rb +6 -4
- data/lib/ephem/io/daf.rb +18 -1
- data/lib/ephem/io/record_parser.rb +2 -2
- data/lib/ephem/io/summary_manager.rb +14 -15
- data/lib/ephem/pck.rb +118 -0
- data/lib/ephem/segments/base_segment.rb +25 -8
- data/lib/ephem/segments/chebyshev_type2.rb +142 -0
- data/lib/ephem/segments/orientation_group.rb +59 -0
- data/lib/ephem/segments/orientation_segment.rb +118 -0
- data/lib/ephem/segments/orientation_source.rb +17 -0
- data/lib/ephem/segments/position_group.rb +46 -0
- data/lib/ephem/segments/registry.rb +9 -3
- data/lib/ephem/segments/segment.rb +24 -224
- data/lib/ephem/segments/segment_group.rb +84 -0
- data/lib/ephem/spk.rb +20 -9
- data/lib/ephem/version.rb +1 -1
- data/lib/ephem.rb +9 -0
- metadata +27 -17
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ephem
|
|
4
|
+
module Core
|
|
5
|
+
# The orientation of a body, expressed as the three Euler angles that rotate
|
|
6
|
+
# a reference frame (e.g. J2000/ICRF) into the body-fixed frame, optionally
|
|
7
|
+
# with their time derivatives.
|
|
8
|
+
#
|
|
9
|
+
# For binary PCK orientation kernels the angles are the classical 3-1-3
|
|
10
|
+
# (Z-X-Z) sequence: +phi+ and +theta+ orient the pole and +psi+ is the
|
|
11
|
+
# rotation about it (the prime meridian). Angles are in radians and rates,
|
|
12
|
+
# when present, in radians per day — matching ephem's per-day rate
|
|
13
|
+
# convention for SPK velocities (divide by 86400 for radians per second).
|
|
14
|
+
class Orientation
|
|
15
|
+
# @return [Numeric] first Euler angle (radians)
|
|
16
|
+
attr_reader :phi
|
|
17
|
+
|
|
18
|
+
# @return [Numeric] second Euler angle (radians)
|
|
19
|
+
attr_reader :theta
|
|
20
|
+
|
|
21
|
+
# @return [Numeric] third Euler angle (radians)
|
|
22
|
+
attr_reader :psi
|
|
23
|
+
|
|
24
|
+
# @return [Array<Numeric>, nil] [phi, theta, psi] rates (radians/day),
|
|
25
|
+
# or nil when the orientation carries no rates
|
|
26
|
+
attr_reader :rates
|
|
27
|
+
|
|
28
|
+
# @param phi [Numeric] first Euler angle (radians)
|
|
29
|
+
# @param theta [Numeric] second Euler angle (radians)
|
|
30
|
+
# @param psi [Numeric] third Euler angle (radians)
|
|
31
|
+
# @param rates [Array<Numeric>, nil] optional [phi, theta, psi] rates
|
|
32
|
+
# (radians/day)
|
|
33
|
+
# @raise [Ephem::InvalidInputError] if any angle or rate is not numeric
|
|
34
|
+
def initialize(phi, theta, psi, rates: nil)
|
|
35
|
+
unless phi.is_a?(Numeric) && theta.is_a?(Numeric) && psi.is_a?(Numeric)
|
|
36
|
+
raise InvalidInputError, "Orientation angles must be numeric"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless rates.nil? || valid_rates?(rates)
|
|
40
|
+
raise InvalidInputError, "Orientation rates must be three numerics"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@phi = phi
|
|
44
|
+
@theta = theta
|
|
45
|
+
@psi = psi
|
|
46
|
+
@rates = rates&.freeze
|
|
47
|
+
freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.[](phi, theta, psi, rates: nil)
|
|
51
|
+
new(phi, theta, psi, rates: rates)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Boolean] whether this orientation carries rates
|
|
55
|
+
def rates?
|
|
56
|
+
!@rates.nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Array<Numeric>] the three Euler angles [phi, theta, psi]
|
|
60
|
+
def to_a
|
|
61
|
+
[phi, theta, psi]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The rotation matrix that maps the reference frame into the body-fixed
|
|
65
|
+
# frame, built from the 3-1-3 (Z-X-Z) Euler angles:
|
|
66
|
+
# +M = Rz(psi) * Rx(theta) * Rz(phi)+. Rates are ignored.
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Array<Float>>] a 3x3 rotation matrix
|
|
69
|
+
def to_matrix
|
|
70
|
+
Rotation.multiply(
|
|
71
|
+
Rotation.about_z(psi),
|
|
72
|
+
Rotation.about_x(theta),
|
|
73
|
+
Rotation.about_z(phi)
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @param index [Integer] 0 for phi, 1 for theta, 2 for psi
|
|
78
|
+
# @return [Numeric] the angle at the given index
|
|
79
|
+
# @raise [Ephem::IndexError] if index is not 0, 1, or 2
|
|
80
|
+
def [](index)
|
|
81
|
+
case index
|
|
82
|
+
when 0 then phi
|
|
83
|
+
when 1 then theta
|
|
84
|
+
when 2 then psi
|
|
85
|
+
else raise IndexError, "Invalid index: #{index}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def inspect
|
|
90
|
+
body = "phi: #{phi}, theta: #{theta}, psi: #{psi}"
|
|
91
|
+
body += ", rates: #{rates}" if rates?
|
|
92
|
+
"Orientation[#{body}]"
|
|
93
|
+
end
|
|
94
|
+
alias_method :to_s, :inspect
|
|
95
|
+
|
|
96
|
+
def hash
|
|
97
|
+
[phi, theta, psi, rates, self.class].hash
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ==(other)
|
|
101
|
+
unless other.is_a?(self.class)
|
|
102
|
+
raise InvalidInputError, "Can only compare with another Orientation"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
to_a == other.to_a && rates == other.rates
|
|
106
|
+
end
|
|
107
|
+
alias_method :eql?, :==
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def valid_rates?(rates)
|
|
112
|
+
rates.is_a?(Array) &&
|
|
113
|
+
rates.size == 3 &&
|
|
114
|
+
rates.all?(Numeric)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ephem
|
|
4
|
+
module Core
|
|
5
|
+
# Builds and applies 3x3 rotation matrices. Kernel-agnostic: callers choose
|
|
6
|
+
# the axis sequence and order that matches their frame convention.
|
|
7
|
+
#
|
|
8
|
+
# The elementary rotations use the coordinate-frame (passive) convention:
|
|
9
|
+
# they express a fixed vector in a frame rotated by +angle about the axis
|
|
10
|
+
module Rotation
|
|
11
|
+
# @param angle [Numeric] rotation angle in radians
|
|
12
|
+
# @return [Array<Array<Float>>] rotation about the X axis
|
|
13
|
+
def self.about_x(angle)
|
|
14
|
+
cosine = Math.cos(angle)
|
|
15
|
+
sine = Math.sin(angle)
|
|
16
|
+
[
|
|
17
|
+
[1.0, 0.0, 0.0],
|
|
18
|
+
[0.0, cosine, sine],
|
|
19
|
+
[0.0, -sine, cosine]
|
|
20
|
+
]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param angle [Numeric] rotation angle in radians
|
|
24
|
+
# @return [Array<Array<Float>>] rotation about the Y axis
|
|
25
|
+
def self.about_y(angle)
|
|
26
|
+
cosine = Math.cos(angle)
|
|
27
|
+
sine = Math.sin(angle)
|
|
28
|
+
[
|
|
29
|
+
[cosine, 0.0, -sine],
|
|
30
|
+
[0.0, 1.0, 0.0],
|
|
31
|
+
[sine, 0.0, cosine]
|
|
32
|
+
]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param angle [Numeric] rotation angle in radians
|
|
36
|
+
# @return [Array<Array<Float>>] rotation about the Z axis
|
|
37
|
+
def self.about_z(angle)
|
|
38
|
+
cosine = Math.cos(angle)
|
|
39
|
+
sine = Math.sin(angle)
|
|
40
|
+
[
|
|
41
|
+
[cosine, sine, 0.0],
|
|
42
|
+
[-sine, cosine, 0.0],
|
|
43
|
+
[0.0, 0.0, 1.0]
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Product of rotation matrices in the given order, as standard matrix
|
|
48
|
+
# multiplication: +multiply(a, b, c)+ returns +a * b * c+.
|
|
49
|
+
#
|
|
50
|
+
# @param matrices [Array<Array<Array<Float>>>] one or more 3x3 matrices
|
|
51
|
+
# @return [Array<Array<Float>>] the combined rotation matrix
|
|
52
|
+
def self.multiply(*matrices)
|
|
53
|
+
matrices.reduce { |product, matrix| multiply_pair(product, matrix) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Applies a rotation matrix to a vector.
|
|
57
|
+
#
|
|
58
|
+
# @param matrix [Array<Array<Float>>] a 3x3 rotation matrix
|
|
59
|
+
# @param vector [Core::Vector, Array<Numeric>] the vector to rotate
|
|
60
|
+
# @return [Core::Vector] the rotated vector
|
|
61
|
+
def self.apply(matrix, vector)
|
|
62
|
+
x, y, z = vector.to_a
|
|
63
|
+
Vector.new(
|
|
64
|
+
matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z,
|
|
65
|
+
matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z,
|
|
66
|
+
matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.multiply_pair(left, right)
|
|
71
|
+
Array.new(3) do |row|
|
|
72
|
+
Array.new(3) do |column|
|
|
73
|
+
left[row][0] * right[0][column] +
|
|
74
|
+
left[row][1] * right[1][column] +
|
|
75
|
+
left[row][2] * right[2][column]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
private_class_method :multiply_pair
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/ephem/core/state.rb
CHANGED
|
@@ -51,6 +51,11 @@ module Ephem
|
|
|
51
51
|
)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def inspect
|
|
55
|
+
"State[position: #{position}, velocity: #{velocity}]"
|
|
56
|
+
end
|
|
57
|
+
alias_method :to_s, :inspect
|
|
58
|
+
|
|
54
59
|
# Converts the state vectors to arrays.
|
|
55
60
|
#
|
|
56
61
|
# @return [Array<Array<Numeric>>] Array containing position and velocity
|
data/lib/ephem/download.rb
CHANGED
|
@@ -11,6 +11,8 @@ module Ephem
|
|
|
11
11
|
class Download
|
|
12
12
|
JPL_BASE_URL = "https://ssd.jpl.nasa.gov/ftp/eph/planets/bsp/"
|
|
13
13
|
IMCCE_BASE_URL = "https://ftp.imcce.fr/pub/ephem/planets/"
|
|
14
|
+
NAIF_PCK_BASE_URL =
|
|
15
|
+
"https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/"
|
|
14
16
|
|
|
15
17
|
JPL_KERNELS = %w[
|
|
16
18
|
de102.bsp
|
|
@@ -59,6 +61,11 @@ module Ephem
|
|
|
59
61
|
de441.bsp
|
|
60
62
|
].freeze
|
|
61
63
|
|
|
64
|
+
NAIF_PCK_KERNELS = %w[
|
|
65
|
+
moon_pa_de421_1900-2050.bpc
|
|
66
|
+
moon_pa_de440_200625.bpc
|
|
67
|
+
].freeze
|
|
68
|
+
|
|
62
69
|
IMCCE_KERNELS = {
|
|
63
70
|
"inpop10b.bsp" => "inpop10b_TDB_m100_p100_spice.bsp",
|
|
64
71
|
"inpop10b_large.bsp" => "inpop10b_TDB_m1000_p1000_spice.bsp",
|
|
@@ -90,7 +97,8 @@ module Ephem
|
|
|
90
97
|
"inpop21a_large.bsp" => "inpop21a/inpop21a_TDB_m1000_p1000_spice.tar.gz"
|
|
91
98
|
}.freeze
|
|
92
99
|
|
|
93
|
-
SUPPORTED_KERNELS =
|
|
100
|
+
SUPPORTED_KERNELS =
|
|
101
|
+
(JPL_KERNELS + NAIF_PCK_KERNELS + IMCCE_KERNELS.keys).freeze
|
|
94
102
|
|
|
95
103
|
def self.call(name:, target:)
|
|
96
104
|
new(name, target).call
|
|
@@ -104,12 +112,22 @@ module Ephem
|
|
|
104
112
|
|
|
105
113
|
def call
|
|
106
114
|
FileUtils.mkdir_p(@target_path.dirname)
|
|
107
|
-
|
|
115
|
+
download_kernel
|
|
108
116
|
true
|
|
109
117
|
end
|
|
110
118
|
|
|
111
119
|
private
|
|
112
120
|
|
|
121
|
+
def download_kernel
|
|
122
|
+
if jpl_kernel?
|
|
123
|
+
download_jpl
|
|
124
|
+
elsif pck_kernel?
|
|
125
|
+
download_naif_pck
|
|
126
|
+
else
|
|
127
|
+
download_imcce
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
113
131
|
def validate_requested_kernel!
|
|
114
132
|
unless SUPPORTED_KERNELS.include?(@name)
|
|
115
133
|
raise UnsupportedError,
|
|
@@ -121,8 +139,20 @@ module Ephem
|
|
|
121
139
|
JPL_KERNELS.include?(@name)
|
|
122
140
|
end
|
|
123
141
|
|
|
142
|
+
def pck_kernel?
|
|
143
|
+
NAIF_PCK_KERNELS.include?(@name)
|
|
144
|
+
end
|
|
145
|
+
|
|
124
146
|
def download_jpl
|
|
125
|
-
|
|
147
|
+
download_direct(JPL_BASE_URL)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def download_naif_pck
|
|
151
|
+
download_direct(NAIF_PCK_BASE_URL)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def download_direct(base_url)
|
|
155
|
+
uri = URI.join(base_url, @name)
|
|
126
156
|
@target_path.open("wb") do |file|
|
|
127
157
|
stream_http_to_file(uri, file)
|
|
128
158
|
end
|
data/lib/ephem/excerpt.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ephem
|
|
4
|
-
# The Excerpt class creates SPK
|
|
5
|
-
#
|
|
6
|
-
# the data needed for specific applications.
|
|
4
|
+
# The Excerpt class creates DAF excerpts (SPK or binary PCK) with reduced time
|
|
5
|
+
# spans and bodies. This is useful for creating smaller files that focus only
|
|
6
|
+
# on the data needed for specific applications.
|
|
7
7
|
#
|
|
8
8
|
# @example Create an excerpt with specific time range and bodies
|
|
9
9
|
# spk = Ephem::SPK.open("de421.bsp")
|
|
@@ -19,11 +19,11 @@ module Ephem
|
|
|
19
19
|
J2000_EPOCH = Core::Constants::Time::J2000_EPOCH
|
|
20
20
|
RECORD_SIZE = 1024
|
|
21
21
|
|
|
22
|
-
# @param
|
|
23
|
-
def initialize(
|
|
24
|
-
@
|
|
25
|
-
@daf =
|
|
26
|
-
@binary_reader = @daf.
|
|
22
|
+
# @param kernel [Ephem::SPK, Ephem::PCK] The kernel to excerpt from
|
|
23
|
+
def initialize(kernel)
|
|
24
|
+
@kernel = kernel
|
|
25
|
+
@daf = kernel.daf
|
|
26
|
+
@binary_reader = @daf.binary_reader
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Creates an excerpt of the SPK file
|
|
@@ -31,11 +31,12 @@ module Ephem
|
|
|
31
31
|
# @param output_path [String] Path where the excerpt will be written
|
|
32
32
|
# @param start_jd [Float] Start time as Julian Date
|
|
33
33
|
# @param end_jd [Float] End time as Julian Date
|
|
34
|
-
# @param target_ids [Array<Integer>, nil] Optional list of target IDs
|
|
35
|
-
# include
|
|
34
|
+
# @param target_ids [Array<Integer>, nil] Optional list of target/body IDs
|
|
35
|
+
# to include
|
|
36
36
|
# @param debug [Boolean] Whether to print debug information
|
|
37
37
|
#
|
|
38
|
-
# @return [Ephem::SPK] A new
|
|
38
|
+
# @return [Ephem::SPK, Ephem::PCK] A new instance for the excerpt file,
|
|
39
|
+
# matching the source kernel kind
|
|
39
40
|
def extract(output_path:, start_jd:, end_jd:, target_ids: nil, debug: false)
|
|
40
41
|
start_seconds = seconds_since_j2000(start_jd)
|
|
41
42
|
end_seconds = seconds_since_j2000(end_jd)
|
|
@@ -46,11 +47,15 @@ module Ephem
|
|
|
46
47
|
process_segments(writer, start_seconds, end_seconds, target_ids, debug)
|
|
47
48
|
output_file.close
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
reopen(output_path)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
private
|
|
53
54
|
|
|
55
|
+
def reopen(path)
|
|
56
|
+
(@daf.file_type == :pck) ? PCK.open(path) : SPK.open(path)
|
|
57
|
+
end
|
|
58
|
+
|
|
54
59
|
def seconds_since_j2000(jd)
|
|
55
60
|
(jd - J2000_EPOCH) * S_PER_DAY
|
|
56
61
|
end
|
|
@@ -4,6 +4,8 @@ module Ephem
|
|
|
4
4
|
module IO
|
|
5
5
|
class BinaryReader
|
|
6
6
|
RECORD_SIZE = 1024
|
|
7
|
+
ENDIANNESS_DOUBLE_FORMATS = {little: "E", big: "G"}.freeze
|
|
8
|
+
ENDIANNESS_UINT32_FORMATS = {little: "V", big: "N"}.freeze
|
|
7
9
|
|
|
8
10
|
def initialize(file_object)
|
|
9
11
|
validate_file_object(file_object)
|
|
@@ -81,20 +83,20 @@ module Ephem
|
|
|
81
83
|
def read_and_pad_record
|
|
82
84
|
data = @file.read(RECORD_SIZE)
|
|
83
85
|
raise IOError, "Failed to read record" unless data
|
|
86
|
+
|
|
84
87
|
data.ljust(RECORD_SIZE, "\0")
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
def read_array_data(length, endianness)
|
|
88
91
|
data = @file.read(8 * length)
|
|
89
92
|
raise IOError, "Failed to read array" unless data
|
|
93
|
+
|
|
90
94
|
data.unpack("#{endianness_format(endianness)}#{length}")
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
def endianness_format(endianness)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
when :big then "G"
|
|
97
|
-
else raise ArgumentError, "Invalid endianness: #{endianness}"
|
|
98
|
+
ENDIANNESS_DOUBLE_FORMATS.fetch(endianness) do
|
|
99
|
+
raise ArgumentError, "Invalid endianness: #{endianness}"
|
|
98
100
|
end
|
|
99
101
|
end
|
|
100
102
|
end
|
data/lib/ephem/io/daf.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Ephem
|
|
4
4
|
module IO
|
|
5
5
|
class DAF
|
|
6
|
-
attr_reader :record_data, :endianness
|
|
6
|
+
attr_reader :binary_reader, :record_data, :endianness
|
|
7
7
|
|
|
8
8
|
def initialize(file_object)
|
|
9
9
|
@binary_reader = BinaryReader.new(file_object)
|
|
@@ -15,6 +15,11 @@ module Ephem
|
|
|
15
15
|
@comments ||= load_comments
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# @return [Symbol, nil]
|
|
19
|
+
def file_type
|
|
20
|
+
@file_type ||= detect_file_type
|
|
21
|
+
end
|
|
22
|
+
|
|
18
23
|
def summaries(&block)
|
|
19
24
|
return enum_for(:summaries) unless block_given?
|
|
20
25
|
|
|
@@ -36,6 +41,18 @@ module Ephem
|
|
|
36
41
|
|
|
37
42
|
private
|
|
38
43
|
|
|
44
|
+
def detect_file_type
|
|
45
|
+
case @record_data.locator_identifier
|
|
46
|
+
when "DAF/SPK" then :spk
|
|
47
|
+
when "DAF/PCK" then :pck
|
|
48
|
+
else
|
|
49
|
+
case @record_data.integer_count
|
|
50
|
+
when 6 then :spk
|
|
51
|
+
when 5 then :pck
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
39
56
|
def setup_file_format
|
|
40
57
|
file_record = @binary_reader.read_record(1)
|
|
41
58
|
endianness_info = EndiannessManager.new(file_record).detect_endianness
|
|
@@ -43,11 +43,11 @@ module Ephem
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def endian_uint32
|
|
46
|
-
|
|
46
|
+
BinaryReader::ENDIANNESS_UINT32_FORMATS[@endianness]
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def endian_double
|
|
50
|
-
|
|
50
|
+
BinaryReader::ENDIANNESS_DOUBLE_FORMATS[@endianness]
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
end
|
|
@@ -57,6 +57,7 @@ module Ephem
|
|
|
57
57
|
@record_data = record_data
|
|
58
58
|
@binary_reader = binary_reader
|
|
59
59
|
@endianness = endianness
|
|
60
|
+
@record_parser = RecordParser.new(endianness: @endianness)
|
|
60
61
|
setup_summary_format
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -125,10 +126,10 @@ module Ephem
|
|
|
125
126
|
|
|
126
127
|
def iterate_summary_chain(record_number)
|
|
127
128
|
while record_number != 0
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
record_number =
|
|
130
|
+
process_summary_record(record_number) do |name, values|
|
|
131
|
+
yield name, values
|
|
132
|
+
end
|
|
132
133
|
end
|
|
133
134
|
end
|
|
134
135
|
|
|
@@ -145,13 +146,14 @@ module Ephem
|
|
|
145
146
|
) do |name, values|
|
|
146
147
|
yield name, values
|
|
147
148
|
end
|
|
149
|
+
|
|
150
|
+
control[:next_record]
|
|
148
151
|
end
|
|
149
152
|
|
|
150
153
|
def parse_control_data(data)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.parse_summary_control(control_data)
|
|
154
|
+
@record_parser.parse_summary_control(
|
|
155
|
+
data[0, RecordParser::SUMMARY_CONTROL_SIZE]
|
|
156
|
+
)
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
def extract_summary_data(data)
|
|
@@ -169,25 +171,22 @@ module Ephem
|
|
|
169
171
|
|
|
170
172
|
def extract_values(data)
|
|
171
173
|
return [] if data.nil? || data.empty?
|
|
174
|
+
|
|
172
175
|
data.unpack(@summary_format)
|
|
173
176
|
end
|
|
174
177
|
|
|
175
178
|
def extract_name(data)
|
|
176
179
|
return "" if data.nil? || data.empty?
|
|
177
|
-
data.strip
|
|
178
|
-
end
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
data = @binary_reader.read_record(record_number)
|
|
182
|
-
parse_control_data(data)[:next_record]
|
|
181
|
+
data.strip
|
|
183
182
|
end
|
|
184
183
|
|
|
185
184
|
def endian_double
|
|
186
|
-
|
|
185
|
+
BinaryReader::ENDIANNESS_DOUBLE_FORMATS[@endianness]
|
|
187
186
|
end
|
|
188
187
|
|
|
189
188
|
def endian_uint32
|
|
190
|
-
|
|
189
|
+
BinaryReader::ENDIANNESS_UINT32_FORMATS[@endianness]
|
|
191
190
|
end
|
|
192
191
|
end
|
|
193
192
|
end
|
data/lib/ephem/pck.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ephem
|
|
4
|
+
# Reads a binary PCK (+DAF/PCK+) orientation kernel: the orientation of one or
|
|
5
|
+
# more body frames over time, expressed as Euler angles.
|
|
6
|
+
class PCK
|
|
7
|
+
# Index of the data type within a PCK summary descriptor
|
|
8
|
+
# ([start, end, body, reference_frame, data_type, start_i, end_i]).
|
|
9
|
+
DATA_TYPE_IDENTIFIER = 4
|
|
10
|
+
|
|
11
|
+
attr_reader :daf, :segments
|
|
12
|
+
|
|
13
|
+
# @param daf [Ephem::IO::DAF] DAF containing PCK data
|
|
14
|
+
# @raise [ArgumentError] if the DAF is nil
|
|
15
|
+
def initialize(daf:)
|
|
16
|
+
raise ArgumentError, "DAF cannot be nil" if daf.nil?
|
|
17
|
+
|
|
18
|
+
@daf = daf
|
|
19
|
+
@segments = load_segments
|
|
20
|
+
@bodies = build_bodies
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Opens a binary PCK file.
|
|
24
|
+
#
|
|
25
|
+
# @param path [String] Path to the PCK (+.bpc+) file
|
|
26
|
+
# @return [PCK]
|
|
27
|
+
# @raise [ArgumentError] if the file is not a binary PCK or cannot be read
|
|
28
|
+
def self.open(path)
|
|
29
|
+
daf = IO::DAF.new(File.open(path, "rb"))
|
|
30
|
+
unless daf.file_type == :pck
|
|
31
|
+
raise ArgumentError, "#{path} is not a binary PCK (DAF/PCK) file"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
new(daf: daf)
|
|
35
|
+
rescue Errno::EACCES => e
|
|
36
|
+
raise ArgumentError, "File permission denied: #{path} (#{e.message})"
|
|
37
|
+
rescue
|
|
38
|
+
daf&.close
|
|
39
|
+
raise
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [void]
|
|
43
|
+
def close
|
|
44
|
+
@daf&.close
|
|
45
|
+
@segments&.each(&:clear_data)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Retrieves the orientation source for a body frame.
|
|
49
|
+
#
|
|
50
|
+
# @param body [Integer] NAIF frame ID of the oriented body
|
|
51
|
+
# @return [Segments::OrientationSegment, Segments::OrientationGroup] a
|
|
52
|
+
# single segment, or a group routing each query to the covering segment
|
|
53
|
+
# when the body spans several time intervals
|
|
54
|
+
# @raise [KeyError] if no segment is found for the given body
|
|
55
|
+
def [](body)
|
|
56
|
+
@bodies.fetch(body) do
|
|
57
|
+
raise KeyError, "No orientation segment found for body: #{body}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [String] the comments stored in the PCK file
|
|
62
|
+
def comments
|
|
63
|
+
@daf.comments
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @yieldparam segment [Segments::OrientationSegment]
|
|
67
|
+
# @return [Enumerator] if no block is given
|
|
68
|
+
def each_segment(&block)
|
|
69
|
+
return enum_for(:each_segment) unless block_given?
|
|
70
|
+
|
|
71
|
+
@segments.each(&block)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [String] a description of the PCK file and its segments
|
|
75
|
+
def to_s
|
|
76
|
+
<<~DESCRIPTION
|
|
77
|
+
PCK file with #{@segments.size} segments:
|
|
78
|
+
#{@segments.join("\n")}
|
|
79
|
+
DESCRIPTION
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def excerpt(output_path:, start_jd:, end_jd:, target_ids: nil, debug: false)
|
|
83
|
+
Excerpt
|
|
84
|
+
.new(self)
|
|
85
|
+
.extract(
|
|
86
|
+
output_path: output_path,
|
|
87
|
+
start_jd: start_jd,
|
|
88
|
+
end_jd: end_jd,
|
|
89
|
+
target_ids: target_ids,
|
|
90
|
+
debug: debug
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def load_segments
|
|
97
|
+
@daf.summaries.map do |source, descriptor|
|
|
98
|
+
build_segment(source: source, descriptor: descriptor)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_bodies
|
|
103
|
+
@segments.group_by(&:body).transform_values do |segments|
|
|
104
|
+
Segments::OrientationGroup.wrap(segments)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_segment(source:, descriptor:)
|
|
109
|
+
data_type = descriptor[DATA_TYPE_IDENTIFIER]
|
|
110
|
+
segment_class = Segments::Registry.lookup(:pck, data_type)
|
|
111
|
+
unless segment_class
|
|
112
|
+
raise UnsupportedError, "Unsupported PCK data type: #{data_type}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
segment_class.new(daf: @daf, source: source, descriptor: descriptor)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -38,6 +38,10 @@ module Ephem
|
|
|
38
38
|
attr_reader :center
|
|
39
39
|
# @return [String] the source of the segment
|
|
40
40
|
attr_reader :source
|
|
41
|
+
# @return [Float] start of coverage as a Julian Date
|
|
42
|
+
attr_reader :start_jd
|
|
43
|
+
# @return [Float] end of coverage as a Julian Date
|
|
44
|
+
attr_reader :end_jd
|
|
41
45
|
|
|
42
46
|
# Initialize a new segment
|
|
43
47
|
#
|
|
@@ -55,14 +59,7 @@ module Ephem
|
|
|
55
59
|
def initialize(daf:, source:, descriptor:)
|
|
56
60
|
@daf = daf
|
|
57
61
|
@source = source
|
|
58
|
-
|
|
59
|
-
@end_second,
|
|
60
|
-
@target,
|
|
61
|
-
@center,
|
|
62
|
-
@frame,
|
|
63
|
-
@data_type,
|
|
64
|
-
@start_i,
|
|
65
|
-
@end_i = descriptor
|
|
62
|
+
parse_descriptor(descriptor)
|
|
66
63
|
@start_jd = compute_julian_date(@start_second)
|
|
67
64
|
@end_jd = compute_julian_date(@end_second)
|
|
68
65
|
end
|
|
@@ -97,6 +94,15 @@ module Ephem
|
|
|
97
94
|
DESCRIPTION
|
|
98
95
|
end
|
|
99
96
|
|
|
97
|
+
# Whether the given time falls within this segment's coverage.
|
|
98
|
+
#
|
|
99
|
+
# @param tdb [Numeric] time in TDB Julian Date
|
|
100
|
+
# @param tdb2 [Numeric] optional fractional part of the TDB date
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
def covers?(tdb, tdb2 = 0.0)
|
|
103
|
+
(tdb + tdb2).between?(@start_jd, @end_jd)
|
|
104
|
+
end
|
|
105
|
+
|
|
100
106
|
def compute(_tdb, _tdb2 = 0.0)
|
|
101
107
|
raise NotImplementedError,
|
|
102
108
|
"#{self.class} has not implemented compute() for data type #{@data_type}"
|
|
@@ -113,6 +119,17 @@ module Ephem
|
|
|
113
119
|
|
|
114
120
|
private
|
|
115
121
|
|
|
122
|
+
def parse_descriptor(descriptor)
|
|
123
|
+
@start_second,
|
|
124
|
+
@end_second,
|
|
125
|
+
@target,
|
|
126
|
+
@center,
|
|
127
|
+
@frame,
|
|
128
|
+
@data_type,
|
|
129
|
+
@start_i,
|
|
130
|
+
@end_i = descriptor
|
|
131
|
+
end
|
|
132
|
+
|
|
116
133
|
def compute_julian_date(seconds)
|
|
117
134
|
Time::J2000_EPOCH + seconds / Time::SECONDS_PER_DAY
|
|
118
135
|
end
|