Class: Zodiacly::Ephemeris
- Inherits:
-
Object
- Object
- Zodiacly::Ephemeris
- Defined in:
- lib/zodiacly/ephemeris.rb
Constant Summary collapse
- BODY_TO_COMMAND =
Horizons target IDs (common major bodies)
{ sun: "10", moon: "301", mercury: "199", venus: "299", mars: "499", jupiter: "599", saturn: "699", uranus: "799", neptune: "899", pluto: "999" }.freeze
- DEFAULT_BODIES =
%i[sun moon].freeze
Class Method Summary collapse
-
.at(time_utc = Time.now.utc, center: "500@399", bodies: DEFAULT_BODIES, cache_ttl: 3600) ⇒ Object
Public API: Zodiacly::Ephemeris.at(time, bodies:).
- .cache_fetch(key, ttl) ⇒ Object
-
.ecliptic_longitude_deg(x, y) ⇒ Object
---- Math ----.
-
.ecliptic_longitude_speed_deg_per_day(x, y, vx, vy) ⇒ Object
Angular speed of longitude using planar angular velocity: omega = (xvy - yvx) / (x^2 + y^2) [rad/s] convert to deg/day.
- .extract_soe_line(result_text) ⇒ Object
-
.parse_vectors(json) ⇒ Object
Horizons JSON has a "result" string which contains a table between $$SOE and $$EOE.
Class Method Details
.at(time_utc = Time.now.utc, center: "500@399", bodies: DEFAULT_BODIES, cache_ttl: 3600) ⇒ Object
Public API: Zodiacly::Ephemeris.at(time, bodies:)
bodies:
- Array of symbols (e.g. %i[sun moon mars])
- :all (for all supported major bodies)
cache_ttl:
- seconds to keep cached results (default: 3600)
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/zodiacly/ephemeris.rb', line 37 def self.at(time_utc = Time.now.utc, center: "500@399", bodies: DEFAULT_BODIES, cache_ttl: 3600) time = Time.parse(time_utc.to_s).utc iso = time.iso8601 bodies = if bodies == :all %i[ sun moon mercury venus mars jupiter saturn uranus neptune pluto ] else Array(bodies).map(&:to_sym) end cache_key = Digest::SHA256.hexdigest([ iso, center, bodies.sort.join(",") ].join("|")) cache_fetch(cache_key, cache_ttl) do results = {} bodies.each do |body| command = BODY_TO_COMMAND.fetch(body) do raise Zodiacly::Error, "Unknown body: #{body.inspect}. Known: #{BODY_TO_COMMAND.keys.join(", ")}" end json = Zodiacly::Horizons.vectors(command: command, center: center, time_utc: iso) vec = parse_vectors(json) lon = ecliptic_longitude_deg(vec[:x], vec[:y]) speed = ecliptic_longitude_speed_deg_per_day(vec[:x], vec[:y], vec[:vx], vec[:vy]) results[body] = { lon: lon.round(2), sign: Zodiacly::Zodiac.sign_for_lon(lon), retrograde: speed.negative?, speed: speed.round(4) } end { time_utc: iso, center: center, bodies: results } end end |
.cache_fetch(key, ttl) ⇒ Object
145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/zodiacly/ephemeris.rb', line 145 def self.cache_fetch(key, ttl) dir = File.join(Dir.tmpdir, "zodiacly-cache") FileUtils.mkdir_p(dir) path = File.join(dir, "#{key}.json") if File.exist?(path) && (Time.now - File.mtime(path) < ttl) return JSON.parse(File.read(path), symbolize_names: true) end data = yield File.write(path, JSON.pretty_generate(data)) data end |
.ecliptic_longitude_deg(x, y) ⇒ Object
---- Math ----
126 127 128 129 130 131 132 |
# File 'lib/zodiacly/ephemeris.rb', line 126 def self.ecliptic_longitude_deg(x, y) rad = Math.atan2(y, x) deg = rad * 180.0 / Math::PI deg %= 360.0 deg += 360.0 if deg.negative? deg end |
.ecliptic_longitude_speed_deg_per_day(x, y, vx, vy) ⇒ Object
Angular speed of longitude using planar angular velocity: omega = (xvy - yvx) / (x^2 + y^2) [rad/s] convert to deg/day
137 138 139 140 141 142 143 |
# File 'lib/zodiacly/ephemeris.rb', line 137 def self.ecliptic_longitude_speed_deg_per_day(x, y, vx, vy) r2 = (x * x) + (y * y) return 0.0 if r2.zero? omega = (x * vy - y * vx) / r2 # rad / s omega * (180.0 / Math::PI) * 86_400.0 end |
.extract_soe_line(result_text) ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/zodiacly/ephemeris.rb', line 112 def self.extract_soe_line(result_text) start_idx = result_text.index("$$SOE") stop_idx = result_text.index("$$EOE") raise Zodiacly::Error, "Horizons result missing $$SOE/$$EOE markers" unless start_idx && stop_idx block = result_text[(start_idx + 5)...stop_idx].strip # Take the first non-empty line line = block.lines.map(&:strip).find { |l| !l.empty? } raise Zodiacly::Error, "Horizons SOE block was empty" unless line line end |
.parse_vectors(json) ⇒ Object
Horizons JSON has a "result" string which contains a table between $$SOE and $$EOE. With CSV_FORMAT=YES and VEC_TABLE=2, each row is CSV and includes x,y,z,vx,vy,vz.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/zodiacly/ephemeris.rb', line 91 def self.parse_vectors(json) result = json.fetch("result") { raise Zodiacly::Error, "Horizons response missing 'result' key" } line = extract_soe_line(result) parts = line.split(",").map(&:strip) # We expect the last 6 CSV columns to be numeric x,y,z,vx,vy,vz (in km and km/s typically) nums = parts.last(6).map { |v| Float(v) } { x: nums[0], y: nums[1], z: nums[2], vx: nums[3], vy: nums[4], vz: nums[5] } rescue KeyError, ArgumentError => e raise Zodiacly::Error, "Failed to parse Horizons vectors: #{e.}" end |