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.
@@ -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
- class Registry
6
- def self.register(type, klass)
7
- SPK::SEGMENT_CLASSES[type] = klass
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