36d6645e7f
El rectificador deja la aproximación y pasa a la trigonometría exacta, con precisión de segundo — el "microajuste argentino". LA MATEMÁTICA. El rectificador ya NO usa el modelo simplificado (directed_longitude, rotación uniforme de RA + convergencia GR). Ahora usa `eternal_astrology::primary_direction::all_directions` — el método Placidus-mundano: semi-arcos diurnos/nocturnos bajo el polo de cada cuerpo, la trigonometría esférica de la escuela ascensional. No se reimplementó nada: la matemática, ya probada, vive en eternal; el engine sólo aporta la capa de optimización. - error_de_carta: por cada evento, la distancia en años a la dirección primaria que perfecciona más cerca; el error total es la suma. Es la función de coste del microajuste — el valle es la hora real. PRECISIÓN DE SEGUNDO. compute_natal_chart / build_eternal_inputs / natal_cache pasan a trabajar en SEGUNDOS (compose convierte ×60). El rectificador barre en dos pasadas: gruesa minuto a minuto sobre la ventana (el perfil que dibuja la curva), fina segundo a segundo en ±60 s alrededor del mejor minuto. - Rectificacion: mejor_offset_segundos; el perfil va en segundos. - UI: panel y curva muestran «±Xm Ys · error N.NNa». Las barras siguen siendo clicables (scrub a esa hora candidata). Tests verdes (engine 12, render 28). Limitación conocida: all_directions es sólo directo — converso necesita crecer en eternal (upstream). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
134 lines
5.2 KiB
Rust
134 lines
5.2 KiB
Rust
//! Rectificador automático — microajuste por direcciones primarias.
|
|
//!
|
|
//! La rectificación horaria responde a una pregunta vieja: si la hora de
|
|
//! nacimiento registrada es incierta, ¿cuál es la verdadera? El método
|
|
//! ascensional la ataca con direcciones primarias: en la hora correcta,
|
|
//! los eventos reales de la vida del sujeto **coinciden** con la
|
|
//! perfección de una dirección primaria — el arco que la esfera celeste
|
|
//! rota tras el nacimiento hasta que un promisor alcanza la posición
|
|
//! mundana de un significador.
|
|
//!
|
|
//! La trigonometría esférica de esos arcos —el método Placidus-mundano,
|
|
//! semi-arcos diurnos/nocturnos bajo el polo de cada cuerpo— **no se
|
|
//! reimplementa aquí**: la aporta, ya probada, `eternal-astrology`
|
|
//! (`primary_direction::all_directions`). Este módulo es la capa de
|
|
//! OPTIMIZACIÓN: barre las horas candidatas y minimiza el desajuste
|
|
//! entre los eventos conocidos y los arcos teóricos.
|
|
//!
|
|
//! El barrido es de **dos pasadas**: una gruesa, minuto a minuto sobre
|
|
//! toda la ventana (el perfil que la UI dibuja como curva), y una fina,
|
|
//! segundo a segundo alrededor del mejor minuto — de ahí la precisión
|
|
//! de segundo del microajuste.
|
|
|
|
use eternal_astrology::primary_direction::{all_directions, DirectionMethod};
|
|
use eternal_astrology::{DirectionKey as EDirectionKey, NatalChart};
|
|
|
|
use crate::bridge::compute_natal_chart;
|
|
use crate::{Chart, EngineError, EventoConocido, Rectificacion};
|
|
|
|
/// Edad máxima (años) hasta la que se computan direcciones primarias —
|
|
/// cubre con holgura cualquier evento de una vida humana.
|
|
const EDAD_MAX: f64 = 100.0;
|
|
|
|
/// Penalización (años) que se imputa a un evento cuando ninguna
|
|
/// dirección primaria cae cerca. Mayor que cualquier desajuste real
|
|
/// plausible: un candidato sin dirección queda inequívocamente peor.
|
|
const SIN_DIRECCION: f32 = 20.0;
|
|
|
|
/// Error de una carta candidata frente a los eventos conocidos: por
|
|
/// cada evento, la distancia en años a la dirección primaria más
|
|
/// cercana; el error total es la suma. Es la función de coste del
|
|
/// microajuste — el segundo de nacimiento correcto la lleva a un valle.
|
|
fn error_de_carta(
|
|
natal: &NatalChart,
|
|
eventos: &[EventoConocido],
|
|
key: EDirectionKey,
|
|
) -> f32 {
|
|
// Todas las direcciones primarias (Placidus-mundano) y la edad a la
|
|
// que cada una perfecciona. La matemática esférica vive en eternal.
|
|
let dirs = all_directions(natal, DirectionMethod::PlacidusMundane, key, EDAD_MAX);
|
|
let mut total = 0.0_f32;
|
|
for evento in eventos {
|
|
// La dirección cuya perfección cae más cerca de la edad del
|
|
// evento: en la hora correcta, esa distancia tiende a cero.
|
|
let cercania = dirs
|
|
.iter()
|
|
.map(|d| (evento.edad_years - d.age_years).abs() as f32)
|
|
.reduce(f32::min)
|
|
.unwrap_or(SIN_DIRECCION);
|
|
total += cercania.min(SIN_DIRECCION);
|
|
}
|
|
total
|
|
}
|
|
|
|
/// Barre los offsets de `[desde, hasta]` segundos con paso `paso` y
|
|
/// devuelve `(offset_segundos, error)` por candidato.
|
|
fn barrer(
|
|
chart: &Chart,
|
|
eventos: &[EventoConocido],
|
|
key: EDirectionKey,
|
|
desde: i64,
|
|
hasta: i64,
|
|
paso: i64,
|
|
) -> Result<Vec<(i64, f32)>, EngineError> {
|
|
let mut perfil = Vec::new();
|
|
let mut offset = desde;
|
|
while offset <= hasta {
|
|
// Una carta natal por hora candidata (cacheada en el bridge).
|
|
let (natal, _, _) = compute_natal_chart(chart, offset)?;
|
|
perfil.push((offset, error_de_carta(&natal, eventos, key)));
|
|
offset += paso;
|
|
}
|
|
Ok(perfil)
|
|
}
|
|
|
|
/// El candidato de menor error. Ante empate, el offset más cercano a 0
|
|
/// — la hora registrada se respeta si nada la mejora.
|
|
fn mejor_de(perfil: &[(i64, f32)]) -> (i64, f32) {
|
|
perfil
|
|
.iter()
|
|
.copied()
|
|
.min_by(|(oa, pa), (ob, pb)| {
|
|
pa.partial_cmp(pb)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
.then(oa.abs().cmp(&ob.abs()))
|
|
})
|
|
.unwrap_or((0, 0.0))
|
|
}
|
|
|
|
/// Barre las horas candidatas y devuelve la rectificación. Ver
|
|
/// [`crate::rectificar`] para la documentación pública.
|
|
pub(crate) fn rectificar(
|
|
chart: &Chart,
|
|
eventos: &[EventoConocido],
|
|
ventana_min: i64,
|
|
key_str: &str,
|
|
) -> Result<Rectificacion, EngineError> {
|
|
if eventos.is_empty() {
|
|
return Err(EngineError::Eternal(
|
|
"rectificar: sin eventos conocidos que anclar la búsqueda".into(),
|
|
));
|
|
}
|
|
let ventana = ventana_min.max(1);
|
|
let key = match key_str {
|
|
"ptolemy" => EDirectionKey::Ptolemy,
|
|
_ => EDirectionKey::Naibod,
|
|
};
|
|
|
|
// PASADA 1 — gruesa, minuto a minuto sobre toda la ventana. Es el
|
|
// perfil que la UI dibuja como curva: el valle salta a la vista.
|
|
let perfil = barrer(chart, eventos, key, -ventana * 60, ventana * 60, 60)?;
|
|
let (mejor_minuto, _) = mejor_de(&perfil);
|
|
|
|
// PASADA 2 — fina, segundo a segundo en ±60 s alrededor del mejor
|
|
// minuto. Aquí nace la precisión de segundo del microajuste.
|
|
let fino = barrer(chart, eventos, key, mejor_minuto - 60, mejor_minuto + 60, 1)?;
|
|
let (mejor_offset_segundos, mejor_puntaje) = mejor_de(&fino);
|
|
|
|
Ok(Rectificacion {
|
|
mejor_offset_segundos,
|
|
mejor_puntaje,
|
|
perfil,
|
|
})
|
|
}
|