diff --git a/lib/astronoby.rb b/lib/astronoby.rb index 2f4980f..98ba4fb 100644 --- a/lib/astronoby.rb +++ b/lib/astronoby.rb @@ -8,6 +8,7 @@ require "astronoby/ephem" require "astronoby/epoch" require "astronoby/instant" +require "astronoby/vector" require "astronoby/astronomical_models/ephemeride_lunaire_parisienne" require "astronoby/astronomical_models/moon_phases_periodic_terms" require "astronoby/bodies/moon" diff --git a/lib/astronoby/bodies/planet.rb b/lib/astronoby/bodies/planet.rb index 066513e..b07351e 100644 --- a/lib/astronoby/bodies/planet.rb +++ b/lib/astronoby/bodies/planet.rb @@ -46,9 +46,20 @@ def compute_barycentric(ephem:, instant:) velocity = state1.velocity end + # TODO: Implement Velocity value object + # TODO: Convert velocity into a vector of Velocity + + position_vector = Vector[ + Distance.from_kilometers(position.x), + Distance.from_kilometers(position.y), + Distance.from_kilometers(position.z) + ] + + velocity_vector = Vector[velocity.x, velocity.y, velocity.z] + Position::Barycentric.new( - position: position, - velocity: velocity, + position: position_vector, + velocity: velocity_vector, instant: instant, target_body: self.class ) diff --git a/lib/astronoby/position/barycentric.rb b/lib/astronoby/position/barycentric.rb index aa58075..1ee8783 100644 --- a/lib/astronoby/position/barycentric.rb +++ b/lib/astronoby/position/barycentric.rb @@ -10,9 +10,7 @@ class Barycentric < ICRF LIGHT_SPEED_CORRECTION_MAXIMUM_ITERATIONS = 10 # TODO: Move constants into Astronomy::Constants - # TODO: Create Velocity value object # TODO: Add Velocity.light_speed - # TODO: Wrap Ephem vectors into Distance and Velocity instances? def initialize( position:, @@ -24,7 +22,7 @@ def initialize( position: position, velocity: velocity, instant: instant, - center_identifier: 0, # TODO: replace with constant + center_identifier: Planet::SOLAR_SYSTEM_BARYCENTER, target_body: target_body ) end diff --git a/lib/astronoby/precession.rb b/lib/astronoby/precession.rb index 3e7d0c3..829978d 100644 --- a/lib/astronoby/precession.rb +++ b/lib/astronoby/precession.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "matrix" - module Astronoby class Precession def self.for_equatorial_coordinates(coordinates:, epoch:) diff --git a/lib/astronoby/vector.rb b/lib/astronoby/vector.rb new file mode 100644 index 0000000..62f71a7 --- /dev/null +++ b/lib/astronoby/vector.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "matrix" + +module Astronoby + class Vector < ::Vector + def initialize(...) + super + freeze + end + + def x + self[0] + end + + def y + self[1] + end + + def z + self[2] + end + end +end diff --git a/spec/astronoby/bodies/earth_spec.rb b/spec/astronoby/bodies/earth_spec.rb index 530c8bc..709f8ea 100644 --- a/spec/astronoby/bodies/earth_spec.rb +++ b/spec/astronoby/bodies/earth_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) earth = described_class.new(instant: instant, ephem: ephem) @@ -13,8 +16,15 @@ barycentric = earth.barycentric expect(barycentric).to be_a(Astronoby::Position::Barycentric) - expect(barycentric.position).to eq(Vector[2, 4, 6]) - expect(barycentric.velocity).to eq(Vector[8, 10, 12]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(4), + Astronoby::Distance.from_kilometers(6) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[8, 10, 12]) end end end diff --git a/spec/astronoby/bodies/jupiter_spec.rb b/spec/astronoby/bodies/jupiter_spec.rb index 125b90c..b37a344 100644 --- a/spec/astronoby/bodies/jupiter_spec.rb +++ b/spec/astronoby/bodies/jupiter_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) jupiter = described_class.new(instant: instant, ephem: ephem) @@ -13,8 +16,15 @@ barycentric = jupiter.barycentric expect(barycentric).to be_a(Astronoby::Position::Barycentric) - expect(barycentric.position).to eq(Vector[1, 2, 3]) - expect(barycentric.velocity).to eq(Vector[4, 5, 6]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(1), + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(3) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[4, 5, 6]) end end end diff --git a/spec/astronoby/bodies/mars_spec.rb b/spec/astronoby/bodies/mars_spec.rb index 984b6df..de7c35e 100644 --- a/spec/astronoby/bodies/mars_spec.rb +++ b/spec/astronoby/bodies/mars_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) mars = described_class.new(instant: instant, ephem: ephem) @@ -13,8 +16,15 @@ barycentric = mars.barycentric expect(barycentric).to be_a(Astronoby::Position::Barycentric) - expect(barycentric.position).to eq(Vector[1, 2, 3]) - expect(barycentric.velocity).to eq(Vector[4, 5, 6]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(1), + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(3) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[4, 5, 6]) end end end diff --git a/spec/astronoby/bodies/mercury_spec.rb b/spec/astronoby/bodies/mercury_spec.rb index 631e7b4..4b27b58 100644 --- a/spec/astronoby/bodies/mercury_spec.rb +++ b/spec/astronoby/bodies/mercury_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) mercury = described_class.new(instant: instant, ephem: ephem) @@ -13,8 +16,15 @@ barycentric = mercury.barycentric expect(barycentric).to be_a(Astronoby::Position::Barycentric) - expect(barycentric.position).to eq(Vector[2, 4, 6]) - expect(barycentric.velocity).to eq(Vector[8, 10, 12]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(4), + Astronoby::Distance.from_kilometers(6) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[8, 10, 12]) end end end diff --git a/spec/astronoby/bodies/neptune_spec.rb b/spec/astronoby/bodies/neptune_spec.rb index b9cfee7..3d01729 100644 --- a/spec/astronoby/bodies/neptune_spec.rb +++ b/spec/astronoby/bodies/neptune_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) neptune = described_class.new(instant: instant, ephem: ephem) @@ -13,8 +16,15 @@ barycentric = neptune.barycentric expect(barycentric).to be_a(Astronoby::Position::Barycentric) - expect(barycentric.position).to eq(Vector[1, 2, 3]) - expect(barycentric.velocity).to eq(Vector[4, 5, 6]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(1), + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(3) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[4, 5, 6]) end end end diff --git a/spec/astronoby/bodies/planet_spec.rb b/spec/astronoby/bodies/planet_spec.rb index 1a7af5d..0cdaee8 100644 --- a/spec/astronoby/bodies/planet_spec.rb +++ b/spec/astronoby/bodies/planet_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: [1, 2, 3], velocity: [4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) planet = build_planet.new(instant: instant, ephem: ephem) @@ -18,21 +21,34 @@ it "returns a Barycentric position with the correct position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: [1, 2, 3], velocity: [4, 5, 6]) + state = double( + position: Astronoby::Vector[1, 2, 3], + velocity: Astronoby::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) planet = build_planet.new(instant: instant, ephem: ephem) barycentric = planet.barycentric - expect(barycentric.position).to eq([1, 2, 3]) - expect(barycentric.velocity).to eq([4, 5, 6]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(1), + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(3) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[4, 5, 6]) end it "returns a Barycentric position with the correct instant" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: [1, 2, 3], velocity: [4, 5, 6]) + state = double( + position: Astronoby::Vector[1, 2, 3], + velocity: Astronoby::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) planet = build_planet.new(instant: instant, ephem: ephem) @@ -45,7 +61,10 @@ it "returns a Barycentric position with the correct target_body" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: [1, 2, 3], velocity: [4, 5, 6]) + state = double( + position: Astronoby::Vector[1, 2, 3], + velocity: Astronoby::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) planet = build_planet.new(instant: instant, ephem: ephem) @@ -59,8 +78,14 @@ it "returns a Barycentric position with the correct position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state1 = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) - state2 = double(position: Vector[7, 8, 9], velocity: Vector[10, 11, 12]) + state1 = double( + position: Astronoby::Vector[1, 2, 3], + velocity: Astronoby::Vector[4, 5, 6] + ) + state2 = double( + position: Astronoby::Vector[7, 8, 9], + velocity: Astronoby::Vector[10, 11, 12] + ) segment1 = double(compute_and_differentiate: state1) segment2 = double(compute_and_differentiate: state2) ephem = double @@ -71,8 +96,15 @@ barycentric = planet.barycentric - expect(barycentric.position).to eq(Vector[8, 10, 12]) - expect(barycentric.velocity).to eq(Vector[14, 16, 18]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(8), + Astronoby::Distance.from_kilometers(10), + Astronoby::Distance.from_kilometers(12) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[14, 16, 18]) end end diff --git a/spec/astronoby/bodies/saturn_spec.rb b/spec/astronoby/bodies/saturn_spec.rb index d429b62..ef903d8 100644 --- a/spec/astronoby/bodies/saturn_spec.rb +++ b/spec/astronoby/bodies/saturn_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) saturn = described_class.new(instant: instant, ephem: ephem) @@ -13,8 +16,15 @@ barycentric = saturn.barycentric expect(barycentric).to be_a(Astronoby::Position::Barycentric) - expect(barycentric.position).to eq(Vector[1, 2, 3]) - expect(barycentric.velocity).to eq(Vector[4, 5, 6]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(1), + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(3) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[4, 5, 6]) end end end diff --git a/spec/astronoby/bodies/uranus_spec.rb b/spec/astronoby/bodies/uranus_spec.rb index 9769d44..3a8fbd1 100644 --- a/spec/astronoby/bodies/uranus_spec.rb +++ b/spec/astronoby/bodies/uranus_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) uranus = described_class.new(instant: instant, ephem: ephem) @@ -13,8 +16,15 @@ barycentric = uranus.barycentric expect(barycentric).to be_a(Astronoby::Position::Barycentric) - expect(barycentric.position).to eq(Vector[1, 2, 3]) - expect(barycentric.velocity).to eq(Vector[4, 5, 6]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(1), + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(3) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[4, 5, 6]) end end end diff --git a/spec/astronoby/bodies/venus_spec.rb b/spec/astronoby/bodies/venus_spec.rb index 266b328..7e97570 100644 --- a/spec/astronoby/bodies/venus_spec.rb +++ b/spec/astronoby/bodies/venus_spec.rb @@ -5,7 +5,10 @@ it "returns a Barycentric position" do time = Time.utc(2025, 2, 7, 12) instant = Astronoby::Instant.from_time(time) - state = double(position: Vector[1, 2, 3], velocity: Vector[4, 5, 6]) + state = double( + position: Ephem::Core::Vector[1, 2, 3], + velocity: Ephem::Core::Vector[4, 5, 6] + ) segment = double(compute_and_differentiate: state) ephem = double(:[] => segment) venus = described_class.new(instant: instant, ephem: ephem) @@ -13,8 +16,15 @@ barycentric = venus.barycentric expect(barycentric).to be_a(Astronoby::Position::Barycentric) - expect(barycentric.position).to eq(Vector[2, 4, 6]) - expect(barycentric.velocity).to eq(Vector[8, 10, 12]) + expect(barycentric.position) + .to eq( + Astronoby::Vector[ + Astronoby::Distance.from_kilometers(2), + Astronoby::Distance.from_kilometers(4), + Astronoby::Distance.from_kilometers(6) + ] + ) + expect(barycentric.velocity).to eq(Astronoby::Vector[8, 10, 12]) end end end diff --git a/spec/astronoby/vector_spec.rb b/spec/astronoby/vector_spec.rb new file mode 100644 index 0000000..fa46988 --- /dev/null +++ b/spec/astronoby/vector_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +RSpec.describe Astronoby::Vector do + describe ".new" do + it "creates a new vector with valid numeric inputs" do + vector = described_class[1, 2, 3] + + expect(vector.x).to eq(1) + expect(vector.y).to eq(2) + expect(vector.z).to eq(3) + end + + it "freezes the object" do + vector = described_class[1, 2, 3] + + expect(vector).to be_frozen + end + end + + describe "#+" do + it "adds two vectors" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[4, 5, 6] + + result = vector1 + vector2 + + expect(result.to_a).to eq([5, 7, 9]) + expect(result).to be_a(described_class) + expect(result).not_to equal(vector1) + end + end + + describe "#-" do + it "subtracts two vectors" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[4, 5, 6] + + result = vector1 - vector2 + + expect(result.to_a).to eq([-3, -3, -3]) + end + end + + describe "#dot" do + it "calculates dot product correctly" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[4, 5, 6] + + result = vector1.dot(vector2) + + expect(result).to eq(32) # (1*4 + 2*5 + 3*6) + end + end + + describe "#cross" do + it "calculates cross product correctly" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[4, 5, 6] + + result = vector1.cross(vector2) + + expect(result.to_a).to eq([-3, 6, -3]) + end + end + + describe "#magnitude" do + it "calculates the magnitude correctly" do + vector = described_class[3, 4, 0] + + result = vector.magnitude + + expect(result).to eq(5) + end + end + + describe "#[]" do + it "accesses coordinates by index" do + vector = described_class[1, 2, 3] + + expect(vector[0]).to eq(1) + expect(vector[1]).to eq(2) + expect(vector[2]).to eq(3) + end + end + + describe "#to_a" do + it "returns array representation" do + vector = described_class[1, 2, 3] + + expect(vector.to_a).to eq([1, 2, 3]) + end + end + + describe "#inspect and #to_s" do + it "returns formatted string representation" do + vector = described_class[1, 2, 3] + + expect(vector.inspect).to eq("Vector[1, 2, 3]") + expect(vector.to_s).to eq("Vector[1, 2, 3]") + end + end + + describe "#hash" do + it "returns consistent hash values for equal vectors" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[1, 2, 3] + + expect(vector1.hash).to eq(vector2.hash) + end + + it "returns different hash values for different vectors" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[4, 5, 6] + + expect(vector1.hash).not_to eq(vector2.hash) + end + end + + describe "#==" do + it "returns true for equal vectors" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[1, 2, 3] + + expect(vector1 == vector2).to be(true) + end + + it "returns false for different vectors" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[4, 5, 6] + + expect(vector1 == vector2).to be(false) + end + + it "returns false for non-vector comparison" do + vector1 = described_class[1, 2, 3] + vector2 = described_class[4, 5, 6] + + expect(vector1 == vector2).to be(false) + end + end +end