← Astronomy 7 / 18

Houses

A house is a division of the sky as seen from one place at one instant. That is what separates it from a position: the planets say WHAT is happening and are the same for everybody looking up at that moment, but the houses say WHERE in a life, and they turn one degree every four minutes of clock time. The geometry underneath is old and published, some of it since the fifteenth century, and none of it needs a licence from anyone: what a commercial ephemeris sells is the positions, not the houses.

What separates one system from another is WHAT gets divided into twelve. Whole Sign divides the zodiac. Campanus divides the prime vertical, the great circle running from due east, through the zenith, to due west. Regiomontanus divides the celestial equator. Placidus does not divide a circle at all: it divides the TIME each degree takes to travel from the horizon to the meridian, which is why it has no closed formula and has to be solved by iterating.

Houses builds every one of its twenty-three systems from that geometry directly, as a great circle cut with the ecliptic or a diurnal arc shared out, written with vectors rather than with a closed formula copied out of a manual. It costs more code, and it buys something specific: each system is written as what it actually is, and a definition written out can be checked against itself, which is exactly how Koch was once caught measuring the wrong body's arc.

Computing a set of houses

use Astronomy\Houses;
use Astronomy\HouseSystem;
use Astronomy\Time;

[$jdTT, $jdUt] = Time::fromClock(new DateTimeImmutable('1981-05-11 07:15:00', new DateTimeZone('UTC')));

$houses = Houses::calculate(HouseSystem::Placidus, $jdUt, latitude: 40.4165, geographicLongitude: -3.7026);

printf("ascendant %.4f\nmidheaven %.4f\ncusp 1    %.4f\n", $houses->ascendant, $houses->midheaven, $houses->cusps[1]);
ascendant 85.9941
midheaven 332.0406
cusp 1    85.9941

calculate() takes a julian day in Universal Time, not Terrestrial Time, and that is not interchangeable with what Ephemeris::position() wants: the houses depend on how far the Earth has turned, and UT is what the civil clock measures. Passing $jdTT there instead is a mistake that does not throw, and it is worth seeing how much it costs. Time::fromClock() returns both scales for the same clock reading, about fifty seconds apart on this particular date:

$right = Houses::calculate(HouseSystem::Placidus, $jdUt, 40.4165, -3.7026);
$wrong = Houses::calculate(HouseSystem::Placidus, $jdTT, 40.4165, -3.7026);

printf("jdUt %.6f  jdTT %.6f  difference %.1f seconds\n", $jdUt, $jdTT, ($jdTT - $jdUt) * 86400);
printf("ascendant with UT  %.6f\n", $right->ascendant);
printf("ascendant with TT  %.6f\n", $wrong->ascendant);
printf("difference         %.1f arcseconds\n", ($wrong->ascendant - $right->ascendant) * 3600);
jdUt 2444735.802077  jdTT 2444735.802676  difference 51.7 seconds
ascendant with UT  85.994148
ascendant with TT  86.198546
difference         735.8 arcseconds

Fifty-two seconds of the wrong clock move this particular ascendant by over twelve arcminutes, a fifth of a degree, on a chart that reads in degrees and minutes. How much it costs on any given chart depends on latitude and on how fast the ascendant happens to be running at that sidereal time (see Time for where that fifty-second gap, delta T, comes from).

Twenty-three systems

use Astronomy\HouseSystem;

foreach (HouseSystem::cases() as $system) {
    printf("%-14s %s\n", $system->key(), $system->name());
}
placidus       Placidus
koch           Koch
regiomontanus  Regiomontanus
campanus       Campanus
porphyry       Porphyry
alcabitius     Alcabitius
topocentric    Topocentric
equal          Equal houses
whole-sign     Whole sign
vehlow         Vehlow
morinus        Morinus
meridian       Meridian
azimuthal      Azimuthal
krusinski      Krusinski
sripati        Sripati
carter         Carter
apc            APC
pullen-sd      Pullen SD
pullen-sr      Pullen SR
equal-mc       Equal from the midheaven
equal-aries    Equal from Aries
savard-a       Savard-A
sunshine       Sunshine

Twelve of those are the classical ones; the other eleven are carried under the same letters as a commercial engine so results can be cross-checked, and every case answers description() in one line for what it actually divides:

foreach ([HouseSystem::Placidus, HouseSystem::WholeSign, HouseSystem::Sunshine] as $system) {
    printf("%s: %s\n", $system->name(), $system->description());
}
Placidus: Divides the time each degree takes to climb from the horizon to noon. The most widely used in western astrology.
Whole sign: Every sign is a house. The oldest one known and the one hellenistic astrology uses.
Sunshine: Divides in three the diurnal and nocturnal arcs of the Sun of that day and draws position circles through the points. From Bob Makransky, 1988.

Without a clock: Houses::fromArmc()

calculate() computes local sidereal time from the julian day and the longitude and then hands off to a private builder; fromArmc() is that same builder with the sidereal time (the ARMC) taken as a parameter instead. Everything a set of houses needs comes in through the ARMC, the latitude and the obliquity, and the date enters through no other door: a house is geometry of the turned sky and does not know what day it is.

use Astronomy\Houses;
use Astronomy\HouseSystem;

$armc = 123.456;
$latitude = 40.4165;
$obliquity = 23.4392;

$worked = 0;
foreach (HouseSystem::cases() as $system) {
    try {
        Houses::fromArmc($system, $armc, $latitude, $obliquity);
        $worked++;
    } catch (\RuntimeException $e) {
        echo $system->value, ' threw: ', $e->getMessage(), "\n";
    }
}
echo $worked, ' of ', count(HouseSystem::cases()), " systems built with no ephemeris and no date\n";
sunshine threw: Sunshine divides the diurnal arc of the Sun: with no declination for it there are no houses to compute.
22 of 23 systems built with no ephemeris and no date

Sunshine is the exception because it shares out the Sun's own diurnal and nocturnal arcs, so it needs the Sun's declination for that day: calculate() computes it for you with one extra ephemeris call, and fromArmc() takes it as an optional parameter instead, which is what lets Houses itself stay free of anything that touches an ephemeris.

The two doors sit behind the same private builder so they cannot diverge, and that is measured rather than assumed: run against each other across twenty-three systems, six places and five dates from 1655 to 2377, 7,943 of 8,040 cusps come back identical to the bit and the rest differ by at most 4e-10 arcseconds, which is the unit round trip of deg2rad(rad2deg()) and not a second computation.

Placing a body: houseOf(), houseWithLatitude(), housePosition()

houseOf() only needs a longitude, and it goes by the next cusp rather than the nearest one, because the houses do not all measure the same: on a high-latitude chart with Placidus one house can span eighty degrees and its neighbour ten, and "nearest cusp" would put half the chart in the wrong house.

housePosition() is what a commercial ephemeris calls swe_house_pos, and it takes the body's ecliptic latitude into account, which matters because a house in a quadrant system (Placidus, Regiomontanus, Campanus...) is a division of the sky and not of the zodiac. A body sitting off the ecliptic can be above the circle its bare longitude would place it under:

use Astronomy\Body;
use Astronomy\Ephemeris;
use Astronomy\Houses;
use Astronomy\HouseSystem;
use Astronomy\Time;

[$jdTT, $jdUt] = Time::fromClock(new DateTimeImmutable('1981-05-11 07:15:00', new DateTimeZone('UTC')));

$houses = Houses::calculate(HouseSystem::Placidus, $jdUt, latitude: 40.4165, geographicLongitude: -3.7026);
$saturn = Ephemeris::position(Body::Saturn, $jdTT);

echo $houses->houseOf($saturn->longitude), "\n";                              // by longitude alone
echo $houses->houseWithLatitude($saturn->longitude, $saturn->latitude), "\n";
echo $houses->housePosition($saturn->longitude, $saturn->latitude), "\n";
echo $saturn->longitude, ' under a cusp at ', $houses->cusps[5], ', latitude ', $saturn->latitude, "\n";
4
5
5.0165278277963
183.5071189058 under a cusp at 185.01126338975, latitude 2.589119460589

This Saturn sits a degree and a half short of cusp five by longitude and over it by two and a half degrees of latitude, so by longitude alone it is in house four and with latitude it is in house five. housePosition() returns a number in [1, 13): the integer part is the house and the fraction how much of it has been covered.

Not every system reads latitude the same way. The ecliptic systems (Equal, Whole Sign, Vehlow, Porphyry, the two Pullens, Morinus) place a body by longitude alone, because the system only exists over the ecliptic in the first place. The circle systems (Regiomontanus, Campanus, Meridian, Carter, Azimuthal, Krusinski) put the body on the great circle that contains it and see where that circle crosses the one being divided. And the time systems (Placidus, Koch, Alcabitius, Topocentric, APC, Sunshine) measure what fraction of the matching arc the body has covered. Ephemeris::position() and how longitude and latitude come out of it are covered in Positions.

How fast a cusp moves: speeds()

A commercial ephemeris publishes cusp speeds and cites no formula for them beyond "daily motions", so speeds() computes them as what they mean: the cusps a fraction of a second of ARMC either side, differenced, times the rotation of the Earth.

$speeds = $houses->speeds();

printf("ascendant speed  %.3f deg/day\n", $speeds->ascendant);
printf("midheaven speed  %.3f deg/day\n", $speeds->midheaven);
printf("cusp 11 speed    %.3f deg/day\n", $speeds->cusps[11]);
printf("ARMC speed       %.3f deg/day\n", $speeds->armc);
ascendant speed  341.830 deg/day
midheaven speed  379.770 deg/day
cusp 11 speed    447.958 deg/day
ARMC speed       360.986 deg/day

That number is what turns "a badly noted birth time" into something concrete: an ascendant running at 283 degrees a day, Madrid's slowest, moves a degree in five minutes of clock time; one running at 624, Madrid's fastest, moves a degree in two and a half.

Differencing the cusps also turns out to be a sharper check of the cusp formulas than comparing cusp values is, and that is the reason the method earns its place independently of whatever it is checked against: two formulations can agree at every sampled point and still be different functions, and the derivative is what tells them apart. That is how a published Porphyry speed formula was caught hanging its correction on the wrong angle (its own cusp eleven is the midheaven plus a third of the quadrant, so its derivative has to move with the midheaven, and the published one moves with the ascendant instead): it agreed with its own cusps at every sampled point and disagreed by up to 2,662 degrees a day in its derivative.

The Gauquelin sectors

Placidus in ninths instead of thirds, numbered from the ascendant in the direction of diurnal motion:

$sectors = $houses->gauquelinSectors();

printf("sector 1  %.4f (= ascendant)\n", $sectors[1]);
printf("sector 10 %.4f (= midheaven)\n", $sectors[10]);
printf("sector 5  %.4f\n", $sectors[5]);
sector 1  85.9941 (= ascendant)
sector 10 332.0406 (= midheaven)
sector 5  32.1104

gauquelinSectorByRiseAndSet() gives a different definition rather than another way into the same one: it finds where a body itself crosses the horizon and asks what fraction of that arc, diurnal or nocturnal, has gone by, instead of dividing the semiarc of the body's zodiacal degree. The two readings depart from each other by up to half a sector, about twenty minutes of the Earth's turn, which is the whole reason the second one is worth having.

The four angles, and three more

printf("ascendant        %.4f\n", $houses->ascendant);
printf("descendant       %.4f\n", $houses->descendant());
printf("midheaven        %.4f\n", $houses->midheaven);
printf("imum coeli       %.4f\n", $houses->imumCoeli());
printf("vertex           %.4f\n", $houses->vertex);
printf("east point       %.4f\n", $houses->eastPoint);
printf("Koch co-asc      %.4f\n", $houses->kochCoAscendant);
printf("Munkasey co-asc  %.4f\n", $houses->munkaseyCoAscendant);
printf("polar ascendant  %.4f\n", $houses->polarAscendant);
ascendant        85.9941
descendant       265.9941
midheaven        332.0406
imum coeli       152.0406
vertex           225.9792
east point       65.9245
Koch co-asc      50.5255
Munkasey co-asc  94.1622
polar ascendant  230.5255

The descendant and the imum coeli are never computed on their own: they are always the ascendant and the midheaven, mirrored. The vertex is different, and it does not always exist. It is where the prime vertical, the great circle through the zenith, cuts the ecliptic on the western side, and at the equator that circle passes through the celestial poles instead of cutting the ecliptic at a point:

$equator = Houses::calculate(HouseSystem::Placidus, $jdUt, 0.0, -3.7026);

var_dump($equator->vertex);
NULL

Returning a number there would be worse than returning nothing: it is not an awkward edge case worth working around, it is that the definition does not reach the equator at all.

Past the polar circle

Four of the twenty-three systems divide a diurnal arc, and past the polar circle some degrees of the zodiac neither rise nor set, so there is no arc left to divide. Rather than invent one, they throw:

try {
    Houses::calculate(HouseSystem::Placidus, $jdUt, 78.2232, 15.6267);
} catch (\RuntimeException $e) {
    echo $e->getMessage(), "\n";
}

foreach ([HouseSystem::Placidus, HouseSystem::Koch, HouseSystem::Alcabitius, HouseSystem::Topocentric] as $system) {
    echo $system->value, ' fails at the poles: ', $system->failsAtThePoles() ? 'yes' : 'no', "\n";
}
The Placidus system is not defined at latitude 78.2: past the polar circle there are degrees of the zodiac that neither rise nor set, and this system divides diurnal arcs.
placidus fails at the poles: yes
koch fails at the poles: yes
alcabitius fails at the poles: yes
topocentric fails at the poles: yes

The rest keep working, because their definitions never depended on rising and setting in the first place:

foreach ([HouseSystem::Regiomontanus, HouseSystem::Campanus, HouseSystem::WholeSign] as $system) {
    $polarHouses = Houses::calculate($system, $jdUt, 78.2232, 15.6267);
    printf("%-13s ascendant %.3f\n", $system->value, $polarHouses->ascendant);
}
regiomontanus ascendant 151.135
campanus      ascendant 151.135
whole-sign    ascendant 151.135

The trigger is arithmetic, not an observation: the altitude of a point at culmination is 90 - |latitude - declination|, and the midheaven's declination never exceeds the obliquity of the ecliptic, so below 66.5 degrees of latitude the midheaven cannot sink and none of this ever fires.

When it does, the ascendant is always straightened to the one that is rising rather than the one setting, in every system. Six systems go further and turn their whole wheel with it, moving the midheaven to the imum coeli and renumbering the houses clockwise, because their cusps hang from the meridian and need it the right way round to keep dividing the correct arc:

foreach (HouseSystem::cases() as $system) {
    if ($system->turnsWithTheMidheaven()) {
        echo $system->value, "\n";
    }
}
regiomontanus
campanus
topocentric
apc
savard-a
sunshine

The rest, Porphyry among them, divide the arc running from the midheaven to the ascendant directly, and that arc is already the right way round once the ascendant alone has been straightened; turning it a second time would undo the fix rather than complete it.

The sidereal copy

sidereal() returns the same houses with every longitude minus an ayanamsa, and the geometry that places a body with latitude stays written in tropical terms underneath: a call to housePosition() on a sidereal Houses converts the longitude it is given back to tropical before asking, so a planet never changes house by changing zodiac.

use Astronomy\Ayanamsa;

$ayanamsa = Ayanamsa::Lahiri->value($jdTT);
$sidereal = $houses->sidereal($ayanamsa);

$saturnTropical = 183.5071189058;
$saturnSidereal = fmod($saturnTropical - $ayanamsa + 360, 360);

printf("tropical housePosition %.4f\n", $houses->housePosition($saturnTropical, 2.589119460589));
printf("sidereal housePosition %.4f\n", $sidereal->housePosition($saturnSidereal, 2.589119460589));
tropical housePosition 5.0165
sidereal housePosition 5.0165

Whole Sign and Equal from Aries are the exception: their cusps are nailed to the boundaries of the signs by definition, so shifting them by the ayanamsa would leave them sitting a few degrees into a sign rather than at its start, and they are rebuilt over the sidereal signs instead. The sidereal zodiac itself, its forty-three ayanamsas and what each one anchors to, is covered in The sidereal zodiac.

Two traps worth knowing

At the equator, the systems that divide the sky by completely different routes collapse into the same cusps. The equator, the prime vertical and every diurnal arc line up there, so six systems that reach a cusp by six unrelated constructions have nowhere left to disagree; if one of them drifts even slightly, that one is wrong. It is the strongest check this class has, because it does not compare against anything external:

$systems = [HouseSystem::Placidus, HouseSystem::Koch, HouseSystem::Regiomontanus,
    HouseSystem::Campanus, HouseSystem::Alcabitius, HouseSystem::Topocentric];

foreach ($systems as $system) {
    $houses = Houses::calculate($system, $jdUt, 0.0, -3.7026);
    printf("%-13s cusp 11 = %.6f\n", $system->value, $houses->cusps[11]);
}

$porphyry = Houses::calculate(HouseSystem::Porphyry, $jdUt, 0.0, -3.7026);
printf("%-13s cusp 11 = %.6f (divides the ecliptic arc, not a circle of the sky)\n", 'porphyry', $porphyry->cusps[11]);
placidus      cusp 11 = 4.395501
koch          cusp 11 = 4.395501
regiomontanus cusp 11 = 4.395501
campanus      cusp 11 = 4.395501
alcabitius    cusp 11 = 4.395501
topocentric   cusp 11 = 4.395501
porphyry      cusp 11 = 3.335198 (divides the ecliptic arc, not a circle of the sky)

Porphyry is not among them and that is not a fault in the check: it divides the arc of the ecliptic between the angles rather than a circle of the sky, so there is no reason for it to land on the same number.

deg2rad(rad2deg()) is not the identity, and it is the reason calculate() and fromArmc() do not come back bit for bit identical even though they share one builder. fromArmc() takes the obliquity in degrees, which is what a caller has; the engine works in radians internally, and converting one way and back is not free:

use Astronomy\Time;

$survived = 0;
$total = 0;
$worst = 0.0;

for ($year = 1600; $year <= 2400; $year += 4) {
    $eps = Time::trueObliquity(Time::centuries(Time::civilJulianDay($year, 6, 15)));
    $total++;

    $roundTripped = deg2rad(rad2deg($eps));
    if ($roundTripped === $eps) {
        $survived++;
    } else {
        $worst = max($worst, abs($roundTripped - $eps));
    }
}

printf("%d of %d survive the round trip intact, worst drift %.2e radians\n", $survived, $total, $worst);
130 of 201 survive the round trip intact, worst drift 5.55e-17 radians

Under a third of the obliquities tested come back changed at all, and the worst drift converts to about a hundredth of a nanoarcsecond, ten orders of magnitude below the 0.18 arcseconds the whole engine differs from JPL Horizons by (see Precision). It costs nothing in the sky. It is worth knowing only so that nobody goes looking for a bug where the two doors differ by 4e-10 arcseconds and finds a unit conversion instead.