astroscript 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astroscript
4
+ class HouseCusp < Body
5
+ attr_reader :house
6
+
7
+ def initialize(house, calc)
8
+ @house = house
9
+ @abbr = :"c#{@house}"
10
+ super(@abbr, calc)
11
+ end
12
+
13
+ def calculate!
14
+ @lon = calc.house_cusps[@house - 1]
15
+ update!
16
+ @lon = calc.flip(@lon) if antipode
17
+ self
18
+ end
19
+
20
+ def symbol
21
+ @symbol = "C#{house}"
22
+ end
23
+
24
+ def name
25
+ "House Cusp #{numeral}"
26
+ end
27
+
28
+ private
29
+
30
+ def numeral
31
+ HOUSES[@house]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astroscript
4
+ class MethodBody < Body
5
+ attr_accessor :symbol, :name, :chart
6
+
7
+ def initialize(deg, name, symbol = nil, _abbr = nil, chart:)
8
+ @lon = deg
9
+ @name = name
10
+ @chart = chart
11
+ @calc = chart.calc
12
+ @harmonic = 1
13
+ body = SwissEphemeris::BODIES.select{|_k, v| v[:name] == name }
14
+ if body.empty?
15
+ $logger.warn "Unknown body: #{name}"
16
+ else
17
+ @abbr = body.keys.first
18
+ @symbol = body[@abbr][:symbol] || symbol
19
+ end
20
+ update!
21
+ end
22
+
23
+ def calculate! # name should equal method
24
+ @lon = @chart.send name
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astroscript
4
+ class Midpoint < Body
5
+ attr_reader :bodies
6
+
7
+ def initialize(b1, b2, opts = {})
8
+ @harmonic = opts[:harmonic] || 1
9
+ @bodies = [b1, b2]
10
+ @name = abbr.sub(operator, "_")
11
+ @symbol = @bodies.map(&:symbol).join(operator)
12
+ @lon = @bodies.map(&:lon).avg
13
+ if ((@lon - b1.lon).abs % 360) > 90
14
+ @lon = (@lon + 180) % 360 # flip!
15
+ end
16
+ update!
17
+ end
18
+
19
+ def calculate!
20
+ bodies.each(&:calculate!)
21
+ end
22
+
23
+ def abbr
24
+ bodies.map(&:abbr).join(operator)
25
+ end
26
+
27
+ def symbol
28
+ bodies.map(&:symbol).join(operator)
29
+ end
30
+
31
+ def to_sym
32
+ abbr.to_sym
33
+ end
34
+
35
+ def inspect
36
+ return super unless defined?(IRB)
37
+
38
+ "#{bodies.first.abbr}/#{bodies.last.abbr}\t#{AstroHelper.deg_to_s(lon)}"
39
+ end
40
+
41
+ protected
42
+
43
+ def operator
44
+ "/"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,495 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astroscript
4
+ class Body
5
+ attr_reader :abbr, :lat, :distance, :velocity, :decl, :ra, :azimuth, :altitude, :calc, :harmonic, :hlat, :hlon,
6
+ :hdist, :hvel
7
+ attr_accessor :antipode
8
+
9
+ def initialize(abbr, calc)
10
+ @abbr = abbr
11
+ @calc = calc
12
+ @abbr = @abbr.upcase if @abbr.size == 2
13
+ @harmonic = 1
14
+ calculate!
15
+ end
16
+
17
+ def inspect
18
+ return super unless defined?(IRB)
19
+
20
+ "#{abbr}\t#{AstroHelper.deg_to_s(lon)}"
21
+ end
22
+
23
+ def radix
24
+ harmonize(1)
25
+ end
26
+
27
+ def ==(other)
28
+ lon == other.lon
29
+ end
30
+
31
+ def self.phase(b1, b2, opts = {})
32
+ opts[:method] ||= :lon
33
+ # $logger.debug("#{b1}, #{b2} #{opts[:method]}")
34
+ bb1 = b1.send(opts[:method])
35
+ bb2 = b2.send(opts[:method])
36
+ (bb2 - bb1) % 360
37
+ # if b1.speed_order > b2.speed_order
38
+ # (bb2-bb1)%360
39
+ # else
40
+ # (bb1-bb2)%360
41
+ # end
42
+ # rescue => e
43
+ # raise e, "#{b1.abbr} - #{b2.abbr} => #{opts[:method]} == nil!"
44
+ end
45
+
46
+ def transit?
47
+ @transit
48
+ end
49
+
50
+ def transit!
51
+ @transit = true
52
+ end
53
+
54
+ def flip
55
+ @lon += 180
56
+ @lon % 360
57
+ end
58
+
59
+ def flip!
60
+ self.antipode = !antipode
61
+ calculate!
62
+ end
63
+
64
+ def lon
65
+ output = @lon # @calc.in_mundo? ? prime_degree : @lon
66
+ output *= @harmonic
67
+ output %= 360
68
+ output += @calc.arc if @calc
69
+ # TODO: make harmonic charts a calc transformer?
70
+ if (t = calc&.transformer)
71
+ t.call(self, output)
72
+ else
73
+ output
74
+ end
75
+ end
76
+
77
+ alias degree lon
78
+ alias to_f lon
79
+
80
+ def to_dms(opts = {})
81
+ lon.to_dms(opts)
82
+ end
83
+
84
+ def prefix
85
+ @calc&.prefix
86
+ end
87
+
88
+ def handle
89
+ "#{prefix}#{abbr}"
90
+ end
91
+
92
+ def name
93
+ return unless (body = SwissEphemeris::BODIES[abbr])
94
+
95
+ body[:name].capitalize
96
+ end
97
+
98
+ def symbol
99
+ return unless (body = SwissEphemeris::BODIES[abbr])
100
+
101
+ body[:symbol]
102
+ end
103
+
104
+ def calculate!
105
+ # $logger.debug "calculating #{abbr}"
106
+ if @abbr == :SVP
107
+ @lon = -@calc.ayanamsha
108
+ @lat = 0
109
+ @distance = nil
110
+ @velocity = 0
111
+ end
112
+ raise ArgumentError, "unknown Body #{@abbr}!" unless (body = SwissEphemeris::BODIES[@abbr])
113
+
114
+ if (id = body[:id])
115
+ results = @calc.calc(id)
116
+ @lon, @lat, @distance, @velocity = results[0..4]
117
+ results = @calc.calc(id, equatorial: true)
118
+ # right ascension and declination
119
+ @ra, @decl = results[0..2]
120
+ @azimuth, @altitude, * = Swe4r.swe_azalt(calc.jd, Swe4r::SE_ECL2HOR, calc.longitude, calc.latitude,
121
+ calc.altitude, 1013.25, 15.0, @lon + calc.ayanamsha, @lat.to_f, @distance.to_f)
122
+ @azimuth = (@azimuth + 180) % 360 # adjusts to measure clockwise from north
123
+ # heliocentric
124
+ id = Swe4r::SE_EARTH if abbr == :EA
125
+ results = @calc.calc(id, heliocentric: true)
126
+ @hlon, @hlat, @hdist, @hvel = results[0..4]
127
+ else # virtual body - angles and nodes
128
+ method = if (a = body[:antipode])
129
+ SwissEphemeris::BODIES[a][:name]
130
+ else
131
+ body[:name]
132
+ end
133
+ @calc.get_houses
134
+ method = AstroHelper.symbolize(method)
135
+ # $logger.debug "sending #{method} to @calc"
136
+ if %i[NN SN].include?(@abbr)
137
+ node = @calc.true_node? ? Swe4r::SE_TRUE_NODE : Swe4r::SE_MEAN_NODE
138
+ @lon, @lat, @distance, @velocity = @calc.calc(node)
139
+ # @lon = calc.flip(@lon) if @abbr == :SN # TODO flip after harmonic
140
+ else
141
+ @lon = @calc.send(method)
142
+ end
143
+ # TODO: get values from sun at this degree
144
+ end
145
+ @lon = calc.flip(@lon) if body[:antipode] || antipode
146
+ update!
147
+ end
148
+
149
+ # https://www.timeanddate.com/astronomy/planets/distance
150
+
151
+ # According to BPHS the victor is the Graha who is further north.
152
+ # Surya Siddhanta adds that the brighter will win even if in the south.
153
+ # By brighter is meant an unusually brighter planet. To determine this we need to know the natural
154
+ # order of brightness from least bright to most bright which is:
155
+ # Saturn, Mars, Mercury, Jupiter, Venus. On very rare occasions when a planet in its oval shaped
156
+ # revolution is much closer to Earth and the other much further away, one planet can gain a lot of
157
+ # brightness while the other loses brightness. On these occasions a planet that is normally darker
158
+ # than another may appear brighter – in which case it wins the war. To determine the victor in a war,
159
+ # therefore, requires first that elaborate calculations are done based on the diameter of the planet
160
+ # and its proximity to Earth in order to determine the brightness. If the brightness of the two
161
+ # planets follows the regular order, then the Graha with the most northern declination wins the war.
162
+ # If, on the other hand, the planet that is regularly less bright is brighter, than it wins the war
163
+ # and no check for most northern position need be made. Venus, regardless, always wins the war, due
164
+ # to her much greater brilliance she is considered to never be defeated.
165
+ # https://astrology-videos.com/CourseMaterials/PlanetaryWar.pdf
166
+ def prime_degree
167
+ # lon, decl = Swe4r::swe_cotrans( 90, @azimuth+calc.ayanamsha, @altitude)
168
+ (Swe4r.swe_house_pos(calc.ramc, calc.latitude, calc.oe, "C".ord, @lon + calc.ayanamsha, @lat.to_f) * 30) - 30
169
+ end
170
+ alias pvl prime_degree
171
+
172
+ def meridian_degree # longitude
173
+ azimuth, altitude, * = Swe4r.swe_azalt(calc.jd, Swe4r::SE_ECL2HOR, calc.longitude, calc.latitude, calc.altitude,
174
+ 0, 0, @lon, @lat.to_f, @distance.to_f)
175
+ azimuth = (azimuth + 90) % 360
176
+ lon, decl, * = Swe4r.swe_cotrans(90, azimuth, altitude)
177
+ output = [((lon + 180) % 360).round(4), decl.round(4)]
178
+ output[0]
179
+ end
180
+
181
+ # def /(body)
182
+ # Midpoint.new(self, body)
183
+ # end
184
+
185
+ ORBS = { tight: 2.0, close: 6.0, max: 12.0 }.freeze # Hamblin
186
+ # ORBS = {tight: 2.0, close: 4.0, max: 16.0} # Cochrane
187
+
188
+ def sign_i
189
+ sign
190
+ end
191
+
192
+ def sign
193
+ (lon / 30).floor
194
+ end
195
+
196
+ def sign_sym
197
+ SwissEphemeris::SIGNS[sign]
198
+ end
199
+
200
+ def to_s(_opts = {})
201
+ opts = { angle: true, symbol: true }.merge!(_opts)
202
+ postfix = " "
203
+ if opts[:angle]
204
+ postfix = "r" if retrograde?
205
+ postfix = "s" if stationary?
206
+ dignity = nil
207
+ # dignity = SYMBOLS[:exalted] if in_exaltation? # TODO
208
+ # dignity = SYMBOLS[:detriment] if in_detriment?
209
+ end
210
+ if opts[:symbol]
211
+ opts[:prefix] = format("%4s", symbol) if opts[:symbol]
212
+ opts[:padding] = 2
213
+ end
214
+ opts[:prefix] = "#{opts[:symbol] ? symbol : name}#{dignity}" if opts[:prefix]
215
+ if opts[:house]
216
+ opts[:postfix] = "#{postfix} #{format('%-5s', HOUSES[house])}"
217
+ opts[:postfix] += " (#{AstroHelper::HOUSE_THEMES[house]})" if opts[:theme]
218
+ end
219
+ AstroHelper.deg_to_s(lon, { postfix: postfix }.merge!(opts)) # .strip
220
+ end
221
+
222
+ def gate
223
+ AstroHelper.deg_to_gate(lon)
224
+ end
225
+
226
+ def print(opts = {})
227
+ puts to_s({ prefix: true, gate: true, spectrum: true, house: true, theme: true, symbol: true }.merge(opts))
228
+ end
229
+
230
+ def distance_to_sun
231
+ SwissEphemeris::BODIES.keys.index(abbr) || -1
232
+ end
233
+
234
+ # this assumes the that BODIES hash is defined in proper order
235
+ def closer_to_sun?(b2)
236
+ i1 = distance_to_sun
237
+ i2 = b2.distance_to_sun
238
+ i1 && i2 && i1 < i2
239
+ end
240
+
241
+ ANGLES = %i[AC MC DC IC VX POF POS POH].freeze
242
+ def is_angle?
243
+ ANGLES.include?(abbr)
244
+ end
245
+
246
+ def personal_angle?
247
+ (ANGLES + %i[VX POF POS POH]).include?(abbr)
248
+ end
249
+
250
+ def speed_order
251
+ a = abbr
252
+ a = :NN if %i[MN SN].include?(a)
253
+ return -1 * ANGLES.reverse.index(a) if is_angle?
254
+
255
+ # https://groups.io/g/SpiritusMundi/topic/speed_of_ascendant_and/83787680
256
+ SPEED_ORDER.index(a)
257
+ end
258
+
259
+ #################
260
+ ### RULERSHIP ###
261
+ #################
262
+
263
+ def ruler(chart = nil)
264
+ ruler = RULERSHIPS.find{|_k, v| v.include? sign_sym }.first
265
+ if chart
266
+ chart.get_body(ruler)
267
+ else
268
+ ruler
269
+ end
270
+ end
271
+ alias dispositor ruler
272
+
273
+ def in_domicile?
274
+ RULERSHIPS[abbr].include?(sign_sym)
275
+ rescue NoMethodError
276
+ false
277
+ end
278
+
279
+ def in_fall? # opposite to rulership
280
+ RULERSHIPS[abbr].include?(rotate_sign(6))
281
+ rescue NoMethodError
282
+ false
283
+ end
284
+
285
+ def in_exaltation?
286
+ EXALTATION[abbr].first == sign_sym
287
+ rescue NoMethodError
288
+ false
289
+ end
290
+
291
+ def in_detriment?
292
+ EXALTATION[abbr].first == rotate_sign(6)
293
+ rescue NoMethodError
294
+ false
295
+ end
296
+
297
+ def retrograde?
298
+ @velocity&.negative?
299
+ end
300
+
301
+ # RETROGRADE = { # days
302
+ # ME: 24, VE: 42, MA: 80, JU: 120, SA: 140, UR: 150, NE: 160, PL: 160, CE: 60
303
+ # }
304
+ STATIONARY_VELOCITY = { # seconds/day from https://www.astro.com/astrowiki/en/Stationary_Phase
305
+ ME: 300, VE: 180, MA: 90, JU: 60, SA: 60, UR: 20, NE: 10, PL: 10, CE: 7
306
+ }.freeze
307
+ MAX_VELOCITY = { # in minutes from https://en.wikipedia.org/wiki/Planets_in_astrology
308
+ ME: (2 * 60) + 25, VE: (1 * 60) + 25, MA: 52, CE: 30, JU: 14 + (40 / 60.0), SA: 8 + (48 / 60.0),
309
+ UR: 4, NE: 2 + (25 / 60.0), PL: 2 + (30 / 60.0), PA: 40 + (30 / 60.0), JN: 39, VA: 36, CH: 10
310
+ }.freeze
311
+ AVG_VELOCITY = { # in minutes
312
+ SO: 59 + (8 / 60.0), MO: (13 * 60) + 11 + (36 / 60.0), ME: (1 * 60) + 23, VE: (1 * 60) + 12, MA: 31 + (27 / 60.0), CE: 12 + (40 / 60.0),
313
+ JU: 4 + (59 / 60.0), SA: 2 + (1 / 60.0), UR: 42 / 60.0, NE: 24 / 60.0, PL: 15 / 60.0, CH: 2,
314
+ PA: 12 + (20 / 60.0), JN: 14 + (15 / 60.0), VA: 16 + (15 / 60.0), SD: 0.289, ER: 6.307,
315
+ NN: 3.177
316
+ }.freeze
317
+ SPEED_ORDER = AVG_VELOCITY.sort_by{|_k, v| v }.reverse.map(&:first)
318
+
319
+ # Here is a snippet of the Python code I use for the speeds:
320
+ # # Adjust the speeds to be compatible with the old standalone version
321
+ # # of Haydn's Jyotish. { # Haydn Huntley haydn@hjyotish.com }
322
+ # daysPerEarthYear = 365.242199
323
+ # x = 360.0 / daysPerEarthYear ** 2
324
+ # self.speed[MOON] *= 0.09706 # Magic constant.
325
+ # self.speed[MARS] *= 686.971 * x # Orbital period in days,
326
+ # self.speed[JUPITER] *= 4332.59 * x # according to Wikipedia.
327
+ # self.speed[SATURN] *= 10759.22 * x
328
+ # self.speed[URANUS] *= 30799.095 * x
329
+ # self.speed[NEPTUNE] *= 60190.03 * x
330
+ # self.speed[PLUTO] *= 89865.65 * x
331
+ # self.speed[RAHU] *= 6585.3213 * x # Saros cycle in days.
332
+ # self.speed[KETU] *= 6585.3213 * x
333
+
334
+ def stationary?
335
+ return nil unless (minutes = MAX_VELOCITY[abbr])
336
+
337
+ velocity.abs <= (0.05 * minutes / 60.0) # 5% of velocity in degrees/day
338
+ end
339
+
340
+ def relative_velocity
341
+ return nil unless (minutes = AVG_VELOCITY[abbr])
342
+
343
+ begin
344
+ velocity / (minutes / 60.0)
345
+ rescue StandardError
346
+ 1.0
347
+ end
348
+ end
349
+
350
+ def house
351
+ @calc.which_house(lon)
352
+ end
353
+
354
+ def yin?
355
+ sign.odd?
356
+ end
357
+
358
+ def yang?
359
+ sign.even?
360
+ end
361
+
362
+ def modality
363
+ %i[c f m][sign % 3]
364
+ end
365
+
366
+ def element
367
+ %i[F E A W][sign % 4]
368
+ end
369
+
370
+ def element_sym
371
+ %w[🜂 🜃 🜁 🜄][sign % 4]
372
+ end
373
+
374
+ # qualities for interpretations
375
+ def feminine?
376
+ %i[MO VE NE].include?(abbr)
377
+ end
378
+
379
+ def masculine?
380
+ %i[SO MA JU UR PL].include?(abbr)
381
+ end
382
+
383
+ def outer?
384
+ %i[SA UR NE PL].include?(abbr)
385
+ end
386
+
387
+ def inner?
388
+ luminary? || %i[ME VE MA].include?(abbr)
389
+ end
390
+
391
+ def luminary?
392
+ %i[SO MO].include?(abbr)
393
+ end
394
+
395
+ def decan
396
+ ((to_f % 30) / 10).floor + 1
397
+ end
398
+
399
+ #######################
400
+ # Harmonic Divisions
401
+ #######################
402
+
403
+ def harmonic_sign(h) # always use 1st harmonic longitude
404
+ SwissEphemeris::SIGNS[varga_sign(h)]
405
+ end
406
+
407
+ def triple_harmonic_signs(h)
408
+ harmonic_signs(1, h, h * h)
409
+ end
410
+
411
+ def harmonic_signs *args
412
+ args.map{|h| harmonic_sign(h) }
413
+ end
414
+
415
+ def harmonize!(h)
416
+ @harmonic = h
417
+ calculate!
418
+ self
419
+ end
420
+
421
+ def harmonize(h)
422
+ dup.harmonize!(h)
423
+ end
424
+
425
+ def *(other)
426
+ harmonize(other)
427
+ end
428
+
429
+ def pentan
430
+ harmonic_sign(6)
431
+ end
432
+
433
+ def subpentan
434
+ harmonic_sign(36)
435
+ end
436
+
437
+ def duad
438
+ SwissEphemeris::SIGNS[(varga_sign(12) - sign) % 12]
439
+ end
440
+
441
+ def subduad # is this correct
442
+ SwissEphemeris::SIGNS[(varga_sign(144) - sign) % 12]
443
+ end
444
+
445
+ def navamsha
446
+ harmonic_sign(9)
447
+ end
448
+
449
+ def dwad
450
+ harmonic_sign(12)
451
+ end
452
+
453
+ def subdwad
454
+ harmonic_sign(144)
455
+ end
456
+
457
+ def sign_and_house
458
+ "#{sign_sym.capitalize} in #{house.ordinalize}"
459
+ end
460
+
461
+ def midpoint?
462
+ is_a?(Astroscript::Midpoint)
463
+ end
464
+
465
+ def to_json(*_args)
466
+ d, m, * = AstroHelper.dms(lon)
467
+ key = "#{name.downcase} in #{SwissEphemeris::SIGNS[sign]}"
468
+ {
469
+ house: house,
470
+ name: name,
471
+ sign_degrees: d,
472
+ sign_minutes: m.round,
473
+ sign_name: SwissEphemeris::SIGNS[sign],
474
+ total_angle: @lon.round(2),
475
+ description: key.titleize,
476
+ key: key
477
+ }
478
+ end
479
+
480
+ private
481
+
482
+ def update!
483
+ @sign, @degree = (@lon * @harmonic).divmod(360)
484
+ @degree, m, s = AstroHelper.dms(@degree)
485
+ @minute = m.round
486
+ @minute_ = m.floor
487
+ @seconds = s.round
488
+ self
489
+ end
490
+
491
+ def varga_sign(h)
492
+ (harmonized_degree(h) / 30).floor
493
+ end
494
+ end
495
+ end