diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3839e5fd9..a321f1be1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.8', '3.9', '3.10', '3.11'] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12'] name: Tests on ${{ matrix.python-version }} steps: - name: Setup python diff --git a/docs/changelog/v3.2.x.rst b/docs/changelog/v3.2.x.rst index 56380de5b..fb89f2b92 100644 --- a/docs/changelog/v3.2.x.rst +++ b/docs/changelog/v3.2.x.rst @@ -10,6 +10,12 @@ Bug Fixes - Fix incorrect lap number (zero instead of one) for generated laps that are added when a driver crashes on the very first lap. +- Fix broken schedule backend 'f1timing' (failed to load 2023 season schedule + after Qatar GP due to unexpected data) + +- Fix: failing lap accuracy check for one driver caused laps of all drivers to + be marked as inaccurate (#464) + New Features ^^^^^^^^^^^^ diff --git a/fastf1/core.py b/fastf1/core.py index 4d3590747..faa6e37ee 100644 --- a/fastf1/core.py +++ b/fastf1/core.py @@ -1421,6 +1421,11 @@ def _load_laps_data(self, livedata=None): mask = pd.isna(result['LapStartTime']) & (~pd.isna(result['PitOutTime'])) result.loc[mask, 'LapStartTime'] = result.loc[mask, 'PitOutTime'] + # remove first lap pitout time if it is before session_start_time + mask = (result["PitOutTime"] < self.session_start_time) & \ + (result["NumberOfLaps"] == 1) + result.loc[mask, 'PitOutTime'] = pd.NaT + # create total laps counter for each tyre used for npit in result['Stint'].unique(): sel = result['Stint'] == npit @@ -1867,6 +1872,7 @@ def _check_lap_accuracy(self): prev_lap = None integrity_errors = 0 for _, lap in self.laps[self.laps['DriverNumber'] == drv].iterrows(): + lap_integrity_ok = True # require existence, non-existence and specific values for some variables check_1 = (pd.isnull(lap['PitInTime']) & pd.isnull(lap['PitOutTime']) @@ -1884,7 +1890,7 @@ def _check_lap_accuracy(self): lap['LapTime'].total_seconds(), atol=0.003, rtol=0, equal_nan=False) if not check_2: - integrity_errors += 1 + lap_integrity_ok = False else: check_2 = False # data not available means fail @@ -1894,16 +1900,44 @@ def _check_lap_accuracy(self): else: check_3 = True # no previous lap, no SC error - result = check_1 and check_2 and check_3 + pre_check_4 = (((not pd.isnull(lap['Time'])) + & (not pd.isnull(lap['LapTime']))) + and (prev_lap is not None) + and (not pd.isnull(prev_lap['Time']))) + + if pre_check_4: # needed condition for check_4 + time_diff = np.sum((lap['Time'], + -1 * prev_lap['Time'])).total_seconds() + lap_time = lap['LapTime'].total_seconds() + # If the difference between the two times is within a + # certain tolerance, the lap time data is considered + # to be valid. + check_4 = np.allclose(time_diff, lap_time, + atol=0.003, rtol=0, equal_nan=False) + + if not check_4: + lap_integrity_ok = False + + else: + check_4 = True + + if not lap_integrity_ok: + integrity_errors += 1 + + result = check_1 and check_2 and check_3 and check_4 is_accurate.append(result) prev_lap = lap if len(is_accurate) > 0: - self._laps.loc[self.laps['DriverNumber'] == drv, 'IsAccurate'] = is_accurate + self._laps.loc[ + self.laps['DriverNumber'] == drv, 'IsAccurate' + ] = is_accurate else: - _logger.warning("Failed to perform lap accuracy check - all " - "laps marked as inaccurate.") - self.laps['IsAccurate'] = False # default should be inaccurate + _logger.warning(f"Failed to perform lap accuracy check - all " + f"laps marked as inaccurate (driver {drv})") + self._laps.loc[ + self.laps['DriverNumber'] == drv, 'IsAccurate' + ] = False # default should be inaccurate # necessary to explicitly cast to bool self._laps[['IsAccurate']] \ diff --git a/fastf1/events.py b/fastf1/events.py index c378825eb..042bb7962 100644 --- a/fastf1/events.py +++ b/fastf1/events.py @@ -110,7 +110,7 @@ identifier to differentiate between the various sessions of one event. This identifier can currently be one of the following: - - session name abbreviation: ``'FP1', 'FP2', 'FP3', 'Q', 'S', 'SS', R'`` + - session name abbreviation: ``'FP1', 'FP2', 'FP3', 'Q', 'S', 'SS', 'R'`` - full session name: ``'Practice 1', 'Practice 2', 'Practice 3', 'Sprint', 'Sprint Shootout', 'Qualifying', 'Race'``; provided names will be normalized, so that the name is @@ -607,15 +607,21 @@ def _get_schedule_from_f1_timing(year): data['EventName'].append(event['Name']) data['OfficialEventName'].append(event['OfficialName']) - n_events = min(len(event['Sessions']), 5) + # select only valid sessions + sessions = list() + for ses in event['Sessions']: + if (ses.get('Key') != -1) and ses.get('Name'): + sessions.append(ses) + + n_events = min(len(sessions), 5) # number of events, usually 3 for testing, 5 for race weekends # in special cases there are additional unrelated events - if (n_events >= 4) and ('Sprint' in event['Sessions'][3]['Name']): - if event['Sessions'][3]['Name'] == 'Sprint Qualifying': + if (n_events >= 4) and ('Sprint' in sessions[3]['Name']): + if sessions[3]['Name'] == 'Sprint Qualifying': # fix for 2021 where Sprint was called Sprint Qualifying - event['Sessions'][3]['Name'] = 'Sprint' - if event['Sessions'][2]['Name'] == 'Sprint Shootout': + sessions[3]['Name'] = 'Sprint' + if sessions[2]['Name'] == 'Sprint Shootout': data['EventFormat'].append('sprint_shootout') else: data['EventFormat'].append('sprint') @@ -632,7 +638,7 @@ def _get_schedule_from_f1_timing(year): for i in range(0, 5): # parse the up to five sessions for each event try: - session = event['Sessions'][i] + session = sessions[i] except IndexError: data[f'Session{i+1}'].append(None) data[f'Session{i+1}Date'].append(None) @@ -730,6 +736,9 @@ def _get_schedule_from_ergast(year) -> "EventSchedule": class EventSchedule(pd.DataFrame): """This class implements a per-season event schedule. + For detailed information about the information that is available for each + event, see `Event Schedule Data`_. + This class is usually not instantiated directly. You should use :func:`fastf1.get_event_schedule` to get an event schedule for a specific season. @@ -928,6 +937,9 @@ class Event(pd.Series): Each event consists of one or multiple sessions, depending on the type of event and depending on the event format. + For detailed information about the information that is available for each + event, see `Event Schedule Data`_. + This class is usually not instantiated directly. You should use :func:`fastf1.get_event` or similar to get a specific event. diff --git a/fastf1/tests/test_core.py b/fastf1/tests/test_core.py index 561e23c12..5294577ad 100644 --- a/fastf1/tests/test_core.py +++ b/fastf1/tests/test_core.py @@ -39,6 +39,23 @@ def test_lap_data_loading_position_calculation(): assert (delta == 0).all() # assert that the delta is zero for all laps +@pytest.mark.f1telapi +def test_first_lap_pitout_times(): + sprint_session = fastf1.get_session(2023, 4, "Sprint") + sprint_session.load(telemetry=False, weather=False, messages=False) + sprint_laps = sprint_session.laps + sprint_mask = (sprint_laps["LapNumber"] == 1) & \ + (~sprint_laps["PitOutTime"].isna()) + assert sprint_laps[sprint_mask]["Driver"].tolist() == ["OCO"] + + race_session = fastf1.get_session(2023, 5, "R") + race_session.load(telemetry=False, weather=False, messages=False) + race_laps = race_session.laps + race_mask = (race_laps["LapNumber"] == 1) & \ + (~race_laps["PitOutTime"].isna()) + assert race_laps[race_mask]["Driver"].tolist() == [] + + def test_laps_constructor_metadata_propagation(reference_laps_data): session, laps = reference_laps_data diff --git a/pytest.ini b/pytest.ini index d9f6b8845..5c25857b3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -23,3 +23,10 @@ filterwarnings = # external, verified as not relevant # (10/2022) ignore:.*df.iloc.*will attempt to set the values inplace.*:FutureWarning + # external, datetime module deprecation + # (11/2023) + ignore:.*datetime\.datetime.utcnow\(\) is deprecated.*:DeprecationWarning + ignore:.*datetime\.datetime.utcfromtimestamp\(\) is deprecated.*:DeprecationWarning + # external, attribute removal planned for Python 3.14 + # (11/2023) + ignore:.*Attribute s is deprecated and will be removed in Python 3.14.*:DeprecationWarning