Files
brahman/crates/modules/cosmobiologia/cosmobiologia-engine/src/rectify.rs
T
sergio 36d6645e7f feat(cosmobiologia): rectificador per-segundo + direcciones primarias reales
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>
2026-05-22 16:56:06 +00:00

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,
})
}