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>
This commit is contained in:
@@ -186,7 +186,7 @@ fn aspect_kind_id(k: EAspectKind) -> &'static str {
|
||||
/// (transits, sinastría) sin re-traducir.
|
||||
fn build_eternal_inputs(
|
||||
chart: &Chart,
|
||||
offset_minutes: i64,
|
||||
offset_seconds: i64,
|
||||
) -> Result<(BirthData, ChartConfig, Observer), EngineError> {
|
||||
chart.validate()?;
|
||||
let bd = &chart.birth_data;
|
||||
@@ -209,10 +209,12 @@ fn build_eternal_inputs(
|
||||
)
|
||||
.map_err(|e| EngineError::Eternal(format!("Instant::from_civil_local: {:?}", e)))?;
|
||||
|
||||
let instant = if offset_minutes == 0 {
|
||||
// Microajuste temporal en SEGUNDOS — el rectificador automático
|
||||
// barre la hora candidata con resolución de segundo.
|
||||
let instant = if offset_seconds == 0 {
|
||||
base_instant
|
||||
} else {
|
||||
let shifted_utc = base_instant.utc().add_seconds((offset_minutes as f64) * 60.0);
|
||||
let shifted_utc = base_instant.utc().add_seconds(offset_seconds as f64);
|
||||
ESInstant::from_utc(shifted_utc)
|
||||
};
|
||||
|
||||
@@ -241,10 +243,10 @@ fn build_eternal_inputs(
|
||||
/// automáticamente la entrada.
|
||||
pub(crate) fn compute_natal_chart(
|
||||
chart: &Chart,
|
||||
offset_minutes: i64,
|
||||
offset_seconds: i64,
|
||||
) -> Result<(Arc<NatalChart>, ChartConfig, Observer), EngineError> {
|
||||
let (birth_e, config_e, observer) = build_eternal_inputs(chart, offset_minutes)?;
|
||||
let key = crate::natal_cache::key_for(&chart.birth_data, &chart.config, offset_minutes);
|
||||
let (birth_e, config_e, observer) = build_eternal_inputs(chart, offset_seconds)?;
|
||||
let key = crate::natal_cache::key_for(&chart.birth_data, &chart.config, offset_seconds);
|
||||
if let Some(cached) = crate::natal_cache::get(key) {
|
||||
return Ok((cached, config_e, observer));
|
||||
}
|
||||
@@ -265,7 +267,9 @@ pub fn compose(
|
||||
natal_options: &crate::NatalOptions,
|
||||
) -> Result<RenderModel, EngineError> {
|
||||
let t0 = Instant::now();
|
||||
let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes)?;
|
||||
// `compute_natal_chart` trabaja en segundos; `compose` recibe el
|
||||
// offset en minutos (el scrub del jog-dial, la API pública).
|
||||
let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes * 60)?;
|
||||
let orb_table = build_orb_table(natal_options.orb_multiplier);
|
||||
let all_aspects = find_aspects(&natal, &orb_table);
|
||||
let aspects: Vec<Aspect> = all_aspects
|
||||
|
||||
@@ -213,12 +213,12 @@ impl Default for NatalOptions {
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Rectificador automático (Sistema GR)
|
||||
// Rectificador automático (direcciones primarias)
|
||||
// =====================================================================
|
||||
|
||||
/// Un evento conocido de la vida del sujeto — el ancla de la
|
||||
/// rectificación. La hora de nacimiento verdadera es la que hace caer
|
||||
/// los eventos reales sobre convergencias GR cerradas.
|
||||
/// los eventos reales sobre la perfección de una dirección primaria.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EventoConocido {
|
||||
/// Edad del sujeto, en años, cuando ocurrió el evento.
|
||||
@@ -228,23 +228,25 @@ pub struct EventoConocido {
|
||||
/// Resultado de un barrido de rectificación (ver [`rectificar`]).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rectificacion {
|
||||
/// Desplazamiento, en minutos, sobre la hora registrada, que mejor
|
||||
/// explica los eventos. `0` = la hora registrada ya es la mejor.
|
||||
pub mejor_offset_minutos: i64,
|
||||
/// Puntaje del mejor candidato: la suma de orbes de convergencia GR
|
||||
/// sobre todos los eventos. Menor = mejor; es la «tensión» residual.
|
||||
/// Desplazamiento, en **segundos**, sobre la hora registrada, que
|
||||
/// mejor explica los eventos. `0` = la hora registrada ya es la
|
||||
/// mejor. La precisión de segundo viene de la pasada fina.
|
||||
pub mejor_offset_segundos: i64,
|
||||
/// Error del mejor candidato: la suma, en años, del desajuste entre
|
||||
/// cada evento y su dirección primaria más cercana. Menor = mejor.
|
||||
pub mejor_puntaje: f32,
|
||||
/// El barrido completo: `(offset_minutos, puntaje)` por candidato,
|
||||
/// ordenado por offset ascendente. La UI lo dibuja como una curva —
|
||||
/// su valle marca la hora rectificada.
|
||||
/// El barrido grueso: `(offset_segundos, error)` por candidato a
|
||||
/// resolución de minuto, ordenado por offset. La UI lo dibuja como
|
||||
/// curva — su valle marca la hora rectificada.
|
||||
pub perfil: Vec<(i64, f32)>,
|
||||
}
|
||||
|
||||
/// Rectifica la hora de nacimiento por el Sistema GR. Barre las horas
|
||||
/// candidatas en `[-ventana_min, +ventana_min]` minutos sobre la
|
||||
/// registrada, paso a paso (`paso_min`); para cada candidata computa la
|
||||
/// carta y, por cada evento conocido, mide la convergencia GR más
|
||||
/// cerrada a esa edad. La hora del puntaje mínimo es la rectificada.
|
||||
/// Rectifica la hora de nacimiento por direcciones primarias. Barre las
|
||||
/// horas candidatas en `±ventana_min` minutos sobre la registrada —una
|
||||
/// pasada gruesa por minuto y una fina por segundo alrededor del mejor
|
||||
/// minuto— y, por cada candidata, mide el desajuste entre los eventos
|
||||
/// conocidos y los arcos de dirección primaria (método Placidus-mundano
|
||||
/// de `eternal-astrology`). La hora del error mínimo es la rectificada.
|
||||
///
|
||||
/// `key` es la clave arco↔año: `"naibod"` (default) o `"ptolemy"`.
|
||||
/// `Err` si la lista de eventos está vacía — sin anclas no hay búsqueda.
|
||||
@@ -253,10 +255,9 @@ pub fn rectificar(
|
||||
chart: &Chart,
|
||||
eventos: &[EventoConocido],
|
||||
ventana_min: i64,
|
||||
paso_min: i64,
|
||||
key: &str,
|
||||
) -> Result<Rectificacion, EngineError> {
|
||||
rectify::rectificar(chart, eventos, ventana_min, paso_min, key)
|
||||
rectify::rectificar(chart, eventos, ventana_min, key)
|
||||
}
|
||||
|
||||
/// Composición canónica: carta natal + todos los overlays pedidos.
|
||||
@@ -585,8 +586,8 @@ mod tests {
|
||||
assert!(moved, "el armónico debe mover los cuerpos");
|
||||
}
|
||||
|
||||
/// El rectificador barre la ventana entera, devuelve un perfil
|
||||
/// ordenado y elige como mejor el candidato de puntaje mínimo.
|
||||
/// El rectificador barre la ventana en dos pasadas, devuelve un
|
||||
/// perfil grueso ordenado y un offset fino de resolución de segundo.
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
#[test]
|
||||
fn rectificar_barre_la_ventana_y_elige_el_minimo() {
|
||||
@@ -595,21 +596,20 @@ mod tests {
|
||||
EventoConocido { edad_years: 20.0 },
|
||||
EventoConocido { edad_years: 35.0 },
|
||||
];
|
||||
let r = rectificar(&chart, &eventos, 10, 2, "naibod").expect("rectificar");
|
||||
let r = rectificar(&chart, &eventos, 10, "naibod").expect("rectificar");
|
||||
|
||||
// Ventana ±10 min, paso 2 → offsets -10,-8,…,10 = 11 candidatos.
|
||||
assert_eq!(r.perfil.len(), 11);
|
||||
// El perfil va ordenado por offset ascendente.
|
||||
// Pasada gruesa: ±10 min a paso de minuto → 21 candidatos.
|
||||
assert_eq!(r.perfil.len(), 21);
|
||||
// El perfil (en segundos) va ordenado por offset ascendente.
|
||||
for par in r.perfil.windows(2) {
|
||||
assert!(par[0].0 < par[1].0, "perfil desordenado");
|
||||
}
|
||||
// El mejor offset cae dentro de la ventana.
|
||||
assert!(r.mejor_offset_minutos.abs() <= 10);
|
||||
// Y su puntaje es, en efecto, el mínimo del perfil.
|
||||
let minimo = r.perfil.iter().map(|(_, p)| *p).fold(f32::INFINITY, f32::min);
|
||||
assert!((r.mejor_puntaje - minimo).abs() < 1e-4);
|
||||
// El mejor offset cae dentro de la ventana + el margen de la
|
||||
// pasada fina (±60 s).
|
||||
assert!(r.mejor_offset_segundos.abs() <= 10 * 60 + 60);
|
||||
assert!(r.mejor_puntaje >= 0.0);
|
||||
|
||||
// Sin eventos no hay ancla — debe ser un error.
|
||||
assert!(rectificar(&chart, &[], 10, 2, "naibod").is_err());
|
||||
assert!(rectificar(&chart, &[], 10, "naibod").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! Este cache de 8 entradas es suficiente: el usuario rara vez tiene
|
||||
//! más de 2 cartas activas a la vez (natal + partner) y el LRU bota la
|
||||
//! más vieja cuando se llena. La clave es el **contenido** de
|
||||
//! `StoredBirthData + StoredChartConfig + offset_minutes`, así que
|
||||
//! `StoredBirthData + StoredChartConfig + offset_seconds`, así que
|
||||
//! editar una carta invalida automáticamente su entrada.
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -70,7 +70,7 @@ fn cache() -> &'static Mutex<Cache> {
|
||||
pub fn key_for(
|
||||
birth: &StoredBirthData,
|
||||
config: &StoredChartConfig,
|
||||
offset_minutes: i64,
|
||||
offset_seconds: i64,
|
||||
) -> u64 {
|
||||
let mut h = DefaultHasher::new();
|
||||
// Birth data — fecha/hora/lugar.
|
||||
@@ -95,8 +95,8 @@ pub fn key_for(
|
||||
config.include_lilith.hash(&mut h);
|
||||
config.include_main_belt_asteroids.hash(&mut h);
|
||||
config.include_fixed_stars.hash(&mut h);
|
||||
// Offset temporal (rectificación rápida).
|
||||
offset_minutes.hash(&mut h);
|
||||
// Offset temporal en segundos (microajuste de rectificación).
|
||||
offset_seconds.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,101 +1,99 @@
|
||||
//! Rectificador automático — Sistema GR.
|
||||
//! 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 GR
|
||||
//! (García Rosas) la ataca con direcciones primarias: en la hora correcta,
|
||||
//! los eventos reales de la vida del sujeto caen sobre **convergencias** —
|
||||
//! un promisor directo y otro converso que se cruzan sobre un mismo punto
|
||||
//! natal.
|
||||
//! 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.
|
||||
//!
|
||||
//! Este módulo automatiza la búsqueda. Dada una carta, una ventana de horas
|
||||
//! candidatas alrededor de la registrada, y una lista de eventos conocidos
|
||||
//! (cada uno, una edad), **barre** las candidatas: para cada hora, computa
|
||||
//! la carta y mide —con [`convergencia_minima`]— qué tan cerrada es la mejor
|
||||
//! convergencia GR a la edad de cada evento. La hora cuyo puntaje total es
|
||||
//! mínimo es la rectificada.
|
||||
//! 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 cómputo pesado —la carta natal por hora candidata— se delega a
|
||||
//! `bridge::compute_natal_chart`, que cachea; la proyección primaria por
|
||||
//! cuerpo es aritmética barata. La función de puntaje, [`convergencia_minima`],
|
||||
//! es lógica pura y vive en `cosmobiologia-render`.
|
||||
//! 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::{
|
||||
directed_longitude, primary_direction::PrimaryDirection, DirectionKey as EDirectionKey,
|
||||
NatalChart,
|
||||
};
|
||||
use eternal_astrology::primary_direction::{all_directions, DirectionMethod};
|
||||
use eternal_astrology::{DirectionKey as EDirectionKey, NatalChart};
|
||||
|
||||
use crate::bridge::{
|
||||
body_symbol, compute_natal_chart, GR_EVENT_ORB_DEG, GR_HUD_ORB_DEG, GR_MAX_TRIGGERS,
|
||||
};
|
||||
use crate::{
|
||||
compute_gr_triggers, convergencia_minima, Chart, EngineError, EventoConocido, GrDirection,
|
||||
GrTrigger, Rectificacion,
|
||||
};
|
||||
use crate::bridge::compute_natal_chart;
|
||||
use crate::{Chart, EngineError, EventoConocido, Rectificacion};
|
||||
|
||||
/// Puntaje que se imputa a un evento cuando la carta candidata no halla
|
||||
/// convergencia GR alguna a esa edad. Debe superar a cualquier suma real
|
||||
/// de orbes (el HUD acota cada orbe a 2°, así que una convergencia real
|
||||
/// nunca pasa de ~4°): así un candidato sin convergencias queda
|
||||
/// inequívocamente por detrás de uno que sí las tiene.
|
||||
const SIN_CONVERGENCIA: f32 = 8.0;
|
||||
/// 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;
|
||||
|
||||
/// Computa los triggers GR de una carta natal ya calculada, a una edad
|
||||
/// dada. Proyecta cada cuerpo en ambos sentidos (directo y converso) y los
|
||||
/// empareja contra los puntos natales —cuerpos y los cuatro ángulos—.
|
||||
///
|
||||
/// Es la misma matemática que `bridge::build_primary_directions_overlay`,
|
||||
/// pero sin construir el dual-ring de glifos: el rectificador sólo necesita
|
||||
/// los triggers, no la capa visual.
|
||||
fn gr_triggers_de_natal(
|
||||
/// 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,
|
||||
edad_years: f64,
|
||||
eventos: &[EventoConocido],
|
||||
key: EDirectionKey,
|
||||
) -> Vec<GrTrigger> {
|
||||
let eps = natal.obliquity_rad;
|
||||
|
||||
// Proyectar cada cuerpo natal por dirección primaria, en ambos sentidos.
|
||||
let mut directed: Vec<(String, GrDirection, f32)> = Vec::new();
|
||||
for (gr_dir, pd_dir) in [
|
||||
(GrDirection::Direct, PrimaryDirection::Direct),
|
||||
(GrDirection::Converse, PrimaryDirection::Converse),
|
||||
] {
|
||||
for p in &natal.placements {
|
||||
let lon_rad = directed_longitude(
|
||||
p.right_ascension_rad,
|
||||
p.declination_rad,
|
||||
edad_years,
|
||||
pd_dir,
|
||||
key,
|
||||
eps,
|
||||
);
|
||||
let deg = (lon_rad.to_degrees() as f32).rem_euclid(360.0);
|
||||
directed.push((body_symbol(p.body).to_string(), gr_dir, deg));
|
||||
}
|
||||
) -> 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
|
||||
}
|
||||
|
||||
// Puntos natales objetivo: los cuerpos + los cuatro ángulos.
|
||||
let mut natal_targets: Vec<(String, f32)> = natal
|
||||
.placements
|
||||
/// 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()
|
||||
.map(|p| {
|
||||
(
|
||||
body_symbol(p.body).to_string(),
|
||||
p.longitude.longitude_deg() as f32,
|
||||
)
|
||||
.copied()
|
||||
.min_by(|(oa, pa), (ob, pb)| {
|
||||
pa.partial_cmp(pb)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
.then(oa.abs().cmp(&ob.abs()))
|
||||
})
|
||||
.collect();
|
||||
natal_targets.push(("asc".into(), natal.ascendant().longitude_deg() as f32));
|
||||
natal_targets.push(("mc".into(), natal.midheaven().longitude_deg() as f32));
|
||||
natal_targets.push(("desc".into(), natal.descendant().longitude_deg() as f32));
|
||||
natal_targets.push(("ic".into(), natal.imum_coeli().longitude_deg() as f32));
|
||||
|
||||
compute_gr_triggers(
|
||||
&directed,
|
||||
&natal_targets,
|
||||
GR_HUD_ORB_DEG,
|
||||
GR_EVENT_ORB_DEG,
|
||||
GR_MAX_TRIGGERS,
|
||||
)
|
||||
.unwrap_or((0, 0.0))
|
||||
}
|
||||
|
||||
/// Barre las horas candidatas y devuelve la rectificación. Ver
|
||||
@@ -104,7 +102,6 @@ pub(crate) fn rectificar(
|
||||
chart: &Chart,
|
||||
eventos: &[EventoConocido],
|
||||
ventana_min: i64,
|
||||
paso_min: i64,
|
||||
key_str: &str,
|
||||
) -> Result<Rectificacion, EngineError> {
|
||||
if eventos.is_empty() {
|
||||
@@ -112,46 +109,24 @@ pub(crate) fn rectificar(
|
||||
"rectificar: sin eventos conocidos que anclar la búsqueda".into(),
|
||||
));
|
||||
}
|
||||
let ventana = ventana_min.max(0);
|
||||
let paso = paso_min.max(1);
|
||||
let ventana = ventana_min.max(1);
|
||||
let key = match key_str {
|
||||
"ptolemy" => EDirectionKey::Ptolemy,
|
||||
_ => EDirectionKey::Naibod,
|
||||
};
|
||||
|
||||
// Barrer las horas candidatas: cada offset es una hora de nacimiento a
|
||||
// probar, en minutos sobre la registrada.
|
||||
let mut perfil: Vec<(i64, f32)> = Vec::new();
|
||||
let mut offset = -ventana;
|
||||
while offset <= ventana {
|
||||
// Una sola carta natal por hora candidata (cacheada en el bridge);
|
||||
// la proyección por edad de evento es barata sobre ella.
|
||||
let (natal, _, _) = compute_natal_chart(chart, offset)?;
|
||||
let mut puntaje = 0.0_f32;
|
||||
for evento in eventos {
|
||||
let triggers = gr_triggers_de_natal(&natal, evento.edad_years, key);
|
||||
// Menor orbe de convergencia = mejor explicación del evento;
|
||||
// sin convergencia, la penalización.
|
||||
puntaje += convergencia_minima(&triggers).unwrap_or(SIN_CONVERGENCIA);
|
||||
}
|
||||
perfil.push((offset, puntaje));
|
||||
offset += paso;
|
||||
}
|
||||
// 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);
|
||||
|
||||
// El mejor candidato: puntaje mínimo. Ante empate, el offset más
|
||||
// cercano a 0 — la hora registrada se respeta si nada la mejora.
|
||||
let (mejor_offset_minutos, mejor_puntaje) = 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()))
|
||||
})
|
||||
.expect("el perfil tiene al menos un candidato — la ventana incluye el 0");
|
||||
// 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_minutos,
|
||||
mejor_offset_segundos,
|
||||
mejor_puntaje,
|
||||
perfil,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user