layout: mercator_layout emits -inf / NaN positions for |lat| >= 90 with no validation (and the two poles are asymmetric)

Aperta
#1,967 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Tranquilla
Stack tecnologico
numpy, pandas, python

Direzione di ricerca

Start with graphistry/layout/mercator.py, especially lines 70–111, and compare its position checks with circle.py:362-363; reproduce the latitude cases in the issue. Check numeric-input validation and the described cuDF fallback as part of the scope. Done means invalid or non-numeric coordinates are handled deliberately, valid positions remain finite, and tests cover the reported cases.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Found by the 2026-08 layouts/viz amplification probe. Repros on master @ 42b4f7f7d, numpy 2.2.6 / pandas 2.3.3 / py3.12.

mercator_layout() applies the Mercator formula with no domain validation and writes whatever falls out — -inf, +23810, or NaN — straight into the bound x/y columns.

graphistry/layout/mercator.py:107-111:

107:        lat_rad = np.radians(lat_deg)
108:        lon_rad = np.radians(lon_deg)
110:        x_vals = R * lon_rad
111:        y_vals = R * np.log(np.tan(np.pi / 4 + lat_rad / 2))

Mercator is undefined at the poles and outside [-90, 90]; there is no clamp, no range check, and no post-assert (contrast circle.py:362-363, which does assert non-NaN positions).

Repro

import pandas as pd, graphistry
e = pd.DataFrame({'s':[0,1,2], 'd':[1,2,0]})

def run(label, lat, lon):
    n = pd.DataFrame({'id':[0,1,2], 'lat':lat, 'lon':lon})
    out = (graphistry.edges(e,'s','d').nodes(n,'id')
             .bind(point_latitude='lat', point_longitude='lon')
             .mercator_layout())
    print(label, out._nodes['y'].tolist())

run('poles      ', [90.0, -90.0, 0.0], [0.,0.,0.])
run('lat=200    ', [200.0, 0., 0.],    [0.,0.,0.])
run('lat=NaN    ', [37.7, float('nan'), 51.5], [-122.4, -74.0, -0.1])

Observed

poles       [23810.77, -inf, -0.0]
lat=200     [nan, -0.0, -0.0]
lat=NaN     [453.71, nan, 671.02]

Expected: a typed decline naming the out-of-domain latitude, or the standard Web-Mercator clamp to ±85.051129°, which is what every mapping library does. Whatever the choice, -inf should never reach a position column.

Note the pole case is also asymmetric: lat=+90 gives a large finite 23810.77 (floating-point tan(π/2) is finite), lat=-90 gives -inf. The two poles are not mirror images, which is a numerical artifact rather than a design decision.

Why inf/NaN in a position column is not benign

  • -inf in x/y destroys zoom-to-fit and any bounds computation over the node frame.
  • Non-finite floats do not survive JSON: #1960 documents that NaN/inf leaks a requests.InvalidJSONError out of the remote path. mercator_layout() is a supported way to produce exactly that from data that looks entirely valid.
  • |lat| > 90 producing NaN with only a numpy RuntimeWarning is a silent-wrong: bad input is accepted and turned into an unpositioned node rather than rejected.

Out-of-range latitude is not exotic — it is what you get from a lat/lon column swap, from projected coordinates mistaken for degrees, or from a sentinel value like 999.

Secondary (bare-crash): non-numeric lat/lon dies deep in numpy

n = pd.DataFrame({'id':[0,1,2], 'lat':['37.7','40.7','51.5'], 'lon':['-122.4','-74.0','-0.1']})
graphistry.edges(e,'s','d').nodes(n,'id').bind(point_latitude='lat', point_longitude='lon').mercator_layout()

Observed: TypeError: loop of ufunc does not support argument 0 of type str which has no callable radians method
Expected: a typed error at the binding check (mercator.py:70-71 already validates that the columns exist; it does not validate that they are numeric).

Secondary (dead fallback): the advertised cupy→numpy fallback cannot work

mercator.py:82-87 warns "cuDF DataFrame detected but cupy is not available. Falling back to NumPy (CPU)", then the fallback at :103-104 does g._nodes[lat_col].values — which on a cuDF frame returns a cupy array, and np.radians on a cupy array raises TypeError: Implicit conversion to a NumPy array is not allowed. The documented fallback path is unreachable-as-intended. INDICATIVE — stated from inspection; not executed, since cupy in this environment cannot load libnvrtc.so.12 and the cuDF-without-cupy combination could not be constructed.

(The polars misrouting at mercator.py:73 is filed separately in the layout engine-parity issue.)

Category

silent-wrong (non-finite and NaN positions accepted and emitted, no warning) + bare-crash on non-numeric input.

Lingua principale
Python
Stelle
2.6k
Fork
229
Merge medio
1g 5h
PR unite (30g)
100

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di graphistry/pygraphistry

Tutte le issue di graphistry/pygraphistry

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.