astroscript 0.3.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,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