natus wheel: Semisquare & Sesquiquadrate; Ott orb pair detection; intensity sort
- pyswiss ASPECTS: uncomment Semisquare (45°, 4°) & Sesquiquadrate (135°, 4°) in angle order; update test known-types + max-orbs - ALL_ASPECTS replaces MAJOR_ASPECTS (client-side angle detection now covers all 9 types) - Ott's Orb system: PLANET_ORB per body (Sun/Moon=10, Mer/Ven/Mar=8, Jup/Sat=6, Ura/Nep/Plu=4); allowed orb = avg of two bodies - _angleAspectWith uses per-planet Ott orb as detection threshold - ASPECT_AV values + _sortAspects: intensity = A.V. × (1 − orb/ottOrb); aspects exceeding Ott orb filtered out; ties: separating first → partner importance → aspect importance Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,13 +22,13 @@ SIGN_ELEMENT = {
|
||||
ASPECTS = [
|
||||
('Conjunction', 0, 8.0),
|
||||
('Semisextile', 30, 4.0),
|
||||
('Semisquare', 45, 4.0),
|
||||
('Sextile', 60, 6.0),
|
||||
('Square', 90, 8.0),
|
||||
('Trine', 120, 8.0),
|
||||
('Sesquiquadrate', 135, 4.0),
|
||||
('Quincunx', 150, 5.0),
|
||||
('Opposition', 180, 10.0),
|
||||
# ('Semisquare', 45, 4.0),
|
||||
# ('Sesquiquadrate', 135, 4.0),
|
||||
]
|
||||
|
||||
PLANET_CODES = {
|
||||
|
||||
@@ -233,8 +233,8 @@ class CalculateAspectsTest(SimpleTestCase):
|
||||
|
||||
def test_each_aspect_type_is_a_known_name(self):
|
||||
known = {
|
||||
'Conjunction', 'Semisextile', 'Sextile', 'Square',
|
||||
'Trine', 'Quincunx', 'Opposition',
|
||||
'Conjunction', 'Semisextile', 'Semisquare', 'Sextile', 'Square',
|
||||
'Trine', 'Sesquiquadrate', 'Quincunx', 'Opposition',
|
||||
}
|
||||
for aspect in self.aspects:
|
||||
with self.subTest(aspect=aspect):
|
||||
@@ -307,9 +307,11 @@ class CalculateAspectsTest(SimpleTestCase):
|
||||
max_orbs = {
|
||||
'Conjunction': 8.0,
|
||||
'Semisextile': 4.0,
|
||||
'Semisquare': 4.0,
|
||||
'Sextile': 6.0,
|
||||
'Square': 8.0,
|
||||
'Trine': 8.0,
|
||||
'Sesquiquadrate': 4.0,
|
||||
'Quincunx': 5.0,
|
||||
'Opposition': 10.0,
|
||||
}
|
||||
|
||||
@@ -77,9 +77,11 @@ const NatusWheel = (() => {
|
||||
const ASPECT_SYMBOLS = {
|
||||
Conjunction: '☌',
|
||||
Semisextile: '⚺',
|
||||
Semisquare: '∠',
|
||||
Sextile: '⚹',
|
||||
Square: '□',
|
||||
Trine: '△',
|
||||
Sesquiquadrate: '⊼',
|
||||
Quincunx: '⚻',
|
||||
Opposition: '☍',
|
||||
};
|
||||
@@ -91,9 +93,11 @@ const NatusWheel = (() => {
|
||||
const ASPECT_STYLES = {
|
||||
Conjunction: { dash: 'none', width: 1.2 },
|
||||
Semisextile: { dash: '1 4', width: 0.6 },
|
||||
Semisquare: { dash: '3 4', width: 0.7 },
|
||||
Sextile: { dash: '6 4', width: 0.8 },
|
||||
Square: { dash: '2 4', width: 1.2 },
|
||||
Trine: { dash: '12 4', width: 0.8 },
|
||||
Sesquiquadrate: { dash: '8 4 3 4', width: 0.7 },
|
||||
Quincunx: { dash: '12 4 2 4', width: 0.8 },
|
||||
Opposition: { dash: '10 4 2 4 2 4', width: 1.2 },
|
||||
};
|
||||
@@ -108,16 +112,47 @@ const NatusWheel = (() => {
|
||||
MC: { label: 'Midheaven', sym: 'MC', house: 10 },
|
||||
};
|
||||
|
||||
// Major aspect angles and their exact expected separations
|
||||
const MAJOR_ASPECTS = [
|
||||
// All aspect angles — used for client-side angle (ASC/MC) aspect detection.
|
||||
const ALL_ASPECTS = [
|
||||
{ type: 'Conjunction', angle: 0 },
|
||||
{ type: 'Semisextile', angle: 30 },
|
||||
{ type: 'Semisquare', angle: 45 },
|
||||
{ type: 'Sextile', angle: 60 },
|
||||
{ type: 'Square', angle: 90 },
|
||||
{ type: 'Trine', angle: 120 },
|
||||
{ type: 'Sesquiquadrate', angle: 135 },
|
||||
{ type: 'Quincunx', angle: 150 },
|
||||
{ type: 'Opposition', angle: 180 },
|
||||
];
|
||||
|
||||
const ANGLE_ORB = 10;
|
||||
// Ott's Orb: per-body allowed orb; angle points (ASC/MC) use ANGLE_BODY_ORB.
|
||||
// Allowed orb for a pair = (orbOtt_body1 + orbOtt_body2) / 2.
|
||||
const PLANET_ORB = {
|
||||
Sun: 10, Moon: 10,
|
||||
Mercury: 8, Venus: 8, Mars: 8,
|
||||
Jupiter: 6, Saturn: 6,
|
||||
Uranus: 4, Neptune: 4, Pluto: 4,
|
||||
};
|
||||
const ANGLE_BODY_ORB = 10;
|
||||
|
||||
// Aspect Values for intensity scoring: A.V. × (1 − actual_orb / ott_orb)
|
||||
const ASPECT_AV = {
|
||||
Conjunction: 12, Opposition: 6, Trine: 4, Square: 3, Sextile: 2,
|
||||
Sesquiquadrate: 1.5, Semisquare: 1.5, Quincunx: 1, Semisextile: 1,
|
||||
};
|
||||
|
||||
// Tie-break 2: partner body importance (lower = more important)
|
||||
const BODY_IMPORTANCE = {
|
||||
Sun: 0, Moon: 1, ASC: 2, MC: 3,
|
||||
Mercury: 4, Venus: 5, Mars: 6,
|
||||
Jupiter: 7, Saturn: 8, Uranus: 9, Neptune: 10, Pluto: 11,
|
||||
};
|
||||
|
||||
// Tie-break 3: aspect importance (lower = more important)
|
||||
const ASPECT_IMPORTANCE = {
|
||||
Conjunction: 0, Opposition: 1, Trine: 2, Square: 3, Sextile: 4,
|
||||
Sesquiquadrate: 5, Semisquare: 6, Quincunx: 7, Semisextile: 8,
|
||||
};
|
||||
|
||||
// Cardinal / Fixed / Mutable — parallel index to SIGNS
|
||||
const SIGN_MODALITIES = [
|
||||
@@ -287,6 +322,37 @@ const NatusWheel = (() => {
|
||||
return `<img src="${src}" class="tt-el-vec" width="1em" height="1em" alt="${info.classical}" aria-hidden="true">`;
|
||||
}
|
||||
|
||||
/** Average of two bodies' Ott orbs — the allowed orb for their aspects. */
|
||||
function _ottOrb(body1, body2) {
|
||||
const o1 = PLANET_ORB[body1] ?? ANGLE_BODY_ORB;
|
||||
const o2 = PLANET_ORB[body2] ?? ANGLE_BODY_ORB;
|
||||
return (o1 + o2) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort an aspect list for display: descending intensity = A.V. × (1 − orb/ottOrb).
|
||||
* Ties resolved by: separating before applying → partner importance → aspect importance.
|
||||
*/
|
||||
function _sortAspects(aspects, thisBody) {
|
||||
return aspects.filter(a => a.orb <= _ottOrb(thisBody, a.partner)).sort((a, b) => {
|
||||
const ottA = _ottOrb(thisBody, a.partner);
|
||||
const ottB = _ottOrb(thisBody, b.partner);
|
||||
const intA = (ASPECT_AV[a.type] ?? 1) * (1 - a.orb / ottA);
|
||||
const intB = (ASPECT_AV[b.type] ?? 1) * (1 - b.orb / ottB);
|
||||
if (Math.abs(intA - intB) > 1e-9) return intB - intA;
|
||||
// tie-break 1: separating (applying_planet !== thisBody) before applying
|
||||
const aApplying = a.applying_planet === thisBody ? 1 : 0;
|
||||
const bApplying = b.applying_planet === thisBody ? 1 : 0;
|
||||
if (aApplying !== bApplying) return aApplying - bApplying;
|
||||
// tie-break 2: partner body importance
|
||||
const impA = BODY_IMPORTANCE[a.partner] ?? 99;
|
||||
const impB = BODY_IMPORTANCE[b.partner] ?? 99;
|
||||
if (impA !== impB) return impA - impB;
|
||||
// tie-break 3: aspect importance
|
||||
return (ASPECT_IMPORTANCE[a.type] ?? 99) - (ASPECT_IMPORTANCE[b.type] ?? 99);
|
||||
});
|
||||
}
|
||||
|
||||
/** CSS color string for an aspect line, keyed to the applying planet. */
|
||||
function _aspectColor(applying_planet) {
|
||||
const code = PLANET_ELEMENTS[applying_planet];
|
||||
@@ -446,7 +512,7 @@ const NatusWheel = (() => {
|
||||
const elKey = (sign.element || '').toLowerCase();
|
||||
|
||||
let aspectHtml = '';
|
||||
const myAspects = _aspectIndex[angleName] || [];
|
||||
const myAspects = _sortAspects(_aspectIndex[angleName] || [], angleName);
|
||||
if (myAspects.length) {
|
||||
aspectHtml = '<small class="tt-aspects">';
|
||||
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
|
||||
@@ -564,7 +630,7 @@ const NatusWheel = (() => {
|
||||
|
||||
// Aspect list — always shown in small font; lines on SVG toggled separately.
|
||||
let aspectHtml = '';
|
||||
const myAspects = _aspectIndex[item.name] || [];
|
||||
const myAspects = _sortAspects(_aspectIndex[item.name] || [], item.name);
|
||||
if (myAspects.length) {
|
||||
aspectHtml = '<small class="tt-aspects">';
|
||||
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
|
||||
@@ -1230,13 +1296,14 @@ const NatusWheel = (() => {
|
||||
* Build the aspect index (planet → aspects list) and create the persistent
|
||||
* nw-aspects group. Lines are drawn per-planet on click, not here.
|
||||
*/
|
||||
/** Compute aspect type + orb between an angle (deg) and a planet (deg), or null. */
|
||||
function _angleAspectWith(angleDeg, planetDeg) {
|
||||
/** Compute aspect type + orb between an angle (ASC/MC) and a planet, or null. */
|
||||
function _angleAspectWith(angleDeg, planetDeg, planetName) {
|
||||
let diff = Math.abs(planetDeg - angleDeg) % 360;
|
||||
if (diff > 180) diff = 360 - diff;
|
||||
for (const { type, angle } of MAJOR_ASPECTS) {
|
||||
const maxOrb = _ottOrb(planetName, 'ASC');
|
||||
for (const { type, angle } of ALL_ASPECTS) {
|
||||
const orb = Math.abs(diff - angle);
|
||||
if (orb <= ANGLE_ORB) return { type, orb: +orb.toFixed(1) };
|
||||
if (orb <= maxOrb) return { type, orb: +orb.toFixed(1) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1257,7 +1324,7 @@ const NatusWheel = (() => {
|
||||
const angleDeg = angleName === 'ASC' ? data.houses.asc : data.houses.mc;
|
||||
_aspectIndex[angleName] = [];
|
||||
Object.entries(data.planets).forEach(([pname, pdata]) => {
|
||||
const asp = _angleAspectWith(angleDeg, pdata.degree);
|
||||
const asp = _angleAspectWith(angleDeg, pdata.degree, pname);
|
||||
if (!asp) return;
|
||||
const shared = { type: asp.type, orb: asp.orb, applying_planet: pname };
|
||||
_aspectIndex[angleName].push({ partner: pname, ...shared });
|
||||
|
||||
Reference in New Issue
Block a user