@@ -906,7 +906,8 @@ def test_streamTrack_sample_consistency(_simple_spdf):
906906def test_streamTrack_interface (_simple_spdf ):
907907 numpy .random .seed (2 )
908908 track = _simple_spdf .streamTrack (n = 2000 , ntp = 41 , tail = "leading" )
909- tps = numpy .linspace (- _simple_spdf ._tdisrupt , 0.0 , 5 )
909+ g = track .tp_grid ()
910+ tps = numpy .linspace (g [0 ], g [- 1 ], 5 )
910911 for meth in (
911912 "x" ,
912913 "y" ,
@@ -938,7 +939,8 @@ def test_streamTrack_interface(_simple_spdf):
938939def test_streamTrack_covariance_psd (_simple_spdf ):
939940 numpy .random .seed (3 )
940941 track = _simple_spdf .streamTrack (n = 2000 , ntp = 41 , tail = "leading" )
941- tps = numpy .linspace (- _simple_spdf ._tdisrupt , 0.0 , 7 )
942+ g = track .tp_grid ()
943+ tps = numpy .linspace (g [0 ], g [- 1 ], 7 )
942944 covs = track .cov (tps )
943945 assert covs .shape == (len (tps ), 6 , 6 )
944946 for k in range (len (tps )):
@@ -956,10 +958,13 @@ def test_streamTrack_both_tails(_simple_spdf):
956958 px , py = prog .x (0.0 ), prog .y (0.0 )
957959 assert abs (pair .leading .x (0.0 ) - px ) < 0.1
958960 assert abs (pair .trailing .x (0.0 ) - px ) < 0.1
959- # Deep into the stream, the two arms should diverge
960- tp_deep = - 0.7 * _simple_spdf ._tdisrupt
961- d_lead = (pair .leading .x (tp_deep ) - pair .trailing .x (tp_deep )) ** 2 + (
962- pair .leading .y (tp_deep ) - pair .trailing .y (tp_deep )
961+ # Deep into the stream, the two arms diverge — pick the deepest in-range
962+ # point for each arm (leading arm: largest positive tp; trailing arm:
963+ # most negative tp).
964+ tp_lead = pair .leading .tp_grid ()[- 1 ]
965+ tp_trail = pair .trailing .tp_grid ()[0 ]
966+ d_lead = (pair .leading .x (tp_lead ) - pair .trailing .x (tp_trail )) ** 2 + (
967+ pair .leading .y (tp_lead ) - pair .trailing .y (tp_trail )
963968 ) ** 2
964969 assert d_lead > 0.01 , "Leading and trailing arms do not diverge at large |tp|"
965970
@@ -971,7 +976,10 @@ def test_streamTrack_iteration_changes_track(_simple_spdf):
971976 tr0 = _simple_spdf .streamTrack (n = 2000 , ntp = 41 , niter = 0 , tail = "leading" )
972977 numpy .random .seed (5 )
973978 tr1 = _simple_spdf .streamTrack (n = 2000 , ntp = 41 , niter = 1 , tail = "leading" )
974- tps = numpy .linspace (- _simple_spdf ._tdisrupt , 0.0 , 101 )
979+ # Compare on the in-range grid common to both tracks.
980+ lo = max (tr0 .tp_grid ()[0 ], tr1 .tp_grid ()[0 ])
981+ hi = min (tr0 .tp_grid ()[- 1 ], tr1 .tp_grid ()[- 1 ])
982+ tps = numpy .linspace (lo , hi , 101 )
975983 # Track-to-track difference should be small compared to the stream size
976984 ampl = numpy .ptp (tr0 .x (tps ))
977985 dmax = numpy .max (numpy .abs (tr0 .x (tps ) - tr1 .x (tps )))
@@ -994,9 +1002,16 @@ def test_streamTrack_chen24_works():
9941002 )
9951003 numpy .random .seed (6 )
9961004 track = spdf .streamTrack (n = 1500 , ntp = 41 , tail = "both" )
997- for tp in [- spdf ._tdisrupt / 2 , 0.0 ]:
998- assert numpy .isfinite (track .leading .x (tp ))
999- assert numpy .isfinite (track .trailing .x (tp ))
1005+ # Sample at midpoint of each arm's tp grid (guaranteed in-range).
1006+ tp_lead = track .leading .tp_grid ()[len (track .leading .tp_grid ()) // 2 ]
1007+ tp_trail = track .trailing .tp_grid ()[len (track .trailing .tp_grid ()) // 2 ]
1008+ for tp , arm in [
1009+ (tp_lead , track .leading ),
1010+ (tp_trail , track .trailing ),
1011+ (0.0 , track .leading ),
1012+ (0.0 , track .trailing ),
1013+ ]:
1014+ assert numpy .isfinite (arm .x (tp ))
10001015
10011016
10021017def test_streamTrack_with_center ():
@@ -1015,22 +1030,24 @@ def test_streamTrack_with_center():
10151030 )
10161031 numpy .random .seed (7 )
10171032 track = spdf .streamTrack (n = 1500 , ntp = 31 , tail = "leading" )
1018- tps = numpy .linspace (- spdf ._tdisrupt , 0.0 , 5 )
1033+ g = track .tp_grid ()
1034+ tps = numpy .linspace (g [0 ], g [- 1 ], 5 )
10191035 vals = track .x (tps )
10201036 assert numpy .all (numpy .isfinite (vals ))
10211037
10221038
10231039def test_streamTrack_physical_units (_simple_spdf ):
10241040 numpy .random .seed (8 )
10251041 track = _simple_spdf .streamTrack (n = 1500 , ntp = 31 , tail = "leading" )
1026- x0 = track .x (- 10.0 )
1042+ tp = track .tp_grid ()[len (track .tp_grid ()) // 2 ]
1043+ x0 = track .x (tp )
10271044 track .turn_physical_on (ro = 8.0 , vo = 220.0 )
1028- x0_phys = track .x (- 10.0 )
1045+ x0_phys = track .x (tp )
10291046 # physical x should be ~ ro * internal
10301047 val = x0_phys .value if hasattr (x0_phys , "value" ) else x0_phys
10311048 assert abs (val - 8.0 * x0 ) < 1e-6
10321049 track .turn_physical_off ()
1033- assert abs (track .x (- 10.0 ) - x0 ) < 1e-10
1050+ assert abs (track .x (tp ) - x0 ) < 1e-10
10341051
10351052
10361053def test_streamTrack_cov_physical_units (_simple_spdf ):
@@ -1039,10 +1056,11 @@ def test_streamTrack_cov_physical_units(_simple_spdf):
10391056 # by vo^2, cross terms by ro*vo.
10401057 numpy .random .seed (18 )
10411058 track = _simple_spdf .streamTrack (n = 1500 , ntp = 31 , tail = "leading" )
1059+ tp = track .tp_grid ()[len (track .tp_grid ()) // 2 ]
10421060 track .turn_physical_off ()
1043- C_int = track .cov (- 10.0 )
1061+ C_int = track .cov (tp )
10441062 track .turn_physical_on (ro = 8.0 , vo = 220.0 )
1045- C_phys = track .cov (- 10.0 )
1063+ C_phys = track .cov (tp )
10461064 ro , vo = 8.0 , 220.0
10471065 scale = numpy .array ([ro , ro , ro , vo , vo , vo ])
10481066 expected = C_int * numpy .outer (scale , scale )
@@ -1078,7 +1096,7 @@ def test_streamTrack_physical_accessors_all(_simple_spdf):
10781096 numpy .random .seed (11 )
10791097 track = _simple_spdf .streamTrack (n = 1500 , ntp = 31 , tail = "leading" )
10801098 track .turn_physical_on (ro = 8.0 , vo = 220.0 )
1081- tp = - 10.0
1099+ tp = track . tp_grid ()[ len ( track . tp_grid ()) // 2 ]
10821100 for meth in (
10831101 "x" ,
10841102 "y" ,
@@ -1117,11 +1135,12 @@ def test_streamTrack_pair_physical_toggles(_simple_spdf):
11171135 numpy .random .seed (12 )
11181136 pair = _simple_spdf .streamTrack (n = 1500 , ntp = 31 , tail = "both" )
11191137 pair .turn_physical_on (ro = 8.0 , vo = 220.0 )
1120- v = pair .leading .x (- 5.0 )
1138+ tp_lead = pair .leading .tp_grid ()[len (pair .leading .tp_grid ()) // 2 ]
1139+ v = pair .leading .x (tp_lead )
11211140 val = getattr (v , "value" , v )
11221141 assert val > 0.0
11231142 pair .turn_physical_off ()
1124- v2 = pair .leading .x (- 5.0 )
1143+ v2 = pair .leading .x (tp_lead )
11251144 assert not hasattr (v2 , "unit" )
11261145 import matplotlib
11271146
@@ -1149,6 +1168,40 @@ def test_streamTrack_tp_grid(_simple_spdf):
11491168 assert g [- 1 ] > 0.0
11501169
11511170
1171+ def test_streamTrack_out_of_range_returns_nan (_simple_spdf ):
1172+ # Out-of-range tp must return NaN — never silent cubic-spline
1173+ # extrapolation. For an array tp, only the offending entries are NaN.
1174+ numpy .random .seed (20 )
1175+ track = _simple_spdf .streamTrack (n = 800 , ntp = 31 , tail = "leading" )
1176+ g = track .tp_grid ()
1177+ tp_lo , tp_hi = g [0 ], g [- 1 ]
1178+ # Scalar out-of-range: NaN
1179+ assert numpy .isnan (track .x (tp_hi + 5.0 ))
1180+ assert numpy .isnan (track .R (tp_hi + 5.0 ))
1181+ assert numpy .isnan (track .ra (tp_hi + 5.0 ))
1182+ # Negative side (leading arm has tp_lo == 0)
1183+ assert numpy .isnan (track .x (tp_lo - 1.0 ))
1184+ # Scalar in-range: finite
1185+ tp_mid = 0.5 * (tp_lo + tp_hi )
1186+ assert numpy .isfinite (track .x (tp_mid ))
1187+ # Array tp with mixed in/out: NaN only at out-of-range entries
1188+ tps = numpy .array ([tp_lo - 1.0 , tp_mid , tp_hi + 5.0 ])
1189+ xs = numpy .asarray (track .x (tps ))
1190+ assert numpy .isnan (xs [0 ])
1191+ assert numpy .isfinite (xs [1 ])
1192+ assert numpy .isnan (xs [2 ])
1193+ # cov() honors the same convention; out-of-range entries are NaN
1194+ Cs = track .cov (tps )
1195+ assert numpy .all (numpy .isnan (Cs [0 ]))
1196+ assert numpy .all (numpy .isfinite (Cs [1 ]))
1197+ assert numpy .all (numpy .isnan (Cs [2 ]))
1198+ # cov(basis=...) too — NaN entries skip the Jacobian path safely
1199+ Cs_sky = track .cov (tps , basis = "sky" )
1200+ assert numpy .all (numpy .isnan (Cs_sky [0 ]))
1201+ assert numpy .all (numpy .isfinite (Cs_sky [1 ]))
1202+ assert numpy .all (numpy .isnan (Cs_sky [2 ]))
1203+
1204+
11521205def test_streamTrack_order1_no_cov (_simple_spdf ):
11531206 numpy .random .seed (15 )
11541207 track = _simple_spdf .streamTrack (n = 800 , ntp = 31 , tail = "leading" , order = 1 )
@@ -1176,36 +1229,39 @@ def test_streamTrack_particles_reuse_both(_simple_spdf):
11761229def test_streamTrack_scalar_cov (_simple_spdf ):
11771230 numpy .random .seed (17 )
11781231 track = _simple_spdf .streamTrack (n = 800 , ntp = 31 , tail = "leading" )
1179- C = track .cov (- 20.0 )
1232+ tp = track .tp_grid ()[len (track .tp_grid ()) // 2 ]
1233+ C = track .cov (tp )
11801234 assert C .shape == (6 , 6 )
1235+ assert numpy .all (numpy .isfinite (C ))
11811236
11821237
11831238def test_streamTrack_cov_per_call_unit_overrides (_simple_spdf ):
11841239 # cov() honors per-call ro=, vo=, use_physical= (the same way the mean
11851240 # accessors do) so callers don't need to flip the track-wide toggle.
11861241 numpy .random .seed (18 )
11871242 track = _simple_spdf .streamTrack (n = 1500 , ntp = 31 , tail = "leading" )
1243+ tp = track .tp_grid ()[len (track .tp_grid ()) // 2 ]
11881244 track .turn_physical_off ()
1189- C_int = track .cov (- 10.0 )
1245+ C_int = track .cov (tp )
11901246 # ro=, vo= scale the entries even when the track is in internal mode
11911247 ro , vo = 8.0 , 220.0
1192- C_phys_via_kw = track .cov (- 10.0 , ro = ro , vo = vo , use_physical = True )
1248+ C_phys_via_kw = track .cov (tp , ro = ro , vo = vo , use_physical = True )
11931249 scale = numpy .array ([ro , ro , ro , vo , vo , vo ])
11941250 assert numpy .allclose (C_phys_via_kw , C_int * numpy .outer (scale , scale ), rtol = 1e-10 )
11951251 # use_physical=False on a physical-mode track gives back internal cov
11961252 track .turn_physical_on (ro = ro , vo = vo )
1197- C_back_to_int = track .cov (- 10.0 , use_physical = False )
1253+ C_back_to_int = track .cov (tp , use_physical = False )
11981254 assert numpy .allclose (C_back_to_int , C_int , rtol = 1e-10 )
11991255 # quantity=True is explicitly not supported (heterogeneous units)
12001256 with pytest .raises (NotImplementedError ):
1201- track .cov (- 10.0 , quantity = True )
1257+ track .cov (tp , quantity = True )
12021258 # Per-call overrides also have to thread through the analytical
12031259 # Jacobian for non-galcenrect bases — exercise the sky path with
12041260 # explicit ro/vo so the override branches in _cart_mean_at and
12051261 # _analytical_jacobian (where ``ro``/``vo``/``use_physical`` are
12061262 # forwarded to the accessors and used as Xsun) are taken.
12071263 track .turn_physical_off ()
1208- C_sky_with_kw = track .cov (- 10.0 , basis = "sky" , ro = ro , vo = vo , use_physical = True )
1264+ C_sky_with_kw = track .cov (tp , basis = "sky" , ro = ro , vo = vo , use_physical = True )
12091265 assert C_sky_with_kw .shape == (6 , 6 )
12101266 assert numpy .allclose (C_sky_with_kw , C_sky_with_kw .T , atol = 1e-8 )
12111267
@@ -1228,7 +1284,8 @@ def test_streamTrack_degenerate_few_particles(_simple_spdf):
12281284 # exercising the degenerate paths in _bin_offsets and _smooth_series.
12291285 numpy .random .seed (19 )
12301286 track = _simple_spdf .streamTrack (n = 10 , ntp = 51 , tail = "leading" )
1231- assert numpy .isfinite (track .x (- 10.0 ))
1287+ tp = track .tp_grid ()[len (track .tp_grid ()) // 2 ]
1288+ assert numpy .isfinite (track .x (tp ))
12321289
12331290
12341291def test_streamTrack_custom_track_time_range (_simple_spdf ):
@@ -1250,8 +1307,8 @@ def test_streamTrack_smoothing_variants(_simple_spdf):
12501307 tail = "leading" ,
12511308 smoothing = {"x" : 20.0 , "y" : 20.0 },
12521309 )
1253- assert numpy .isfinite (tr_f .x (- 10.0 ))
1254- assert numpy .isfinite (tr_d .x (- 10.0 ))
1310+ assert numpy .isfinite (tr_f .x (tr_f . tp_grid ()[ len ( tr_f . tp_grid ()) // 2 ] ))
1311+ assert numpy .isfinite (tr_d .x (tr_d . tp_grid ()[ len ( tr_d . tp_grid ()) // 2 ] ))
12551312 # Array-like smoothing: reuse smoothing_s from a previous fit
12561313 numpy .random .seed (18 )
12571314 tr_gcv = _simple_spdf .streamTrack (n = 800 , tail = "leading" , order = 2 )
0 commit comments