From e83d0a26f6a80621ed197bef221c0687b81e8dda Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 29 Apr 2024 21:41:34 +0200 Subject: [PATCH] passing tests ... --- rowers/datautils.py | 43 +++++++++----------------- rowers/plannedsessions.py | 13 +++++--- rowers/tests/mocks.py | 6 ++++ rowers/tests/test_unit_tests.py | 4 +-- rowers/tests/test_units.py | 17 +++++----- rowers/tests/test_uploads.py | 8 ++--- rowers/tests/testdata/testdata.tcx.gz | Bin 4003 -> 3999 bytes 7 files changed, 44 insertions(+), 47 deletions(-) diff --git a/rowers/datautils.py b/rowers/datautils.py index c156f307..0b24d252 100644 --- a/rowers/datautils.py +++ b/rowers/datautils.py @@ -1,8 +1,10 @@ import pandas as pd +import polars as pl import numpy as np from scipy.interpolate import griddata from scipy import optimize + from rowers.mytypes import otwtypes, otetypes, rowtypes from rowers.models import Workout @@ -358,8 +360,8 @@ def getmaxwattinterval(tt, ww, i): def getfastest(df, thevalue, mode='distance'): - tt = df['time'].copy() - dd = df['cumdist'].copy() + tt = df['time'].clone() + dd = df['cumdist'].clone() tmax = tt.max() if mode == 'distance': # pragma: no cover @@ -370,40 +372,28 @@ def getfastest(df, thevalue, mode='distance'): return 0 -# if tmax > 500000: -# newlen=int(tmax/2000.) -# newt = np.arange(newlen)*tmax/float(newlen) -# deltat = newt[1]-newt[0] -# else: -# newt = np.arange(0,tmax,10.) -# deltat = 10. - newlen = 1000 newt = np.arange(newlen)*tmax/float(newlen) deltat = newt[1]-newt[0] - dd = griddata(tt.values, - dd.values, newt, method='linear', rescale=True) + dd = griddata(tt.to_numpy(), + dd.to_numpy(), newt, method='linear', rescale=True) - tt = pd.Series(newt, dtype='float') - dd = pd.Series(dd, dtype='float') + tt = pl.Series(newt, dtype=pl.Float64) + dd = pl.Series(dd, dtype=pl.Float64) + + G = pl.concat([pl.Series([0.0]), dd]) - G = pd.concat([pd.Series([0]), dd]) - # T = pd.concat([pd.Series([0]), dd]) - # h = np.mgrid[0:len(tt)+1:1, 0:len(tt)+1:1] - # distances = pd.DataFrame(h[1]-h[0]) ones = 1+np.zeros(len(G)) Ghor = np.outer(ones, G) - # Thor = np.outer(ones, T) - # Tver = np.outer(T, ones) + Gver = np.outer(G, ones) Gdif = Ghor-Gver Gdif = np.tril(Gdif.T).T - Gdif = pd.DataFrame(Gdif) + Gdif = pl.DataFrame(Gdif) F = Gdif - F.fillna(inplace=True, method='ffill', axis=1) - F.fillna(inplace=True, value=0) + F = F.fill_nan(0) restime = [] distance = [] @@ -414,7 +404,7 @@ def getfastest(df, thevalue, mode='distance'): restime.append(deltat*i) cp = np.diag(F, i).max() loc = np.argmax(np.diag(F, i)) - thestarttime = tt[loc] + thestarttime = tt.to_numpy()[loc] starttimes.append(thestarttime) distance.append(cp) @@ -424,10 +414,6 @@ def getfastest(df, thevalue, mode='distance'): distance = np.array(distance) starttimes = np.array(starttimes) - # for i in range(len(restime)): - # if restime[i] 0: w.plannedsession = ps w.save() @@ -1685,8 +1686,9 @@ def add_workout_fastestrace(ws, race, r, recordid=0, doregister=False): record.coursecompleted = True record.workoutid = ws[0].id if race.sessiontype == 'fastest_distance': - df = dataprep.getsmallrowdata_db( + df = dataprep.read_data( ['time', 'cumdist'], ids=[ws[0].id]) + df = dataprep.remove_nulls_pl(df) fastest_milliseconds, startsecond, endsecond = datautils.getfastest( df, race.sessionvalue, mode='distance') velo = race.sessionvalue/fastest_milliseconds @@ -1702,8 +1704,9 @@ def add_workout_fastestrace(ws, race, r, recordid=0, doregister=False): record.endsecond = endsecond record.save() if race.sessiontype == 'fastest_time': # pragma: no cover - df = dataprep.getsmallrowdata_db( + df = dataprep.read_data( ['time', 'cumdist'], ids=[ws[0].id]) + df = dataprep.remove_nulls_pl(df) fastest_meters, startsecond, endsecond = datautils.getfastest( df, race.sessionvalue, mode='time') velo = fastest_meters/(60.*race.sessionvalue) diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index ed73b421..cd73dd7a 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -309,11 +309,17 @@ def mocked_getrowdata_uh(*args, **kwargs): # pragma: no cover return df, row + def mocked_getsmallrowdata_uh(*args, **kwargs): # pragma: no cover df = pl.read_csv('rowers/tests/testdata/uhfull.csv') return df +def mocked_getsmallrowdata_uh_pd(*args, **kwargs): # pragma: no cover + df = pd.read_csv('rowers/tests/testdata/uhfull.csv') + + return df + def mocked_getsmallrowdata_forfusion(*args, **kwargs): df = pl.read_csv('rowers/tests/testdata/getrowdata_mock.csv') diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py index 3c59620a..e1311d81 100644 --- a/rowers/tests/test_unit_tests.py +++ b/rowers/tests/test_unit_tests.py @@ -597,8 +597,8 @@ class DataPrepTests(TestCase): result = getagegrouprecord(25) self.assertEqual(int(result),590) - @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_uh) - def test_get_videodata(self,mocked_getsmallrowdata_uh): + @patch('rowers.dataprep.getsmallrowdata_pd',side_effect=mocked_getsmallrowdata_uh_pd) + def test_get_videodata(self,mocked_getsmallrowdata_uh_pd): data, metrics, maxtime = dataprep.get_video_data(self.wuh_otw) self.assertEqual(len(data),9) diff --git a/rowers/tests/test_units.py b/rowers/tests/test_units.py index 68754ddc..9fb66dac 100644 --- a/rowers/tests/test_units.py +++ b/rowers/tests/test_units.py @@ -71,9 +71,10 @@ class ForceUnits(TestCase): w = Workout.objects.get(id=1) self.assertEqual(w.forceunit,'lbs') - df = dataprep.getsmallrowdata_db(['averageforce'],ids=[13]) + df = dataprep.read_data(['averageforce'],ids=[13]) + df = dataprep.remove_nulls_pl(df) average_N = int(df['averageforce'].mean()) - self.assertEqual(average_N,398) + self.assertEqual(average_N,400) data = dataprep.read_df_sql(13) average_N = int(data['averageforce'].mean()) @@ -119,9 +120,10 @@ class ForceUnits(TestCase): w = Workout.objects.get(id=13) self.assertEqual(w.forceunit,'N') - df = dataprep.getsmallrowdata_db(['averageforce'],ids=[13]) + df = dataprep.read_data(['averageforce'],ids=[13]) + df = dataprep.remove_nulls_pl(df) average_N = int(df['averageforce'].mean()) - self.assertEqual(average_N,263) + self.assertEqual(average_N,271) def test_upload_speedcoach_colin(self): login = self.c.login(username=self.u.username, password=self.password) @@ -155,9 +157,10 @@ class ForceUnits(TestCase): w = Workout.objects.get(id=13) self.assertEqual(w.forceunit,'N') - df = dataprep.getsmallrowdata_db(['averageforce'],ids=[13]) + df = dataprep.read_data(['averageforce'],ids=[13]) + df = dataprep.remove_nulls_pl(df) average_N = int(df['averageforce'].mean()) - self.assertEqual(average_N,105) + self.assertEqual(average_N,122) @override_settings(TESTING=True) class TestForceUnit(TestCase): @@ -206,7 +209,7 @@ class TestForceUnit(TestCase): rowdata = dataprep.rdata('rowers/tests/testdata/PainsledForce.csv') df = dataprep.dataprep(rowdata.df) - #df = dataprep.getsmallrowdata_db(['averageforce'],ids=[w[0].id],doclean=False, + #df = dataprep.read_data(['averageforce'],ids=[w[0].id],doclean=False, # compute=False) try: average_N = int(df['averageforce'].mean()) diff --git a/rowers/tests/test_uploads.py b/rowers/tests/test_uploads.py index b36335c1..b27a198a 100644 --- a/rowers/tests/test_uploads.py +++ b/rowers/tests/test_uploads.py @@ -29,8 +29,8 @@ class ViewTest(TestCase): self.nu = datetime.datetime.now() @patch('rowers.dataprep.create_engine') - @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) - def test_upload_view_sled(self, mocked_sqlalchemy,mocked_getsmallrowdata_db): + @patch('rowers.dataprep.read_data',side_effect=mocked_read_data) + def test_upload_view_sled(self, mocked_sqlalchemy,mocked_read_data): login = self.c.login(username='john',password='koeinsloot') self.assertTrue(login) @@ -120,8 +120,8 @@ class ViewTest(TestCase): pass @patch('rowers.dataprep.create_engine') - @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) - def test_upload_view_sled_bg(self, mocked_sqlalchemy,mocked_getsmallrowdata_db): + @patch('rowers.dataprep.read_data',side_effect=mocked_read_data) + def test_upload_view_sled_bg(self, mocked_sqlalchemy,mocked_read_data): login = self.c.login(username='john',password='koeinsloot') self.assertTrue(login) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index fc88d4937d88aac8c7941b1fc71f93d7b02ffc45..ef4b2add63ebad16a95789e1316cacca12240742 100644 GIT binary patch literal 3999 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 F0RU~;Ja7O2 literal 4003 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