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,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ephem
|
|
4
|
+
module Segments
|
|
5
|
+
# Shared evaluation machinery for DAF "type 2" Chebyshev segments.
|
|
6
|
+
module ChebyshevType2
|
|
7
|
+
include Core::Constants
|
|
8
|
+
|
|
9
|
+
# @return [void]
|
|
10
|
+
def clear_data
|
|
11
|
+
@data_lock.synchronize do
|
|
12
|
+
@data_loaded = false
|
|
13
|
+
@midpoints = nil
|
|
14
|
+
@radii = nil
|
|
15
|
+
@coefficients = nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def load_data
|
|
22
|
+
@data_lock.synchronize do
|
|
23
|
+
return if @data_loaded
|
|
24
|
+
|
|
25
|
+
process_coefficient_data(load_coefficient_data)
|
|
26
|
+
|
|
27
|
+
@data_loaded = true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def load_coefficient_data
|
|
32
|
+
metadata = @daf.read_array(@end_i - 3, @end_i)
|
|
33
|
+
_start_index, _end_index, record_size, segment_count = metadata
|
|
34
|
+
|
|
35
|
+
coefficient_count = ((record_size - 2) / component_count).to_i
|
|
36
|
+
coefficients_raw = @daf.map_array(@start_i, @end_i - 4)
|
|
37
|
+
|
|
38
|
+
[
|
|
39
|
+
coefficients_raw,
|
|
40
|
+
record_size.to_i,
|
|
41
|
+
segment_count.to_i,
|
|
42
|
+
coefficient_count
|
|
43
|
+
]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def process_coefficient_data(data)
|
|
47
|
+
coefficients_raw, record_size, segment_count, coefficient_count = data
|
|
48
|
+
|
|
49
|
+
coefficients = coefficients_raw.each_slice(record_size).to_a
|
|
50
|
+
|
|
51
|
+
@midpoints = coefficients.map { |row| row[0] }
|
|
52
|
+
@radii = coefficients.map { |row| row[1] }
|
|
53
|
+
n_terms = coefficient_count
|
|
54
|
+
n_components = component_count
|
|
55
|
+
|
|
56
|
+
@coefficients = Array.new(segment_count) do |i|
|
|
57
|
+
row = coefficients[i][2..]
|
|
58
|
+
Array.new(n_terms) do |k|
|
|
59
|
+
Array.new(n_components) do |j|
|
|
60
|
+
row[k + j * n_terms]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def convert_to_seconds(tdb, tdb2)
|
|
67
|
+
case tdb
|
|
68
|
+
when Array
|
|
69
|
+
tdb.map { |t| time_to_seconds(t, tdb2) }
|
|
70
|
+
else
|
|
71
|
+
time_to_seconds(tdb, tdb2)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def time_to_seconds(time, offset)
|
|
76
|
+
(time - Time::J2000_EPOCH + offset) * Time::SECONDS_PER_DAY
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def generate_position(tdb_seconds)
|
|
80
|
+
interval = find_interval(tdb_seconds)
|
|
81
|
+
normalized_time = compute_normalized_time(tdb_seconds, interval)
|
|
82
|
+
coeffs = @coefficients[interval]
|
|
83
|
+
Computation::ChebyshevPolynomial.evaluate(coeffs, normalized_time)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def generate_single(tdb_seconds)
|
|
87
|
+
interval = find_interval(tdb_seconds)
|
|
88
|
+
normalized_time = compute_normalized_time(tdb_seconds, interval)
|
|
89
|
+
|
|
90
|
+
coeffs = @coefficients[interval] # already [n_terms][3]
|
|
91
|
+
Computation::ChebyshevPolynomial.evaluate_with_derivative(
|
|
92
|
+
coeffs,
|
|
93
|
+
normalized_time,
|
|
94
|
+
@radii[interval]
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def generate_multiple(tdb_seconds)
|
|
99
|
+
tdb_seconds.map { |time| generate_single(time) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def find_interval(tdb_seconds)
|
|
103
|
+
left = 0
|
|
104
|
+
right = @midpoints.size - 1
|
|
105
|
+
|
|
106
|
+
if @last_interval && time_in_interval?(tdb_seconds, @last_interval)
|
|
107
|
+
return @last_interval
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
while left <= right
|
|
111
|
+
mid = (left + right) / 2
|
|
112
|
+
min_time = @midpoints[mid] - @radii[mid]
|
|
113
|
+
max_time = @midpoints[mid] + @radii[mid]
|
|
114
|
+
|
|
115
|
+
if tdb_seconds < min_time
|
|
116
|
+
right = mid - 1
|
|
117
|
+
elsif tdb_seconds > max_time
|
|
118
|
+
left = mid + 1
|
|
119
|
+
else
|
|
120
|
+
@last_interval = mid
|
|
121
|
+
return mid
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
raise OutOfRangeError.new(
|
|
126
|
+
"Time #{tdb_seconds} is outside the coverage of this segment",
|
|
127
|
+
tdb_seconds
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def time_in_interval?(time, interval)
|
|
132
|
+
min_time = @midpoints[interval] - @radii[interval]
|
|
133
|
+
max_time = @midpoints[interval] + @radii[interval]
|
|
134
|
+
time.between?(min_time, max_time)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def compute_normalized_time(time_seconds, interval)
|
|
138
|
+
(time_seconds - @midpoints[interval]) / @radii[interval]
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ephem
|
|
4
|
+
module Segments
|
|
5
|
+
# The orientation segments for one PCK body frame. Routes each query to the
|
|
6
|
+
# segment covering the requested time. Returned by PCK#[].
|
|
7
|
+
#
|
|
8
|
+
# @see Ephem::Segments::OrientationSegment
|
|
9
|
+
class OrientationGroup < SegmentGroup
|
|
10
|
+
include OrientationSource
|
|
11
|
+
|
|
12
|
+
# @return [Integer] NAIF frame ID of the oriented body frame
|
|
13
|
+
def body
|
|
14
|
+
@segments.first.body
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Integer] NAIF ID of the inertial reference frame
|
|
18
|
+
def reference_frame
|
|
19
|
+
@segments.first.reference_frame
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Euler angles at the given time, without rates. See
|
|
23
|
+
# {OrientationSegment#angles_at}.
|
|
24
|
+
#
|
|
25
|
+
# @param tdb [Numeric, Array<Numeric>] Time(s) in TDB Julian Date
|
|
26
|
+
# @param tdb2 [Numeric] Optional fractional part of TDB date
|
|
27
|
+
# @return [Core::Orientation, Array<Core::Orientation>]
|
|
28
|
+
def angles_at(tdb, tdb2 = 0.0)
|
|
29
|
+
query(tdb, tdb2) do |segment, time, fraction|
|
|
30
|
+
segment.angles_at(time, fraction)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Euler angles and their rates at the given time. See
|
|
35
|
+
# {OrientationSegment#orientation_at}.
|
|
36
|
+
#
|
|
37
|
+
# @param tdb [Numeric, Array<Numeric>] Time(s) in TDB Julian Date
|
|
38
|
+
# @param tdb2 [Numeric] Optional fractional part of TDB date
|
|
39
|
+
# @return [Core::Orientation, Array<Core::Orientation>]
|
|
40
|
+
def orientation_at(tdb, tdb2 = 0.0)
|
|
41
|
+
query(tdb, tdb2) do |segment, time, fraction|
|
|
42
|
+
segment.orientation_at(time, fraction)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The reference-frame to body-fixed rotation matrix at the given time.
|
|
47
|
+
# See {OrientationSegment#matrix_at}.
|
|
48
|
+
#
|
|
49
|
+
# @param tdb [Numeric, Array<Numeric>] Time(s) in TDB Julian Date
|
|
50
|
+
# @param tdb2 [Numeric] Optional fractional part of TDB date
|
|
51
|
+
# @return [Array<Array<Float>>, Array<Array<Array<Float>>>]
|
|
52
|
+
def matrix_at(tdb, tdb2 = 0.0)
|
|
53
|
+
query(tdb, tdb2) do |segment, time, fraction|
|
|
54
|
+
segment.matrix_at(time, fraction)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ephem
|
|
4
|
+
module Segments
|
|
5
|
+
# Binary PCK orientation segment (data type 2): the orientation of a body
|
|
6
|
+
# frame relative to an inertial reference frame, stored as three Euler
|
|
7
|
+
# angles in Chebyshev coefficients.
|
|
8
|
+
class OrientationSegment < BaseSegment
|
|
9
|
+
include ChebyshevType2
|
|
10
|
+
include OrientationSource
|
|
11
|
+
|
|
12
|
+
COMPONENT_COUNT = 3 # phi, theta, psi
|
|
13
|
+
|
|
14
|
+
def initialize(daf:, source:, descriptor:)
|
|
15
|
+
super
|
|
16
|
+
@data_loaded = false
|
|
17
|
+
@data_lock = Mutex.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Integer] NAIF frame ID of the oriented body frame
|
|
21
|
+
alias_method :body, :target
|
|
22
|
+
# @return [Integer] NAIF ID of the inertial reference frame
|
|
23
|
+
alias_method :reference_frame, :center
|
|
24
|
+
|
|
25
|
+
# Euler angles at the given time, without rates.
|
|
26
|
+
#
|
|
27
|
+
# @param tdb [Numeric, Array<Numeric>] Time(s) in TDB Julian Date
|
|
28
|
+
# @param tdb2 [Numeric] Optional fractional part of TDB date
|
|
29
|
+
# @return [Core::Orientation, Array<Core::Orientation>] angles in radians
|
|
30
|
+
# @raise [Ephem::OutOfRangeError] if time is outside segment coverage
|
|
31
|
+
def angles_at(tdb, tdb2 = 0.0)
|
|
32
|
+
load_data
|
|
33
|
+
tdb_seconds = convert_to_seconds(tdb, tdb2)
|
|
34
|
+
|
|
35
|
+
case tdb_seconds
|
|
36
|
+
when Numeric
|
|
37
|
+
to_orientation(generate_position(tdb_seconds))
|
|
38
|
+
else
|
|
39
|
+
tdb_seconds.map do |seconds|
|
|
40
|
+
to_orientation(generate_position(seconds))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Euler angles and their rates at the given time.
|
|
46
|
+
#
|
|
47
|
+
# @param tdb [Numeric, Array<Numeric>] Time(s) in TDB Julian Date
|
|
48
|
+
# @param tdb2 [Numeric] Optional fractional part of TDB date
|
|
49
|
+
# @return [Core::Orientation, Array<Core::Orientation>] angles (radians)
|
|
50
|
+
# carrying rates (radians/day)
|
|
51
|
+
# @raise [Ephem::OutOfRangeError] if time is outside segment coverage
|
|
52
|
+
def orientation_at(tdb, tdb2 = 0.0)
|
|
53
|
+
load_data
|
|
54
|
+
tdb_seconds = convert_to_seconds(tdb, tdb2)
|
|
55
|
+
|
|
56
|
+
case tdb_seconds
|
|
57
|
+
when Numeric
|
|
58
|
+
to_orientation(*generate_single(tdb_seconds))
|
|
59
|
+
else
|
|
60
|
+
generate_multiple(tdb_seconds).map do |angles, rates|
|
|
61
|
+
to_orientation(angles, rates)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The reference-frame to body-fixed rotation matrix at the given time,
|
|
67
|
+
# built from the 3-1-3 Euler angles. See {Core::Orientation#to_matrix}.
|
|
68
|
+
#
|
|
69
|
+
# @param tdb [Numeric, Array<Numeric>] Time(s) in TDB Julian Date
|
|
70
|
+
# @param tdb2 [Numeric] Optional fractional part of TDB date
|
|
71
|
+
# @return [Array<Array<Float>>, Array<Array<Array<Float>>>] a 3x3 matrix,
|
|
72
|
+
# or one per time for an array input
|
|
73
|
+
# @raise [Ephem::OutOfRangeError] if time is outside segment coverage
|
|
74
|
+
def matrix_at(tdb, tdb2 = 0.0)
|
|
75
|
+
angles = angles_at(tdb, tdb2)
|
|
76
|
+
angles.is_a?(Array) ? angles.map(&:to_matrix) : angles.to_matrix
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def describe(verbose: false)
|
|
80
|
+
start_date = format_date(*julian_to_gregorian(@start_jd))
|
|
81
|
+
end_date = format_date(*julian_to_gregorian(@end_jd))
|
|
82
|
+
|
|
83
|
+
description =
|
|
84
|
+
"#{start_date}..#{end_date} Type #{@data_type} orientation of " \
|
|
85
|
+
"frame #{body} relative to frame #{reference_frame}"
|
|
86
|
+
return description unless verbose
|
|
87
|
+
|
|
88
|
+
<<~DESCRIPTION.chomp
|
|
89
|
+
#{description}
|
|
90
|
+
source=#{@source}
|
|
91
|
+
DESCRIPTION
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def parse_descriptor(descriptor)
|
|
97
|
+
@start_second,
|
|
98
|
+
@end_second,
|
|
99
|
+
@target,
|
|
100
|
+
@center,
|
|
101
|
+
@data_type,
|
|
102
|
+
@start_i,
|
|
103
|
+
@end_i = descriptor
|
|
104
|
+
@frame = @center
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def component_count
|
|
108
|
+
COMPONENT_COUNT
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_orientation(angles, rates = nil)
|
|
112
|
+
Core::Orientation.new(angles[0], angles[1], angles[2], rates: rates)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Registry.register(:pck, 2, self)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ephem
|
|
4
|
+
module Segments
|
|
5
|
+
module OrientationSource
|
|
6
|
+
def compute(*)
|
|
7
|
+
raise NotImplementedError,
|
|
8
|
+
"Use #angles_at or #orientation_at for orientation kernels"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def compute_and_differentiate(*)
|
|
12
|
+
raise NotImplementedError,
|
|
13
|
+
"Use #orientation_at for orientation kernels"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ephem
|
|
4
|
+
module Segments
|
|
5
|
+
# The position segments for one SPK center/target pair. Routes each query to
|
|
6
|
+
# the segment covering the requested time. Returned by SPK#[].
|
|
7
|
+
#
|
|
8
|
+
# @see Ephem::Segments::Segment
|
|
9
|
+
class PositionGroup < SegmentGroup
|
|
10
|
+
# @return [Integer] the center body ID
|
|
11
|
+
def center
|
|
12
|
+
@segments.first.center
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Integer] the target body ID
|
|
16
|
+
def target
|
|
17
|
+
@segments.first.target
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Position at the given time. See {Segment#compute}.
|
|
21
|
+
#
|
|
22
|
+
# @param tdb [Numeric, Array<Numeric>] Time(s) in TDB Julian Date
|
|
23
|
+
# @param tdb2 [Numeric] Optional fractional part of TDB date
|
|
24
|
+
# @return [Core::Vector, Array<Core::Vector>]
|
|
25
|
+
def compute(tdb, tdb2 = 0.0)
|
|
26
|
+
query(tdb, tdb2) do |segment, time, fraction|
|
|
27
|
+
segment.compute(time, fraction)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
alias_method :position_at, :compute
|
|
31
|
+
|
|
32
|
+
# Position and velocity at the given time. See
|
|
33
|
+
# {Segment#compute_and_differentiate}.
|
|
34
|
+
#
|
|
35
|
+
# @param tdb [Numeric, Array<Numeric>] Time(s) in TDB Julian Date
|
|
36
|
+
# @param tdb2 [Numeric] Optional fractional part of TDB date
|
|
37
|
+
# @return [Core::State, Array<Core::State>]
|
|
38
|
+
def compute_and_differentiate(tdb, tdb2 = 0.0)
|
|
39
|
+
query(tdb, tdb2) do |segment, time, fraction|
|
|
40
|
+
segment.compute_and_differentiate(time, fraction)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
alias_method :state_at, :compute_and_differentiate
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Ephem
|
|
4
4
|
module Segments
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
module Registry
|
|
6
|
+
TABLES = {spk: {}, pck: {}}.freeze
|
|
7
|
+
|
|
8
|
+
def self.register(kind, type, klass)
|
|
9
|
+
TABLES.fetch(kind)[type] = klass
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.lookup(kind, type, default = nil)
|
|
13
|
+
TABLES.fetch(kind).fetch(type, default)
|
|
8
14
|
end
|
|
9
15
|
end
|
|
10
16
|
end
|