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 = [
|
ASPECTS = [
|
||||||
('Conjunction', 0, 8.0),
|
('Conjunction', 0, 8.0),
|
||||||
('Semisextile', 30, 4.0),
|
('Semisextile', 30, 4.0),
|
||||||
|
('Semisquare', 45, 4.0),
|
||||||
('Sextile', 60, 6.0),
|
('Sextile', 60, 6.0),
|
||||||
('Square', 90, 8.0),
|
('Square', 90, 8.0),
|
||||||
('Trine', 120, 8.0),
|
('Trine', 120, 8.0),
|
||||||
|
('Sesquiquadrate', 135, 4.0),
|
||||||
('Quincunx', 150, 5.0),
|
('Quincunx', 150, 5.0),
|
||||||
('Opposition', 180, 10.0),
|
('Opposition', 180, 10.0),
|
||||||
# ('Semisquare', 45, 4.0),
|
|
||||||
# ('Sesquiquadrate', 135, 4.0),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
PLANET_CODES = {
|
PLANET_CODES = {
|
||||||
|
|||||||
@@ -233,8 +233,8 @@ class CalculateAspectsTest(SimpleTestCase):
|
|||||||
|
|
||||||
def test_each_aspect_type_is_a_known_name(self):
|
def test_each_aspect_type_is_a_known_name(self):
|
||||||
known = {
|
known = {
|
||||||
'Conjunction', 'Semisextile', 'Sextile', 'Square',
|
'Conjunction', 'Semisextile', 'Semisquare', 'Sextile', 'Square',
|
||||||
'Trine', 'Quincunx', 'Opposition',
|
'Trine', 'Sesquiquadrate', 'Quincunx', 'Opposition',
|
||||||
}
|
}
|
||||||
for aspect in self.aspects:
|
for aspect in self.aspects:
|
||||||
with self.subTest(aspect=aspect):
|
with self.subTest(aspect=aspect):
|
||||||
@@ -307,9 +307,11 @@ class CalculateAspectsTest(SimpleTestCase):
|
|||||||
max_orbs = {
|
max_orbs = {
|
||||||
'Conjunction': 8.0,
|
'Conjunction': 8.0,
|
||||||
'Semisextile': 4.0,
|
'Semisextile': 4.0,
|
||||||
|
'Semisquare': 4.0,
|
||||||
'Sextile': 6.0,
|
'Sextile': 6.0,
|
||||||
'Square': 8.0,
|
'Square': 8.0,
|
||||||
'Trine': 8.0,
|
'Trine': 8.0,
|
||||||
|
'Sesquiquadrate': 4.0,
|
||||||
'Quincunx': 5.0,
|
'Quincunx': 5.0,
|
||||||
'Opposition': 10.0,
|
'Opposition': 10.0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ const NatusWheel = (() => {
|
|||||||
const ASPECT_SYMBOLS = {
|
const ASPECT_SYMBOLS = {
|
||||||
Conjunction: '☌',
|
Conjunction: '☌',
|
||||||
Semisextile: '⚺',
|
Semisextile: '⚺',
|
||||||
|
Semisquare: '∠',
|
||||||
Sextile: '⚹',
|
Sextile: '⚹',
|
||||||
Square: '□',
|
Square: '□',
|
||||||
Trine: '△',
|
Trine: '△',
|
||||||
|
Sesquiquadrate: '⊼',
|
||||||
Quincunx: '⚻',
|
Quincunx: '⚻',
|
||||||
Opposition: '☍',
|
Opposition: '☍',
|
||||||
};
|
};
|
||||||
@@ -91,9 +93,11 @@ const NatusWheel = (() => {
|
|||||||
const ASPECT_STYLES = {
|
const ASPECT_STYLES = {
|
||||||
Conjunction: { dash: 'none', width: 1.2 },
|
Conjunction: { dash: 'none', width: 1.2 },
|
||||||
Semisextile: { dash: '1 4', width: 0.6 },
|
Semisextile: { dash: '1 4', width: 0.6 },
|
||||||
|
Semisquare: { dash: '3 4', width: 0.7 },
|
||||||
Sextile: { dash: '6 4', width: 0.8 },
|
Sextile: { dash: '6 4', width: 0.8 },
|
||||||
Square: { dash: '2 4', width: 1.2 },
|
Square: { dash: '2 4', width: 1.2 },
|
||||||
Trine: { dash: '12 4', width: 0.8 },
|
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 },
|
Quincunx: { dash: '12 4 2 4', width: 0.8 },
|
||||||
Opposition: { dash: '10 4 2 4 2 4', width: 1.2 },
|
Opposition: { dash: '10 4 2 4 2 4', width: 1.2 },
|
||||||
};
|
};
|
||||||
@@ -108,16 +112,47 @@ const NatusWheel = (() => {
|
|||||||
MC: { label: 'Midheaven', sym: 'MC', house: 10 },
|
MC: { label: 'Midheaven', sym: 'MC', house: 10 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Major aspect angles and their exact expected separations
|
// All aspect angles — used for client-side angle (ASC/MC) aspect detection.
|
||||||
const MAJOR_ASPECTS = [
|
const ALL_ASPECTS = [
|
||||||
{ type: 'Conjunction', angle: 0 },
|
{ type: 'Conjunction', angle: 0 },
|
||||||
|
{ type: 'Semisextile', angle: 30 },
|
||||||
|
{ type: 'Semisquare', angle: 45 },
|
||||||
{ type: 'Sextile', angle: 60 },
|
{ type: 'Sextile', angle: 60 },
|
||||||
{ type: 'Square', angle: 90 },
|
{ type: 'Square', angle: 90 },
|
||||||
{ type: 'Trine', angle: 120 },
|
{ type: 'Trine', angle: 120 },
|
||||||
|
{ type: 'Sesquiquadrate', angle: 135 },
|
||||||
|
{ type: 'Quincunx', angle: 150 },
|
||||||
{ type: 'Opposition', angle: 180 },
|
{ 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
|
// Cardinal / Fixed / Mutable — parallel index to SIGNS
|
||||||
const SIGN_MODALITIES = [
|
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">`;
|
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. */
|
/** CSS color string for an aspect line, keyed to the applying planet. */
|
||||||
function _aspectColor(applying_planet) {
|
function _aspectColor(applying_planet) {
|
||||||
const code = PLANET_ELEMENTS[applying_planet];
|
const code = PLANET_ELEMENTS[applying_planet];
|
||||||
@@ -446,7 +512,7 @@ const NatusWheel = (() => {
|
|||||||
const elKey = (sign.element || '').toLowerCase();
|
const elKey = (sign.element || '').toLowerCase();
|
||||||
|
|
||||||
let aspectHtml = '';
|
let aspectHtml = '';
|
||||||
const myAspects = _aspectIndex[angleName] || [];
|
const myAspects = _sortAspects(_aspectIndex[angleName] || [], angleName);
|
||||||
if (myAspects.length) {
|
if (myAspects.length) {
|
||||||
aspectHtml = '<small class="tt-aspects">';
|
aspectHtml = '<small class="tt-aspects">';
|
||||||
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
|
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.
|
// Aspect list — always shown in small font; lines on SVG toggled separately.
|
||||||
let aspectHtml = '';
|
let aspectHtml = '';
|
||||||
const myAspects = _aspectIndex[item.name] || [];
|
const myAspects = _sortAspects(_aspectIndex[item.name] || [], item.name);
|
||||||
if (myAspects.length) {
|
if (myAspects.length) {
|
||||||
aspectHtml = '<small class="tt-aspects">';
|
aspectHtml = '<small class="tt-aspects">';
|
||||||
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
|
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
|
||||||
@@ -1230,13 +1296,14 @@ const NatusWheel = (() => {
|
|||||||
* Build the aspect index (planet → aspects list) and create the persistent
|
* Build the aspect index (planet → aspects list) and create the persistent
|
||||||
* nw-aspects group. Lines are drawn per-planet on click, not here.
|
* 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. */
|
/** Compute aspect type + orb between an angle (ASC/MC) and a planet, or null. */
|
||||||
function _angleAspectWith(angleDeg, planetDeg) {
|
function _angleAspectWith(angleDeg, planetDeg, planetName) {
|
||||||
let diff = Math.abs(planetDeg - angleDeg) % 360;
|
let diff = Math.abs(planetDeg - angleDeg) % 360;
|
||||||
if (diff > 180) diff = 360 - diff;
|
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);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1324,7 @@ const NatusWheel = (() => {
|
|||||||
const angleDeg = angleName === 'ASC' ? data.houses.asc : data.houses.mc;
|
const angleDeg = angleName === 'ASC' ? data.houses.asc : data.houses.mc;
|
||||||
_aspectIndex[angleName] = [];
|
_aspectIndex[angleName] = [];
|
||||||
Object.entries(data.planets).forEach(([pname, pdata]) => {
|
Object.entries(data.planets).forEach(([pname, pdata]) => {
|
||||||
const asp = _angleAspectWith(angleDeg, pdata.degree);
|
const asp = _angleAspectWith(angleDeg, pdata.degree, pname);
|
||||||
if (!asp) return;
|
if (!asp) return;
|
||||||
const shared = { type: asp.type, orb: asp.orb, applying_planet: pname };
|
const shared = { type: asp.type, orb: asp.orb, applying_planet: pname };
|
||||||
_aspectIndex[angleName].push({ partner: pname, ...shared });
|
_aspectIndex[angleName].push({ partner: pname, ...shared });
|
||||||
|
|||||||
Reference in New Issue
Block a user