astroscript 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astroscript
4
+ class Calculator
5
+ attr_accessor :name, :latitude, :longitude, :altitude, :jd, :house_method, :tz, :arc, :prefix
6
+ attr_reader :flags, :asc, :mc, :vertex, :transformer, :equasc
7
+
8
+ def initialize *args
9
+ @arc = 0
10
+ params = args.last.is_a?(Hash) ? args.pop : {}
11
+ init_params(params)
12
+ # defaults
13
+ set_topo(51.476852, -0.000500) # Royal Observatory Greenwich, London, UK
14
+ self.datetime = DateTime.now.new_offset # Time.now.utc
15
+ end
16
+
17
+ def transformer=(method)
18
+ return unless method
19
+ raise ArgumentError, "must be a method or proc" unless method.respond_to?(:call)
20
+
21
+ @transformer = method
22
+ end
23
+
24
+ def options
25
+ opts = {}
26
+ opts[:house_method] = house_method
27
+ opts[:latitude] = AstroHelper.print_dms(latitude, lat: true)
28
+ opts[:longitude] = AstroHelper.print_dms(longitude, lon: true)
29
+ opts[:jd] = @jd
30
+ opts[:tz] = @tz.to_s
31
+ # date = @tz.to_local(DateTime.new(*Swe4r::swe_revjul(@jd))).to_s
32
+ date = DateTime.new(*Swe4r.swe_revjul(@jd)).to_s
33
+ opts[:date] = DateTime.parse(date).strftime("%Y-%m-%d %I:%M %p")
34
+ opts[:ayanamsha] = AstroHelper.print_dms ayanamsha, seconds: true
35
+ opts[:flags] = @flags
36
+ opts
37
+ end
38
+
39
+ def timezone
40
+ TZInfo::Timezone.get(@tz)
41
+ end
42
+
43
+ def utc
44
+ DateTime.new(*Swe4r.swe_revjul(@jd))
45
+ end
46
+
47
+ def datetime
48
+ @tz ? timezone.to_local(utc) : utc
49
+ end
50
+
51
+ def to_a
52
+ [@tz, @jd, @latitude, @longitude, @altitude]
53
+ end
54
+
55
+ def init *args
56
+ AstroHelper.init_calc(self, *args)
57
+ end
58
+
59
+ def update(params)
60
+ init_params(params)
61
+ end
62
+
63
+ def ayanamsha
64
+ @ayanamsha ? Swe4r.swe_get_ayanamsa_ex_ut(@jd, @flags) : 0
65
+ end
66
+
67
+ def svp
68
+ -ayanamsha
69
+ end
70
+
71
+ def true_node?
72
+ !!@true_node
73
+ end
74
+
75
+ def oe # obliquity of the ecliptic
76
+ @oe ||= Swe4r.swe_calc_ut(@jd, -1, 0).first
77
+ end
78
+
79
+ def datetime=(time)
80
+ @oe = nil
81
+ @ra = nil
82
+ @planetary_hours = nil
83
+ @jd = AstroHelper.datetime_to_jd(time)
84
+ end
85
+
86
+ def set_topo(latitude, longitude, altitude = nil)
87
+ @latitude = latitude
88
+ @longitude = longitude
89
+ @altitude = altitude.to_f
90
+ Swe4r.swe_set_topo(@longitude, @latitude, @altitude)
91
+ end
92
+
93
+ def calc(id, equatorial: false, heliocentric: false)
94
+ flags = self.flags
95
+ flags |= Swe4r::SEFLG_EQUATORIAL if equatorial
96
+ flags |= Swe4r::SEFLG_HELCTR if heliocentric
97
+
98
+ Swe4r.swe_calc_ut(jd, id, flags)
99
+ end
100
+
101
+ def get_body(abbr, jd = nil)
102
+ @jd = jd if jd
103
+ $logger.warn "use :NN with @true_node configuration instead of #{abbr}" if %i[MN TN].include?(abbr)
104
+ if abbr =~ /^C(\d+)$/
105
+ HouseCusp.new(::Regexp.last_match(1).to_i, self)
106
+ else
107
+ Body.new(abbr, self)
108
+ end
109
+ end
110
+
111
+ def [](abbr)
112
+ abbr = abbr.upcase if abbr.size == 2
113
+ get_body(abbr)
114
+ end
115
+
116
+ def ascendant
117
+ @asc || (get_houses && @asc)
118
+ end
119
+
120
+ def midheaven
121
+ @mc
122
+ end
123
+
124
+ def house_cusps(calc_method = nil)
125
+ case calc_method
126
+ when :asc
127
+ AstroHelper.harmonic_cusps(get_position(:asc).lon)
128
+ when :mc
129
+ AstroHelper.harmonic_cusps(get_position(:mc).lon, 10)
130
+ else
131
+ get_houses(calc_method) unless @house_cusps && calc_method.nil?
132
+ @house_cusps[1..12]
133
+ end
134
+ end
135
+
136
+ def get_houses(calc_method = nil)
137
+ if calc_method
138
+ @house_method = calc_method
139
+ else
140
+ @house_method ||= "P" # 'C'
141
+ end
142
+ raise "you must call set_topo with latitude and longitude before get_houses" unless @latitude && @longitude
143
+
144
+ flag = @ayanamsha ? Swe4r::SEFLG_SIDEREAL : 0
145
+ @house_cusps, ascmc, *, ascmc_speeds = Swe4r.swe_houses_ex2(jd, flag, @latitude, @longitude,
146
+ @house_method)
147
+ # @house_cusps.pop if @house_cusps.first.zero? # FIXME bug
148
+ @asc, @mc, @ramc, @vertex = *ascmc[0..3]
149
+ @equasc, = *ascmc[4..7]
150
+ # * ascmc[4] = equasc * "equatorial ascendant" *
151
+ # * ascmc[5] = coasc1 * "co-ascendant" (W. Koch) *
152
+ # * ascmc[6] = coasc2 * "co-ascendant" (M. Munkasey) *
153
+ # * ascmc[7] = polasc * "polar ascendant" (M. Munkasey) *
154
+ @asc_speed, @mc_speed, *, @vertex_speed = *ascmc_speeds[0..3]
155
+ true
156
+ end
157
+
158
+ def ramc
159
+ @ramc || (get_houses && @ramc)
160
+ end
161
+
162
+ def which_house(degree)
163
+ get_houses unless @house_cusps
164
+ min_cusp = house_cusps.min
165
+ degree += 360 if degree < min_cusp
166
+ # Iterate through the house cusps
167
+ house_cusps.each_with_index do |cusp, i|
168
+ # Check if the degree is within the current house
169
+ next_cusp = house_cusps[i + 1]
170
+ return 12 if next_cusp.nil?
171
+
172
+ next_cusp += 360 if next_cusp == min_cusp
173
+ # $logger.debug("#{degree.round(2)}.between? #{cusp},#{next_cusp}")
174
+ return i + 1 if degree.between?(cusp, next_cusp)
175
+ end
176
+ end
177
+
178
+ def dc
179
+ flip(asc)
180
+ end
181
+
182
+ def ic
183
+ flip(mc)
184
+ end
185
+
186
+ def av
187
+ flip(vertex)
188
+ end
189
+ alias avx av
190
+
191
+ def flip(d)
192
+ (d + 180) % 360
193
+ end
194
+
195
+ def before_sunrise?
196
+ ac = house_cusps[0] # sunrise
197
+ ic = house_cusps[3] # midnight
198
+ ic += 360 if ic < ac
199
+ get_body(:SO).to_f.between?(ac, ic) # before sunrise?
200
+ end
201
+
202
+ def solunar_phase
203
+ (get_body(:MO).to_f - get_body(:SO).to_f + 360) % 360
204
+ end
205
+
206
+ private
207
+
208
+ def init_params(params)
209
+ params = { ephemeris: :moshier, center: :geo, light_correction: true }.merge(params)
210
+ @house_method = params[:house_method] ||= "P" # Placidus as default
211
+ # default to true node
212
+ @true_node = true unless params[:true_node] == false || params[:mean_node] || params[:node] == :mean
213
+ # https://www.astro.com/swisseph/swephprg.htm
214
+ @flags = Swe4r::SEFLG_SPEED
215
+ if params[:astrometric]
216
+ @flags |= Swe4r::SEFLG_NOABERR | Swe4r::SEFLG_NOGDEFL
217
+ else
218
+ # no light time correction - return true positions, not apparent
219
+ @flags |= Swe4r::SEFLG_TRUEPOS unless params[:light_correction]
220
+ end
221
+ if (@ayanamsha = params[:sidereal])
222
+ @flags |= Swe4r::SEFLG_SIDEREAL if params[:sidereal]
223
+ if @ayanamsha.is_a? Array
224
+ Swe4r.swe_set_sid_mode(*@ayanamsha[0..2])
225
+ else
226
+ Swe4r.swe_set_sid_mode(@ayanamsha, 0, 0)
227
+ end
228
+ end
229
+ if params[:heliocentric]
230
+ @flags |= Swe4r::SEFLG_HELCTR
231
+ elsif params[:topocentric]
232
+ @flags |= Swe4r::SEFLG_TOPOCTR
233
+ elsif params[:center]
234
+ case params[:center]
235
+ when :helio
236
+ @flags |= Swe4r::SEFLG_HELCTR
237
+ when :topo
238
+ @flags |= Swe4r::SEFLG_TOPOCTR
239
+ when :geo
240
+ else
241
+ raise "bad option! center: #{params[:center]}"
242
+ end
243
+ end
244
+ case params[:ephemeris]
245
+ when :moshier
246
+ @flags |= Swe4r::SEFLG_MOSEPH
247
+ when :jpl
248
+ @flags |= Swe4r::SEFLG_JPLEPH
249
+ when :swiss
250
+ @flags |= Swe4r::SEFLG_SWIEPH
251
+ else
252
+ raise "bad option! ephemeris: #{params[:ephemeris]}"
253
+ end
254
+ @flags |= Swe4r::SEFLG_EQUATORIAL if params[:equatorial] # right ascension and declination instead of lat/lon
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astroscript
4
+ class Chart
5
+ attr_reader :calc
6
+ attr_accessor :harmonic, :location
7
+
8
+ DEFAULT_BODIES = AstroHelper::PLANETS + AstroHelper::EXTRAS - [:VX]
9
+
10
+ # TODO: consider renaming opts to calc(_opts) ?
11
+ def initialize(birth_data, bodies: DEFAULT_BODIES, opts: {})
12
+ @opts = opts || {}
13
+ @opts[:true_node] ||= true
14
+ case birth_data
15
+ when Astroscript::Chart, Astroscript::Calculator
16
+ @calc = birth_data.dup
17
+ @calc.house_method = opts[:house_method]
18
+ @calc = @calc.calc if @calc.is_a?(Chart)
19
+ when Array
20
+ @calc = Astroscript::Calculator.new(opts).init(birth_data)
21
+ @location, name = birth_data[5..6]
22
+ when nil
23
+ raise ArgumentError, "no birth data provided!"
24
+ end
25
+ self.transformer = opts[:transformer]
26
+ @chart = {}
27
+ @harmonic = opts[:harmonic] || 1
28
+ @calc.arc = opts[:arc] if opts[:arc]
29
+ @calc.name = name || opts[:name] || ""
30
+ @calc.prefix = opts[:prefix]
31
+ get_bodies! bodies
32
+ end
33
+
34
+ def initialize_copy(orig)
35
+ super
36
+ # puts "duplicating @calc"
37
+ @calc = @calc.dup # attempt to prevent weirdness with transformers
38
+ self
39
+ end
40
+
41
+ # def dup
42
+ # Chart.new( @calc.dup, @chart.values.map(&:abbr), @opts)
43
+ # end
44
+
45
+ def transformer=(method)
46
+ @calc.transformer = method
47
+ end
48
+
49
+ def bodies
50
+ @chart.values
51
+ end
52
+
53
+ def recalc!(opts = {})
54
+ @calc.update(opts) unless opts.empty?
55
+ bodies.each(&:calculate!)
56
+ self
57
+ end
58
+
59
+ def asc=(deg)
60
+ @asc = deg.to_f
61
+ end
62
+
63
+ def asc
64
+ return @asc if @asc
65
+
66
+ if (a = find_body(:AC) || find_body(:C1))
67
+ a.degree
68
+ else
69
+ @calc.asc
70
+ end
71
+ end
72
+
73
+ def deg(body, method = :to_f)
74
+ body.send(method)
75
+ end
76
+
77
+ def degree(body, asc = nil) # helper for drawing chart
78
+ ((asc || self.asc) - 180 - deg(body)) % 360
79
+ end
80
+
81
+ def get_bodies!(planets)
82
+ planets.each do |abbr|
83
+ body = find_body!(abbr)
84
+ body.harmonize!(@harmonic) if body.is_a?(Astroscript::Body)
85
+ end
86
+ end
87
+
88
+ def find_body!(abbr)
89
+ case abbr
90
+ when *SwissEphemeris::BODIES.keys
91
+ key = AstroHelper.symbolize(SwissEphemeris::BODIES[abbr][:name])
92
+ self[key] = get_body(abbr)
93
+ else
94
+ raise ArgumentError, "unknown body: #{abbr}"
95
+ end
96
+ end
97
+
98
+ def phase_angle(b1, b2)
99
+ b1 = find_body(b1) if b1.is_a?(Symbol) && SwissEphemeris::BODIES.keys.include?(b1)
100
+ b2 = find_body(b2) if b2.is_a?(Symbol) && SwissEphemeris::BODIES.keys.include?(b2)
101
+ # (b1.lon - b2.lon) % 360
102
+ b1 - b2
103
+ end
104
+
105
+ def []=(key, body)
106
+ @chart[key] = body
107
+ # body.chart = self
108
+ end
109
+
110
+ def [](abbr)
111
+ abbr = abbr.upcase if abbr.size == 2
112
+ get_body(abbr)
113
+ end
114
+
115
+ def find_body(abbr)
116
+ if (result = @chart.find{|_k, v| v.abbr == abbr })
117
+ result[1]
118
+ else
119
+ # calc.get_body(abbr)
120
+ end
121
+ end
122
+
123
+ def get_body(abbr)
124
+ body = SwissEphemeris::BODIES[abbr]
125
+ if (method = body[:method])
126
+ method = body[:name] if method == true
127
+ output = MethodBody.new(send(method), method, chart: self)
128
+ else
129
+ output = find_body(abbr) || calc.get_body(abbr)
130
+ end
131
+ output.harmonize(@harmonic)
132
+ end
133
+
134
+ def find_bodies(ary)
135
+ ary.map{|a| find_body(a) }
136
+ end
137
+
138
+ def aries_point
139
+ # ConstBody.new( 0, 'AP' )
140
+ 0
141
+ end
142
+
143
+ def method_missing(method_name, ...)
144
+ if @calc.respond_to?(method_name)
145
+ @calc.send(method_name, ...)
146
+ elsif @chart.respond_to?(method_name)
147
+ @chart.send(method_name, ...)
148
+ else
149
+ raise "unknown method! :#{method_name}"
150
+ end
151
+ end
152
+
153
+ def night_birth?
154
+ sun = get_body(:SO).lon
155
+ desc = asc + 180
156
+ if desc > 360
157
+ sun.between?(desc % 360, asc)
158
+ else
159
+ sun.between?(asc, asc + 180)
160
+ end
161
+ end
162
+
163
+ def harmonic?
164
+ @harmonic > 1
165
+ end
166
+
167
+ def harmonize(h)
168
+ # output = Marshal.load( Marshal.dump(self) ) # deep copy
169
+ output = initialize_copy(self)
170
+ output.harmonize!(h)
171
+ end
172
+
173
+ def harmonize!(h)
174
+ @harmonic = h
175
+ bodies.each{|b| b.harmonize!(h) }
176
+ self
177
+ end
178
+
179
+ alias * harmonize
180
+
181
+ def print **opts # prefix: true, seconds: true, gate: true, house: true, spectrum: true
182
+ opts[:seconds] ||= true
183
+ puts @chart.map{|_k, v| v.to_s(opts) }.join("\n")
184
+ end
185
+
186
+ def to_a
187
+ calc.to_a << location
188
+ end
189
+
190
+ def to_json(*_args)
191
+ {
192
+ bodies: bodies.map(&:to_json)
193
+ }.to_json
194
+ end
195
+
196
+ def aspects(opts = {})
197
+ _bodies = bodies + opts[:with].to_a
198
+ if (orbs = opts[:orbs])
199
+ orbs.map do |flavor, orb|
200
+ _bodies.combination(2).map do |b1, b2|
201
+ Aspect.new(b1, b2, orb: orb, ratio: Aspect::FLAVORS.key(flavor))
202
+ end
203
+ end.flatten.uniq
204
+ else
205
+ _bodies.combination(2).map do |b1, b2|
206
+ Aspect.new(b1, b2, opts)
207
+ end.reject(&:invalid?)
208
+ end
209
+ end
210
+
211
+ def conjunctions(orb: 8)
212
+ aspects(orb: orb, harmonic: 1)
213
+ end
214
+
215
+ def trines(orb: 8)
216
+ aspects(orb: orb, harmonic: 3)
217
+ end
218
+
219
+ def midpoints
220
+ bodies.combination(2).map do |b1, b2|
221
+ Midpoint.new(b1, b2)
222
+ end
223
+ end
224
+
225
+ def midpoint_aspects(opts = {})
226
+ opts[:max_harmonic] ||= 2
227
+ opts[:orb] ||= 1.5
228
+ aspects(opts.merge(with: midpoints))
229
+ .select(&:midpoint?)
230
+ .reject(&:overlap?).reject(&:isotrap?)
231
+ .sort_by(&:orb)
232
+ end
233
+
234
+ # def isotraps( opts={} )
235
+ # opts[:orb] ||= 1.5
236
+ # aspects( opts.merge( with: midpoints ) ).select(&:isotrap?).reject(&:conjunction)
237
+ # end
238
+ end
239
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astroscript
4
+ VERSION = "0.3.0"
5
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # $LOAD_PATH.unshift File.expand_path(".", __dir__)
4
+
5
+ require "dotenv/load" if Gem.loaded_specs.key?("dotenv") # do this first
6
+ require "byebug" if Gem.loaded_specs.key?("byebug")
7
+
8
+ require "active_support"
9
+ require "active_support/core_ext/string"
10
+ require "forwardable"
11
+ require "geocoder"
12
+ require "logger"
13
+ require "ostruct"
14
+
15
+ require_relative "ruby_extensions"
16
+ require_relative "swiss_ephemeris"
17
+
18
+ require_relative "astroscript/version"
19
+ require_relative "astro_helper"
20
+ require_relative "astroscript/calculator"
21
+ require_relative "astroscript/body"
22
+ require_relative "astroscript/body/midpoint"
23
+ require_relative "astroscript/body/const_body"
24
+ require_relative "astroscript/body/method_body"
25
+ # require_relative './astro/body/house_cusp'
26
+ require_relative "astroscript/chart"
27
+ require_relative "astroscript/aspect"
28
+
29
+ $logger = Logger.new($stdout)
30
+ $logger.level = Logger::WARN
31
+ # $logger.level = Logger::DEBUG
32
+
33
+ require 'dotenv/load'
34
+ Geocoder.configure(lookup: :geoapify, api_key: ENV.fetch("GEOCODER_API_KEY", nil))
35
+ SwissEphemeris.path = ENV.fetch("SE_EPHE_PATH", File.expand_path("../vendor/swe_data", __dir__))
36
+
37
+ module Astroscript
38
+ class Error < StandardError; end
39
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module ArrayExtensions
6
+ def diff
7
+ diffop{|x, y| y - x }
8
+ end
9
+
10
+ def diffop
11
+ d1 = dup.unshift(0)
12
+ d2 = dup.push(0)
13
+ (0..size).map{|i| yield(d1[i], d2[i]) }[1...-1]
14
+ end
15
+
16
+ def avg
17
+ sum / size.to_f
18
+ end
19
+
20
+ def gmean
21
+ reduce(:*).to_f**(1.0 / size)
22
+ end
23
+
24
+ def **(other)
25
+ map{|x| x * other }
26
+ end
27
+
28
+ # harmonic operator
29
+ def >>(other)
30
+ map{|x| (x * other) % 360.0 }
31
+ end
32
+ end
33
+ Array.include ArrayExtensions
34
+
35
+ module NumericExtensions
36
+ def sgn
37
+ negative? ? -1 : 1
38
+ end
39
+
40
+ def mod360
41
+ self % 360
42
+ end
43
+ end
44
+ Numeric.include NumericExtensions
45
+
46
+ module IntegerExtensions
47
+ def ordinalize
48
+ n = self
49
+ return "#{n}th" if (11..13).include?(n % 100)
50
+
51
+ case n % 10
52
+ when 1 then "#{n}st"
53
+ when 2 then "#{n}nd"
54
+ when 3 then "#{n}rd"
55
+ else "#{n}th"
56
+ end
57
+ end
58
+
59
+ PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103,
60
+ 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349].freeze
61
+
62
+ def hph
63
+ n = self
64
+ return 1 if n == 1
65
+
66
+ PRIMES.reverse.find{|p| n.gcd(p) != 1 }
67
+ end
68
+ end
69
+ Integer.include IntegerExtensions
70
+
71
+ module DateTimeExtensions
72
+ def round(granularity = 1.hour)
73
+ Time.at((to_time.to_i / granularity).round * granularity).to_datetime
74
+ end
75
+ end
76
+ DateTime.include DateTimeExtensions
77
+
78
+ module FloatExtensions
79
+ def to_dms(opts = {})
80
+ AstroHelper.print_dms(self, opts)
81
+ end
82
+ end
83
+ Float.include FloatExtensions