From d709ca2dae9e89ae7dd9417f7bf87601c8cb25bf Mon Sep 17 00:00:00 2001 From: gchure Date: Sun, 21 Jun 2026 15:34:10 -0700 Subject: [PATCH 1/3] Fix known-peak indexing, unmixed-column order, interpeak split, and infeasible bounds Addresses four correctness/robustness defects in `Chromatogram`: - E: `_assign_windows` mapped `known_peaks` to array indices assuming the time axis starts at t=0. Anchor the conversion to the chromatogram's first time value so enforced peaks land correctly on a nonzero-start or cropped trace. - F: `fit_peaks` filled `unmixed_chromatograms` columns in window/detection order, which could disagree with the retention-time-sorted `peak_id` and mislabel per-peak traces in `show`. Build columns directly from the sorted peak table so column i corresponds to peak_id i+1. - G: `_assign_windows` skipped background-segment boundary construction when a gap fell at the first sample (`split_inds[0] == 0`), dropping the final background segment. Run the boundary logic for all multi-segment cases. - H: the location initial guess is rounded to `_time_precision` while its bounds are the window's raw time range; rounding could push the guess outside the bounds, which scipy rejects as "`x0` is infeasible" (issues #8/#14/#15/#18/ #19/#22). Clamp the guess into its bounds, and add a safety net that raises an actionable message for any degenerate/inverted bound instead of scipy's opaque error. Adds regression tests for E, F, and H (each fails on the prior code) plus a smoke test for G, and a fixture derived from the issue #15 chromatogram. Co-Authored-By: Claude Opus 4.8 --- hplc/quant.py | 73 ++- tests/test_chromatogram.py | 107 ++++ .../test_infeasible_bounds_chrom.csv | 550 ++++++++++++++++++ 3 files changed, 715 insertions(+), 15 deletions(-) create mode 100644 tests/test_data/test_infeasible_bounds_chrom.csv diff --git a/hplc/quant.py b/hplc/quant.py index 49d0941..a978aee 100644 --- a/hplc/quant.py +++ b/hplc/quant.py @@ -257,6 +257,12 @@ def _assign_windows( # Determine if peaks should be added. if len(known_peaks) > 0: + # Anchor the index math to the first time value of the (possibly + # cropped) chromatogram. `find_peaks` returns positional indices into + # this array, so converting a user-specified time to an index must be + # measured relative to the start time rather than assuming t = 0. + t0 = self.df[self.time_col].values[0] + # Get the enforced peak positions if type(known_peaks) == dict: _known_peaks = list(known_peaks.keys()) @@ -264,13 +270,13 @@ def _assign_windows( _known_peaks = known_peaks # Find the nearest location in the time array given the user-specified time - enforced_location_inds = ( - np.int_(np.array(_known_peaks) / self._dt) - self._crop_offset + enforced_location_inds = np.int_( + (np.array(_known_peaks) - t0) / self._dt ) # Update the user specified times with the nearest location updated_loc = np.round( - self._dt * (enforced_location_inds + self._crop_offset), + t0 + self._dt * enforced_location_inds, decimals=self._time_precision, ) if type(known_peaks) == dict: @@ -305,7 +311,7 @@ def _assign_windows( self._peak_indices = np.append(self._peak_indices, loc) if self._added_peaks is None: self._added_peaks = [] - self._added_peaks.append((loc + self._crop_offset) * self._dt) + self._added_peaks.append(t0 + loc * self._dt) if type(known_peaks) == dict: _sel_loc = updated_known_peaks[_known_peaks[i]] if "width" in _sel_loc.keys(): @@ -369,9 +375,12 @@ def _assign_windows( "window_type", ] = "interpeak" - # If more than one split ind, set up all ranges. - elif split_inds[0] != 0: - split_inds += 1 + # Otherwise, build the boundaries for every background segment. This + # must run for all multi-segment cases; a gap at the first sample + # (split_inds[0] == 0) was previously skipped, which dropped the final + # background segment and mislabeled interpeak windows. + else: + split_inds = split_inds + 1 split_inds = np.insert(split_inds, 0, 0) split_inds = np.append(split_inds, len(tidx)) @@ -687,11 +696,43 @@ def deconvolve_peaks( raise ValueError( f"Could not adjust bounds for peak at {v['location'][i]} because bound keys do not contain at least one of the following: `location`, `amplitude`, `scale`, `skew`. " ) + # Clamp the location initial guess into its bounds. The guess is + # the peak time rounded to `_time_precision`, while the bounds are + # the window's raw (unrounded) time range; rounding can push the + # guess just outside that range, which scipy rejects as + # "`x0` is infeasible". The true (unrounded) peak time always lies + # inside the window, so clamping only undoes the rounding excursion. + _loc_lo, _loc_hi = _param_bounds["location"] + p0[paridx["location"]] = min( + max(p0[paridx["location"]], _loc_lo), _loc_hi + ) + for _, val in _param_bounds.items(): bounds[0].append(val[0]) bounds[1].append(val[1]) self._param_bounds.append(_param_bounds) + # Safety net: ensure every parameter's bounds are valid and contain + # the initial guess before calling the optimizer, so a degenerate or + # inverted bound surfaces as an actionable message instead of scipy's + # opaque "`x0` is infeasible" / "lower bound ... strictly less ...". + for _j, (_g, _lo, _hi) in enumerate(zip(p0, bounds[0], bounds[1])): + _name = parorder[_j % 4] + _pk = v["location"][_j // 4] + if _lo >= _hi: + raise ValueError( + f"Invalid bounds for '{_name}' of the peak near retention " + f"time {_pk}: lower bound ({_lo:.4g}) is not less than upper " + f"bound ({_hi:.4g}). Try adjusting `param_bounds` or cropping " + f"the chromatogram to exclude edge artifacts." + ) + if not (_lo <= _g <= _hi): + raise ValueError( + f"Initial guess for '{_name}' of the peak near retention " + f"time {_pk} ({_g:.4g}) lies outside its bounds " + f"[{_lo:.4g}, {_hi:.4g}]. Try adjusting `param_bounds`." + ) + # Perform the inference popt, _ = scipy.optimize.curve_fit( self._fit_skewnorms, @@ -869,16 +910,18 @@ def fit_peaks( peak_df["peak_id"] = peak_df["peak_id"].astype(int) self.peaks = peak_df - # Compute the mixture + # Compute the mixture. Build the columns directly from the sorted peak + # table so that column `i` always corresponds to `peak_id == i + 1`. The + # previous implementation filled columns in window/detection order, which + # could disagree with the retention-time-sorted `peak_id` (e.g. when a + # large skew shifts a fitted retention time across a neighbor) and + # mislabel per-peak traces in `show`. time = self.df[self.time_col].values out = np.zeros((len(time), len(peak_df))) - iter = 0 - for _, _v in self._peak_props.items(): - for _, v in _v.items(): - params = [v["amplitude"], v["retention_time"], - v["scale"], v["alpha"]] - out[:, iter] = self._compute_skewnorm(time, *params) - iter += 1 + for i, (_, row) in enumerate(peak_df.iterrows()): + params = [row["amplitude"], row["retention_time"], + row["scale"], row["skew"]] + out[:, i] = self._compute_skewnorm(time, *params) self.unmixed_chromatograms = np.round(out, decimals=precision) if return_peaks: return peak_df diff --git a/tests/test_chromatogram.py b/tests/test_chromatogram.py index 8720bfe..133b6fc 100644 --- a/tests/test_chromatogram.py +++ b/tests/test_chromatogram.py @@ -515,3 +515,110 @@ def test_generic_param_bounding(): assert False except ValueError: assert True + + +def _skewnorm_signal(t, params): + """Build a signal as a sum of amplitude-weighted (skew)normal peaks.""" + import scipy.stats + sig = np.zeros_like(t, dtype=float) + for amp, loc, scale, alpha in params: + sig += amp * scipy.stats.skewnorm(alpha, loc, scale).pdf(t) + return sig + + +def test_known_peaks_nonzero_start_time(): + """ + Regression test for bug E: enforced (`known_peaks`) locations are mapped to + array indices relative to the chromatogram's start time, not by assuming the + time axis begins at t = 0. On a chromatogram whose time starts at t != 0, a + shallow peak that is not auto-detected must still be enforced at the correct + retention time. + """ + t = np.arange(10, 30, 0.01) + # A tall auto-detected peak and a shallow peak that prominence filtering skips. + sig = _skewnorm_signal(t, [(1000, 16.0, 0.3, 0), (80, 22.0, 0.3, 0)]) + df = pd.DataFrame({'time': t, 'signal': sig}) + chrom = hplc.quant.Chromatogram(df) + peaks = chrom.fit_peaks(known_peaks={22.0: {'width': 1}}, + prominence=0.5, correct_baseline=False, verbose=False) + # The enforced peak is placed at ~22 (would land out-of-range on the t0=0 bug). + assert np.any(np.abs(peaks['retention_time'].values - 22.0) < 0.5) + assert np.any(np.abs(peaks['retention_time'].values - 16.0) < 0.5) + + +def test_unmixed_columns_match_peak_id(): + """ + Regression test for bug F: `unmixed_chromatograms[:, peak_id - 1]` must hold + the trace for that `peak_id`. When detection order differs from + retention-time order (here forced by enforcing an early peak in a window that + also contains a later auto-detected peak), the columns were previously left + in detection order and disagreed with the sorted `peak_id`. + """ + t = np.arange(0, 30, 0.01) + # Auto-detected tall peak at 16.0; shallow enforced peak earlier at 15.0. + sig = _skewnorm_signal(t, [(1000, 16.0, 0.2, 0), (90, 15.0, 0.2, 0)]) + df = pd.DataFrame({'time': t, 'signal': sig}) + chrom = hplc.quant.Chromatogram(df) + peaks = chrom.fit_peaks(known_peaks={15.0: {'width': 1}}, prominence=0.5, + correct_baseline=False, buffer=200, verbose=False) + + time = chrom.df['time'].values + for _, row in peaks.iterrows(): + col = chrom.unmixed_chromatograms[:, int(row['peak_id']) - 1] + t_at_max = time[np.argmax(col)] + assert np.abs(t_at_max - row['retention_time']) < 0.5, ( + f"column for peak_id {row['peak_id']} peaks at {t_at_max}, " + f"expected near {row['retention_time']}") + + +def test_infeasible_location_guess_does_not_crash(): + """ + Regression test for bug H: with a small time step the location initial guess + (rounded to `_time_precision`) can fall just outside the window's raw time + range, which scipy rejects as "`x0` is infeasible". The fixture + (`test_infeasible_bounds_chrom.csv`, the data from issue #15) reproduces this; + clamping the guess into its bounds must let fitting proceed past the bounds + check. The data is sliced to an early offending window so the failure (on the + unfixed code) surfaces quickly. + """ + df = pd.read_csv('./tests/test_data/test_infeasible_bounds_chrom.csv') + if 'Unnamed: 0' in df.columns: + df = df.drop(columns=['Unnamed: 0']) + df = df[df['time'] <= 0.7] + chrom = hplc.quant.Chromatogram(df, cols={'time': 'time', 'signal': 'signal'}) + try: + # Baseline correction (the default) shapes the windows that trigger the + # rounding mismatch, matching the original report in issue #15. `max_iter` + # is capped only to bound runtime on this undersampled slice; the + # infeasible-bounds error (on the unfixed code) is raised before any + # optimizer iterations, so the cap does not mask it. + chrom.fit_peaks(verbose=False, max_iter=2000) + except ValueError as e: + # The clamp removes the bounds/feasibility crash entirely. + assert 'infeasible' not in str(e).lower() + assert 'lower bound' not in str(e).lower() + except RuntimeError: + # A genuine optimizer non-convergence on this undersampled data is fine; + # it is not the bounds bug under test. + pass + + +def test_peak_adjacent_to_start_assigns_interpeak(): + """ + Smoke test for bug G: a chromatogram with a peak right at the start and a + long trailing background must run end-to-end and assign interpeak windows + over the background. The G fix corrects background-window splitting when a + gap lands at the first sample (`split_inds[0] == 0`). That exact branch is + hard to trigger deterministically without depending on `scipy.peak_widths` + internals, so this guards the surrounding behavior rather than isolating the + branch. + """ + t = np.arange(0, 30, 0.01) + sig = _skewnorm_signal(t, [(400, 0.4, 0.15, 0), (600, 18.0, 0.4, 0)]) + df = pd.DataFrame({'time': t, 'signal': sig}) + chrom = hplc.quant.Chromatogram(df) + chrom.fit_peaks(correct_baseline=False, verbose=False, max_iter=5000) + scores = chrom.assess_fit(verbose=False) + # The trailing background between the two peaks is captured as interpeak. + assert (chrom.window_df['window_type'] == 'interpeak').any() + assert (scores['window_type'] == 'interpeak').any() diff --git a/tests/test_data/test_infeasible_bounds_chrom.csv b/tests/test_data/test_infeasible_bounds_chrom.csv new file mode 100644 index 0000000..cbe03ee --- /dev/null +++ b/tests/test_data/test_infeasible_bounds_chrom.csv @@ -0,0 +1,550 @@ +,time,signal +0,0.0058333333581686,0 +1,0.0112833334133029,0 +2,0.0167333334684372,0 +3,0.0221833325922489,0 +4,0.0276333335787058,0 +5,0.0330833345651627,0 +6,0.0385333336889744,0 +7,0.0439833328127861,0 +8,0.0494166649878025,0 +9,0.0548666678369045,0 +10,0.0603166669607162,0 +11,0.0657666698098183,0 +12,0.0712166652083397,0 +13,0.0766666680574417,0 +14,0.0821166634559631,0 +15,0.0875666663050652,0 +16,0.0930166691541672,0 +17,0.0984666645526886,0 +18,0.103916667401791,0 +19,0.109366670250893,0 +20,0.114816665649414,0 +21,0.120266668498516,0 +22,0.125716671347618,0 +23,0.131150007247925,0 +24,0.136600002646446,0 +25,0.142049998044968,0 +26,0.147499993443489,0 +27,0.152950003743172,0 +28,0.158399999141693,0 +29,0.163849994540215,0 +30,0.169300004839897,0 +31,0.174750000238419,0 +32,0.18019999563694,0 +33,0.185650005936623,0 +34,0.191100001335144,0 +35,0.196549996733665,0 +36,0.202000007033348,0 +37,0.207433328032494,0 +38,0.212883338332176,0 +39,0.218333333730698,0 +40,0.223783329129219,0 +41,0.229233339428902,0 +42,0.234683334827423,0 +43,0.240133330225945,0 +44,0.245583340525627,0 +45,0.251033335924149,0 +46,0.256483346223831,0 +47,0.261933326721191,0 +48,0.267383337020874,0 +49,0.272833347320557,0 +50,0.278283327817917,0 +51,0.283716678619385,0 +52,0.289166659116745,0 +53,0.294616669416428,0 +54,0.30006667971611,0 +55,0.30551666021347,0 +56,0.310966670513153,0 +57,0.316416680812836,0 +58,0.321866661310196,0 +59,0.327316671609879,0 +60,0.332766652107239,0 +61,0.338216662406921,0 +62,0.343666672706604,0 +63,0.349116653203964,0 +64,0.354566663503647,0 +65,0.360016673803329,0 +66,0.365449994802475,0 +67,0.370900005102158,0 +68,0.376349985599518,0 +69,0.3817999958992,0 +70,0.387250006198883,0 +71,0.392699986696243,0 +72,0.398149996995926,0 +73,0.403600007295609,0 +74,0.409049987792969,0 +75,0.414499998092651,0 +76,0.419950008392334,0 +77,0.425399988889694,0 +78,0.430849999189377,0 +79,0.436300009489059,0 +80,0.441733330488205,0 +81,0.447183340787888,0 +82,0.452633321285248,0 +83,0.45808333158493,0 +84,0.463533341884613,0 +85,0.468983322381973,0 +86,0.474433332681656,0 +87,0.479883342981339,0 +88,0.485333323478699,0 +89,0.490783333778381,0 +90,0.496233344078064,0 +91,0.501683354377747,0 +92,0.507133305072784,0 +93,0.512583315372467,0 +94,0.518016695976257,0 +95,0.523466646671295,0 +96,0.528916656970978,0 +97,0.53436666727066,0 +98,0.539816677570343,0 +99,0.545266687870026,0 +100,0.550716638565063,0 +101,0.556166648864746,0 +102,0.561616659164429,0 +103,0.567066669464111,0 +104,0.572516679763794,0 +105,0.577966690063477,0 +106,0.583416640758514,2132.291015625 +107,0.588866651058197,1785.1533203125 +108,0.594299972057343,1438.015625 +109,0.599749982357025,0 +110,0.605199992656708,1248.1787109375 +111,0.61065000295639,0 +112,0.616100013256073,0 +113,0.621550023555756,0 +114,0.626999974250793,0 +115,0.632449984550476,0 +116,0.637899994850159,0 +117,0.643350005149841,0 +118,0.648800015449524,0 +119,0.654250025749207,0 +120,0.659699976444244,0 +121,0.665149986743927,0 +122,0.67059999704361,0 +123,0.676033318042755,0 +124,0.681483328342438,0 +125,0.68693333864212,0 +126,0.692383348941803,3691.61328125 +127,0.697833359241486,2918.650390625 +128,0.703283309936523,0 +129,0.708733320236206,0 +130,0.714183330535889,0 +131,0.719633340835571,0 +132,0.725083351135254,0 +133,0.730533361434937,0 +134,0.735983312129974,0 +135,0.741433322429657,0 +136,0.74688333272934,0 +137,0.752316653728485,0 +138,0.757766664028168,0 +139,0.76321667432785,0 +140,0.768666684627533,0 +141,0.774116694927216,0 +142,0.779566645622253,0 +143,0.785016655921936,0 +144,0.790466666221619,4167.640625 +145,0.795916676521301,4478.19140625 +146,0.801366686820984,0 +147,0.806816637516022,0 +148,0.812266647815704,0 +149,0.817716658115387,0 +150,0.82316666841507,0 +151,0.828599989414215,0 +152,0.834049999713898,0 +153,0.83950001001358,0 +154,0.844950020313263,0 +155,0.850399971008301,0 +156,0.855849981307983,0 +157,0.861299991607666,0 +158,0.866750001907349,2099.681640625 +159,0.872200012207031,1049.8408203125 +160,0.877650022506714,0 +161,0.883099973201752,0 +162,0.888549983501434,0 +163,0.893999993801117,0 +164,0.8994500041008,0 +165,0.904900014400482,0 +166,0.910333335399628,0 +167,0.91578334569931,0 +168,0.921233355998993,0 +169,0.926683306694031,0 +170,0.932133316993713,0 +171,0.937583327293396,0 +172,0.943033337593079,0 +173,0.948483347892761,3484.33203125 +174,0.953933358192444,3530.8515625 +175,0.959383308887482,0 +176,0.964833319187164,4340.03125 +177,0.970283329486847,0 +178,0.97573333978653,0 +179,0.981183350086212,3383.119140625 +180,0.986616671085358,3483.30078125 +181,0.99206668138504,1806.205078125 +182,0.997516691684723,3296.77734375 +183,1.00296664237976,0 +184,1.00841665267944,1455.607421875 +185,1.01386666297913,676.4619140625 +186,1.01931667327881,338.23095703125 +187,1.02476668357849,0 +188,1.03021669387817,0 +189,1.03566670417786,0 +190,1.04111671447754,0 +191,1.04656672477722,0 +192,1.05201661586761,7268.1015625 +193,1.0574666261673,6607.24609375 +194,1.06289994716644,0 +195,1.06834995746613,0 +196,1.07379996776581,0 +197,1.07924997806549,0 +198,1.08469998836517,0 +199,1.09014999866486,0 +200,1.09560000896454,0 +201,1.10105001926422,0 +202,1.1065000295639,0 +203,1.11195003986359,0 +204,1.11740005016327,0 +205,1.12284994125366,0 +206,1.12829995155334,0 +207,1.13374996185303,0 +208,1.13919997215271,0 +209,1.14463329315186,0 +210,1.15008330345154,0 +211,1.15553331375122,0 +212,1.1609833240509,0 +213,1.16643333435059,0 +214,1.17188334465027,0 +215,1.17733335494995,0 +216,1.18278336524963,0 +217,1.18823337554932,0 +218,1.193683385849,0 +219,1.19913327693939,0 +220,1.20458328723907,0 +221,1.21003329753876,0 +222,1.21548330783844,0 +223,1.22091662883759,0 +224,1.22636663913727,0 +225,1.23181664943695,0 +226,1.23726665973663,0 +227,1.24271667003632,0 +228,1.248166680336,2982.02734375 +229,1.25361669063568,2766.794921875 +230,1.25906670093536,2551.564453125 +231,1.26451671123505,0 +232,1.26996672153473,0 +233,1.27541661262512,0 +234,1.2808666229248,0 +235,1.28631663322449,0 +236,1.29176664352417,0 +237,1.29719996452332,0 +238,1.302649974823,2595.2890625 +239,1.30809998512268,2702.830078125 +240,1.31354999542236,0 +241,1.31900000572205,3248.12109375 +242,1.32445001602173,492.146240234375 +243,1.32990002632141,0 +244,1.33535003662109,3376.71484375 +245,1.34080004692078,0 +246,1.34625005722046,4911.74609375 +247,1.35169994831085,3171.7890625 +248,1.35714995861053,6902.78125 +249,1.36259996891022,1240.0087890625 +250,1.3680499792099,3106.42578125 +251,1.37349998950958,0 +252,1.37893331050873,0 +253,1.38438332080841,0 +254,1.38983333110809,0 +255,1.39528334140778,0 +256,1.40073335170746,0 +257,1.40618336200714,0 +258,1.41163337230682,0 +259,1.41708338260651,0 +260,1.42253339290619,0 +261,1.42798328399658,0 +262,1.43343329429626,4459.421875 +263,1.43888330459595,4681.72265625 +264,1.44433331489563,1369.19140625 +265,1.44978332519531,0 +266,1.45521664619446,0 +267,1.46066665649414,0 +268,1.46611666679382,0 +269,1.47156667709351,0 +270,1.47701668739319,0 +271,1.48246669769287,0 +272,1.48791670799255,0 +273,1.49336671829224,0 +274,1.49881660938263,0 +275,1.50426661968231,1232.095703125 +276,1.50971662998199,616.0478515625 +277,1.51516664028168,0 +278,1.52061665058136,0 +279,1.52606666088104,0 +280,1.53149998188019,0 +281,1.53694999217987,2222.9765625 +282,1.54240000247955,2529.849609375 +283,1.54785001277924,3531.220703125 +284,1.55330002307892,3233.251953125 +285,1.5587500333786,1185.0966796875 +286,1.56420004367828,0 +287,1.56965005397797,0 +288,1.57509994506836,0 +289,1.58054995536804,0 +290,1.58599996566772,0 +291,1.59144997596741,0 +292,1.59689998626709,0 +293,1.60234999656677,0 +294,1.60778331756592,0 +295,1.6132333278656,2749.79296875 +296,1.61868333816528,3260.267578125 +297,1.62413334846497,0 +298,1.62958335876465,0 +299,1.63503336906433,0 +300,1.64048337936401,424.05859375 +301,1.6459333896637,212.029296875 +302,1.65138328075409,0 +303,1.65683329105377,0 +304,1.66228330135345,0 +305,1.66773331165314,0 +306,1.67318332195282,0 +307,1.6786333322525,0 +308,1.68408334255219,0 +309,1.68951666355133,0 +310,1.69496667385101,0 +311,1.7004166841507,0 +312,1.70586669445038,0 +313,1.71131670475006,0 +314,1.71676671504974,2841.361328125 +315,1.72221672534943,3992.634765625 +316,1.72766661643982,5143.91015625 +317,1.7331166267395,8773.546875 +318,1.73856663703918,19500.359375 +319,1.74401664733887,32176.546875 +320,1.74946665763855,38021.1875 +321,1.75491666793823,43865.8125 +322,1.76036667823792,41356.625 +323,1.76579999923706,59173.28125 +324,1.77125000953674,56369.75 +325,1.77670001983643,92232.6875 +326,1.78215003013611,96299.1875 +327,1.78760004043579,57460.90625 +328,1.79305005073547,45898.71875 +329,1.79849994182587,54743.28125 +330,1.80394995212555,24151.3125 +331,1.80939996242523,24156.90625 +332,1.81484997272491,27262.59375 +333,1.8202999830246,6033.3203125 +334,1.82574999332428,5447.69140625 +335,1.83120000362396,27700.421875 +336,1.83665001392365,19331.0625 +337,1.84208333492279,4002.240234375 +338,1.84753334522247,1361.462890625 +339,1.85298335552216,0 +340,1.85843336582184,0 +341,1.86388337612152,953.78173828125 +342,1.8693333864212,476.890869140625 +343,1.8747832775116,0 +344,1.88023328781128,0 +345,1.88568329811096,2028.35546875 +346,1.89113330841064,7406.265625 +347,1.89658331871033,5545.07421875 +348,1.90203332901001,2940.953125 +349,1.90748333930969,4914.9140625 +350,1.91293334960938,2457.45703125 +351,1.91838335990906,0 +352,1.9238166809082,0 +353,1.92926669120789,5062.49609375 +354,1.93471670150757,4367.6328125 +355,1.94016671180725,7792.33984375 +356,1.94561672210693,0 +357,1.95106661319733,0 +358,1.95651662349701,0 +359,1.96196663379669,0 +360,1.96741664409637,0 +361,1.97286665439606,0 +362,1.97831666469574,0 +363,1.98376667499542,0 +364,1.9892166852951,0 +365,1.99466669559479,0 +366,2.00009989738464,0 +367,2.00554990768433,5629.4296875 +368,2.01099991798401,6105.30859375 +369,2.01644992828369,6343.28515625 +370,2.02189993858337,1966.59765625 +371,2.02734994888306,0 +372,2.03279995918274,0 +373,2.03824996948242,0 +374,2.0436999797821,0 +375,2.04914999008179,0 +376,2.05460000038147,0 +377,2.06005001068115,1338.2392578125 +378,2.06550002098083,2676.478515625 +379,2.07095003128052,3072.787109375 +380,2.07638335227966,0 +381,2.08183336257935,0 +382,2.08728337287903,0 +383,2.09273338317871,0 +384,2.09818339347839,0 +385,2.10363340377808,0 +386,2.10908341407776,0 +387,2.11453342437744,0 +388,2.11998343467712,0 +389,2.12543344497681,0 +390,2.13088321685791,0 +391,2.13633322715759,0 +392,2.14178323745728,594.16796875 +393,2.14723324775696,1188.3359375 +394,2.15268325805664,1296.0986328125 +395,2.15811657905579,0 +396,2.16356658935547,6709.48828125 +397,2.16901659965515,6479.9765625 +398,2.17446660995483,2011.7939453125 +399,2.17991662025452,0 +400,2.1853666305542,1626.453125 +401,2.19081664085388,6619.390625 +402,2.19626665115356,5740.9453125 +403,2.20171666145325,4634.0234375 +404,2.20716667175293,0 +405,2.21261668205261,0 +406,2.21806669235229,0 +407,2.22351670265198,1380.96875 +408,2.22896671295166,690.484375 +409,2.23440003395081,0 +410,2.23985004425049,0 +411,2.24530005455017,0 +412,2.25075006484985,1840.2861328125 +413,2.25620007514954,3754.583984375 +414,2.26165008544922,5668.8828125 +415,2.2671000957489,4593.7578125 +416,2.27255010604858,3810.41796875 +417,2.27800011634827,3027.076171875 +418,2.28344988822937,2505.337890625 +419,2.28889989852905,1252.6689453125 +420,2.29434990882874,0 +421,2.29979991912842,0 +422,2.3052499294281,0 +423,2.31068325042725,4536.125 +424,2.31613326072693,3463.353515625 +425,2.32158327102661,2390.58203125 +426,2.32703328132629,3020.59765625 +427,2.33248329162598,3650.61328125 +428,2.33793330192566,2488.849609375 +429,2.34338331222534,3210.94921875 +430,2.34883332252502,1605.474609375 +431,2.35428333282471,0 +432,2.35973334312439,3392.248046875 +433,2.36518335342407,2404.642578125 +434,2.37063336372375,1417.037109375 +435,2.37608337402344,0 +436,2.38153338432312,4396.9296875 +437,2.3869833946228,8518.4140625 +438,2.39241671562195,5936.8203125 +439,2.39786672592163,9217.546875 +440,2.40331673622131,6873.78125 +441,2.408766746521,0 +442,2.41421675682068,15498.4140625 +443,2.41966676712036,9682.4609375 +444,2.42511677742004,4017.404296875 +445,2.43056654930115,17770.5625 +446,2.43601655960083,2210.388671875 +447,2.44146656990051,4443.3046875 +448,2.4469165802002,10525.3515625 +449,2.45236659049988,13824.78125 +450,2.45781660079956,13802.9921875 +451,2.46326661109924,4914.765625 +452,2.46869993209839,10687.21875 +453,2.47414994239807,13515.1796875 +454,2.47959995269775,4893.05078125 +455,2.48504996299744,8612.4140625 +456,2.49049997329712,16676.59375 +457,2.4959499835968,9856.6171875 +458,2.50139999389648,6151.3125 +459,2.50685000419617,8818 +460,2.51230001449585,3545.353515625 +461,2.51775002479553,10624.28125 +462,2.52320003509521,9228.9921875 +463,2.5286500453949,7833.7109375 +464,2.53410005569458,6030.44921875 +465,2.53955006599426,12059.796875 +466,2.54498338699341,14019.03125 +467,2.55043339729309,7837.83203125 +468,2.55588340759277,12257.796875 +469,2.56133341789246,12636.21875 +470,2.56678342819214,13014.640625 +471,2.57223343849182,20997.859375 +472,2.5776834487915,2582.259765625 +473,2.58313322067261,12180.2890625 +474,2.58858323097229,10645.40625 +475,2.59403324127197,7198.80078125 +476,2.59948325157166,3752.193359375 +477,2.60493326187134,2300.70703125 +478,2.61038327217102,3948.28515625 +479,2.6158332824707,8564.2734375 +480,2.62126660346985,6524.32421875 +481,2.62671661376953,4484.375 +482,2.63216662406921,23349.28125 +483,2.6376166343689,19418.578125 +484,2.64306664466858,15297.7578125 +485,2.64851665496826,4377.9375 +486,2.65396666526794,5292.375 +487,2.65941667556763,14047.8125 +488,2.66486668586731,15633.5625 +489,2.67031669616699,18461.8125 +490,2.67576670646667,7410.38671875 +491,2.68121671676636,6094.5625 +492,2.68666672706604,7499.6015625 +493,2.69211673736572,3533.943359375 +494,2.69756674766541,4967.875 +495,2.70300006866455,6401.8046875 +496,2.70845007896423,4559.1640625 +497,2.71390008926392,6017.8984375 +498,2.7193500995636,7476.63671875 +499,2.72480010986328,7268.69140625 +500,2.73024988174438,6216.60546875 +501,2.73569989204407,5164.51953125 +502,2.74114990234375,3175.54296875 +503,2.74659991264343,1186.568359375 +504,2.75204992294312,8393.2109375 +505,2.7574999332428,9177.2890625 +506,2.76294994354248,6693.07421875 +507,2.76839995384216,6306.11328125 +508,2.77384996414185,8373.7734375 +509,2.77928328514099,14563.6640625 +510,2.78473329544067,14531.4921875 +511,2.79018330574036,0 +512,2.79563331604004,9474.8828125 +513,2.80108332633972,10087.96875 +514,2.8065333366394,3343.666015625 +515,2.81198334693909,0 +516,2.81743335723877,0 +517,2.82288336753845,1799.4326171875 +518,2.82833337783813,3598.865234375 +519,2.83378338813782,4424.9375 +520,2.8392333984375,5251.0078125 +521,2.84468340873718,8909.203125 +522,2.85013341903687,9991.171875 +523,2.85556674003601,6329.00390625 +524,2.86101675033569,2666.8359375 +525,2.86646676063538,8342.4921875 +526,2.87191677093506,6318.625 +527,2.87736678123474,10284.796875 +528,2.88281655311584,13361.8515625 +529,2.88826656341553,15126.3203125 +530,2.89371657371521,8066.01953125 +531,2.89916658401489,19851.171875 +532,2.90461659431458,17250.21875 +533,2.91006660461426,2503.203125 +534,2.91551661491394,11386.71875 +535,2.92096662521362,15917.171875 +536,2.92641663551331,18256.671875 +537,2.93186664581299,9098.453125 +538,2.93729996681213,10058.234375 +539,2.94274997711182,10775.9140625 +540,2.9481999874115,10863.5078125 +541,2.95364999771118,2632.38671875 +542,2.95910000801086,13206.90625 +543,2.96455001831055,0 +544,2.97000002861023,25491.875 +545,2.97545003890991,20134.578125 +546,2.98090004920959,5036.0703125 +547,2.98635005950928,3378.482421875 +548,2.99180006980896,3029.13671875 From 9cec949b787cb682b7ce1eab8509417b91517eb5 Mon Sep 17 00:00:00 2001 From: gchure Date: Sun, 21 Jun 2026 19:26:53 -0700 Subject: [PATCH 2/3] Add coverage for the pre-fit bounds safety-net errors The two `raise ValueError` branches in the deconvolve_peaks bounds validation (degenerate `lb >= ub` and guess-outside-bounds) were untested, dropping patch coverage on the PR. Add a test that triggers both deterministically via `param_bounds` (amplitude [0,0] and [2,3]x the peak value), which also exercises the issue #22 degenerate-amplitude-bound case. Co-Authored-By: Claude Opus 4.8 --- tests/test_chromatogram.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_chromatogram.py b/tests/test_chromatogram.py index 133b6fc..880dddf 100644 --- a/tests/test_chromatogram.py +++ b/tests/test_chromatogram.py @@ -622,3 +622,27 @@ def test_peak_adjacent_to_start_assigns_interpeak(): # The trailing background between the two peaks is captured as interpeak. assert (chrom.window_df['window_type'] == 'interpeak').any() assert (scores['window_type'] == 'interpeak').any() + + +def test_degenerate_bounds_raise_clear_error(): + """ + The pre-fit bounds validation (the safety net added for bug H) must surface + degenerate or infeasible parameter bounds as a clear, actionable ValueError + instead of scipy's opaque message. Issue #22 is exactly the degenerate + amplitude-bound case. + """ + t = np.arange(0, 20, 0.01) + sig = _skewnorm_signal(t, [(1000, 10.0, 0.3, 0)]) + df = pd.DataFrame({'time': t, 'signal': sig}) + + # Degenerate amplitude bound [0, 0]: lower bound not strictly less than upper. + chrom = hplc.quant.Chromatogram(df) + with pytest.raises(ValueError, match='Invalid bounds'): + chrom.fit_peaks(param_bounds={'amplitude': [0, 0]}, + correct_baseline=False, verbose=False) + + # Amplitude bound [2x, 3x] the peak value excludes the initial guess (1x). + chrom = hplc.quant.Chromatogram(df) + with pytest.raises(ValueError, match='lies outside its bounds'): + chrom.fit_peaks(param_bounds={'amplitude': [2, 3]}, + correct_baseline=False, verbose=False) From 2324b7964fde942e628865be7db65850b52e06c6 Mon Sep 17 00:00:00 2001 From: gchure Date: Sun, 21 Jun 2026 22:53:16 -0700 Subject: [PATCH 3/3] Replace real-data infeasible-bounds fixture with a synthetic reproduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The infeasible-location-guess crash is purely a function of `dt` versus `_time_precision = |ceil(log10(dt))|`, not of any property of the recorded issue #15 signal. Reproduce it synthetically instead of committing ~550 rows of third-party data of uncertain provenance/licensing: - `dt = 0.011` gives `_time_precision = 1`, so a peak near the end of the record (~2.985) rounds up to 3.0, past its window's raw max — exactly the excursion the clamp fixes. Verified non-vacuous: with the clamp removed this signal raises "Initial guess for 'location' ... lies outside its bounds"; with it, the fit completes and recovers both peaks. The test is now self-documenting (explains why it triggers), asserts the `_time_precision == 1` precondition so it can't silently stop reproducing, and runs in ~2s instead of ~7min. Deletes test_infeasible_bounds_chrom.csv. Co-Authored-By: Claude Opus 4.8 --- tests/test_chromatogram.py | 54 +- .../test_infeasible_bounds_chrom.csv | 550 ------------------ 2 files changed, 30 insertions(+), 574 deletions(-) delete mode 100644 tests/test_data/test_infeasible_bounds_chrom.csv diff --git a/tests/test_chromatogram.py b/tests/test_chromatogram.py index 880dddf..e20c527 100644 --- a/tests/test_chromatogram.py +++ b/tests/test_chromatogram.py @@ -573,34 +573,40 @@ def test_unmixed_columns_match_peak_id(): def test_infeasible_location_guess_does_not_crash(): """ - Regression test for bug H: with a small time step the location initial guess - (rounded to `_time_precision`) can fall just outside the window's raw time - range, which scipy rejects as "`x0` is infeasible". The fixture - (`test_infeasible_bounds_chrom.csv`, the data from issue #15) reproduces this; - clamping the guess into its bounds must let fitting proceed past the bounds - check. The data is sliced to an early offending window so the failure (on the - unfixed code) surfaces quickly. - """ - df = pd.read_csv('./tests/test_data/test_infeasible_bounds_chrom.csv') - if 'Unnamed: 0' in df.columns: - df = df.drop(columns=['Unnamed: 0']) - df = df[df['time'] <= 0.7] - chrom = hplc.quant.Chromatogram(df, cols={'time': 'time', 'signal': 'signal'}) + Regression test for bug H: the location initial guess is the peak time + *rounded* to `_time_precision`, while its bounds are the window's *raw* + (unrounded) time range. When `_time_precision = |ceil(log10(dt))|` is coarser + than the actual sample spacing, rounding can push the guess just past the + window's max, which scipy rejects as "`x0` is infeasible". The clamp must pull + the guess back inside its bounds so fitting proceeds. + + This is purely a function of `dt` vs `_time_precision`, so it is reproduced + synthetically rather than from recorded data: a step of `dt = 0.011` gives + `_time_precision = |ceil(log10(0.011))| = 1`, so a peak near the end of the + record (here ~2.985) rounds *up* to 3.0 — past its window's raw max (~2.98). + On the unfixed code this raises before any optimizer iteration; with the clamp + the fit completes. (Verified: removing the clamp makes this signal raise + "Initial guess for 'location' ... lies outside its bounds".) + """ + t = np.arange(0, 3.0, 0.011) + sig = _skewnorm_signal(t, [(6000, 1.0, 0.03, 0), (6000, 2.985, 0.03, 0)]) + df = pd.DataFrame({'time': t, 'signal': sig}) + assert int(np.abs(np.ceil(np.log10(np.mean(np.diff(t)))))) == 1, ( + 'fixture must keep `_time_precision == 1` for the rounding excursion') + + chrom = hplc.quant.Chromatogram(df) try: - # Baseline correction (the default) shapes the windows that trigger the - # rounding mismatch, matching the original report in issue #15. `max_iter` - # is capped only to bound runtime on this undersampled slice; the - # infeasible-bounds error (on the unfixed code) is raised before any - # optimizer iterations, so the cap does not mask it. - chrom.fit_peaks(verbose=False, max_iter=2000) + peaks = chrom.fit_peaks(correct_baseline=False, verbose=False, + max_iter=5000) except ValueError as e: - # The clamp removes the bounds/feasibility crash entirely. + # The clamp removes the bounds/feasibility crash entirely; if a ValueError + # still surfaces it must not be the infeasible-bounds one under test. assert 'infeasible' not in str(e).lower() + assert 'outside its bounds' not in str(e).lower() assert 'lower bound' not in str(e).lower() - except RuntimeError: - # A genuine optimizer non-convergence on this undersampled data is fine; - # it is not the bounds bug under test. - pass + raise + # The fit proceeds past the bounds check and recovers both peaks. + assert len(peaks) == 2 def test_peak_adjacent_to_start_assigns_interpeak(): diff --git a/tests/test_data/test_infeasible_bounds_chrom.csv b/tests/test_data/test_infeasible_bounds_chrom.csv deleted file mode 100644 index cbe03ee..0000000 --- a/tests/test_data/test_infeasible_bounds_chrom.csv +++ /dev/null @@ -1,550 +0,0 @@ -,time,signal -0,0.0058333333581686,0 -1,0.0112833334133029,0 -2,0.0167333334684372,0 -3,0.0221833325922489,0 -4,0.0276333335787058,0 -5,0.0330833345651627,0 -6,0.0385333336889744,0 -7,0.0439833328127861,0 -8,0.0494166649878025,0 -9,0.0548666678369045,0 -10,0.0603166669607162,0 -11,0.0657666698098183,0 -12,0.0712166652083397,0 -13,0.0766666680574417,0 -14,0.0821166634559631,0 -15,0.0875666663050652,0 -16,0.0930166691541672,0 -17,0.0984666645526886,0 -18,0.103916667401791,0 -19,0.109366670250893,0 -20,0.114816665649414,0 -21,0.120266668498516,0 -22,0.125716671347618,0 -23,0.131150007247925,0 -24,0.136600002646446,0 -25,0.142049998044968,0 -26,0.147499993443489,0 -27,0.152950003743172,0 -28,0.158399999141693,0 -29,0.163849994540215,0 -30,0.169300004839897,0 -31,0.174750000238419,0 -32,0.18019999563694,0 -33,0.185650005936623,0 -34,0.191100001335144,0 -35,0.196549996733665,0 -36,0.202000007033348,0 -37,0.207433328032494,0 -38,0.212883338332176,0 -39,0.218333333730698,0 -40,0.223783329129219,0 -41,0.229233339428902,0 -42,0.234683334827423,0 -43,0.240133330225945,0 -44,0.245583340525627,0 -45,0.251033335924149,0 -46,0.256483346223831,0 -47,0.261933326721191,0 -48,0.267383337020874,0 -49,0.272833347320557,0 -50,0.278283327817917,0 -51,0.283716678619385,0 -52,0.289166659116745,0 -53,0.294616669416428,0 -54,0.30006667971611,0 -55,0.30551666021347,0 -56,0.310966670513153,0 -57,0.316416680812836,0 -58,0.321866661310196,0 -59,0.327316671609879,0 -60,0.332766652107239,0 -61,0.338216662406921,0 -62,0.343666672706604,0 -63,0.349116653203964,0 -64,0.354566663503647,0 -65,0.360016673803329,0 -66,0.365449994802475,0 -67,0.370900005102158,0 -68,0.376349985599518,0 -69,0.3817999958992,0 -70,0.387250006198883,0 -71,0.392699986696243,0 -72,0.398149996995926,0 -73,0.403600007295609,0 -74,0.409049987792969,0 -75,0.414499998092651,0 -76,0.419950008392334,0 -77,0.425399988889694,0 -78,0.430849999189377,0 -79,0.436300009489059,0 -80,0.441733330488205,0 -81,0.447183340787888,0 -82,0.452633321285248,0 -83,0.45808333158493,0 -84,0.463533341884613,0 -85,0.468983322381973,0 -86,0.474433332681656,0 -87,0.479883342981339,0 -88,0.485333323478699,0 -89,0.490783333778381,0 -90,0.496233344078064,0 -91,0.501683354377747,0 -92,0.507133305072784,0 -93,0.512583315372467,0 -94,0.518016695976257,0 -95,0.523466646671295,0 -96,0.528916656970978,0 -97,0.53436666727066,0 -98,0.539816677570343,0 -99,0.545266687870026,0 -100,0.550716638565063,0 -101,0.556166648864746,0 -102,0.561616659164429,0 -103,0.567066669464111,0 -104,0.572516679763794,0 -105,0.577966690063477,0 -106,0.583416640758514,2132.291015625 -107,0.588866651058197,1785.1533203125 -108,0.594299972057343,1438.015625 -109,0.599749982357025,0 -110,0.605199992656708,1248.1787109375 -111,0.61065000295639,0 -112,0.616100013256073,0 -113,0.621550023555756,0 -114,0.626999974250793,0 -115,0.632449984550476,0 -116,0.637899994850159,0 -117,0.643350005149841,0 -118,0.648800015449524,0 -119,0.654250025749207,0 -120,0.659699976444244,0 -121,0.665149986743927,0 -122,0.67059999704361,0 -123,0.676033318042755,0 -124,0.681483328342438,0 -125,0.68693333864212,0 -126,0.692383348941803,3691.61328125 -127,0.697833359241486,2918.650390625 -128,0.703283309936523,0 -129,0.708733320236206,0 -130,0.714183330535889,0 -131,0.719633340835571,0 -132,0.725083351135254,0 -133,0.730533361434937,0 -134,0.735983312129974,0 -135,0.741433322429657,0 -136,0.74688333272934,0 -137,0.752316653728485,0 -138,0.757766664028168,0 -139,0.76321667432785,0 -140,0.768666684627533,0 -141,0.774116694927216,0 -142,0.779566645622253,0 -143,0.785016655921936,0 -144,0.790466666221619,4167.640625 -145,0.795916676521301,4478.19140625 -146,0.801366686820984,0 -147,0.806816637516022,0 -148,0.812266647815704,0 -149,0.817716658115387,0 -150,0.82316666841507,0 -151,0.828599989414215,0 -152,0.834049999713898,0 -153,0.83950001001358,0 -154,0.844950020313263,0 -155,0.850399971008301,0 -156,0.855849981307983,0 -157,0.861299991607666,0 -158,0.866750001907349,2099.681640625 -159,0.872200012207031,1049.8408203125 -160,0.877650022506714,0 -161,0.883099973201752,0 -162,0.888549983501434,0 -163,0.893999993801117,0 -164,0.8994500041008,0 -165,0.904900014400482,0 -166,0.910333335399628,0 -167,0.91578334569931,0 -168,0.921233355998993,0 -169,0.926683306694031,0 -170,0.932133316993713,0 -171,0.937583327293396,0 -172,0.943033337593079,0 -173,0.948483347892761,3484.33203125 -174,0.953933358192444,3530.8515625 -175,0.959383308887482,0 -176,0.964833319187164,4340.03125 -177,0.970283329486847,0 -178,0.97573333978653,0 -179,0.981183350086212,3383.119140625 -180,0.986616671085358,3483.30078125 -181,0.99206668138504,1806.205078125 -182,0.997516691684723,3296.77734375 -183,1.00296664237976,0 -184,1.00841665267944,1455.607421875 -185,1.01386666297913,676.4619140625 -186,1.01931667327881,338.23095703125 -187,1.02476668357849,0 -188,1.03021669387817,0 -189,1.03566670417786,0 -190,1.04111671447754,0 -191,1.04656672477722,0 -192,1.05201661586761,7268.1015625 -193,1.0574666261673,6607.24609375 -194,1.06289994716644,0 -195,1.06834995746613,0 -196,1.07379996776581,0 -197,1.07924997806549,0 -198,1.08469998836517,0 -199,1.09014999866486,0 -200,1.09560000896454,0 -201,1.10105001926422,0 -202,1.1065000295639,0 -203,1.11195003986359,0 -204,1.11740005016327,0 -205,1.12284994125366,0 -206,1.12829995155334,0 -207,1.13374996185303,0 -208,1.13919997215271,0 -209,1.14463329315186,0 -210,1.15008330345154,0 -211,1.15553331375122,0 -212,1.1609833240509,0 -213,1.16643333435059,0 -214,1.17188334465027,0 -215,1.17733335494995,0 -216,1.18278336524963,0 -217,1.18823337554932,0 -218,1.193683385849,0 -219,1.19913327693939,0 -220,1.20458328723907,0 -221,1.21003329753876,0 -222,1.21548330783844,0 -223,1.22091662883759,0 -224,1.22636663913727,0 -225,1.23181664943695,0 -226,1.23726665973663,0 -227,1.24271667003632,0 -228,1.248166680336,2982.02734375 -229,1.25361669063568,2766.794921875 -230,1.25906670093536,2551.564453125 -231,1.26451671123505,0 -232,1.26996672153473,0 -233,1.27541661262512,0 -234,1.2808666229248,0 -235,1.28631663322449,0 -236,1.29176664352417,0 -237,1.29719996452332,0 -238,1.302649974823,2595.2890625 -239,1.30809998512268,2702.830078125 -240,1.31354999542236,0 -241,1.31900000572205,3248.12109375 -242,1.32445001602173,492.146240234375 -243,1.32990002632141,0 -244,1.33535003662109,3376.71484375 -245,1.34080004692078,0 -246,1.34625005722046,4911.74609375 -247,1.35169994831085,3171.7890625 -248,1.35714995861053,6902.78125 -249,1.36259996891022,1240.0087890625 -250,1.3680499792099,3106.42578125 -251,1.37349998950958,0 -252,1.37893331050873,0 -253,1.38438332080841,0 -254,1.38983333110809,0 -255,1.39528334140778,0 -256,1.40073335170746,0 -257,1.40618336200714,0 -258,1.41163337230682,0 -259,1.41708338260651,0 -260,1.42253339290619,0 -261,1.42798328399658,0 -262,1.43343329429626,4459.421875 -263,1.43888330459595,4681.72265625 -264,1.44433331489563,1369.19140625 -265,1.44978332519531,0 -266,1.45521664619446,0 -267,1.46066665649414,0 -268,1.46611666679382,0 -269,1.47156667709351,0 -270,1.47701668739319,0 -271,1.48246669769287,0 -272,1.48791670799255,0 -273,1.49336671829224,0 -274,1.49881660938263,0 -275,1.50426661968231,1232.095703125 -276,1.50971662998199,616.0478515625 -277,1.51516664028168,0 -278,1.52061665058136,0 -279,1.52606666088104,0 -280,1.53149998188019,0 -281,1.53694999217987,2222.9765625 -282,1.54240000247955,2529.849609375 -283,1.54785001277924,3531.220703125 -284,1.55330002307892,3233.251953125 -285,1.5587500333786,1185.0966796875 -286,1.56420004367828,0 -287,1.56965005397797,0 -288,1.57509994506836,0 -289,1.58054995536804,0 -290,1.58599996566772,0 -291,1.59144997596741,0 -292,1.59689998626709,0 -293,1.60234999656677,0 -294,1.60778331756592,0 -295,1.6132333278656,2749.79296875 -296,1.61868333816528,3260.267578125 -297,1.62413334846497,0 -298,1.62958335876465,0 -299,1.63503336906433,0 -300,1.64048337936401,424.05859375 -301,1.6459333896637,212.029296875 -302,1.65138328075409,0 -303,1.65683329105377,0 -304,1.66228330135345,0 -305,1.66773331165314,0 -306,1.67318332195282,0 -307,1.6786333322525,0 -308,1.68408334255219,0 -309,1.68951666355133,0 -310,1.69496667385101,0 -311,1.7004166841507,0 -312,1.70586669445038,0 -313,1.71131670475006,0 -314,1.71676671504974,2841.361328125 -315,1.72221672534943,3992.634765625 -316,1.72766661643982,5143.91015625 -317,1.7331166267395,8773.546875 -318,1.73856663703918,19500.359375 -319,1.74401664733887,32176.546875 -320,1.74946665763855,38021.1875 -321,1.75491666793823,43865.8125 -322,1.76036667823792,41356.625 -323,1.76579999923706,59173.28125 -324,1.77125000953674,56369.75 -325,1.77670001983643,92232.6875 -326,1.78215003013611,96299.1875 -327,1.78760004043579,57460.90625 -328,1.79305005073547,45898.71875 -329,1.79849994182587,54743.28125 -330,1.80394995212555,24151.3125 -331,1.80939996242523,24156.90625 -332,1.81484997272491,27262.59375 -333,1.8202999830246,6033.3203125 -334,1.82574999332428,5447.69140625 -335,1.83120000362396,27700.421875 -336,1.83665001392365,19331.0625 -337,1.84208333492279,4002.240234375 -338,1.84753334522247,1361.462890625 -339,1.85298335552216,0 -340,1.85843336582184,0 -341,1.86388337612152,953.78173828125 -342,1.8693333864212,476.890869140625 -343,1.8747832775116,0 -344,1.88023328781128,0 -345,1.88568329811096,2028.35546875 -346,1.89113330841064,7406.265625 -347,1.89658331871033,5545.07421875 -348,1.90203332901001,2940.953125 -349,1.90748333930969,4914.9140625 -350,1.91293334960938,2457.45703125 -351,1.91838335990906,0 -352,1.9238166809082,0 -353,1.92926669120789,5062.49609375 -354,1.93471670150757,4367.6328125 -355,1.94016671180725,7792.33984375 -356,1.94561672210693,0 -357,1.95106661319733,0 -358,1.95651662349701,0 -359,1.96196663379669,0 -360,1.96741664409637,0 -361,1.97286665439606,0 -362,1.97831666469574,0 -363,1.98376667499542,0 -364,1.9892166852951,0 -365,1.99466669559479,0 -366,2.00009989738464,0 -367,2.00554990768433,5629.4296875 -368,2.01099991798401,6105.30859375 -369,2.01644992828369,6343.28515625 -370,2.02189993858337,1966.59765625 -371,2.02734994888306,0 -372,2.03279995918274,0 -373,2.03824996948242,0 -374,2.0436999797821,0 -375,2.04914999008179,0 -376,2.05460000038147,0 -377,2.06005001068115,1338.2392578125 -378,2.06550002098083,2676.478515625 -379,2.07095003128052,3072.787109375 -380,2.07638335227966,0 -381,2.08183336257935,0 -382,2.08728337287903,0 -383,2.09273338317871,0 -384,2.09818339347839,0 -385,2.10363340377808,0 -386,2.10908341407776,0 -387,2.11453342437744,0 -388,2.11998343467712,0 -389,2.12543344497681,0 -390,2.13088321685791,0 -391,2.13633322715759,0 -392,2.14178323745728,594.16796875 -393,2.14723324775696,1188.3359375 -394,2.15268325805664,1296.0986328125 -395,2.15811657905579,0 -396,2.16356658935547,6709.48828125 -397,2.16901659965515,6479.9765625 -398,2.17446660995483,2011.7939453125 -399,2.17991662025452,0 -400,2.1853666305542,1626.453125 -401,2.19081664085388,6619.390625 -402,2.19626665115356,5740.9453125 -403,2.20171666145325,4634.0234375 -404,2.20716667175293,0 -405,2.21261668205261,0 -406,2.21806669235229,0 -407,2.22351670265198,1380.96875 -408,2.22896671295166,690.484375 -409,2.23440003395081,0 -410,2.23985004425049,0 -411,2.24530005455017,0 -412,2.25075006484985,1840.2861328125 -413,2.25620007514954,3754.583984375 -414,2.26165008544922,5668.8828125 -415,2.2671000957489,4593.7578125 -416,2.27255010604858,3810.41796875 -417,2.27800011634827,3027.076171875 -418,2.28344988822937,2505.337890625 -419,2.28889989852905,1252.6689453125 -420,2.29434990882874,0 -421,2.29979991912842,0 -422,2.3052499294281,0 -423,2.31068325042725,4536.125 -424,2.31613326072693,3463.353515625 -425,2.32158327102661,2390.58203125 -426,2.32703328132629,3020.59765625 -427,2.33248329162598,3650.61328125 -428,2.33793330192566,2488.849609375 -429,2.34338331222534,3210.94921875 -430,2.34883332252502,1605.474609375 -431,2.35428333282471,0 -432,2.35973334312439,3392.248046875 -433,2.36518335342407,2404.642578125 -434,2.37063336372375,1417.037109375 -435,2.37608337402344,0 -436,2.38153338432312,4396.9296875 -437,2.3869833946228,8518.4140625 -438,2.39241671562195,5936.8203125 -439,2.39786672592163,9217.546875 -440,2.40331673622131,6873.78125 -441,2.408766746521,0 -442,2.41421675682068,15498.4140625 -443,2.41966676712036,9682.4609375 -444,2.42511677742004,4017.404296875 -445,2.43056654930115,17770.5625 -446,2.43601655960083,2210.388671875 -447,2.44146656990051,4443.3046875 -448,2.4469165802002,10525.3515625 -449,2.45236659049988,13824.78125 -450,2.45781660079956,13802.9921875 -451,2.46326661109924,4914.765625 -452,2.46869993209839,10687.21875 -453,2.47414994239807,13515.1796875 -454,2.47959995269775,4893.05078125 -455,2.48504996299744,8612.4140625 -456,2.49049997329712,16676.59375 -457,2.4959499835968,9856.6171875 -458,2.50139999389648,6151.3125 -459,2.50685000419617,8818 -460,2.51230001449585,3545.353515625 -461,2.51775002479553,10624.28125 -462,2.52320003509521,9228.9921875 -463,2.5286500453949,7833.7109375 -464,2.53410005569458,6030.44921875 -465,2.53955006599426,12059.796875 -466,2.54498338699341,14019.03125 -467,2.55043339729309,7837.83203125 -468,2.55588340759277,12257.796875 -469,2.56133341789246,12636.21875 -470,2.56678342819214,13014.640625 -471,2.57223343849182,20997.859375 -472,2.5776834487915,2582.259765625 -473,2.58313322067261,12180.2890625 -474,2.58858323097229,10645.40625 -475,2.59403324127197,7198.80078125 -476,2.59948325157166,3752.193359375 -477,2.60493326187134,2300.70703125 -478,2.61038327217102,3948.28515625 -479,2.6158332824707,8564.2734375 -480,2.62126660346985,6524.32421875 -481,2.62671661376953,4484.375 -482,2.63216662406921,23349.28125 -483,2.6376166343689,19418.578125 -484,2.64306664466858,15297.7578125 -485,2.64851665496826,4377.9375 -486,2.65396666526794,5292.375 -487,2.65941667556763,14047.8125 -488,2.66486668586731,15633.5625 -489,2.67031669616699,18461.8125 -490,2.67576670646667,7410.38671875 -491,2.68121671676636,6094.5625 -492,2.68666672706604,7499.6015625 -493,2.69211673736572,3533.943359375 -494,2.69756674766541,4967.875 -495,2.70300006866455,6401.8046875 -496,2.70845007896423,4559.1640625 -497,2.71390008926392,6017.8984375 -498,2.7193500995636,7476.63671875 -499,2.72480010986328,7268.69140625 -500,2.73024988174438,6216.60546875 -501,2.73569989204407,5164.51953125 -502,2.74114990234375,3175.54296875 -503,2.74659991264343,1186.568359375 -504,2.75204992294312,8393.2109375 -505,2.7574999332428,9177.2890625 -506,2.76294994354248,6693.07421875 -507,2.76839995384216,6306.11328125 -508,2.77384996414185,8373.7734375 -509,2.77928328514099,14563.6640625 -510,2.78473329544067,14531.4921875 -511,2.79018330574036,0 -512,2.79563331604004,9474.8828125 -513,2.80108332633972,10087.96875 -514,2.8065333366394,3343.666015625 -515,2.81198334693909,0 -516,2.81743335723877,0 -517,2.82288336753845,1799.4326171875 -518,2.82833337783813,3598.865234375 -519,2.83378338813782,4424.9375 -520,2.8392333984375,5251.0078125 -521,2.84468340873718,8909.203125 -522,2.85013341903687,9991.171875 -523,2.85556674003601,6329.00390625 -524,2.86101675033569,2666.8359375 -525,2.86646676063538,8342.4921875 -526,2.87191677093506,6318.625 -527,2.87736678123474,10284.796875 -528,2.88281655311584,13361.8515625 -529,2.88826656341553,15126.3203125 -530,2.89371657371521,8066.01953125 -531,2.89916658401489,19851.171875 -532,2.90461659431458,17250.21875 -533,2.91006660461426,2503.203125 -534,2.91551661491394,11386.71875 -535,2.92096662521362,15917.171875 -536,2.92641663551331,18256.671875 -537,2.93186664581299,9098.453125 -538,2.93729996681213,10058.234375 -539,2.94274997711182,10775.9140625 -540,2.9481999874115,10863.5078125 -541,2.95364999771118,2632.38671875 -542,2.95910000801086,13206.90625 -543,2.96455001831055,0 -544,2.97000002861023,25491.875 -545,2.97545003890991,20134.578125 -546,2.98090004920959,5036.0703125 -547,2.98635005950928,3378.482421875 -548,2.99180006980896,3029.13671875