Masques d'horizon et géométrie du ciel
Jusqu'à la 0,36, chaque test de visibilité terrestre du moteur reposait sur un seul seuil d'élévation, mask_deg : un satellite comptait dès qu'il était à ce nombre ou au-dessus, quelle que soit sa direction. Une rue, un quai, un fjord ou une grue bloque une partie du ciel et laisse le reste dégagé. Une ligne d'horizon (skyline) est une coupure d'élévation qui dépend de l'azimut ; les fonctions de géométrie du ciel transforment un site, un instant, des constellations et une ligne d'horizon en ensemble visible, dilution de précision (DOP), covariance géométrique, ellipse d'erreur horizontale, nuage Monte Carlo et diagramme du ciel (skyplot). La conception est décrite dans docs/design/2026-10-12-geometry-and-skyline-design.md (points 1 et 2).
Preuves : la décision de visibilité masquée (angles de visée géodésiques, masque tabulé, ensemble visible et son DOP) est VALIDATED par rapport à Orekit 12,2 et à gnss_lib_py 1.0.4 dans une comparaison préenregistrée, de même que l'ellipse d'erreur et le cofacteur par système (numpy, préenregistré) ; les réductions de canyon, de secteur et de relief, les préréglages et le reste des fonctions de géométrie du ciel sont MODELLED (Preuves). Une ligne d'horizon retire les satellites en ligne de visée directe, et rien de plus. Les préréglages sont des géométries REPRESENTATIVE, et non des relevés.
La ligne d'horizon#
Une ligne d'horizon est le maximum point par point de quatre descriptions. L'azimut est exprimé en degrés dans le sens horaire à partir du nord vrai, dans le repère local est-nord-haut du site ; les élévations sont dans [0, 90).
| Description | TOML | Ce que c'est |
|---|---|---|
| Table | table = [[az, el], ...] |
Élévations interpolées linéairement en azimut, avec bouclage à 360°. Azimuts strictement croissants dans [0, 360) ; 3600 lignes au plus. |
| Canyon urbain | street_az_deg, width_m, height_left_m, height_right_m, antenna_m, length_m |
Deux murs rectilignes parallèles (ci-dessous). |
| Secteurs | sectors = [{ az_from, az_to, el_deg }] |
Intervalles à bords francs, dans le sens horaire de az_from (inclus) à az_to (exclu), qui peuvent passer par le nord ; 360 au plus. |
| Relief | terrain = [{ az, distance_m, height_m }] |
Points ramenés à une table : élévation atan((height_m − d²/(2 R_eff)) / d), avec la chute due à la courbure pour R_eff = 4/3 × 6371 km (une constante choisie et déclarée). height_m est mesuré au-dessus de l'antenne. |
preset = "<name>" part d'une géométrie nommée ; les champs du canyon remplacent alors, champ par champ, le canyon du préréglage, et table, sectors et terrain s'y ajoutent.
Le mask_deg du récepteur garde son sens : c'est une ligne d'horizon plate. La coupure effective à l'azimut az est max(mask_deg, skyline(az)), et un satellite est visible lorsque son élévation est égale ou supérieure à celle-ci : le même >= que celui qu'utilisait toujours le masque plat. Un satellite au-dessus de mask_deg mais sous la ligne d'horizon est masqué par la ligne d'horizon.
Le canyon urbain#
Une rue court selon l'azimut s, large de w mètres entre les murs, l'antenne se trouvant à a mètres de hauteur sur l'axe central. Selon l'azimut az, avec θ = az − s, le rayon atteint un mur après d(az) = (w/2) / |sin θ| ; un mur de hauteur h coupe donc à
cut(az) = atan( (h − a) / d(az) ) (the right wall for sin θ > 0, the side at s + 90°)
La bande dégagée s'étend le long de la rue. Avec length_m = L (l'antenne au milieu), un rayon sort par une extrémité ouverte, et le mur ne coupe rien, lorsque d(az) · |cos θ| > L/2. Un mur pas plus haut que l'antenne ne coupe rien. antenna_m vaut par défaut 1,5 m (un toit de voiture ou un récepteur portatif, un choix déclaré) ; un nouveau canyon exige width_m.
Préréglages#
Dimensions choisies et déclarées (celles de la note de conception), et non relevés d'un lieu réel. kshana skyline --preset <name> en affiche une avec sa provenance et sa coupure à chaque degré.
| Préréglage | Géométrie | Provenance |
|---|---|---|
city-street |
Canyon : rue à 0°, largeur 20 m, murs de 30 m et 30 m, antenne à 1,5 m. 70,7° en travers de la rue, dégagé le long de celle-ci. | Rapport hauteur/largeur de 1,5, à l'intérieur de la fourchette de 0,5 à 2 des rapports d'aspect des canyons urbains donnée par Oke (1988), Street design and urban canopy layer climate, Energy and Buildings 11, 103–113. |
harbour-approach |
Secteurs : ouvert vers le large sur 090–270 (180°) ; bâtiments du quai 8° sur 315–045 (90°) ; la digue et son feu 3° sur 045–090 et 270–315. | Choix déclarés (par exemple des entrepôts de 40 m à environ 285 m). |
fjord |
Relief : parois à 600 m au-dessus de l'antenne, à 1500 m de part et d'autre d'un chenal nord-sud, un point par degré (à 1500/|sin az| m), chute de courbure appliquée. 21,8° en travers, moins de 1° le long de l'axe. |
De l'ordre de la largeur et du relief des profonds fjords de l'ouest de la Norvège (Holtedahl (1967), Notes on the formation of fjords and fjord-valleys, Geografiska Annaler 49A, 193–203). |
container-terminal |
Secteurs : piles de conteneurs 12° sur la moitié côté terre 000–180 ; flèches de grues 35° sur 245–255 et 25° sur 285–295 ; dégagé vers l'eau ailleurs. | Cinq conteneurs ISO 668 high-cube (14,5 m) à environ 60 m d'une antenne située à 2 m de hauteur ; une flèche à environ 50 m au-dessus du quai, à 70 m et 100 m, de l'ordre d'une grue post-Panamax. Un secteur de grue est opaque. |
Dans un scénario#
Une section [skyline] facultative, par-dessus le seuil propre au type de scénario :
| Type | Ce que la ligne d'horizon change | Par échantillon |
|---|---|---|
orbit (avec user_site) |
La chronologie de visibilité (disponibilité GNSS) et le résumé du DOP. | — |
integrity (avec user_site) |
Les satellites utilisés par RAIM et ARAIM (araim_dual). |
epochs[].masked_by_skyline |
gnss-sim |
Les satellites dont les pseudodistances sont simulées et utilisées par RAIM. | gnss_measurements[].masked_by_skyline |
jamming |
Les satellites que le récepteur peut suivre, avant le brouilleur. | epochs[].masked_by_skyline |
leo-pvt |
L'ensemble en vue de chaque mode au-dessus du masque propre à chaque système, balayage polaire compris (la même ligne d'horizon à chaque site). L'enveloppe du mode Doppler et le passage unique conçu sont des bornes de ce qu'un récepteur doit pouvoir gérer et conservent le masque de chaque système. | joint.epochs[].masked_by_skyline |
leo-pass |
Quelles époques comptent comme au-dessus du masque, la durée passée au-dessus, les satellites de la comparaison GNSS. | satellites[].pass.epochs_masked_by_skyline |
nmea-scenario (NMEA d'entraînement) |
Les satellites présents dans GSV et dans la position calculée. Les azimuts sont des relèvements vrais fixés au sol, et non le cap du navire. | — |
Le site au sol de orbit et de integrity. Ces deux types de scénario modélisent un utilisateur en orbite avec une élévation géocentrique et sans azimut lié à la Terre ; ils n'acceptent donc une ligne d'horizon qu'avec le mode site au sol de la note de conception : user_site = { lat_deg, lon_deg, height_m } à la place de user (exactement l'un des deux). Chaque échantillon est un instant absolu, epoch + t : chaque satellite y est propagé (un jeu d'éléments, des éphémérides diffusées ou un fichier SP3 sont avancés à partir de leur propre époque, un satellite Walker compte t à partir de epoch) et mis en rotation dans le repère terrestre à cet instant, de sorte que le site voit un seul ciel cohérent. epoch est celle du scénario d'orbite ; sans elle (et toujours pour integrity, qui n'en a pas), c'est l'époque la plus récente parmi les satellites qui en portent une, ou J2000 pour les seules constellations Walker. L'élévation et l'azimut sont géodésiques (WGS-84). La séparation horizontale et verticale des niveaux de protection ARAIM du type intégrité et de l'erreur verticale de Stanford utilise le même haut géodésique (la normale à l'ellipsoïde WGS-84, à environ 0,19° de la radiale géocentrique à 45° de latitude) ; le RAIM instantané l'utilisait déjà. Un user_site sans ligne d'horizon est un simple utilisateur au sol ; le chemin user en orbite est inchangé.
kind = "integrity"
mask_deg = 5.0
# ...
[user_site]
lat_deg = 59.45
lon_deg = 24.75
height_m = 15.0
[skyline]
preset = "harbour-approach"
Rétrocompatibilité. Sans section [skyline], un scénario est identique octet pour octet à ce que le même moteur donnait avant l'existence du code de ligne d'horizon : tests/skyline_scenarios.rs vérifie le JSON, le graphique et le résumé de chaque scénario livré qui exécute un de ces types de scénario, ainsi que le texte NMEA de chaque scénario d'entraînement, par rapport aux empreintes SHA-256 de la référence « 0,36 portable » (les empreintes du commit de la version 0.35.0, rebasées lorsque le moteur est passé aux mathématiques portables et conservées dans le test à titre de trace : docs/revisions/STUDIO15-portable-math-rebaseline.md). Une section [skyline] vide ne coupe rien ; le résultat est alors égal à l'exécution sans elle, à l'exception du bloc skyline ajouté et des compteurs masked_by_skyline à zéro.
Ce qui est refusé, avec la raison dans le message : une ligne d'horizon sur un user en orbite ; user et user_site ensemble ; un préréglage inconnu ; un canyon sans width_m ; une table qui n'est pas strictement croissante dans [0, 360) ; une élévation hors de [0, 90) ; un secteur vide ; une distance de relief non positive ; une entrée dépassant les plafonds ; toute clé inconnue.
Le résultat gagne skyline: { source, open_fraction, provenance } lorsque la section est présente : source vaut preset:<name>, preset:<name>+overrides ou custom ; open_fraction est la fraction de l'angle solide de l'hémisphère supérieur au-dessus de max(mask_deg, skyline) (la moyenne de 1 − sin(cut) sur l'azimut : 1 pour un masque nul, 0,5 pour une coupure plate à 30°) ; provenance est la ligne du préréglage ou celle du scénario lui-même. Les unités et les classes de provenance figurent dans le bloc units.
Exemples fournis (kshana example <name>) :
| Exemple | Ce qu'il montre (retirez la section [skyline] pour comparer) |
|---|---|
orbit-urban-canyon-skyline |
GPS + Galileo au fond de city-street : 17 échantillons sur 289 conservent une position calculée, PDOP médian de 70,5 (1,14 à ciel dégagé). |
integrity-harbour-approach |
ARAIM à deux constellations à l'entrée d'un port : 70 époques-satellites masquées, disponibilité inchangée (145 sur 145). |
gnss-sim-container-terminal-skyline |
Pseudodistances et RAIM sur un quai : 44 époques-satellites masquées ; disponibilité RAIM de 0,59 (0,85 à ciel dégagé). |
jamming-harbour-approach-skyline |
Un brouilleur de 2 W dans un port : 83 époques-satellites masquées, suivi inchangé (le brouilleur les avait déjà prises). |
leo-pvt-fjord-skyline |
GPS + Galileo + une couche LEO dans un fjord : PDOP fusionné médian de 1,14 (0,93 à ciel dégagé). |
leo-vertical-autonomous-vehicle-skyline |
Le canyon de leo-vertical-autonomous-vehicle en coupure plate à 35° et sous la forme de city-street tourné selon le cap de la voiture : 8 contre 4 à 5 satellites GPS et Galileo ; erreur moyenne fusionnée de 2,1 m dans les deux cas ; l'alarme à 1,5 m pour 51 % et 59 % des époques. |
Géométrie du ciel#
Cinq fonctions du moteur (src/geometry.rs), identiques sur toutes les surfaces :
| Fonction | Entrée → sortie |
|---|---|
sky_geometry(request) |
par satellite id, system, az_deg, el_deg, cut_deg, visible, masked ; n_visible, n_masked, open_fraction ; dop (GDOP, PDOP, HDOP, VDOP, TDOP, tdop_by_clock) ; states, cofactor_enu (variance unitaire), covariance_enu_m2 ; ellipse_1sigma et ellipse à la confiance demandée ; label, statement, advisory |
sky_monte_carlo(request, seed, n) |
n (au plus 5000, un plafond d'entrée) erreurs des moindres carrés sous des erreurs de distance gaussiennes indépendantes de σ_UERE, tirées d'un flux ChaCha8 initialisé par seed : samples_enu_m, la covariance empirique, l'erreur quadratique moyenne horizontale et 3D à côté de σ_UERE × HDOP et σ_UERE × PDOP, la fraction à l'intérieur de l'ellipse à 95 % |
skyline_presets() |
les préréglages avec libellé, description, provenance, la coupure à chaque degré et open_fraction |
skyline_profile(skyline) |
une section de ligne d'horizon réduite à la table de 360 entrées qu'utilise le moteur, avec source, provenance, open_fraction |
skyplot_svg(request) |
le diagramme du ciel en SVG statique (sans script, sans référence externe, chaque chaîne d'entrée échappée) |
Une requête :
{
"site": {"lat_deg": 59.91, "lon_deg": 10.75, "height_m": 5},
"time_s": 3600,
"systems": ["gps-baseline", "galileo", "beidou-meo", "xona-pulsar"],
"mask_deg": 5,
"skyline": {"preset": "fjord"},
"clock": "per-system",
"sigma_uere_m": 1.5,
"confidence": 0.95
}
systems sont les préréglages de constellation du moteur (gps-baseline, galileo, beidou-meo, glonass, ... de constellation-design ; les préréglages LEO xona-pulsar, iridium-stl, ...) ou une table [[system]] complète de leo-pvt, propagée jusqu'à time_s (képlérienne avec J2 séculaire, les orbites en mode conjoint de leo-pvt). Leurs phases relatives proviennent d'époques de référence différentes ; le ciel est donc une géométrie représentative, et non le ciel de cette date à cet endroit. Les satellites peuvent aussi (ou en plus) être donnés directement : "satellites": [{"id": "G05", "system": "GPS", "az_deg": 120, "el_deg": 35}, ...], ou avec "ecef_m": [x, y, z] et un site ou user_ecef_m. Valeurs par défaut : le masque propre à chaque système (5° GNSS, 10° LEO ; 5° pour les satellites donnés directement), une horloge de récepteur par système ("common" pour une seule horloge partagée), σ_UERE de 1 m (une ligne de budget bifréquence représentative), confiance de 0,95. Plafonds d'entrée : 2000 satellites au plus au-dessus de l'horizon (une table de système qui en demande davantage est refusée avant d'être construite) et 16 horloges de récepteur.
Un ciel daté à partir de jeux d'éléments. "tle" accepte du texte de jeux d'éléments à deux lignes (une ligne de nom avant chaque jeu est facultative ; 4 MiB au plus, un plafond d'entrée), propagé par le SGP4 du moteur (WGS-72, le propagateur qu'utilise orbit-real-tle) jusqu'à "epoch" plus time_s, mis en rotation dans le repère terrestre avec UT1 pris égal à UTC et sans mouvement du pôle, et vu depuis site. "epoch" est en UTC ISO 8601 (2026-06-07T12:00:00Z) et ne date que les jeux d'éléments (les préréglages ne portent aucune date : un epoch sans tle est donc refusé) ; "tle_system" les étiquette pour l'horloge par système (par défaut TLE). Chaque résultat indique de quel ciel il s'agit dans sky_source et sky_statement :
sky_source |
Le ciel |
|---|---|
presets |
les préréglages de constellation du moteur : une géométrie représentative |
tle-at-epoch |
les jeux d'éléments propagés jusqu'à l'époque indiquée : le ciel que ces jeux d'éléments donnent à cette époque, et non une mesure de celui-ci (leur propre exactitude est celle de SGP4 et de l'âge des jeux) |
tle-at-element-epoch |
pas de epoch : les jeux d'éléments propagés jusqu'à l'époque propre du jeu le plus récent plus time_s, un ciel représentatif |
satellites |
satellites donnés directement, tels qu'indiqués |
Un mélange les réunit avec +. epoch_utc est l'instant jusqu'auquel les jeux d'éléments ont été propagés, et tle_skipped nomme la partie du texte qui n'a pas été utilisée, avec la raison : lignes de jeux d'éléments mal formées ou orphelines (avec leurs numéros de ligne), jeux que l'analyseur a refusés, et jeux que SGP4 n'a pas pu propager jusqu'à cet instant (en rentrée atmosphérique, ou au-delà de ses limites) ; 100 entrées au plus en tout, puis une dernière entrée qui compte le reste. Une liste systems vide n'ajoute aucun préréglage. Le diagramme du ciel indique la base dans son pied de page.
Un seul noyau. La matrice de cofacteurs est orbit::cofactor_from_los, l'assemblage de la matrice de conception et l'inverse de Gauss-Jordan derrière orbit::dop. orbit::dop_cov expose la covariance et orbit::dop en est la réduction ; les scalaires VALIDATED sont donc inchangés et toujours figés par tests/dop_reference.rs. Il n'existe pas de seconde implémentation du DOP.
L'ellipse. Demi-axes k σ √λ pour les valeurs propres λ du bloc est-nord, avec k = √(−2 ln(1 − p)) (√5.991 = 2.448 à 95 %, 1 à 39,35 %), et l'azimut ½ atan2(2 Q_EN, Q_NN − Q_EE) du grand axe dans [0, 180) à partir du nord.
| Surface | Points d'entrée |
|---|---|
| CLI | kshana sky <request.json|toml> ou kshana sky --site <lat,lon[,h]> (--systems <a,b> | --tle <file>) [--epoch] [--tle-system] [--time] [--mask] [--skyline-preset] [--uere] [--clock], avec --monte-carlo <n> --seed <s> ou --svg ; kshana skyline --preset <name> | --presets | --profile <file> |
| Rust | kshana::geometry::{sky_geometry, sky_monte_carlo, skyline_presets, skyline_profile, skyplot_svg}, kshana::surface::* aux mêmes noms |
| Python | kshana.sky_geometry, sky_monte_carlo, skyline_presets, skyline_profile, skyplot_svg |
| WASM | sky_geometry(request_json), sky_monte_carlo(request_json, seed, n), skyline_presets(), skyline_profile(skyline_json), skyplot_svg(request_json) |
| MCP | sky_geometry (request, ou skyline_preset) ; pas de Monte Carlo via MCP (des échantillons en masse qu'un agent devrait résumer, non transporter) |
Chaque résultat porte label (MODELLED), le statement (géométrie sous un modèle d'erreur déclaré : erreurs de distance gaussiennes indépendantes, égales, de moyenne nulle ; pas une prédiction de l'erreur réelle d'un récepteur, qui comporte aussi des biais, des trajets multiples et des erreurs hors ligne de visée ; pas une borne d'intégrité) et l'avis indicatif : Logiciel à titre consultatif, et non un équipement de navigation homologué (IEC 61108, IEC 61162) : l'opérateur reste responsable de la navigation du navire.
Preuves#
Cinq lignes du registre (docs/VERIFICATION-MATRIX.md), telles que la note de conception les délimite :
| Ligne | État | Référence indépendante |
|---|---|---|
| Masque de ligne d'horizon dépendant de l'azimut : ensemble visible, azimut et élévation, et DOP sous le masque | VALIDATED | Orekit 12,2 (angles de visée TopocentricFrame, ElevationMask) avec gnss_lib_py 1.0.4 (get_dop et la matrice du DOP), préenregistrés dans xval/orekit-skyline/PREREGISTRATION.md (commit e4b9d912) avant l'exécution de l'un comme de l'autre : 1173 angles de visée sur six sites et quatre époques, six tables, 142 ensembles visibles ; pire élévation 1.3e-13°, azimut 2.3e-13°, masque 1.1e-12° contre 1e-9° ; ensembles visibles identiques, aucun dans la bande limite ; DOP 3.3e-13 en relatif et cofacteur 6.6e-13 de l'échelle de la matrice contre 1e-9. Portée : masques tabulés, angles de visée géodésiques, l'ensemble masqué et son DOP et cofacteur à horloge commune. |
| Réductions de canyon urbain, de secteur et de relief en table | MODELLED | valeurs calculées à la main ; la comparaison ci-dessus lit leurs tables comme entrées et ne les vérifie pas |
| Préréglages de ligne d'horizon | MODELLED | aucune : dimensions choisies et déclarées (vérifiées seulement par rapport à elles-mêmes) |
| Ellipse d'erreur horizontale et cofacteur par système à partir de la covariance de géométrie | VALIDATED | numpy 2.5.4 linalg.inv et linalg.eigh (P2 standard) sur les 142 ensembles visibles versionnés, horloge commune et une horloge par système, préenregistrés dans xval/numpy-ellipse/PREREGISTRATION.md (commit 0bbeeaee) : 270 cas, axes 5.1e-13 en relatif, orientation 5.3e-12°, cofacteur 1.0e-12 de l'échelle de la matrice, contre 1e-9 et 1e-6° |
| Fonctions de géométrie du ciel | MODELLED | la composition : propagation du préréglage vers un ciel représentatif, le nuage Monte Carlo (vérifié par rapport à la géométrie qu'il échantillonne), les surfaces |
L'absence de section [skyline] est vérifiée octet pour octet par rapport aux sorties de la 0.35.0, ce qui démontre la rétrocompatibilité et ne valide rien. gnss_lib_py n'applique qu'un seul masque d'élévation ; c'est donc la référence indépendante pour le DOP ici, et non pour le masque.
Ce qu'une ligne d'horizon ne modélise pas : la diffraction sur le bord d'un mur, les réflexions et les signaux hors ligne de visée qu'un vrai récepteur peut encore suivre (ils sont écartés, et non dégradés), la transparence partielle (une grue à treillis est opaque sur tout son secteur), le relief au-delà des points donnés, et la courbure de la Terre à l'intérieur des formes de canyon et de secteur.