2025 Aug 3 – pyswisseph
I've been using Astrolog daily for most of this year. Because I built it from source and neither understand C++ nor where Makefiles come from (I tried), this requires me being in the same directory of my astrolog.as
so it knows where the ephemeris files are, which I symlink into every subdirectory of my astro database. This is fine for doing readings for clients or any other focused astrological work, but sometimes I am in the middle of another task and don't want to break my concentration or add a tmux window just to navigate to my database folder in order to pull up the chart of the moment with astrolog -n
.
The main thing I need this for is electional astrology, so the first and often times only thing I look at is the ascendant, its ruler, and the Moon. What this sounded like to me is:
- I want a clock in my swaybar for my current local ascendant;
- I want a CLI version of the Chart of the Moment on Astro.com, with no houses or angles.
This meant it's time for me to finally tangle with the Swiss Ephemeris. The first piece of "astrology software" I made was a sidereal time calculator that simply did arithmetic out of Bruce Scofield's Astrological Chart Calculations and required an astronomical (ha) amount of user input when there's a whole sweph function just for this.
I don't know who or what Astro Origin is, but their website that purportedly has documentation for the Python Swiss Ephemeris has been down for a minute. Happily, the function names are the same as in the original C library whose docs aren't going anywhere thanks to AstroDienst.
ascendant clock
This was an adventure in I don't understand astrology or astronomy enough to know what all these values I'm printing are, and realizing many astrologers I respect probably wouldn't have a clue, either. I also discovered just how many nested tuples I can specify and slice through.
The output of houses()
, central to my ascendant clock, for example, made sense to me at first because I use whole sign house. It printed a list of two tuples, the first of which had 12 integers 30 degrees apart for the 30-degree signs. I was able to surmise the second one, with eight clearly decimal degree floats, at minimum included the points of the ascendant and midheaven within these whole sign houses. I guessed at least some of the others could be the midheaven's ascensions? You tell me from reading this and looking at this tuple:
(261.50028001940655, 182.4609321606867, 182.25809588867747, 131.66417052837792, 272.0719460498313, 282.50586769166114, 230.73433572771262, 102.50586769166115)
Whatever the case, I know that tropical Sagittarius's absolute longitude spans 240-70 and checked this against Astrolog and was reasonably confident the first value had to be the ascendant, which is all I need for electional astrology. The function that translates decimal degrees to human (astrologer)-readable sign degrees is split_deg
, which prints yet another tuple that made more sense to me thanks to me self-imposed crash course in analog math with Bruce Scofield: (21, 30, 1, 0.008069863564742263, 8)
. Here index 4 is one of 0-11 for the signs, so the rest of my little clock program is just a long, convoluted elif
trail translating these to truncated sign strings.
In practice, I wrote a script that prints this to my swaybar! It looks like this, to the left of my vanilla date
output.
(might be a bit illegible since my bar background matches my website styling!)
chart of the moment
A bit more comfortable with throwing function names at the wall, I set out to replicate Astro.com's Chart of the Moment, which you can get to by clicking the ☉☽☿ at the top right of their header.
I wrote this beautiful succession of loops that made me appreciate indexing the planets and signs as integers:
positions = []
for planet in range(12):
position = swe.calc_ut(jd, planet)
positions.append(position)
planets = []
for position in range(12):
planet = positions[position][0][0]
planets.append(planet)
dms = []
for planet in range(12):
split = swe.split_deg(planets[planet], 8)
dms.append(split)
Because my first foolish impulse was to write string variables for each of them really early on just so I knew what I meant, but this was just add more and more lines of specifying each instead of using range()
. I did make a multiline comment telling me what each of the planets' indices were, though.
Something I'm still not sure about is argparse
. I knew I wanted it to use timedate.now(timezone.utc)
if passed no arguments, but wasn't sure if that meant I needed to have timedate
as the default value if I defined it in the else
statement at the start of the function. It seems like I do, and I don't understand why. I probably just need to read the docs slowly, especially about nargs
. I'm also annoyed datetime
has the date.today()
type but not a corresponding time
type, so I had to slice through timedate.UTC
.
The other thing is I knew I wanted to send the output to myself with notify-send
from wmenu, so I couldn't have the pretty sect-based ANSI coloring I'm so proud of for the Mercury condition, so that's the origin of the --plain
flag.
You can see example plain text outputs and a screenshot on the README. Relatedly, I'm making it a goal to collect Linux distribution birth information and wish everyone was like the Debian community.