From 17321027561cb54e0fd30d11609b6f60878750e8 Mon Sep 17 00:00:00 2001 From: Ziqi Yin Date: Thu, 13 Sep 2012 20:23:30 -0700 Subject: [PATCH 01/37] h5_file and defaults_config --- defaults.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaults.cfg b/defaults.cfg index d0df2b1..793c870 100755 --- a/defaults.cfg +++ b/defaults.cfg @@ -1,5 +1,5 @@ [data] -base = /media/Data/Documents/School/UC Davis/Bicycle Mechanics +base = /home/stefenyin/bicycle/bi_pypa pathToDatabase = %(base)s/BicycleDataProcessor/InstrumentedBicycleData.h5 pathToCorruption = %(base)s/BicycleDataProcessor/data-corruption.csv pathToRunMat = %(base)s/BicycleDAQ/data From ec9c67ec899b1e0525150e553050296ff06199d8 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Fri, 28 Sep 2012 13:29:13 -0700 Subject: [PATCH 02/37] Changed the default.cfg because I changed the name of the home directory --- defaults.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaults.cfg b/defaults.cfg index 793c870..b815bb2 100644 --- a/defaults.cfg +++ b/defaults.cfg @@ -1,5 +1,5 @@ [data] -base = /home/stefenyin/bicycle/bi_pypa +base = /home/stefenstudy/bicycle/bi_pypa pathToDatabase = %(base)s/BicycleDataProcessor/InstrumentedBicycleData.h5 pathToCorruption = %(base)s/BicycleDataProcessor/data-corruption.csv pathToRunMat = %(base)s/BicycleDAQ/data From 8b9ac8633dfc0b6fdc333f6d37d1fe32c7ef4985 Mon Sep 17 00:00:00 2001 From: Ziqi Yin Date: Fri, 21 Sep 2012 13:27:48 -0700 Subject: [PATCH 03/37] Added contact forces function --- bicycledataprocessor/main.py | 103 ++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index aa186a0..8c66350 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -24,9 +24,10 @@ from tables import NoSuchNodeError import dtk.process as process -from dtk.bicycle import front_contact, benchmark_to_moore +from dtk.bicycle import front_contact, benchmark_to_moore, steer_torque_slip, contact_forces_slip, contact_forces_nonslip import bicycleparameters as bp + # local dependencies from database import get_row_num, get_cell, pad_with_zeros, run_id_string import signalprocessing as sigpro @@ -735,6 +736,7 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() + self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' @@ -848,6 +850,105 @@ def compute_front_wheel_contact_points(self): self.taskSignals['LongitudinalFrontContact'] = q9 self.taskSignals['LateralFrontContact'] = q10 + def compute_front_rear_wheel_contact_forces(self): + """Calculate the contact forces for each + wheel with respect to inertial frame under + the slip and nonslip condition. Also, provide the steer torque T4.""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + rf = mp['rf'] + + #signals assignment --slip condition + V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() + #V = self.taskSignals['ForwardSpeed'] + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['ForwardSpeed']/(-rf) + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = u7.time_derivative() + u8d = u8.time_derivative() + u9d = u9.time_derivative() + u10d = u10.time_derivative() + + #import function + f_1 = np.vectorize(steer_torque_slip) + f_2 = np.vectorize(contact_forces_slip) + + f_3 = np.vectorize(contact_forces_nonslip) + + #calculation + #steer torque slip + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + + #attributes: name and units, and taskSignals + T4_slip.name = 'SteerTorque_Slip' + T4_slip.units = 'newton*meter' + self.taskSignals[T4_slip.name] = T4_slip + + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s + + Fy_r_s.name = 'LateralRearContactForce_Slip' + Fy_r_s.units = 'newton' + self.taskSignals[Fy_r_s.name] = Fy_r_s + + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' + Fx_f_s.units = 'newton' + self.taskSignals[Fx_f_s.name] = Fx_f_s + + Fy_f_s.name = 'LateralFrontContactForce_Slip' + Fy_f_s.units = 'newton' + self.taskSignals[Fy_f_s.name] = Fy_f_s + + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' + Fx_r_ns.units = 'newton' + self.taskSignals[Fx_r_ns.name] = Fx_r_ns + + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' + Fy_r_ns.units = 'newton' + self.taskSignals[Fy_r_ns.name] = Fy_r_ns + + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' + Fx_f_ns.units = 'newton' + self.taskSignals[Fx_f_ns.name] = Fx_f_ns + + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' + Fy_f_ns.units = 'newton' + self.taskSignals[Fy_f_ns.name] = Fy_f_ns + + def compute_rear_wheel_contact_rates(self): """Calculates the rates of the wheel contact points in the ground plane.""" From c5800aab023988ed6179b12e5ee3c9bb29616a47 Mon Sep 17 00:00:00 2001 From: Ziqi Yin Date: Fri, 21 Sep 2012 22:59:27 -0700 Subject: [PATCH 04/37] Fixed indentation --- bicycledataprocessor/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 8c66350..4664f4c 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -736,7 +736,7 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() - self.compute_front_rear_wheel_contact_forces() + self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' From 54933fcb9f3a35e8602d41b7e2c108178727c26e Mon Sep 17 00:00:00 2001 From: StefenYin Date: Fri, 28 Sep 2012 13:26:42 -0700 Subject: [PATCH 05/37] I did not know --- bicycledataprocessor/select_runs.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 bicycledataprocessor/select_runs.py diff --git a/bicycledataprocessor/select_runs.py b/bicycledataprocessor/select_runs.py new file mode 100644 index 0000000..338526f --- /dev/null +++ b/bicycledataprocessor/select_runs.py @@ -0,0 +1,42 @@ +import bicycledataprocessor as bdp + +def select_runs(riders, maneuvers, environments): + """Returns a list of runs given a set of conditions. + + Parameters + ---------- + riders : list + All or a subset of ['Charlie', 'Jason', 'Luke']. + maneuvers : list + All or a subset of ['Balance', 'Balance With Disturbance', 'Track Straight Line', 'Track Straight Line With Disturbance']. + environments : list + All or a subset of ['Horse Treadmill', 'Pavillion Floor']. + + Returns + ------- + runs : list + List of run numbers for the given conditions. + + """ + + dataset = bdp.DataSet() + dataset.open() + + table = dataset.database.root.runTable + + runs = [] + for row in table.iterrows(): + con = [] + con.append(row['Rider'] in riders) + con.append(row['Maneuver'] in maneuvers) + con.append(row['Environment'] in environments) + con.append(row['corrupt'] is not True) + con.append(int(row['RunID']) > 100) + if False not in con: + runs.append(row['RunID']) + + dataset.close() + + return runs + + From 1db6594e63072f6de048c28ad92596262aa0acde Mon Sep 17 00:00:00 2001 From: StefenYin Date: Sun, 30 Sep 2012 23:35:33 -0700 Subject: [PATCH 06/37] Added compute_front_wheel_rate func --- bicycledataprocessor/main.py | 37 ++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 4664f4c..e5d79c9 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -24,7 +24,7 @@ from tables import NoSuchNodeError import dtk.process as process -from dtk.bicycle import front_contact, benchmark_to_moore, steer_torque_slip, contact_forces_slip, contact_forces_nonslip +from dtk.bicycle import front_contact, benchmark_to_moore, front_wheel_rate, steer_torque_slip, contact_forces_slip, contact_forces_nonslip import bicycleparameters as bp @@ -736,6 +736,7 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() + self.compute_front_wheel_rate() self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' @@ -850,6 +851,37 @@ def compute_front_wheel_contact_points(self): self.taskSignals['LongitudinalFrontContact'] = q9 self.taskSignals['LateralFrontContact'] = q10 + + def compute_front_wheel_rate(self): + """Calculates the front wheel rate, based on the Jason's data + of front_wheel_contact_point. Alternatively, you can use the + sympy to get the front_wheel_contact_rate directly first.""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + bp = self.bicycleRiderParameters + + lam = bp['lam'] + rF = bp['rF'] + + q4_wheel = q4 * cos(lam) * cos(q2) + + q4_wheel.name = 'SteerAngle_FrontWheel' + q4_wheel.units = 'radian' + self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel + + f = np.vectorize(front_wheel_rate) + + u6 = f(q1, q2, q4, u9, u10, lam, rF) + u6.name = 'FrontWheelRate' + u6.units = 'radian/second' + self.taskSignals['FrontWheelRate'] = u6 + + def compute_front_rear_wheel_contact_forces(self): """Calculate the contact forces for each wheel with respect to inertial frame under @@ -871,6 +903,7 @@ def compute_front_rear_wheel_contact_forces(self): #signals assignment --slip condition V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() #V = self.taskSignals['ForwardSpeed'] + q1 = self.taskSignals['YawAngle'] q2 = self.taskSignals['RollAngle'] q4 = self.taskSignals['SteerAngle'] @@ -880,7 +913,7 @@ def compute_front_rear_wheel_contact_forces(self): u3 = self.taskSignals['PitchRate'] u4 = self.taskSignals['SteerRate'] u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['ForwardSpeed']/(-rf) + u6 = self.taskSignals['FrontWheelRate'] u7 = self.taskSignals['LongitudinalRearContactRate'] u8 = self.taskSignals['LateralRearContactRate'] u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() From d337c72b3349c61baf3ebeae33b3eab3c28afa0c Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 1 Oct 2012 23:17:08 -0700 Subject: [PATCH 07/37] Updated the default.cfg --- defaults.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaults.cfg b/defaults.cfg index b815bb2..793c870 100644 --- a/defaults.cfg +++ b/defaults.cfg @@ -1,5 +1,5 @@ [data] -base = /home/stefenstudy/bicycle/bi_pypa +base = /home/stefenyin/bicycle/bi_pypa pathToDatabase = %(base)s/BicycleDataProcessor/InstrumentedBicycleData.h5 pathToCorruption = %(base)s/BicycleDataProcessor/data-corruption.csv pathToRunMat = %(base)s/BicycleDAQ/data From 9a17c894133b57dd17adb2daf9ea9f80df4658c9 Mon Sep 17 00:00:00 2001 From: Ziqi Yin Date: Fri, 21 Sep 2012 13:27:48 -0700 Subject: [PATCH 08/37] Added contact forces function --- bicycledataprocessor/main.py | 103 ++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index aa186a0..8c66350 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -24,9 +24,10 @@ from tables import NoSuchNodeError import dtk.process as process -from dtk.bicycle import front_contact, benchmark_to_moore +from dtk.bicycle import front_contact, benchmark_to_moore, steer_torque_slip, contact_forces_slip, contact_forces_nonslip import bicycleparameters as bp + # local dependencies from database import get_row_num, get_cell, pad_with_zeros, run_id_string import signalprocessing as sigpro @@ -735,6 +736,7 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() + self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' @@ -848,6 +850,105 @@ def compute_front_wheel_contact_points(self): self.taskSignals['LongitudinalFrontContact'] = q9 self.taskSignals['LateralFrontContact'] = q10 + def compute_front_rear_wheel_contact_forces(self): + """Calculate the contact forces for each + wheel with respect to inertial frame under + the slip and nonslip condition. Also, provide the steer torque T4.""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + rf = mp['rf'] + + #signals assignment --slip condition + V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() + #V = self.taskSignals['ForwardSpeed'] + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['ForwardSpeed']/(-rf) + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = u7.time_derivative() + u8d = u8.time_derivative() + u9d = u9.time_derivative() + u10d = u10.time_derivative() + + #import function + f_1 = np.vectorize(steer_torque_slip) + f_2 = np.vectorize(contact_forces_slip) + + f_3 = np.vectorize(contact_forces_nonslip) + + #calculation + #steer torque slip + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + + #attributes: name and units, and taskSignals + T4_slip.name = 'SteerTorque_Slip' + T4_slip.units = 'newton*meter' + self.taskSignals[T4_slip.name] = T4_slip + + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s + + Fy_r_s.name = 'LateralRearContactForce_Slip' + Fy_r_s.units = 'newton' + self.taskSignals[Fy_r_s.name] = Fy_r_s + + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' + Fx_f_s.units = 'newton' + self.taskSignals[Fx_f_s.name] = Fx_f_s + + Fy_f_s.name = 'LateralFrontContactForce_Slip' + Fy_f_s.units = 'newton' + self.taskSignals[Fy_f_s.name] = Fy_f_s + + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' + Fx_r_ns.units = 'newton' + self.taskSignals[Fx_r_ns.name] = Fx_r_ns + + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' + Fy_r_ns.units = 'newton' + self.taskSignals[Fy_r_ns.name] = Fy_r_ns + + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' + Fx_f_ns.units = 'newton' + self.taskSignals[Fx_f_ns.name] = Fx_f_ns + + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' + Fy_f_ns.units = 'newton' + self.taskSignals[Fy_f_ns.name] = Fy_f_ns + + def compute_rear_wheel_contact_rates(self): """Calculates the rates of the wheel contact points in the ground plane.""" From f1cb57ab37a676ecbea8cc3822bb5d294d2aecc9 Mon Sep 17 00:00:00 2001 From: Ziqi Yin Date: Fri, 21 Sep 2012 22:59:27 -0700 Subject: [PATCH 09/37] Fixed indentation --- bicycledataprocessor/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 8c66350..4664f4c 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -736,7 +736,7 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() - self.compute_front_rear_wheel_contact_forces() + self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' From 7cf0941b03875684bba0c493e35b1e5b0c805e46 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Fri, 28 Sep 2012 13:26:42 -0700 Subject: [PATCH 10/37] I did not know --- bicycledataprocessor/select_runs.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 bicycledataprocessor/select_runs.py diff --git a/bicycledataprocessor/select_runs.py b/bicycledataprocessor/select_runs.py new file mode 100644 index 0000000..338526f --- /dev/null +++ b/bicycledataprocessor/select_runs.py @@ -0,0 +1,42 @@ +import bicycledataprocessor as bdp + +def select_runs(riders, maneuvers, environments): + """Returns a list of runs given a set of conditions. + + Parameters + ---------- + riders : list + All or a subset of ['Charlie', 'Jason', 'Luke']. + maneuvers : list + All or a subset of ['Balance', 'Balance With Disturbance', 'Track Straight Line', 'Track Straight Line With Disturbance']. + environments : list + All or a subset of ['Horse Treadmill', 'Pavillion Floor']. + + Returns + ------- + runs : list + List of run numbers for the given conditions. + + """ + + dataset = bdp.DataSet() + dataset.open() + + table = dataset.database.root.runTable + + runs = [] + for row in table.iterrows(): + con = [] + con.append(row['Rider'] in riders) + con.append(row['Maneuver'] in maneuvers) + con.append(row['Environment'] in environments) + con.append(row['corrupt'] is not True) + con.append(int(row['RunID']) > 100) + if False not in con: + runs.append(row['RunID']) + + dataset.close() + + return runs + + From 2d605739b86b7dc6f3ac0e64d28478510c28bd25 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Sun, 30 Sep 2012 23:35:33 -0700 Subject: [PATCH 11/37] Added compute_front_wheel_rate func --- bicycledataprocessor/main.py | 37 ++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 4664f4c..e5d79c9 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -24,7 +24,7 @@ from tables import NoSuchNodeError import dtk.process as process -from dtk.bicycle import front_contact, benchmark_to_moore, steer_torque_slip, contact_forces_slip, contact_forces_nonslip +from dtk.bicycle import front_contact, benchmark_to_moore, front_wheel_rate, steer_torque_slip, contact_forces_slip, contact_forces_nonslip import bicycleparameters as bp @@ -736,6 +736,7 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() + self.compute_front_wheel_rate() self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' @@ -850,6 +851,37 @@ def compute_front_wheel_contact_points(self): self.taskSignals['LongitudinalFrontContact'] = q9 self.taskSignals['LateralFrontContact'] = q10 + + def compute_front_wheel_rate(self): + """Calculates the front wheel rate, based on the Jason's data + of front_wheel_contact_point. Alternatively, you can use the + sympy to get the front_wheel_contact_rate directly first.""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + bp = self.bicycleRiderParameters + + lam = bp['lam'] + rF = bp['rF'] + + q4_wheel = q4 * cos(lam) * cos(q2) + + q4_wheel.name = 'SteerAngle_FrontWheel' + q4_wheel.units = 'radian' + self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel + + f = np.vectorize(front_wheel_rate) + + u6 = f(q1, q2, q4, u9, u10, lam, rF) + u6.name = 'FrontWheelRate' + u6.units = 'radian/second' + self.taskSignals['FrontWheelRate'] = u6 + + def compute_front_rear_wheel_contact_forces(self): """Calculate the contact forces for each wheel with respect to inertial frame under @@ -871,6 +903,7 @@ def compute_front_rear_wheel_contact_forces(self): #signals assignment --slip condition V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() #V = self.taskSignals['ForwardSpeed'] + q1 = self.taskSignals['YawAngle'] q2 = self.taskSignals['RollAngle'] q4 = self.taskSignals['SteerAngle'] @@ -880,7 +913,7 @@ def compute_front_rear_wheel_contact_forces(self): u3 = self.taskSignals['PitchRate'] u4 = self.taskSignals['SteerRate'] u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['ForwardSpeed']/(-rf) + u6 = self.taskSignals['FrontWheelRate'] u7 = self.taskSignals['LongitudinalRearContactRate'] u8 = self.taskSignals['LateralRearContactRate'] u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() From 16e06fcd9dc3afa51d2b7a77faa65e04675cc1eb Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 1 Oct 2012 15:23:39 -0700 Subject: [PATCH 12/37] Deleted the attributes in the task_signals(self) function --- bicycledataprocessor/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index e5d79c9..8a01d48 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -736,8 +736,6 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() - self.compute_front_wheel_rate() - self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' From 91716963ca35769f5ef63e0c53e1872d3ed6937c Mon Sep 17 00:00:00 2001 From: StefenYin Date: Tue, 2 Oct 2012 00:01:16 -0700 Subject: [PATCH 13/37] Updatedthe default.cfg --- bicycledataprocessor/main.py | 219 ++++++++++++++++++----------------- 1 file changed, 112 insertions(+), 107 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 8a01d48..65c4322 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -736,6 +736,10 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() + self.compute_front_wheel_rate() + self.steer_torque_slip() + self.contact_forces_slip() + self.contact_forces_nonslip() self.topSig = 'task' @@ -850,134 +854,135 @@ def compute_front_wheel_contact_points(self): self.taskSignals['LateralFrontContact'] = q10 - def compute_front_wheel_rate(self): - """Calculates the front wheel rate, based on the Jason's data - of front_wheel_contact_point. Alternatively, you can use the - sympy to get the front_wheel_contact_rate directly first.""" + def compute_front_wheel_rate(self): + """Calculates the front wheel rate, based on the Jason's data + of front_wheel_contact_point. Alternatively, you can use the + sympy to get the front_wheel_contact_rate directly first.""" - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() - bp = self.bicycleRiderParameters + bp = self.bicycleRiderParameters - lam = bp['lam'] - rF = bp['rF'] + lam = bp['lam'] + rF = bp['rF'] - q4_wheel = q4 * cos(lam) * cos(q2) + q4_wheel = q4 * cos(lam) * cos(q2) + q4_wheel.name = 'SteerAngle_FrontWheel' + q4_wheel.units = 'radian' + self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel - q4_wheel.name = 'SteerAngle_FrontWheel' - q4_wheel.units = 'radian' - self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel + f = np.vectorize(front_wheel_rate) + + u6 = f(q1, q2, q4, u9, u10, lam, rF) + + u6.name = 'FrontWheelRate' + u6.units = 'radian/second' + self.taskSignals['FrontWheelRate'] = u6 + + + def compute_front_rear_wheel_contact_forces(self): + """Calculate the contact forces for each + wheel with respect to inertial frame under + the slip and nonslip condition. Also, provide + the steer torque T4.""" - f = np.vectorize(front_wheel_rate) + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) - u6 = f(q1, q2, q4, u9, u10, lam, rF) - u6.name = 'FrontWheelRate' - u6.units = 'radian/second' - self.taskSignals['FrontWheelRate'] = u6 - - - def compute_front_rear_wheel_contact_forces(self): - """Calculate the contact forces for each - wheel with respect to inertial frame under - the slip and nonslip condition. Also, provide the steer torque T4.""" - - #parameters - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(self.bicycleRiderParameters) - - l1 = mp['l1'] - l2 = mp['l2'] - mc = mp['mc'] - ic11 = mp['ic11'] - ic22 = mp['ic22'] - ic33 = mp['ic33'] - ic31 = mp['ic31'] - rf = mp['rf'] - - #signals assignment --slip condition - V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() - #V = self.taskSignals['ForwardSpeed'] - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - u1 = self.taskSignals['YawRate'] - u2 = self.taskSignals['RollRate'] - u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] - u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] - u7 = self.taskSignals['LongitudinalRearContactRate'] - u8 = self.taskSignals['LateralRearContactRate'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - u1d = u1.time_derivative() - u2d = u2.time_derivative() - u3d = u3.time_derivative() - u4d = u4.time_derivative() - u5d = u5.time_derivative() - u6d = u6.time_derivative() - u7d = u7.time_derivative() - u8d = u8.time_derivative() - u9d = u9.time_derivative() - u10d = u10.time_derivative() + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + rf = mp['rf'] + + #signals assignment --slip condition + V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() + #V = self.taskSignals['ForwardSpeed'] - #import function - f_1 = np.vectorize(steer_torque_slip) - f_2 = np.vectorize(contact_forces_slip) + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = u7.time_derivative() + u8d = u8.time_derivative() + u9d = u9.time_derivative() + u10d = u10.time_derivative() - f_3 = np.vectorize(contact_forces_nonslip) + #import function + f_1 = np.vectorize(steer_torque_slip) + f_2 = np.vectorize(contact_forces_slip) + + f_3 = np.vectorize(contact_forces_nonslip) - #calculation - #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + #calculation + #steer torque slip + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - #attributes: name and units, and taskSignals - T4_slip.name = 'SteerTorque_Slip' - T4_slip.units = 'newton*meter' - self.taskSignals[T4_slip.name] = T4_slip + #attributes: name and units, and taskSignals + T4_slip.name = 'SteerTorque_Slip' + T4_slip.units = 'newton*meter' + self.taskSignals[T4_slip.name] = T4_slip - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' - Fx_r_s.units = 'newton' - self.taskSignals[Fx_r_s.name] = Fx_r_s + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s - Fy_r_s.name = 'LateralRearContactForce_Slip' - Fy_r_s.units = 'newton' - self.taskSignals[Fy_r_s.name] = Fy_r_s + Fy_r_s.name = 'LateralRearContactForce_Slip' + Fy_r_s.units = 'newton' + self.taskSignals[Fy_r_s.name] = Fy_r_s - Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' - Fx_f_s.units = 'newton' - self.taskSignals[Fx_f_s.name] = Fx_f_s + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' + Fx_f_s.units = 'newton' + self.taskSignals[Fx_f_s.name] = Fx_f_s - Fy_f_s.name = 'LateralFrontContactForce_Slip' - Fy_f_s.units = 'newton' - self.taskSignals[Fy_f_s.name] = Fy_f_s + Fy_f_s.name = 'LateralFrontContactForce_Slip' + Fy_f_s.units = 'newton' + self.taskSignals[Fy_f_s.name] = Fy_f_s - Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' - Fx_r_ns.units = 'newton' - self.taskSignals[Fx_r_ns.name] = Fx_r_ns + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' + Fx_r_ns.units = 'newton' + self.taskSignals[Fx_r_ns.name] = Fx_r_ns - Fy_r_ns.name = 'LateralRearContactForce_Nonslip' - Fy_r_ns.units = 'newton' - self.taskSignals[Fy_r_ns.name] = Fy_r_ns + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' + Fy_r_ns.units = 'newton' + self.taskSignals[Fy_r_ns.name] = Fy_r_ns - Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' - Fx_f_ns.units = 'newton' - self.taskSignals[Fx_f_ns.name] = Fx_f_ns + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' + Fx_f_ns.units = 'newton' + self.taskSignals[Fx_f_ns.name] = Fx_f_ns - Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' - Fy_f_ns.units = 'newton' - self.taskSignals[Fy_f_ns.name] = Fy_f_ns + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' + Fy_f_ns.units = 'newton' + self.taskSignals[Fy_f_ns.name] = Fy_f_ns def compute_rear_wheel_contact_rates(self): From aa548eceb108a57f54bc440dc17d08fb7722482c Mon Sep 17 00:00:00 2001 From: StefenYin Date: Wed, 3 Oct 2012 11:47:41 -0700 Subject: [PATCH 14/37] Removed the whitespace and Changed the type, and Fixed the task_signals() --- bicycledataprocessor/main.py | 56 ++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 65c4322..2c8b2ed 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -24,7 +24,9 @@ from tables import NoSuchNodeError import dtk.process as process -from dtk.bicycle import front_contact, benchmark_to_moore, front_wheel_rate, steer_torque_slip, contact_forces_slip, contact_forces_nonslip +from dtk.bicycle import front_contact, benchmark_to_moore +from dtk.bicycle import front_wheel_rate, steer_torque_slip, + contact_forces_slip, contact_forces_nonslip import bicycleparameters as bp @@ -731,15 +733,13 @@ def task_signals(self): print('Extracting the task portion from the data.') self.extract_task() - # compute task specific variables + # compute task specific variables* self.compute_yaw_angle() self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() self.compute_front_wheel_rate() - self.steer_torque_slip() - self.contact_forces_slip() - self.contact_forces_nonslip() + self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' @@ -858,27 +858,27 @@ def compute_front_wheel_rate(self): """Calculates the front wheel rate, based on the Jason's data of front_wheel_contact_point. Alternatively, you can use the sympy to get the front_wheel_contact_rate directly first.""" - + q1 = self.taskSignals['YawAngle'] q2 = self.taskSignals['RollAngle'] q4 = self.taskSignals['SteerAngle'] u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() u10 = self.taskSignals['LateralFrontContact'].time_derivative() - + bp = self.bicycleRiderParameters - + lam = bp['lam'] rF = bp['rF'] - + q4_wheel = q4 * cos(lam) * cos(q2) q4_wheel.name = 'SteerAngle_FrontWheel' q4_wheel.units = 'radian' self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel - + f = np.vectorize(front_wheel_rate) - + u6 = f(q1, q2, q4, u9, u10, lam, rF) - + u6.name = 'FrontWheelRate' u6.units = 'radian/second' self.taskSignals['FrontWheelRate'] = u6 @@ -889,11 +889,11 @@ def compute_front_rear_wheel_contact_forces(self): wheel with respect to inertial frame under the slip and nonslip condition. Also, provide the steer torque T4.""" - + #parameters bp = self.bicycleRiderParameters mp = benchmark_to_moore(self.bicycleRiderParameters) - + l1 = mp['l1'] l2 = mp['l2'] mc = mp['mc'] @@ -904,14 +904,11 @@ def compute_front_rear_wheel_contact_forces(self): rf = mp['rf'] #signals assignment --slip condition - V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() - #V = self.taskSignals['ForwardSpeed'] - - q1 = self.taskSignals['YawAngle'] + V = self.taskSignals['ForwardSpeed'].mean() + q2 = self.taskSignals['RollAngle'] q4 = self.taskSignals['SteerAngle'] - - u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] u3 = self.taskSignals['PitchRate'] u4 = self.taskSignals['SteerRate'] @@ -921,8 +918,7 @@ def compute_front_rear_wheel_contact_forces(self): u8 = self.taskSignals['LateralRearContactRate'] u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - u1d = u1.time_derivative() + u2d = u2.time_derivative() u3d = u3.time_derivative() u4d = u4.time_derivative() @@ -932,30 +928,28 @@ def compute_front_rear_wheel_contact_forces(self): u8d = u8.time_derivative() u9d = u9.time_derivative() u10d = u10.time_derivative() - - #import function + f_1 = np.vectorize(steer_torque_slip) f_2 = np.vectorize(contact_forces_slip) - f_3 = np.vectorize(contact_forces_nonslip) - + #calculation #steer torque slip T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - + #attributes: name and units, and taskSignals T4_slip.name = 'SteerTorque_Slip' T4_slip.units = 'newton*meter' self.taskSignals[T4_slip.name] = T4_slip - + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' Fx_r_s.units = 'newton' self.taskSignals[Fx_r_s.name] = Fx_r_s - + Fy_r_s.name = 'LateralRearContactForce_Slip' Fy_r_s.units = 'newton' self.taskSignals[Fy_r_s.name] = Fy_r_s From 8606f491b3b84b83a7b44a0f6aadd2ea3694580e Mon Sep 17 00:00:00 2001 From: StefenYin Date: Sat, 6 Oct 2012 19:22:43 -0700 Subject: [PATCH 15/37] Added compute_front_wheel_yaw_angle func, and updated the compute_front_wheel_rate func --- bicycledataprocessor/main.py | 2944 +++++++++++++++++----------------- 1 file changed, 1478 insertions(+), 1466 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 2c8b2ed..9bdf33a 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -9,11 +9,11 @@ # debugging #try: - #from IPython.core.debugger import Tracer + #from IPython.core.debugger import Tracer #except ImportError: - #pass + #pass #else: - #set_trace = Tracer() + #set_trace = Tracer() # dependencies import numpy as np @@ -25,8 +25,8 @@ import dtk.process as process from dtk.bicycle import front_contact, benchmark_to_moore -from dtk.bicycle import front_wheel_rate, steer_torque_slip, - contact_forces_slip, contact_forces_nonslip +from dtk.bicycle import front_wheel_yaw_angle, front_wheel_rate, steer_torque_slip, + contact_forces_slip, contact_forces_nonslip import bicycleparameters as bp @@ -39,1475 +39,1487 @@ config.read(os.path.join(os.path.dirname(__file__), '..', 'defaults.cfg')) class Signal(np.ndarray): - """ - A subclass of ndarray for collecting the data for a single signal in a run. - - Attributes - ---------- - conversions : dictionary - A mapping for unit conversions. - name : str - The name of the signal. Should be CamelCase. - runid : str - A five digit identification number associated with the - trial this signal was collected from (e.g. '00104'). - sampleRate : float - The sample rate in hertz of the signal. - source : str - The source of the data. This should be 'NI' for the - National Instruments USB-6218 and 'VN' for the VN-100 IMU. - units : str - The physcial units of the signal. These should be specified - as lowercase complete words using only multiplication and - division symbols (e.g. 'meter/second/second'). - Signal.conversions will show the avialable options. - - Methods - ------- - plot() - Plot's the signal versus time and returns the line. - frequency() - Returns the frequency spectrum of the signal. - time_derivative() - Returns the time derivative of the signal. - filter(frequency) - Returns the low passed filter of the signal. - truncate(tau) - Interpolates and truncates the signal the based on the time shift, - `tau`, and the signal source. - as_dictionary - Returns a dictionary of the metadata of the signal. - convert_units(units) - Returns a signal with different units. `conversions` specifies the - available options. - - """ - - # define some basic unit converions - conversions = {'degree->radian': pi / 180., - 'degree/second->radian/second': pi / 180., - 'degree/second/second->radian/second/second': pi / 180., - 'inch*pound->newton*meter': 25.4 / 1000. * 4.44822162, - 'pound->newton': 4.44822162, - 'feet/second->meter/second': 12. * 2.54 / 100., - 'mile/hour->meter/second': 0.00254 * 12. / 5280. / 3600.} - - def __new__(cls, inputArray, metadata): - """ - Returns an instance of the Signal class with the additional signal - data. - - Parameters - ---------- - inputArray : ndarray, shape(n,) - A one dimension array representing a single variable's time - history. - metadata : dictionary - This dictionary contains the metadata for the signal. - name : str - The name of the signal. Should be CamelCase. - runid : str - A five digit identification number associated with the - trial this experiment was collected at (e.g. '00104'). - sampleRate : float - The sample rate in hertz of the signal. - source : str - The source of the data. This should be 'NI' for the - National Instruments USB-6218 and 'VN' for the VN-100 IMU. - units : str - The physcial units of the signal. These should be specified - as lowercase complete words using only multiplication and - division symbols (e.g. 'meter/second/second'). - Signal.conversions will show the avialable options. - - Raises - ------ - ValueError - If `inputArray` is not a vector. - - """ - if len(inputArray.shape) > 1: - raise ValueError('Signals must be arrays of one dimension.') - # cast the input array into the Signal class - obj = np.asarray(inputArray).view(cls) - # add the metadata to the object - obj.name = metadata['name'] - obj.runid = metadata['runid'] - obj.sampleRate = metadata['sampleRate'] - obj.source = metadata['source'] - obj.units = metadata['units'] - return obj - - def __array_finalize__(self, obj): - if obj is None: return - self.name = getattr(obj, 'name', None) - self.runid = getattr(obj, 'runid', None) - self.sampleRate = getattr(obj, 'sampleRate', None) - self.source = getattr(obj, 'source', None) - self.units = getattr(obj, 'units', None) - - def __array_wrap__(self, outputArray, context=None): - # doesn't support these things in basic ufunc calls...maybe one day - # That means anytime you add, subtract, multiply, divide, etc, the - # following are not retained. - outputArray.name = None - outputArray.source = None - outputArray.units = None - return np.ndarray.__array_wrap__(self, outputArray, context) - - def as_dictionary(self): - '''Returns the signal metadata as a dictionary.''' - data = {'runid': self.runid, - 'name': self.name, - 'units': self.units, - 'source': self.source, - 'sampleRate': self.sampleRate} - return data - - def convert_units(self, units): - """ - Returns a signal with the specified units. - - Parameters - ---------- - units : str - The units to convert the signal to. The mapping must be in the - attribute `conversions`. - - Returns - ------- - newSig : Signal - The signal with the desired units. - - """ - if units == self.units: - return self - else: - try: - conversion = self.units + '->' + units - newSig = self * self.conversions[conversion] - except KeyError: - try: - conversion = units + '->' + self.units - newSig = self / self.conversions[conversion] - except KeyError: - raise KeyError(('Conversion from {0} to {1} is not ' + - 'possible or not defined.').format(self.units, units)) - # make the new signal - newSig = Signal(newSig, self.as_dictionary()) - newSig.units = units - - return newSig - - def filter(self, frequency): - """Returns the signal filtered by a low pass Butterworth at the given - frequency.""" - filteredArray = process.butterworth(self.spline(), frequency, self.sampleRate) - return Signal(filteredArray, self.as_dictionary()) - - def frequency(self): - """Returns the frequency content of the signal.""" - return process.freq_spectrum(self.spline(), self.sampleRate) - - def integrate(self, initialCondition=0., detrend=False): - """Integrates the signal using the trapezoidal rule.""" - time = self.time() - # integrate using trapz and adjust with the initial condition - grated = np.hstack((0., cumtrapz(self, x=time))) + initialCondition - # this tries to characterize the drift in the integrated signal. It - # works well for signals from straight line tracking but not - # necessarily for lange change. - if detrend is True: - def line(x, a, b, c): - return a * x**2 + b * x + c - popt, pcov = curve_fit(line, time, grated) - grated = grated - line(time, popt[0], popt[1], popt[2]) - grated = Signal(grated, self.as_dictionary()) - grated.units = self.units + '*second' - grated.name = self.name + 'Int' - return grated - - def plot(self, show=True): - """Plots and returns the signal versus time.""" - time = self.time() - line = plt.plot(time, self) - if show: - plt.xlabel('Time [second]') - plt.ylabel('{0} [{1}]'.format(self.name, self.units)) - plt.title('Signal plot during run {0}'.format(self.runid)) - plt.show() - return line - - def spline(self): - """Returns the signal with nans replaced by the results of a cubic - spline.""" - splined = process.spline_over_nan(self.time(), self) - return Signal(splined, self.as_dictionary()) - - def subtract_mean(self): - """Returns the mean subtracted data.""" - return Signal(process.subtract_mean(self), self.as_dictionary()) - - def time(self): - """Returns the time vector of the signal.""" - return sigpro.time_vector(len(self), self.sampleRate) - - def time_derivative(self): - """Returns the time derivative of the signal.""" - # caluculate the numerical time derivative - dsdt = process.derivative(self.time(), self, method='combination') - # map the metadata from self onto the derivative - dsdt = Signal(dsdt, self.as_dictionary()) - dsdt.name = dsdt.name + 'Dot' - dsdt.units = dsdt.units + '/second' - return dsdt - - def truncate(self, tau): - '''Returns the shifted and truncated signal based on the provided - timeshift, tau.''' - # this is now an ndarray instead of a Signal - return Signal(sigpro.truncate_data(self, tau), self.as_dictionary()) + """ + A subclass of ndarray for collecting the data for a single signal in a run. + + Attributes + ---------- + conversions : dictionary + A mapping for unit conversions. + name : str + The name of the signal. Should be CamelCase. + runid : str + A five digit identification number associated with the + trial this signal was collected from (e.g. '00104'). + sampleRate : float + The sample rate in hertz of the signal. + source : str + The source of the data. This should be 'NI' for the + National Instruments USB-6218 and 'VN' for the VN-100 IMU. + units : str + The physcial units of the signal. These should be specified + as lowercase complete words using only multiplication and + division symbols (e.g. 'meter/second/second'). + Signal.conversions will show the avialable options. + + Methods + ------- + plot() + Plot's the signal versus time and returns the line. + frequency() + Returns the frequency spectrum of the signal. + time_derivative() + Returns the time derivative of the signal. + filter(frequency) + Returns the low passed filter of the signal. + truncate(tau) + Interpolates and truncates the signal the based on the time shift, + `tau`, and the signal source. + as_dictionary + Returns a dictionary of the metadata of the signal. + convert_units(units) + Returns a signal with different units. `conversions` specifies the + available options. + + """ + + # define some basic unit converions + conversions = {'degree->radian': pi / 180., + 'degree/second->radian/second': pi / 180., + 'degree/second/second->radian/second/second': pi / 180., + 'inch*pound->newton*meter': 25.4 / 1000. * 4.44822162, + 'pound->newton': 4.44822162, + 'feet/second->meter/second': 12. * 2.54 / 100., + 'mile/hour->meter/second': 0.00254 * 12. / 5280. / 3600.} + + def __new__(cls, inputArray, metadata): + """ + Returns an instance of the Signal class with the additional signal + data. + + Parameters + ---------- + inputArray : ndarray, shape(n,) + A one dimension array representing a single variable's time + history. + metadata : dictionary + This dictionary contains the metadata for the signal. + name : str + The name of the signal. Should be CamelCase. + runid : str + A five digit identification number associated with the + trial this experiment was collected at (e.g. '00104'). + sampleRate : float + The sample rate in hertz of the signal. + source : str + The source of the data. This should be 'NI' for the + National Instruments USB-6218 and 'VN' for the VN-100 IMU. + units : str + The physcial units of the signal. These should be specified + as lowercase complete words using only multiplication and + division symbols (e.g. 'meter/second/second'). + Signal.conversions will show the avialable options. + + Raises + ------ + ValueError + If `inputArray` is not a vector. + + """ + if len(inputArray.shape) > 1: + raise ValueError('Signals must be arrays of one dimension.') + # cast the input array into the Signal class + obj = np.asarray(inputArray).view(cls) + # add the metadata to the object + obj.name = metadata['name'] + obj.runid = metadata['runid'] + obj.sampleRate = metadata['sampleRate'] + obj.source = metadata['source'] + obj.units = metadata['units'] + return obj + + def __array_finalize__(self, obj): + if obj is None: return + self.name = getattr(obj, 'name', None) + self.runid = getattr(obj, 'runid', None) + self.sampleRate = getattr(obj, 'sampleRate', None) + self.source = getattr(obj, 'source', None) + self.units = getattr(obj, 'units', None) + + def __array_wrap__(self, outputArray, context=None): + # doesn't support these things in basic ufunc calls...maybe one day + # That means anytime you add, subtract, multiply, divide, etc, the + # following are not retained. + outputArray.name = None + outputArray.source = None + outputArray.units = None + return np.ndarray.__array_wrap__(self, outputArray, context) + + def as_dictionary(self): + '''Returns the signal metadata as a dictionary.''' + data = {'runid': self.runid, + 'name': self.name, + 'units': self.units, + 'source': self.source, + 'sampleRate': self.sampleRate} + return data + + def convert_units(self, units): + """ + Returns a signal with the specified units. + + Parameters + ---------- + units : str + The units to convert the signal to. The mapping must be in the + attribute `conversions`. + + Returns + ------- + newSig : Signal + The signal with the desired units. + + """ + if units == self.units: + return self + else: + try: + conversion = self.units + '->' + units + newSig = self * self.conversions[conversion] + except KeyError: + try: + conversion = units + '->' + self.units + newSig = self / self.conversions[conversion] + except KeyError: + raise KeyError(('Conversion from {0} to {1} is not ' + + 'possible or not defined.').format(self.units, units)) + # make the new signal + newSig = Signal(newSig, self.as_dictionary()) + newSig.units = units + + return newSig + + def filter(self, frequency): + """Returns the signal filtered by a low pass Butterworth at the given + frequency.""" + filteredArray = process.butterworth(self.spline(), frequency, self.sampleRate) + return Signal(filteredArray, self.as_dictionary()) + + def frequency(self): + """Returns the frequency content of the signal.""" + return process.freq_spectrum(self.spline(), self.sampleRate) + + def integrate(self, initialCondition=0., detrend=False): + """Integrates the signal using the trapezoidal rule.""" + time = self.time() + # integrate using trapz and adjust with the initial condition + grated = np.hstack((0., cumtrapz(self, x=time))) + initialCondition + # this tries to characterize the drift in the integrated signal. It + # works well for signals from straight line tracking but not + # necessarily for lange change. + if detrend is True: + def line(x, a, b, c): + return a * x**2 + b * x + c + popt, pcov = curve_fit(line, time, grated) + grated = grated - line(time, popt[0], popt[1], popt[2]) + grated = Signal(grated, self.as_dictionary()) + grated.units = self.units + '*second' + grated.name = self.name + 'Int' + return grated + + def plot(self, show=True): + """Plots and returns the signal versus time.""" + time = self.time() + line = plt.plot(time, self) + if show: + plt.xlabel('Time [second]') + plt.ylabel('{0} [{1}]'.format(self.name, self.units)) + plt.title('Signal plot during run {0}'.format(self.runid)) + plt.show() + return line + + def spline(self): + """Returns the signal with nans replaced by the results of a cubic + spline.""" + splined = process.spline_over_nan(self.time(), self) + return Signal(splined, self.as_dictionary()) + + def subtract_mean(self): + """Returns the mean subtracted data.""" + return Signal(process.subtract_mean(self), self.as_dictionary()) + + def time(self): + """Returns the time vector of the signal.""" + return sigpro.time_vector(len(self), self.sampleRate) + + def time_derivative(self): + """Returns the time derivative of the signal.""" + # caluculate the numerical time derivative + dsdt = process.derivative(self.time(), self, method='combination') + # map the metadata from self onto the derivative + dsdt = Signal(dsdt, self.as_dictionary()) + dsdt.name = dsdt.name + 'Dot' + dsdt.units = dsdt.units + '/second' + return dsdt + + def truncate(self, tau): + '''Returns the shifted and truncated signal based on the provided + timeshift, tau.''' + # this is now an ndarray instead of a Signal + return Signal(sigpro.truncate_data(self, tau), self.as_dictionary()) class RawSignal(Signal): - """ - A subclass of Signal for collecting the data for a single raw signal in - a run. - - Attributes - ---------- - sensor : Sensor - Each raw signal has a sensor associated with it. Most sensors contain - calibration data for that sensor/signal. - calibrationType : - - Notes - ----- - This is a class for the signals that are the raw measurement outputs - collected by the BicycleDAQ software and are already stored in the pytables - database file. - - """ - - def __new__(cls, runid, signalName, database): - """ - Returns an instance of the RawSignal class with the additional signal - metadata. - - Parameters - ---------- - runid : str - A five digit - signalName : str - A CamelCase signal name that corresponds to the raw signals output - by BicycleDAQ_. - database : pytables object - The hdf5 database for the instrumented bicycle. - - .. _BicycleDAQ: https://github.com/moorepants/BicycleDAQ - - """ - - # get the tables - rTab = database.root.runTable - sTab = database.root.signalTable - cTab = database.root.calibrationTable - - # get the row number for this particular run id - rownum = get_row_num(runid, rTab) - signal = database.getNode('/rawData/' + runid, name=signalName).read() - - # cast the input array into my subclass of ndarray - obj = np.asarray(signal).view(cls) - - obj.runid = runid - obj.timeStamp = matlab_date_to_object(get_cell(rTab, 'DateTime', - rownum)) - obj.calibrationType, obj.units, obj.source = [(row['calibration'], - row['units'], row['source']) - for row in sTab.where('signal == signalName')][0] - obj.name = signalName - - try: - obj.sensor = Sensor(obj.name, cTab) - except KeyError: - pass - # This just means that there was no sensor associated with that - # signal for calibration purposes. - #print "There is no sensor named {0}.".format(signalName) - - # this assumes that the supply voltage for this signal is the same for - # all sensor calibrations - try: - supplySource = [row['runSupplyVoltageSource'] - for row in cTab.where('name == signalName')][0] - if supplySource == 'na': - obj.supply = [row['runSupplyVoltage'] - for row in cTab.where('name == signalName')][0] - else: - obj.supply = database.getNode('/rawData/' + runid, - name=supplySource).read() - except IndexError: - pass - #print "{0} does not have a supply voltage.".format(signalName) - #print "-" * 79 - - # get the appropriate sample rate - if obj.source == 'NI': - sampRateCol = 'NISampleRate' - elif obj.source == 'VN': - sampRateCol = 'VNavSampleRate' - else: - raise ValueError('{0} is not a valid source.'.format(obj.source)) - - obj.sampleRate = rTab[rownum][rTab.colnames.index(sampRateCol)] - - return obj - - def __array_finalize__(self, obj): - if obj is None: return - self.calibrationType = getattr(obj, 'calibrationType', None) - self.name = getattr(obj, 'name', None) - self.runid = getattr(obj, 'runid', None) - self.sampleRate = getattr(obj, 'sampleRate', None) - self.sensor = getattr(obj, 'sensor', None) - self.source = getattr(obj, 'source', None) - self.units = getattr(obj, 'units', None) - self.timeStamp = getattr(obj, 'timeStamp', None) - - def __array_wrap__(self, outputArray, context=None): - # doesn't support these things in basic ufunc calls...maybe one day - outputArray.calibrationType = None - outputArray.name = None - outputArray.sensor = None - outputArray.source = None - outputArray.units = None - return np.ndarray.__array_wrap__(self, outputArray, context) - - def scale(self): - """ - Returns the scaled signal based on the calibration data for the - supplied date. - - Returns - ------- - : ndarray (n,) - Scaled signal. - - """ - try: - self.calibrationType - except AttributeError: - raise AttributeError("Can't scale without the calibration type") - - # these will need to be changed once we start measuring them - doNotScale = ['LeanPotentiometer', - 'HipPotentiometer', - 'TwistPotentiometer'] - if self.calibrationType in ['none', 'matrix'] or self.name in doNotScale: - #print "Not scaling {0}".format(self.name) - return self - else: - pass - #print "Scaling {0}".format(self.name) - - # pick the largest calibration date without surpassing the run date - calibData = self.sensor.get_data_for_date(self.timeStamp) - - slope = calibData['slope'] - bias = calibData['bias'] - intercept = calibData['offset'] - calibrationSupplyVoltage = calibData['calibrationSupplyVoltage'] - - #print "slope {0}, bias {1}, intercept {2}".format(slope, bias, - #intercept) - - if self.calibrationType == 'interceptStar': - # this is for potentiometers, where the slope is ratiometric - # and zero degrees is always zero volts - calibratedSignal = (calibrationSupplyVoltage / self.supply * - slope * self + intercept) - elif self.calibrationType == 'intercept': - # this is the typical calibration that I use for all the - # sensors that I calibrate myself - calibratedSignal = (calibrationSupplyVoltage / self.supply * - (slope * self + intercept)) - elif self.calibrationType == 'bias': - # this is for the accelerometers and rate gyros that are - # "ratiometric", but I'm still not sure this is correct - calibratedSignal = (slope * (self - self.supply / - calibrationSupplyVoltage * bias)) - else: - raise StandardError("None of the calibration equations worked.") - calibratedSignal.name = calibData['signal'] - calibratedSignal.units = calibData['units'] - calibratedSignal.source = self.source - - return calibratedSignal.view(Signal) - - def plot_scaled(self, show=True): - '''Plots and returns the scaled signal versus time.''' - time = self.time() - scaled = self.scale() - line = plt.plot(time, scaled[1]) - plt.xlabel('Time [s]') - plt.ylabel(scaled[2]) - plt.title('{0} signal during run {1}'.format(scaled[0], - str(self.runid))) - if show: - plt.show() - return line + """ + A subclass of Signal for collecting the data for a single raw signal in + a run. + + Attributes + ---------- + sensor : Sensor + Each raw signal has a sensor associated with it. Most sensors contain + calibration data for that sensor/signal. + calibrationType : + + Notes + ----- + This is a class for the signals that are the raw measurement outputs + collected by the BicycleDAQ software and are already stored in the pytables + database file. + + """ + + def __new__(cls, runid, signalName, database): + """ + Returns an instance of the RawSignal class with the additional signal + metadata. + + Parameters + ---------- + runid : str + A five digit + signalName : str + A CamelCase signal name that corresponds to the raw signals output + by BicycleDAQ_. + database : pytables object + The hdf5 database for the instrumented bicycle. + + .. _BicycleDAQ: https://github.com/moorepants/BicycleDAQ + + """ + + # get the tables + rTab = database.root.runTable + sTab = database.root.signalTable + cTab = database.root.calibrationTable + + # get the row number for this particular run id + rownum = get_row_num(runid, rTab) + signal = database.getNode('/rawData/' + runid, name=signalName).read() + + # cast the input array into my subclass of ndarray + obj = np.asarray(signal).view(cls) + + obj.runid = runid + obj.timeStamp = matlab_date_to_object(get_cell(rTab, 'DateTime', + rownum)) + obj.calibrationType, obj.units, obj.source = [(row['calibration'], + row['units'], row['source']) + for row in sTab.where('signal == signalName')][0] + obj.name = signalName + + try: + obj.sensor = Sensor(obj.name, cTab) + except KeyError: + pass + # This just means that there was no sensor associated with that + # signal for calibration purposes. + #print "There is no sensor named {0}.".format(signalName) + + # this assumes that the supply voltage for this signal is the same for + # all sensor calibrations + try: + supplySource = [row['runSupplyVoltageSource'] + for row in cTab.where('name == signalName')][0] + if supplySource == 'na': + obj.supply = [row['runSupplyVoltage'] + for row in cTab.where('name == signalName')][0] + else: + obj.supply = database.getNode('/rawData/' + runid, + name=supplySource).read() + except IndexError: + pass + #print "{0} does not have a supply voltage.".format(signalName) + #print "-" * 79 + + # get the appropriate sample rate + if obj.source == 'NI': + sampRateCol = 'NISampleRate' + elif obj.source == 'VN': + sampRateCol = 'VNavSampleRate' + else: + raise ValueError('{0} is not a valid source.'.format(obj.source)) + + obj.sampleRate = rTab[rownum][rTab.colnames.index(sampRateCol)] + + return obj + + def __array_finalize__(self, obj): + if obj is None: return + self.calibrationType = getattr(obj, 'calibrationType', None) + self.name = getattr(obj, 'name', None) + self.runid = getattr(obj, 'runid', None) + self.sampleRate = getattr(obj, 'sampleRate', None) + self.sensor = getattr(obj, 'sensor', None) + self.source = getattr(obj, 'source', None) + self.units = getattr(obj, 'units', None) + self.timeStamp = getattr(obj, 'timeStamp', None) + + def __array_wrap__(self, outputArray, context=None): + # doesn't support these things in basic ufunc calls...maybe one day + outputArray.calibrationType = None + outputArray.name = None + outputArray.sensor = None + outputArray.source = None + outputArray.units = None + return np.ndarray.__array_wrap__(self, outputArray, context) + + def scale(self): + """ + Returns the scaled signal based on the calibration data for the + supplied date. + + Returns + ------- + : ndarray (n,) + Scaled signal. + + """ + try: + self.calibrationType + except AttributeError: + raise AttributeError("Can't scale without the calibration type") + + # these will need to be changed once we start measuring them + doNotScale = ['LeanPotentiometer', + 'HipPotentiometer', + 'TwistPotentiometer'] + if self.calibrationType in ['none', 'matrix'] or self.name in doNotScale: + #print "Not scaling {0}".format(self.name) + return self + else: + pass + #print "Scaling {0}".format(self.name) + + # pick the largest calibration date without surpassing the run date + calibData = self.sensor.get_data_for_date(self.timeStamp) + + slope = calibData['slope'] + bias = calibData['bias'] + intercept = calibData['offset'] + calibrationSupplyVoltage = calibData['calibrationSupplyVoltage'] + + #print "slope {0}, bias {1}, intercept {2}".format(slope, bias, + #intercept) + + if self.calibrationType == 'interceptStar': + # this is for potentiometers, where the slope is ratiometric + # and zero degrees is always zero volts + calibratedSignal = (calibrationSupplyVoltage / self.supply * + slope * self + intercept) + elif self.calibrationType == 'intercept': + # this is the typical calibration that I use for all the + # sensors that I calibrate myself + calibratedSignal = (calibrationSupplyVoltage / self.supply * + (slope * self + intercept)) + elif self.calibrationType == 'bias': + # this is for the accelerometers and rate gyros that are + # "ratiometric", but I'm still not sure this is correct + calibratedSignal = (slope * (self - self.supply / + calibrationSupplyVoltage * bias)) + else: + raise StandardError("None of the calibration equations worked.") + calibratedSignal.name = calibData['signal'] + calibratedSignal.units = calibData['units'] + calibratedSignal.source = self.source + + return calibratedSignal.view(Signal) + + def plot_scaled(self, show=True): + '''Plots and returns the scaled signal versus time.''' + time = self.time() + scaled = self.scale() + line = plt.plot(time, scaled[1]) + plt.xlabel('Time [s]') + plt.ylabel(scaled[2]) + plt.title('{0} signal during run {1}'.format(scaled[0], + str(self.runid))) + if show: + plt.show() + return line class Sensor(): - """This class is a container for calibration data for a sensor.""" - - def __init__(self, name, calibrationTable): - """ - Initializes this sensor class. - - Parameters - ---------- - name : string - The CamelCase name of the sensor (e.g. SteerTorqueSensor). - calibrationTable : pyTables table object - This is the calibration data table that contains all the data taken - during calibrations. - - """ - self.name = name - self._store_calibration_data(calibrationTable) - - def _store_calibration_data(self, calibrationTable): - """ - Stores a dictionary of calibration data for the sensor for all - calibration dates in the object. - - Parameters - ---------- - calibrationTable : pyTables table object - This is the calibration data table that contains all the data taken - during calibrations. - - """ - self.data = {} - - for row in calibrationTable.iterrows(): - if self.name == row['name']: - self.data[row['calibrationID']] = {} - for col in calibrationTable.colnames: - self.data[row['calibrationID']][col] = row[col] - - if self.data == {}: - raise KeyError(('{0} is not a valid sensor ' + - 'name').format(self.name)) - - def get_data_for_date(self, runDate): - """ - Returns the calibration data for the sensor for the most recent - calibration relative to `runDate`. - - Parameters - ---------- - runDate : datetime object - This is the date of the run that the calibration data is needed - for. - - Returns - ------- - calibData : dictionary - A dictionary containing the sensor calibration data for the - calibration closest to but not past `runDate`. - - Notes - ----- - This method will select the calibration data for the date closest to - but not past `runDate`. **All calibrations must be taken before the - runs.** - - """ - # make a list of calibration ids and time stamps - dateIdPairs = [(k, matlab_date_to_object(v['timeStamp'])) - for k, v in self.data.iteritems()] - # sort the pairs with the most recent date first - dateIdPairs.sort(key=lambda x: x[1], reverse=True) - # go through the list and return the index at which the calibration - # date is larger than the run date - for i, pair in enumerate(dateIdPairs): - if runDate >= pair[1]: - break - return self.data[dateIdPairs[i][0]] + """This class is a container for calibration data for a sensor.""" + + def __init__(self, name, calibrationTable): + """ + Initializes this sensor class. + + Parameters + ---------- + name : string + The CamelCase name of the sensor (e.g. SteerTorqueSensor). + calibrationTable : pyTables table object + This is the calibration data table that contains all the data taken + during calibrations. + + """ + self.name = name + self._store_calibration_data(calibrationTable) + + def _store_calibration_data(self, calibrationTable): + """ + Stores a dictionary of calibration data for the sensor for all + calibration dates in the object. + + Parameters + ---------- + calibrationTable : pyTables table object + This is the calibration data table that contains all the data taken + during calibrations. + + """ + self.data = {} + + for row in calibrationTable.iterrows(): + if self.name == row['name']: + self.data[row['calibrationID']] = {} + for col in calibrationTable.colnames: + self.data[row['calibrationID']][col] = row[col] + + if self.data == {}: + raise KeyError(('{0} is not a valid sensor ' + + 'name').format(self.name)) + + def get_data_for_date(self, runDate): + """ + Returns the calibration data for the sensor for the most recent + calibration relative to `runDate`. + + Parameters + ---------- + runDate : datetime object + This is the date of the run that the calibration data is needed + for. + + Returns + ------- + calibData : dictionary + A dictionary containing the sensor calibration data for the + calibration closest to but not past `runDate`. + + Notes + ----- + This method will select the calibration data for the date closest to + but not past `runDate`. **All calibrations must be taken before the + runs.** + + """ + # make a list of calibration ids and time stamps + dateIdPairs = [(k, matlab_date_to_object(v['timeStamp'])) + for k, v in self.data.iteritems()] + # sort the pairs with the most recent date first + dateIdPairs.sort(key=lambda x: x[1], reverse=True) + # go through the list and return the index at which the calibration + # date is larger than the run date + for i, pair in enumerate(dateIdPairs): + if runDate >= pair[1]: + break + return self.data[dateIdPairs[i][0]] class Run(): - """The fluppin fundamental class for a run.""" - - def __init__(self, runid, dataset, pathToParameterData=None, - forceRecalc=False, filterFreq=None, store=True): - """Loads the raw and processed data for a run if available otherwise it - generates the processed data from the raw data. - - Parameters - ---------- - runid : int or str - The run id should be an integer, e.g. 5, or a five digit string with - leading zeros, e.g. '00005'. - dataset : DataSet - A DataSet object with at least some raw data. - pathToParameterData : string, {'', None}, optional - The path to a data directory for the BicycleParameters package. It - should contain the bicycles and riders used in the experiments. - forceRecalc : boolean, optional, default = False - If true then it will force a recalculation of all the processed - data. - filterSigs : float, optional, default = None - If true all of the processed signals will be low pass filtered with - a second order Butterworth filter at the given filter frequency. - store : boolean, optional, default = True - If true the resulting task signals will be stored in the database. - - """ - - if pathToParameterData is None: - pathToParameterData = config.get('data', 'pathToParameters') - - print "Initializing the run object." - - self.filterFreq = filterFreq - - dataset.open() - dataTable = dataset.database.root.runTable - signalTable = dataset.database.root.signalTable - taskTable = dataset.database.root.taskTable - - runid = run_id_string(runid) - - # get the row number for this particular run id - rownum = get_row_num(runid, dataTable) - - # make some dictionaries to store all the data - self.metadata = {} - self.rawSignals = {} - - # make lists of the input and output signals - rawDataCols = [x['signal'] for x in - signalTable.where("isRaw == True")] - computedCols = [x['signal'] for x in - signalTable.where("isRaw == False")] - - # store the metadata for this run - print "Loading metadata from the database." - for col in dataTable.colnames: - if col not in (rawDataCols + computedCols): - self.metadata[col] = get_cell(dataTable, col, rownum) - - print "Loading the raw signals from the database." - for col in rawDataCols: - # rawDataCols includes all possible raw signals, but every run - # doesn't have all the signals, so skip the ones that aren't there - try: - self.rawSignals[col] = RawSignal(runid, col, dataset.database) - except NoSuchNodeError: - pass - - if self.metadata['Rider'] != 'None': - self.load_rider(pathToParameterData) - - self.bumpLength = 1.0 # 1 meter - - # Try to load the task signals if they've already been computed. If - # they aren't in the database, the filter frequencies don't match or - # forceRecalc is true the then compute them. This may save some time - # when repeatedly loading runs for analysis. - self.taskFromDatabase = False - try: - runGroup = dataset.database.root.taskData._f_getChild(runid) - except NoSuchNodeError: - forceRecalc = True - else: - # The filter frequency stored in the task table is either a nan - # value or a valid float. If the stored filter frequency is not the - # same as the the one passed to Run, then a recalculation should be - # forced. - taskRowNum = get_row_num(runid, taskTable) - storedFreq = taskTable.cols.FilterFrequency[taskRowNum] - self.taskSignals = {} - if filterFreq is None: - newFilterFreq = np.nan - else: - newFilterFreq = filterFreq - - if np.isnan(newFilterFreq) and np.isnan(storedFreq): - for node in runGroup._f_walkNodes(): - meta = {k : node._f_getAttr(k) for k in ['units', 'name', - 'runid', 'sampleRate', 'source']} - self.taskSignals[node.name] = Signal(node[:], meta) - self.taskFromDatabase = True - elif np.isnan(newFilterFreq) or np.isnan(storedFreq): - forceRecalc = True - else: - if abs(storedFreq - filterFreq) < 1e-10: - for node in runGroup._f_walkNodes(): - meta = {k : node._f_getAttr(k) for k in ['units', 'name', - 'runid', 'sampleRate', 'source']} - self.taskSignals[node.name] = Signal(node[:], meta) - self.taskFromDatabase = True - else: - forceRecalc = True - - dataset.close() - - if forceRecalc == True: - try: - del self.taskSignals - except AttributeError: - pass - self.process_raw_signals() - - # store the task signals in the database if they are newly computed - if (store == True and self.taskFromDatabase == False - and self.topSig == 'task'): - taskMeta = { - 'Duration' : - self.taskSignals['ForwardSpeed'].time()[-1], - 'FilterFrequency' : self.filterFreq, - 'MeanSpeed' : self.taskSignals['ForwardSpeed'].mean(), - 'RunID' : self.metadata['RunID'], - 'StdSpeed' : self.taskSignals['ForwardSpeed'].std(), - 'Tau' : self.tau, - } - dataset.add_task_signals(self.taskSignals, taskMeta) - - # tell the user about the run - print self - - def process_raw_signals(self): - """Processes the raw signals as far as possible and filters the - result if a cutoff frequency was specified.""" - - print "Computing signals from raw data." - self.calibrate_signals() - - # the following maneuvers should never be calculated beyond the - # calibrated signals - maneuver = self.metadata['Maneuver'] - con1 = maneuver != 'Steer Dynamics Test' - con2 = maneuver != 'System Test' - con3 = maneuver != 'Static Calibration' - if con1 and con2 and con3: - self.compute_time_shift() - self.check_time_shift(0.15) - self.truncate_signals() - self.compute_signals() - self.task_signals() - - if self.filterFreq is not None: - self.filter_top_signals(self.filterFreq) - - def filter_top_signals(self, filterFreq): - """Filters the top most signals with a low pass filter.""" - - if self.topSig == 'task': - print('Filtering the task signals.') - for k, v in self.taskSignals.items(): - self.taskSignals[k] = v.filter(filterFreq) - elif self.topSig == 'computed': - print('Filtering the computed signals.') - for k, v in self.computedSignals.items(): - self.computedSignals[k] = v.filter(filterFreq) - elif self.topSig == 'calibrated': - print('Filtering the calibrated signals.') - for k, v in self.calibratedSignals.items(): - self.calibratedSignals[k] = v.filter(filterFreq) - - def calibrate_signals(self): - """Calibrates the raw signals.""" - - # calibrate the signals for the run - self.calibratedSignals = {} - for sig in self.rawSignals.values(): - calibSig = sig.scale() - self.calibratedSignals[calibSig.name] = calibSig - - self.topSig = 'calibrated' - - def task_signals(self): - """Computes the task signals.""" - print('Extracting the task portion from the data.') - self.extract_task() - - # compute task specific variables* - self.compute_yaw_angle() - self.compute_rear_wheel_contact_rates() - self.compute_rear_wheel_contact_points() - self.compute_front_wheel_contact_points() - self.compute_front_wheel_rate() - self.compute_front_rear_wheel_contact_forces() - - self.topSig = 'task' - - def compute_signals(self): - """Computes the task independent quantities.""" - - self.computedSignals ={} - # transfer some of the signals to computed - noChange = ['FiveVolts', - 'PushButton', - 'RearWheelRate', - 'RollAngle', - 'SteerAngle', - 'ThreeVolts'] - for sig in noChange: - if sig in ['RollAngle', 'SteerAngle']: - self.computedSignals[sig] =\ - self.truncatedSignals[sig].convert_units('radian') - else: - self.computedSignals[sig] = self.truncatedSignals[sig] - - # compute the quantities that aren't task specific - self.compute_pull_force() - self.compute_forward_speed() - self.compute_steer_rate() - self.compute_yaw_roll_pitch_rates() - self.compute_steer_torque() - - def truncate_signals(self): - """Truncates the calibrated signals based on the time shift.""" - - self.truncatedSignals = {} - for name, sig in self.calibratedSignals.items(): - self.truncatedSignals[name] = sig.truncate(self.tau).spline() - self.topSig = 'truncated' - - def compute_time_shift(self): - """Computes the time shift based on the vertical accelerometer - signals.""" - - self.tau = sigpro.find_timeshift( - self.calibratedSignals['AccelerometerAccelerationY'], - self.calibratedSignals['AccelerationZ'], - self.metadata['NISampleRate'], - self.metadata['Speed'], plotError=False) - - def check_time_shift(self, maxNRMS): - """Raises an error if the normalized root mean square of the shifted - accelerometer signals is high.""" - - # Check to make sure the signals were actually good fits by - # calculating the normalized root mean square. If it isn't very - # low, raise an error. - niAcc = self.calibratedSignals['AccelerometerAccelerationY'] - vnAcc = self.calibratedSignals['AccelerationZ'] - vnAcc = vnAcc.truncate(self.tau).spline() - niAcc = niAcc.truncate(self.tau).spline() - # todo: this should probably check the rms of the mean subtracted data - # because both accelerometers don't always give the same value, this - # may work better with a filtered signal too - # todo: this should probably be moved into the time shift code in the - # signalprocessing model - nrms = np.sqrt(np.mean((vnAcc + niAcc)**2)) / (niAcc.max() - niAcc.min()) - if nrms > maxNRMS: - raise TimeShiftError(('The normalized root mean square for this ' + - 'time shift is {}, which is greater '.format(str(nrms)) + - 'than the maximum allowed: {}'.format(str(maxNRMS)))) - - def compute_rear_wheel_contact_points(self): - """Computes the location of the wheel contact points in the ground - plane.""" - - # get the rates - try: - latRate = self.taskSignals['LateralRearContactRate'] - lonRate = self.taskSignals['LongitudinalRearContactRate'] - except AttributeError: - print('At least one of the rates are not available. ' + - 'The YawAngle was not computed.') - else: - # convert to meters per second - latRate = latRate.convert_units('meter/second') - lonRate = lonRate.convert_units('meter/second') - # integrate and try to account for the drift - lat = latRate.integrate(detrend=True) - lon = lonRate.integrate() - # set the new name and units - lat.name = 'LateralRearContact' - lat.units = 'meter' - lon.name = 'LongitudinalRearContact' - lon.units = 'meter' - # store in task signals - self.taskSignals[lat.name] = lat - self.taskSignals[lon.name] = lon - - def compute_front_wheel_contact_points(self): - """Caluculates the front wheel contact points in the ground plane.""" - - q1 = self.taskSignals['LongitudinalRearContact'] - q2 = self.taskSignals['LateralRearContact'] - q3 = self.taskSignals['YawAngle'] - q4 = self.taskSignals['RollAngle'] - q7 = self.taskSignals['SteerAngle'] - - p = benchmark_to_moore(self.bicycleRiderParameters) - - f = np.vectorize(front_contact) - q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], - p['rf']) - - self.taskSignals['LongitudinalFrontContact'] = q9 - self.taskSignals['LateralFrontContact'] = q10 - - - def compute_front_wheel_rate(self): - """Calculates the front wheel rate, based on the Jason's data - of front_wheel_contact_point. Alternatively, you can use the - sympy to get the front_wheel_contact_rate directly first.""" - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - bp = self.bicycleRiderParameters - - lam = bp['lam'] - rF = bp['rF'] - - q4_wheel = q4 * cos(lam) * cos(q2) - q4_wheel.name = 'SteerAngle_FrontWheel' - q4_wheel.units = 'radian' - self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel - - f = np.vectorize(front_wheel_rate) - - u6 = f(q1, q2, q4, u9, u10, lam, rF) - - u6.name = 'FrontWheelRate' - u6.units = 'radian/second' - self.taskSignals['FrontWheelRate'] = u6 - - - def compute_front_rear_wheel_contact_forces(self): - """Calculate the contact forces for each - wheel with respect to inertial frame under - the slip and nonslip condition. Also, provide - the steer torque T4.""" - - #parameters - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(self.bicycleRiderParameters) - - l1 = mp['l1'] - l2 = mp['l2'] - mc = mp['mc'] - ic11 = mp['ic11'] - ic22 = mp['ic22'] - ic33 = mp['ic33'] - ic31 = mp['ic31'] - rf = mp['rf'] - - #signals assignment --slip condition - V = self.taskSignals['ForwardSpeed'].mean() - - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - u2 = self.taskSignals['RollRate'] - u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] - u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] - u7 = self.taskSignals['LongitudinalRearContactRate'] - u8 = self.taskSignals['LateralRearContactRate'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - u2d = u2.time_derivative() - u3d = u3.time_derivative() - u4d = u4.time_derivative() - u5d = u5.time_derivative() - u6d = u6.time_derivative() - u7d = u7.time_derivative() - u8d = u8.time_derivative() - u9d = u9.time_derivative() - u10d = u10.time_derivative() - - f_1 = np.vectorize(steer_torque_slip) - f_2 = np.vectorize(contact_forces_slip) - f_3 = np.vectorize(contact_forces_nonslip) - - #calculation - #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - - #attributes: name and units, and taskSignals - T4_slip.name = 'SteerTorque_Slip' - T4_slip.units = 'newton*meter' - self.taskSignals[T4_slip.name] = T4_slip - - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' - Fx_r_s.units = 'newton' - self.taskSignals[Fx_r_s.name] = Fx_r_s - - Fy_r_s.name = 'LateralRearContactForce_Slip' - Fy_r_s.units = 'newton' - self.taskSignals[Fy_r_s.name] = Fy_r_s - - Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' - Fx_f_s.units = 'newton' - self.taskSignals[Fx_f_s.name] = Fx_f_s - - Fy_f_s.name = 'LateralFrontContactForce_Slip' - Fy_f_s.units = 'newton' - self.taskSignals[Fy_f_s.name] = Fy_f_s - - Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' - Fx_r_ns.units = 'newton' - self.taskSignals[Fx_r_ns.name] = Fx_r_ns - - Fy_r_ns.name = 'LateralRearContactForce_Nonslip' - Fy_r_ns.units = 'newton' - self.taskSignals[Fy_r_ns.name] = Fy_r_ns - - Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' - Fx_f_ns.units = 'newton' - self.taskSignals[Fx_f_ns.name] = Fx_f_ns - - Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' - Fy_f_ns.units = 'newton' - self.taskSignals[Fy_f_ns.name] = Fy_f_ns - - - def compute_rear_wheel_contact_rates(self): - """Calculates the rates of the wheel contact points in the ground - plane.""" - - try: - yawAngle = self.taskSignals['YawAngle'] - rearWheelRate = self.taskSignals['RearWheelRate'] - rR = self.bicycleRiderParameters['rR'] # this should be in meters - except AttributeError: - print('Either the yaw angle, rear wheel rate or ' + - 'front wheel radius is not available. The ' + - 'contact rates were not computed.') - else: - yawAngle = yawAngle.convert_units('radian') - rearWheelRate = rearWheelRate.convert_units('radian/second') - - lon, lat = sigpro.rear_wheel_contact_rate(rR, rearWheelRate, yawAngle) - - lon.name = 'LongitudinalRearContactRate' - lon.units = 'meter/second' - self.taskSignals[lon.name] = lon - - lat.name = 'LateralRearContactRate' - lat.units = 'meter/second' - self.taskSignals[lat.name] = lat - - def compute_yaw_angle(self): - """Computes the yaw angle by integrating the yaw rate.""" - - # get the yaw rate - try: - yawRate = self.taskSignals['YawRate'] - except AttributeError: - print('YawRate is not available. The YawAngle was not computed.') - else: - # convert to radians per second - yawRate = yawRate.convert_units('radian/second') - # integrate and try to account for the drift - yawAngle = yawRate.integrate(detrend=True) - # set the new name and units - yawAngle.name = 'YawAngle' - yawAngle.units = 'radian' - # store in computed signals - self.taskSignals['YawAngle'] = yawAngle - - def compute_steer_torque(self, plot=False): - """Computes the rider applied steer torque. - - Parameters - ---------- - plot : boolean, optional - Default is False, but if True a plot is generated. - - """ - # steer torque - frameAngRate = np.vstack(( - self.truncatedSignals['AngularRateX'], - self.truncatedSignals['AngularRateY'], - self.truncatedSignals['AngularRateZ'])) - frameAngAccel = np.vstack(( - self.truncatedSignals['AngularRateX'].time_derivative(), - self.truncatedSignals['AngularRateY'].time_derivative(), - self.truncatedSignals['AngularRateZ'].time_derivative())) - frameAccel = np.vstack(( - self.truncatedSignals['AccelerationX'], - self.truncatedSignals['AccelerationY'], - self.truncatedSignals['AccelerationZ'])) - handlebarAngRate = self.truncatedSignals['ForkRate'] - handlebarAngAccel = self.truncatedSignals['ForkRate'].time_derivative() - steerAngle = self.truncatedSignals['SteerAngle'] - steerColumnTorque =\ - self.truncatedSignals['SteerTubeTorque'].convert_units('newton*meter') - handlebarMass = self.bicycleRiderParameters['mG'] - handlebarInertia =\ - self.bicycle.steer_assembly_moment_of_inertia(fork=False, - wheel=False, nominal=True) - # this is the distance from the handlebar center of mass to the - # steer axis - w = self.bicycleRiderParameters['w'] - c = self.bicycleRiderParameters['c'] - lam = self.bicycleRiderParameters['lam'] - xG = self.bicycleRiderParameters['xG'] - zG = self.bicycleRiderParameters['zG'] - handlebarCoM = np.array([xG, 0., zG]) - d = bp.geometry.distance_to_steer_axis(w, c, lam, handlebarCoM) - # these are the distances from the point on the steer axis which is - # aligned with the handlebar center of mass to the accelerometer on - # the frame - ds1 = self.bicycle.parameters['Measured']['ds1'] - ds3 = self.bicycle.parameters['Measured']['ds3'] - ds = np.array([ds1, 0., ds3]) # i measured these - # damping and friction values come from Peter's work, I need to verify - # them still - damping = 0.3475 - friction = 0.0861 - - components = sigpro.steer_torque_components( - frameAngRate, frameAngAccel, frameAccel, handlebarAngRate, - handlebarAngAccel, steerAngle, steerColumnTorque, - handlebarMass, handlebarInertia, damping, friction, d, ds) - steerTorque = sigpro.steer_torque(components) - - stDict = {'units':'newton*meter', - 'name':'SteerTorque', - 'runid':self.metadata['RunID'], - 'sampleRate':steerAngle.sampleRate, - 'source':'NA'} - self.computedSignals['SteerTorque'] = Signal(steerTorque, stDict) - - if plot is True: - - time = steerAngle.time() - - hdot = (components['Hdot1'] + components['Hdot2'] + - components['Hdot3'] + components['Hdot4']) - cross = (components['cross1'] + components['cross2'] + - components['cross3']) - - fig = plt.figure() - - frictionAx = fig.add_subplot(4, 1, 1) - frictionAx.plot(time, components['viscous'], - time, components['coulomb'], - time, components['viscous'] + components['coulomb']) - frictionAx.set_ylabel('Torque [N-m]') - frictionAx.legend(('Viscous Friction', 'Coulomb Friction', - 'Total Friction')) - - dynamicAx = fig.add_subplot(4, 1, 2) - dynamicAx.plot(time, hdot, time, cross, time, hdot + cross) - dynamicAx.set_ylabel('Torque [N-m]') - dynamicAx.legend((r'Torque due to $\dot{H}$', - r'Torque due to $r \times m a$', - r'Total Dynamic Torque')) - - additionalAx = fig.add_subplot(4, 1, 3) - additionalAx.plot(time, hdot + cross + components['viscous'] + - components['coulomb'], - label='Total Frictional and Dynamic Torque') - additionalAx.set_ylabel('Torque [N-m]') - additionalAx.legend() - - torqueAx = fig.add_subplot(4, 1, 4) - torqueAx.plot(time, components['steerColumn'], - time, hdot + cross + components['viscous'] + components['coulomb'], - time, steerTorque) - torqueAx.set_xlabel('Time [s]') - torqueAx.set_ylabel('Torque [N-m]') - torqueAx.legend(('Measured Torque', 'Frictional and Dynamic Torque', - 'Rider Applied Torque')) - - plt.show() - - return fig - - def compute_yaw_roll_pitch_rates(self): - """Computes the yaw, roll and pitch rates of the bicycle frame.""" - - try: - omegaX = self.truncatedSignals['AngularRateX'] - omegaY = self.truncatedSignals['AngularRateY'] - omegaZ = self.truncatedSignals['AngularRateZ'] - rollAngle = self.truncatedSignals['RollAngle'] - lam = self.bicycleRiderParameters['lam'] - except AttributeError: - print('All needed signals are not available. ' + - 'Yaw, roll and pitch rates were not computed.') - else: - omegaX = omegaX.convert_units('radian/second') - omegaY = omegaY.convert_units('radian/second') - omegaZ = omegaZ.convert_units('radian/second') - rollAngle = rollAngle.convert_units('radian') - - yr, rr, pr = sigpro.yaw_roll_pitch_rate(omegaX, omegaY, omegaZ, lam, - rollAngle=rollAngle) - yr.units = 'radian/second' - yr.name = 'YawRate' - rr.units = 'radian/second' - rr.name = 'RollRate' - pr.units = 'radian/second' - pr.name = 'PitchRate' - - self.computedSignals['YawRate'] = yr - self.computedSignals['RollRate'] = rr - self.computedSignals['PitchRate'] = pr - - def compute_steer_rate(self): - """Calculate the steer rate from the frame and fork rates.""" - try: - forkRate = self.truncatedSignals['ForkRate'] - omegaZ = self.truncatedSignals['AngularRateZ'] - except AttributeError: - print('ForkRate or AngularRateZ is not available. ' + - 'SteerRate was not computed.') - else: - forkRate = forkRate.convert_units('radian/second') - omegaZ = omegaZ.convert_units('radian/second') - - steerRate = sigpro.steer_rate(forkRate, omegaZ) - steerRate.units = 'radian/second' - steerRate.name = 'SteerRate' - self.computedSignals['SteerRate'] = steerRate - - def compute_forward_speed(self): - """Calculates the magnitude of the main component of velocity of the - center of the rear wheel.""" - - try: - rR = self.bicycleRiderParameters['rR'] - rearWheelRate = self.truncatedSignals['RearWheelRate'] - except AttributeError: - print('rR or RearWheelRate is not availabe. ' + - 'ForwardSpeed was not computed.') - else: - rearWheelRate = rearWheelRate.convert_units('radian/second') - - self.computedSignals['ForwardSpeed'] = -rR * rearWheelRate - self.computedSignals['ForwardSpeed'].units = 'meter/second' - self.computedSignals['ForwardSpeed'].name = 'ForwardSpeed' - - def compute_pull_force(self): - """ - Computes the pull force from the truncated pull force signal. - - """ - try: - pullForce = self.truncatedSignals['PullForce'] - except AttributeError: - print 'PullForce was not available. PullForce was not computed.' - else: - pullForce = pullForce.convert_units('newton') - pullForce.name = 'PullForce' - pullForce.units = 'newton' - self.computedSignals[pullForce.name] = pullForce - - def __str__(self): - '''Prints basic run information to the screen.''' - - line = "=" * 79 - info = 'Run # {0}\nEnvironment: {1}\nRider: {2}\nBicycle: {3}\nSpeed:'\ - '{4}\nManeuver: {5}\nNotes: {6}'.format( - self.metadata['RunID'], - self.metadata['Environment'], - self.metadata['Rider'], - self.metadata['Bicycle'], - self.metadata['Speed'], - self.metadata['Maneuver'], - self.metadata['Notes']) - - return line + '\n' + info + '\n' + line - - def export(self, filetype, directory='exports'): - """ - Exports the computed signals to a file. - - Parameters - ---------- - filetype : str - The type of file to export the data to. Options are 'mat', 'csv', - and 'pickle'. - - """ - - if filetype == 'mat': - if not os.path.exists(directory): - print "Creating {0}".format(directory) - os.makedirs(directory) - exportData = {} - exportData.update(self.metadata) - try: - exportData.update(self.taskSignals) - except AttributeError: - try: - exportData.update(self.truncatedSignals) - except AttributeError: - exportData.update(self.calibratedSignals) - print('Exported calibratedSignals to {}'.format(directory)) - else: - print('Exported truncatedSignals to {}'.format(directory)) - else: - print('Exported taskSignals to {}'.format(directory)) - - filename = pad_with_zeros(str(self.metadata['RunID']), 5) + '.mat' - io.savemat(os.path.join(directory, filename), exportData) - else: - raise NotImplementedError(('{0} method is not available' + - ' yet.').format(filetype)) - - def extract_task(self): - """Slices the computed signals such that data before the end of the - bump is removed and unusable trailng data is removed. - - """ - # get the z acceleration from the VN-100 - acc = -self.truncatedSignals['AccelerometerAccelerationY'].filter(30.) - # find the mean speed during the task (look at one second in the middle - # of the data) - speed = self.computedSignals['ForwardSpeed'] - meanSpeed = speed[len(speed) / 2 - 100:len(speed) / 2 + 100].mean() - wheelbase = self.bicycleRiderParameters['w'] - # find the bump - indices = sigpro.find_bump(acc, acc.sampleRate, meanSpeed, wheelbase, - self.bumpLength) - - - # if it is a pavilion run, then clip the end too - # these are the runs that the length of track method of clipping - # applies to - straight = ['Track Straight Line With Disturbance', - 'Balance With Disturbance', - 'Balance', - 'Track Straight Line'] - if (self.metadata['Environment'] == 'Pavillion Floor' and - self.metadata['Maneuver'] in straight): - - # this is based on the length of the track in the pavilion that we - # measured on September 21st, 2011 - trackLength = 32. - wheelbase - self.bumpLength - end = trackLength / meanSpeed * acc.sampleRate - - # i may need to clip the end based on the forward speed dropping - # below certain threshold around the mean - else: - # if it isn't a pavilion run, don't clip the end - end = -1 - - self.taskSignals = {} - for name, sig in self.computedSignals.items(): - self.taskSignals[name] = sig[indices[2]:end] - - def load_rider(self, pathToParameterData): - """Creates a bicycle/rider attribute which contains the physical - parameters for the bicycle and rider for this run.""" - - print("Loading the bicycle and rider data for " + - "{} on {}".format(self.metadata['Rider'], - self.metadata['Bicycle'])) - - # currently this isn't very generic, it only assumes that there was - # Luke, Jason, and Charlie riding on the instrumented bicycle. - rider = self.metadata['Rider'] - if rider == 'Charlie' or rider == 'Luke': - # Charlie and Luke rode the bike in the same configuration - bicycle = 'Rigidcl' - elif rider == 'Jason' : - bicycle = 'Rigid' - else: - raise StandardError('There are no bicycle parameters ' + - 'for {}'.format(rider)) - - # force a recalculation (but not the period calcs, they take too long) - self.bicycle = bp.Bicycle(bicycle, pathToData=pathToParameterData) - try: - self.bicycle.extras - except AttributeError: - pass - else: - self.bicycle.save_parameters() - # force a recalculation of the human parameters - self.bicycle.add_rider(rider) - if self.bicycle.human is not None: - self.bicycle.save_parameters() - - self.bicycleRiderParameters =\ - bp.io.remove_uncertainties(self.bicycle.parameters['Benchmark']) - - def plot(self, *args, **kwargs): - ''' - Returns a plot of the time series of various signals. - - Parameters - ---------- - signalName : string - These should be strings that correspond to the signals available in - the computed data. If the first character of the string is `-` then - the negative signal will be plotted. You can also scale the values - so by adding a value and an ``*`` such as: ``'-10*RollRate'. The - negative sign always has to come first. - signalType : string, optional - This allows you to plot from the other signal types. Options are - 'task', 'computed', 'truncated', 'calibrated', 'raw'. The default - is 'task'. - - ''' - if not kwargs: - kwargs = {'signalType': 'task'} - - mapping = {} - for x in ['computed', 'truncated', 'calibrated', 'raw', 'task']: - try: - mapping[x] = getattr(self, x + 'Signals') - except AttributeError: - pass - - fig = plt.figure() - ax = fig.add_axes([0.125, 0.125, 0.8, 0.7]) - - leg = [] - for i, arg in enumerate(args): - legName = arg - sign = 1. - # if a negative sign is present - if '-' in arg and arg[0] != '-': - raise ValueError('{} is incorrectly typed'.format(arg)) - elif '-' in arg and arg[0] == '-': - arg = arg[1:] - sign = -1. - # if a multiplication factor is present - if '*' in arg: - mul, arg = arg.split('*') - else: - mul = 1. - signal = sign * float(mul) * mapping[kwargs['signalType']][arg] - ax.plot(signal.time(), signal) - leg.append(legName + ' [' + mapping[kwargs['signalType']][arg].units + ']') - - ax.legend(leg) - runid = pad_with_zeros(str(self.metadata['RunID']), 5) - ax.set_title('Run: ' + runid + ', Rider: ' + self.metadata['Rider'] + - ', Speed: ' + str(self.metadata['Speed']) + 'm/s' + '\n' + - 'Maneuver: ' + self.metadata['Maneuver'] + - ', Environment: ' + self.metadata['Environment'] + '\n' + - 'Notes: ' + self.metadata['Notes']) - - ax.set_xlabel('Time [second]') - - ax.grid() - - return fig - - def plot_wheel_contact(self, show=False): - """Returns a plot of the wheel contact traces. - - Parameters - ---------- - show : boolean - If true the plot will be displayed. - - Returns - ------- - fig : matplotlib.Figure - - """ - - q1 = self.taskSignals['LongitudinalRearContact'] - q2 = self.taskSignals['LateralRearContact'] - q9 = self.taskSignals['LongitudinalFrontContact'] - q10 = self.taskSignals['LateralFrontContact'] - - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - ax.plot(q1, q2, q9, q10) - ax.set_xlabel('Distance [' + q1.units + ']') - ax.set_ylabel('Distance [' + q2.units + ']') - ax.set_ylim((-0.5, 0.5)) - rider = self.metadata['Rider'] - where = self.metadata['Environment'] - speed = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() - maneuver = self.metadata['Maneuver'] - ax.set_title(rider + ', ' + where + ', ' + maneuver + ' @ ' + speed + ' m/s') - - if show is True: - fig.show() - - return fig - - def verify_time_sync(self, show=True, saveDir=None): - """Shows a plot of the acceleration signals that were used to - synchronize the NI and VN data. If it doesn't show a good fit, then - something is wrong. - - Parameters - ---------- - show : boolean - If true, the figure will be displayed. - saveDir : str - The path to a directory in which to save the figure. - - """ - - if self.topSig == 'calibrated': - sigType = 'calibrated' - else: - sigType = 'truncated' - - fig = self.plot('-AccelerometerAccelerationY', 'AccelerationZ', - signalType=sigType) - ax = fig.axes[0] - ax.set_xlim((0, 10)) - title = ax.get_title() - ax.set_title(title + '\nSignal Type: ' + sigType) - if saveDir is not None: - if not os.path.exists(saveDir): - print "Creating {0}".format(saveDir) - os.makedirs(saveDir) - runid = run_id_string(self.metadata['RunID']) - fig.savefig(os.path.join(saveDir, runid + '.png')) - if show is True: - fig.show() - - return fig - - def video(self): - ''' - Plays the video of the run. - - ''' - # get the 5 digit string version of the run id - runid = pad_with_zeros(str(self.metadata['RunID']), 5) - viddir = os.path.join('..', 'Video') - abspath = os.path.abspath(viddir) - # check to see if there is a video for this run - if (runid + '.mp4') in os.listdir(viddir): - path = os.path.join(abspath, runid + '.mp4') - os.system('vlc "' + path + '"') - else: - print "No video for this run" + """The fluppin fundamental class for a run.""" + + def __init__(self, runid, dataset, pathToParameterData=None, + forceRecalc=False, filterFreq=None, store=True): + """Loads the raw and processed data for a run if available otherwise it + generates the processed data from the raw data. + + Parameters + ---------- + runid : int or str + The run id should be an integer, e.g. 5, or a five digit string with + leading zeros, e.g. '00005'. + dataset : DataSet + A DataSet object with at least some raw data. + pathToParameterData : string, {'', None}, optional + The path to a data directory for the BicycleParameters package. It + should contain the bicycles and riders used in the experiments. + forceRecalc : boolean, optional, default = False + If true then it will force a recalculation of all the processed + data. + filterSigs : float, optional, default = None + If true all of the processed signals will be low pass filtered with + a second order Butterworth filter at the given filter frequency. + store : boolean, optional, default = True + If true the resulting task signals will be stored in the database. + + """ + + if pathToParameterData is None: + pathToParameterData = config.get('data', 'pathToParameters') + + print "Initializing the run object." + + self.filterFreq = filterFreq + + dataset.open() + dataTable = dataset.database.root.runTable + signalTable = dataset.database.root.signalTable + taskTable = dataset.database.root.taskTable + + runid = run_id_string(runid) + + # get the row number for this particular run id + rownum = get_row_num(runid, dataTable) + + # make some dictionaries to store all the data + self.metadata = {} + self.rawSignals = {} + + # make lists of the input and output signals + rawDataCols = [x['signal'] for x in + signalTable.where("isRaw == True")] + computedCols = [x['signal'] for x in + signalTable.where("isRaw == False")] + + # store the metadata for this run + print "Loading metadata from the database." + for col in dataTable.colnames: + if col not in (rawDataCols + computedCols): + self.metadata[col] = get_cell(dataTable, col, rownum) + + print "Loading the raw signals from the database." + for col in rawDataCols: + # rawDataCols includes all possible raw signals, but every run + # doesn't have all the signals, so skip the ones that aren't there + try: + self.rawSignals[col] = RawSignal(runid, col, dataset.database) + except NoSuchNodeError: + pass + + if self.metadata['Rider'] != 'None': + self.load_rider(pathToParameterData) + + self.bumpLength = 1.0 # 1 meter + + # Try to load the task signals if they've already been computed. If + # they aren't in the database, the filter frequencies don't match or + # forceRecalc is true the then compute them. This may save some time + # when repeatedly loading runs for analysis. + self.taskFromDatabase = False + try: + runGroup = dataset.database.root.taskData._f_getChild(runid) + except NoSuchNodeError: + forceRecalc = True + else: + # The filter frequency stored in the task table is either a nan + # value or a valid float. If the stored filter frequency is not the + # same as the the one passed to Run, then a recalculation should be + # forced. + taskRowNum = get_row_num(runid, taskTable) + storedFreq = taskTable.cols.FilterFrequency[taskRowNum] + self.taskSignals = {} + if filterFreq is None: + newFilterFreq = np.nan + else: + newFilterFreq = filterFreq + + if np.isnan(newFilterFreq) and np.isnan(storedFreq): + for node in runGroup._f_walkNodes(): + meta = {k : node._f_getAttr(k) for k in ['units', 'name', + 'runid', 'sampleRate', 'source']} + self.taskSignals[node.name] = Signal(node[:], meta) + self.taskFromDatabase = True + elif np.isnan(newFilterFreq) or np.isnan(storedFreq): + forceRecalc = True + else: + if abs(storedFreq - filterFreq) < 1e-10: + for node in runGroup._f_walkNodes(): + meta = {k : node._f_getAttr(k) for k in ['units', 'name', + 'runid', 'sampleRate', 'source']} + self.taskSignals[node.name] = Signal(node[:], meta) + self.taskFromDatabase = True + else: + forceRecalc = True + + dataset.close() + + if forceRecalc == True: + try: + del self.taskSignals + except AttributeError: + pass + self.process_raw_signals() + + # store the task signals in the database if they are newly computed + if (store == True and self.taskFromDatabase == False + and self.topSig == 'task'): + taskMeta = { + 'Duration' : + self.taskSignals['ForwardSpeed'].time()[-1], + 'FilterFrequency' : self.filterFreq, + 'MeanSpeed' : self.taskSignals['ForwardSpeed'].mean(), + 'RunID' : self.metadata['RunID'], + 'StdSpeed' : self.taskSignals['ForwardSpeed'].std(), + 'Tau' : self.tau, + } + dataset.add_task_signals(self.taskSignals, taskMeta) + + # tell the user about the run + print self + + def process_raw_signals(self): + """Processes the raw signals as far as possible and filters the + result if a cutoff frequency was specified.""" + + print "Computing signals from raw data." + self.calibrate_signals() + + # the following maneuvers should never be calculated beyond the + # calibrated signals + maneuver = self.metadata['Maneuver'] + con1 = maneuver != 'Steer Dynamics Test' + con2 = maneuver != 'System Test' + con3 = maneuver != 'Static Calibration' + if con1 and con2 and con3: + self.compute_time_shift() + self.check_time_shift(0.15) + self.truncate_signals() + self.compute_signals() + self.task_signals() + + if self.filterFreq is not None: + self.filter_top_signals(self.filterFreq) + + def filter_top_signals(self, filterFreq): + """Filters the top most signals with a low pass filter.""" + + if self.topSig == 'task': + print('Filtering the task signals.') + for k, v in self.taskSignals.items(): + self.taskSignals[k] = v.filter(filterFreq) + elif self.topSig == 'computed': + print('Filtering the computed signals.') + for k, v in self.computedSignals.items(): + self.computedSignals[k] = v.filter(filterFreq) + elif self.topSig == 'calibrated': + print('Filtering the calibrated signals.') + for k, v in self.calibratedSignals.items(): + self.calibratedSignals[k] = v.filter(filterFreq) + + def calibrate_signals(self): + """Calibrates the raw signals.""" + + # calibrate the signals for the run + self.calibratedSignals = {} + for sig in self.rawSignals.values(): + calibSig = sig.scale() + self.calibratedSignals[calibSig.name] = calibSig + + self.topSig = 'calibrated' + + def task_signals(self): + """Computes the task signals.""" + print('Extracting the task portion from the data.') + self.extract_task() + + # compute task specific variables* + self.compute_yaw_angle() + self.compute_rear_wheel_contact_rates() + self.compute_rear_wheel_contact_points() + self.compute_front_wheel_contact_points() + self.compute_front_wheel_yaw_angle() + self.compute_front_wheel_rate() + self.compute_front_rear_wheel_contact_forces() + + self.topSig = 'task' + + def compute_signals(self): + """Computes the task independent quantities.""" + + self.computedSignals ={} + # transfer some of the signals to computed + noChange = ['FiveVolts', + 'PushButton', + 'RearWheelRate', + 'RollAngle', + 'SteerAngle', + 'ThreeVolts'] + for sig in noChange: + if sig in ['RollAngle', 'SteerAngle']: + self.computedSignals[sig] =\ + self.truncatedSignals[sig].convert_units('radian') + else: + self.computedSignals[sig] = self.truncatedSignals[sig] + + # compute the quantities that aren't task specific + self.compute_pull_force() + self.compute_forward_speed() + self.compute_steer_rate() + self.compute_yaw_roll_pitch_rates() + self.compute_steer_torque() + + def truncate_signals(self): + """Truncates the calibrated signals based on the time shift.""" + + self.truncatedSignals = {} + for name, sig in self.calibratedSignals.items(): + self.truncatedSignals[name] = sig.truncate(self.tau).spline() + self.topSig = 'truncated' + + def compute_time_shift(self): + """Computes the time shift based on the vertical accelerometer + signals.""" + + self.tau = sigpro.find_timeshift( + self.calibratedSignals['AccelerometerAccelerationY'], + self.calibratedSignals['AccelerationZ'], + self.metadata['NISampleRate'], + self.metadata['Speed'], plotError=False) + + def check_time_shift(self, maxNRMS): + """Raises an error if the normalized root mean square of the shifted + accelerometer signals is high.""" + + # Check to make sure the signals were actually good fits by + # calculating the normalized root mean square. If it isn't very + # low, raise an error. + niAcc = self.calibratedSignals['AccelerometerAccelerationY'] + vnAcc = self.calibratedSignals['AccelerationZ'] + vnAcc = vnAcc.truncate(self.tau).spline() + niAcc = niAcc.truncate(self.tau).spline() + # todo: this should probably check the rms of the mean subtracted data + # because both accelerometers don't always give the same value, this + # may work better with a filtered signal too + # todo: this should probably be moved into the time shift code in the + # signalprocessing model + nrms = np.sqrt(np.mean((vnAcc + niAcc)**2)) / (niAcc.max() - niAcc.min()) + if nrms > maxNRMS: + raise TimeShiftError(('The normalized root mean square for this ' + + 'time shift is {}, which is greater '.format(str(nrms)) + + 'than the maximum allowed: {}'.format(str(maxNRMS)))) + + def compute_rear_wheel_contact_points(self): + """Computes the location of the wheel contact points in the ground + plane.""" + + # get the rates + try: + latRate = self.taskSignals['LateralRearContactRate'] + lonRate = self.taskSignals['LongitudinalRearContactRate'] + except AttributeError: + print('At least one of the rates are not available. ' + + 'The YawAngle was not computed.') + else: + # convert to meters per second + latRate = latRate.convert_units('meter/second') + lonRate = lonRate.convert_units('meter/second') + # integrate and try to account for the drift + lat = latRate.integrate(detrend=True) + lon = lonRate.integrate() + # set the new name and units + lat.name = 'LateralRearContact' + lat.units = 'meter' + lon.name = 'LongitudinalRearContact' + lon.units = 'meter' + # store in task signals + self.taskSignals[lat.name] = lat + self.taskSignals[lon.name] = lon + + def compute_front_wheel_contact_points(self): + """Caluculates the front wheel contact points in the ground plane.""" + + q1 = self.taskSignals['LongitudinalRearContact'] + q2 = self.taskSignals['LateralRearContact'] + q3 = self.taskSignals['YawAngle'] + q4 = self.taskSignals['RollAngle'] + q7 = self.taskSignals['SteerAngle'] + + p = benchmark_to_moore(self.bicycleRiderParameters) + + f = np.vectorize(front_contact) + q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], + p['rf']) + + self.taskSignals['LongitudinalFrontContact'] = q9 + self.taskSignals['LateralFrontContact'] = q10 + + def compute_front_wheel_yaw_angle(self): + """Calculates the yaw angle of the front wheel.""" + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(bp) + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + f = np.vectorize(front_wheel_yaw_angle) + q1_front_wheel = f(q1, q2, q4, mp['d1'], mp['d2'], mp['d3'], + bp['lam'], mp['rr'], mp['rf']) + + q1_front_wheel.name = 'FrontWheelYawAngle' + q1_front_wheel.units = 'meter' + self.taskSignals['FrontWheelYawAngle'] = q1_front_wheel + + def compute_front_wheel_rate(self): + """Calculates the front wheel rate, based on the Jason's data + of front_wheel_contact_point. Alternatively, you can use the + sympy to get the front_wheel_contact_rate directly first.""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(bp) + + f = np.vectorize(front_wheel_rate) + + u6 = f(q1, q2, q4, u9, u10, mp['d1'], mp['d2'], mp['d3'], + bp['lam'], mp['rr'], mp['rf']) + + u6.name = 'FrontWheelRate' + u6.units = 'radian/second' + self.taskSignals['FrontWheelRate'] = u6 + + + def compute_front_rear_wheel_contact_forces(self): + """Calculate the contact forces for each + wheel with respect to inertial frame under + the slip and nonslip condition. Also, provide + the steer torque T4.""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + rf = mp['rf'] + + #signals assignment --slip condition + V = self.taskSignals['ForwardSpeed'].mean() + + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = u7.time_derivative() + u8d = u8.time_derivative() + u9d = u9.time_derivative() + u10d = u10.time_derivative() + + f_1 = np.vectorize(steer_torque_slip) + f_2 = np.vectorize(contact_forces_slip) + f_3 = np.vectorize(contact_forces_nonslip) + + #calculation + #steer torque slip + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + + #attributes: name and units, and taskSignals + T4_slip.name = 'SteerTorque_Slip' + T4_slip.units = 'newton*meter' + self.taskSignals[T4_slip.name] = T4_slip + + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s + + Fy_r_s.name = 'LateralRearContactForce_Slip' + Fy_r_s.units = 'newton' + self.taskSignals[Fy_r_s.name] = Fy_r_s + + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' + Fx_f_s.units = 'newton' + self.taskSignals[Fx_f_s.name] = Fx_f_s + + Fy_f_s.name = 'LateralFrontContactForce_Slip' + Fy_f_s.units = 'newton' + self.taskSignals[Fy_f_s.name] = Fy_f_s + + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' + Fx_r_ns.units = 'newton' + self.taskSignals[Fx_r_ns.name] = Fx_r_ns + + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' + Fy_r_ns.units = 'newton' + self.taskSignals[Fy_r_ns.name] = Fy_r_ns + + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' + Fx_f_ns.units = 'newton' + self.taskSignals[Fx_f_ns.name] = Fx_f_ns + + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' + Fy_f_ns.units = 'newton' + self.taskSignals[Fy_f_ns.name] = Fy_f_ns + + + def compute_rear_wheel_contact_rates(self): + """Calculates the rates of the wheel contact points in the ground + plane.""" + + try: + yawAngle = self.taskSignals['YawAngle'] + rearWheelRate = self.taskSignals['RearWheelRate'] + rR = self.bicycleRiderParameters['rR'] # this should be in meters + except AttributeError: + print('Either the yaw angle, rear wheel rate or ' + + 'front wheel radius is not available. The ' + + 'contact rates were not computed.') + else: + yawAngle = yawAngle.convert_units('radian') + rearWheelRate = rearWheelRate.convert_units('radian/second') + + lon, lat = sigpro.rear_wheel_contact_rate(rR, rearWheelRate, yawAngle) + + lon.name = 'LongitudinalRearContactRate' + lon.units = 'meter/second' + self.taskSignals[lon.name] = lon + + lat.name = 'LateralRearContactRate' + lat.units = 'meter/second' + self.taskSignals[lat.name] = lat + + def compute_yaw_angle(self): + """Computes the yaw angle by integrating the yaw rate.""" + + # get the yaw rate + try: + yawRate = self.taskSignals['YawRate'] + except AttributeError: + print('YawRate is not available. The YawAngle was not computed.') + else: + # convert to radians per second + yawRate = yawRate.convert_units('radian/second') + # integrate and try to account for the drift + yawAngle = yawRate.integrate(detrend=True) + # set the new name and units + yawAngle.name = 'YawAngle' + yawAngle.units = 'radian' + # store in computed signals + self.taskSignals['YawAngle'] = yawAngle + + def compute_steer_torque(self, plot=False): + """Computes the rider applied steer torque. + + Parameters + ---------- + plot : boolean, optional + Default is False, but if True a plot is generated. + + """ + # steer torque + frameAngRate = np.vstack(( + self.truncatedSignals['AngularRateX'], + self.truncatedSignals['AngularRateY'], + self.truncatedSignals['AngularRateZ'])) + frameAngAccel = np.vstack(( + self.truncatedSignals['AngularRateX'].time_derivative(), + self.truncatedSignals['AngularRateY'].time_derivative(), + self.truncatedSignals['AngularRateZ'].time_derivative())) + frameAccel = np.vstack(( + self.truncatedSignals['AccelerationX'], + self.truncatedSignals['AccelerationY'], + self.truncatedSignals['AccelerationZ'])) + handlebarAngRate = self.truncatedSignals['ForkRate'] + handlebarAngAccel = self.truncatedSignals['ForkRate'].time_derivative() + steerAngle = self.truncatedSignals['SteerAngle'] + steerColumnTorque =\ + self.truncatedSignals['SteerTubeTorque'].convert_units('newton*meter') + handlebarMass = self.bicycleRiderParameters['mG'] + handlebarInertia =\ + self.bicycle.steer_assembly_moment_of_inertia(fork=False, + wheel=False, nominal=True) + # this is the distance from the handlebar center of mass to the + # steer axis + w = self.bicycleRiderParameters['w'] + c = self.bicycleRiderParameters['c'] + lam = self.bicycleRiderParameters['lam'] + xG = self.bicycleRiderParameters['xG'] + zG = self.bicycleRiderParameters['zG'] + handlebarCoM = np.array([xG, 0., zG]) + d = bp.geometry.distance_to_steer_axis(w, c, lam, handlebarCoM) + # these are the distances from the point on the steer axis which is + # aligned with the handlebar center of mass to the accelerometer on + # the frame + ds1 = self.bicycle.parameters['Measured']['ds1'] + ds3 = self.bicycle.parameters['Measured']['ds3'] + ds = np.array([ds1, 0., ds3]) # i measured these + # damping and friction values come from Peter's work, I need to verify + # them still + damping = 0.3475 + friction = 0.0861 + + components = sigpro.steer_torque_components( + frameAngRate, frameAngAccel, frameAccel, handlebarAngRate, + handlebarAngAccel, steerAngle, steerColumnTorque, + handlebarMass, handlebarInertia, damping, friction, d, ds) + steerTorque = sigpro.steer_torque(components) + + stDict = {'units':'newton*meter', + 'name':'SteerTorque', + 'runid':self.metadata['RunID'], + 'sampleRate':steerAngle.sampleRate, + 'source':'NA'} + self.computedSignals['SteerTorque'] = Signal(steerTorque, stDict) + + if plot is True: + + time = steerAngle.time() + + hdot = (components['Hdot1'] + components['Hdot2'] + + components['Hdot3'] + components['Hdot4']) + cross = (components['cross1'] + components['cross2'] + + components['cross3']) + + fig = plt.figure() + + frictionAx = fig.add_subplot(4, 1, 1) + frictionAx.plot(time, components['viscous'], + time, components['coulomb'], + time, components['viscous'] + components['coulomb']) + frictionAx.set_ylabel('Torque [N-m]') + frictionAx.legend(('Viscous Friction', 'Coulomb Friction', + 'Total Friction')) + + dynamicAx = fig.add_subplot(4, 1, 2) + dynamicAx.plot(time, hdot, time, cross, time, hdot + cross) + dynamicAx.set_ylabel('Torque [N-m]') + dynamicAx.legend((r'Torque due to $\dot{H}$', + r'Torque due to $r \times m a$', + r'Total Dynamic Torque')) + + additionalAx = fig.add_subplot(4, 1, 3) + additionalAx.plot(time, hdot + cross + components['viscous'] + + components['coulomb'], + label='Total Frictional and Dynamic Torque') + additionalAx.set_ylabel('Torque [N-m]') + additionalAx.legend() + + torqueAx = fig.add_subplot(4, 1, 4) + torqueAx.plot(time, components['steerColumn'], + time, hdot + cross + components['viscous'] + components['coulomb'], + time, steerTorque) + torqueAx.set_xlabel('Time [s]') + torqueAx.set_ylabel('Torque [N-m]') + torqueAx.legend(('Measured Torque', 'Frictional and Dynamic Torque', + 'Rider Applied Torque')) + + plt.show() + + return fig + + def compute_yaw_roll_pitch_rates(self): + """Computes the yaw, roll and pitch rates of the bicycle frame.""" + + try: + omegaX = self.truncatedSignals['AngularRateX'] + omegaY = self.truncatedSignals['AngularRateY'] + omegaZ = self.truncatedSignals['AngularRateZ'] + rollAngle = self.truncatedSignals['RollAngle'] + lam = self.bicycleRiderParameters['lam'] + except AttributeError: + print('All needed signals are not available. ' + + 'Yaw, roll and pitch rates were not computed.') + else: + omegaX = omegaX.convert_units('radian/second') + omegaY = omegaY.convert_units('radian/second') + omegaZ = omegaZ.convert_units('radian/second') + rollAngle = rollAngle.convert_units('radian') + + yr, rr, pr = sigpro.yaw_roll_pitch_rate(omegaX, omegaY, omegaZ, lam, + rollAngle=rollAngle) + yr.units = 'radian/second' + yr.name = 'YawRate' + rr.units = 'radian/second' + rr.name = 'RollRate' + pr.units = 'radian/second' + pr.name = 'PitchRate' + + self.computedSignals['YawRate'] = yr + self.computedSignals['RollRate'] = rr + self.computedSignals['PitchRate'] = pr + + def compute_steer_rate(self): + """Calculate the steer rate from the frame and fork rates.""" + try: + forkRate = self.truncatedSignals['ForkRate'] + omegaZ = self.truncatedSignals['AngularRateZ'] + except AttributeError: + print('ForkRate or AngularRateZ is not available. ' + + 'SteerRate was not computed.') + else: + forkRate = forkRate.convert_units('radian/second') + omegaZ = omegaZ.convert_units('radian/second') + + steerRate = sigpro.steer_rate(forkRate, omegaZ) + steerRate.units = 'radian/second' + steerRate.name = 'SteerRate' + self.computedSignals['SteerRate'] = steerRate + + def compute_forward_speed(self): + """Calculates the magnitude of the main component of velocity of the + center of the rear wheel.""" + + try: + rR = self.bicycleRiderParameters['rR'] + rearWheelRate = self.truncatedSignals['RearWheelRate'] + except AttributeError: + print('rR or RearWheelRate is not availabe. ' + + 'ForwardSpeed was not computed.') + else: + rearWheelRate = rearWheelRate.convert_units('radian/second') + + self.computedSignals['ForwardSpeed'] = -rR * rearWheelRate + self.computedSignals['ForwardSpeed'].units = 'meter/second' + self.computedSignals['ForwardSpeed'].name = 'ForwardSpeed' + + def compute_pull_force(self): + """ + Computes the pull force from the truncated pull force signal. + + """ + try: + pullForce = self.truncatedSignals['PullForce'] + except AttributeError: + print 'PullForce was not available. PullForce was not computed.' + else: + pullForce = pullForce.convert_units('newton') + pullForce.name = 'PullForce' + pullForce.units = 'newton' + self.computedSignals[pullForce.name] = pullForce + + def __str__(self): + '''Prints basic run information to the screen.''' + + line = "=" * 79 + info = 'Run # {0}\nEnvironment: {1}\nRider: {2}\nBicycle: {3}\nSpeed:'\ + '{4}\nManeuver: {5}\nNotes: {6}'.format( + self.metadata['RunID'], + self.metadata['Environment'], + self.metadata['Rider'], + self.metadata['Bicycle'], + self.metadata['Speed'], + self.metadata['Maneuver'], + self.metadata['Notes']) + + return line + '\n' + info + '\n' + line + + def export(self, filetype, directory='exports'): + """ + Exports the computed signals to a file. + + Parameters + ---------- + filetype : str + The type of file to export the data to. Options are 'mat', 'csv', + and 'pickle'. + + """ + + if filetype == 'mat': + if not os.path.exists(directory): + print "Creating {0}".format(directory) + os.makedirs(directory) + exportData = {} + exportData.update(self.metadata) + try: + exportData.update(self.taskSignals) + except AttributeError: + try: + exportData.update(self.truncatedSignals) + except AttributeError: + exportData.update(self.calibratedSignals) + print('Exported calibratedSignals to {}'.format(directory)) + else: + print('Exported truncatedSignals to {}'.format(directory)) + else: + print('Exported taskSignals to {}'.format(directory)) + + filename = pad_with_zeros(str(self.metadata['RunID']), 5) + '.mat' + io.savemat(os.path.join(directory, filename), exportData) + else: + raise NotImplementedError(('{0} method is not available' + + ' yet.').format(filetype)) + + def extract_task(self): + """Slices the computed signals such that data before the end of the + bump is removed and unusable trailng data is removed. + + """ + # get the z acceleration from the VN-100 + acc = -self.truncatedSignals['AccelerometerAccelerationY'].filter(30.) + # find the mean speed during the task (look at one second in the middle + # of the data) + speed = self.computedSignals['ForwardSpeed'] + meanSpeed = speed[len(speed) / 2 - 100:len(speed) / 2 + 100].mean() + wheelbase = self.bicycleRiderParameters['w'] + # find the bump + indices = sigpro.find_bump(acc, acc.sampleRate, meanSpeed, wheelbase, + self.bumpLength) + + + # if it is a pavilion run, then clip the end too + # these are the runs that the length of track method of clipping + # applies to + straight = ['Track Straight Line With Disturbance', + 'Balance With Disturbance', + 'Balance', + 'Track Straight Line'] + if (self.metadata['Environment'] == 'Pavillion Floor' and + self.metadata['Maneuver'] in straight): + + # this is based on the length of the track in the pavilion that we + # measured on September 21st, 2011 + trackLength = 32. - wheelbase - self.bumpLength + end = trackLength / meanSpeed * acc.sampleRate + + # i may need to clip the end based on the forward speed dropping + # below certain threshold around the mean + else: + # if it isn't a pavilion run, don't clip the end + end = -1 + + self.taskSignals = {} + for name, sig in self.computedSignals.items(): + self.taskSignals[name] = sig[indices[2]:end] + + def load_rider(self, pathToParameterData): + """Creates a bicycle/rider attribute which contains the physical + parameters for the bicycle and rider for this run.""" + + print("Loading the bicycle and rider data for " + + "{} on {}".format(self.metadata['Rider'], + self.metadata['Bicycle'])) + + # currently this isn't very generic, it only assumes that there was + # Luke, Jason, and Charlie riding on the instrumented bicycle. + rider = self.metadata['Rider'] + if rider == 'Charlie' or rider == 'Luke': + # Charlie and Luke rode the bike in the same configuration + bicycle = 'Rigidcl' + elif rider == 'Jason' : + bicycle = 'Rigid' + else: + raise StandardError('There are no bicycle parameters ' + + 'for {}'.format(rider)) + + # force a recalculation (but not the period calcs, they take too long) + self.bicycle = bp.Bicycle(bicycle, pathToData=pathToParameterData) + try: + self.bicycle.extras + except AttributeError: + pass + else: + self.bicycle.save_parameters() + # force a recalculation of the human parameters + self.bicycle.add_rider(rider) + if self.bicycle.human is not None: + self.bicycle.save_parameters() + + self.bicycleRiderParameters =\ + bp.io.remove_uncertainties(self.bicycle.parameters['Benchmark']) + + def plot(self, *args, **kwargs): + ''' + Returns a plot of the time series of various signals. + + Parameters + ---------- + signalName : string + These should be strings that correspond to the signals available in + the computed data. If the first character of the string is `-` then + the negative signal will be plotted. You can also scale the values + so by adding a value and an ``*`` such as: ``'-10*RollRate'. The + negative sign always has to come first. + signalType : string, optional + This allows you to plot from the other signal types. Options are + 'task', 'computed', 'truncated', 'calibrated', 'raw'. The default + is 'task'. + + ''' + if not kwargs: + kwargs = {'signalType': 'task'} + + mapping = {} + for x in ['computed', 'truncated', 'calibrated', 'raw', 'task']: + try: + mapping[x] = getattr(self, x + 'Signals') + except AttributeError: + pass + + fig = plt.figure() + ax = fig.add_axes([0.125, 0.125, 0.8, 0.7]) + + leg = [] + for i, arg in enumerate(args): + legName = arg + sign = 1. + # if a negative sign is present + if '-' in arg and arg[0] != '-': + raise ValueError('{} is incorrectly typed'.format(arg)) + elif '-' in arg and arg[0] == '-': + arg = arg[1:] + sign = -1. + # if a multiplication factor is present + if '*' in arg: + mul, arg = arg.split('*') + else: + mul = 1. + signal = sign * float(mul) * mapping[kwargs['signalType']][arg] + ax.plot(signal.time(), signal) + leg.append(legName + ' [' + mapping[kwargs['signalType']][arg].units + ']') + + ax.legend(leg) + runid = pad_with_zeros(str(self.metadata['RunID']), 5) + ax.set_title('Run: ' + runid + ', Rider: ' + self.metadata['Rider'] + + ', Speed: ' + str(self.metadata['Speed']) + 'm/s' + '\n' + + 'Maneuver: ' + self.metadata['Maneuver'] + + ', Environment: ' + self.metadata['Environment'] + '\n' + + 'Notes: ' + self.metadata['Notes']) + + ax.set_xlabel('Time [second]') + + ax.grid() + + return fig + + def plot_wheel_contact(self, show=False): + """Returns a plot of the wheel contact traces. + + Parameters + ---------- + show : boolean + If true the plot will be displayed. + + Returns + ------- + fig : matplotlib.Figure + + """ + + q1 = self.taskSignals['LongitudinalRearContact'] + q2 = self.taskSignals['LateralRearContact'] + q9 = self.taskSignals['LongitudinalFrontContact'] + q10 = self.taskSignals['LateralFrontContact'] + + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.plot(q1, q2, q9, q10) + ax.set_xlabel('Distance [' + q1.units + ']') + ax.set_ylabel('Distance [' + q2.units + ']') + ax.set_ylim((-0.5, 0.5)) + rider = self.metadata['Rider'] + where = self.metadata['Environment'] + speed = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() + maneuver = self.metadata['Maneuver'] + ax.set_title(rider + ', ' + where + ', ' + maneuver + ' @ ' + speed + ' m/s') + + if show is True: + fig.show() + + return fig + + def verify_time_sync(self, show=True, saveDir=None): + """Shows a plot of the acceleration signals that were used to + synchronize the NI and VN data. If it doesn't show a good fit, then + something is wrong. + + Parameters + ---------- + show : boolean + If true, the figure will be displayed. + saveDir : str + The path to a directory in which to save the figure. + + """ + + if self.topSig == 'calibrated': + sigType = 'calibrated' + else: + sigType = 'truncated' + + fig = self.plot('-AccelerometerAccelerationY', 'AccelerationZ', + signalType=sigType) + ax = fig.axes[0] + ax.set_xlim((0, 10)) + title = ax.get_title() + ax.set_title(title + '\nSignal Type: ' + sigType) + if saveDir is not None: + if not os.path.exists(saveDir): + print "Creating {0}".format(saveDir) + os.makedirs(saveDir) + runid = run_id_string(self.metadata['RunID']) + fig.savefig(os.path.join(saveDir, runid + '.png')) + if show is True: + fig.show() + + return fig + + def video(self): + ''' + Plays the video of the run. + + ''' + # get the 5 digit string version of the run id + runid = pad_with_zeros(str(self.metadata['RunID']), 5) + viddir = os.path.join('..', 'Video') + abspath = os.path.abspath(viddir) + # check to see if there is a video for this run + if (runid + '.mp4') in os.listdir(viddir): + path = os.path.join(abspath, runid + '.mp4') + os.system('vlc "' + path + '"') + else: + print "No video for this run" def matlab_date_to_object(matDate): - '''Returns a date time object based on a Matlab `datestr()` output. + '''Returns a date time object based on a Matlab `datestr()` output. - Parameters - ---------- - matDate : string - String in the form '21-Mar-2011 14:45:54'. + Parameters + ---------- + matDate : string + String in the form '21-Mar-2011 14:45:54'. - Returns - ------- - python datetime object + Returns + ------- + python datetime object - ''' - return datetime.datetime.strptime(matDate, '%d-%b-%Y %H:%M:%S') + ''' + return datetime.datetime.strptime(matDate, '%d-%b-%Y %H:%M:%S') From e5d6bcc5343223744560580f6d47d9e09c26bcf8 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Sat, 6 Oct 2012 19:25:20 -0700 Subject: [PATCH 16/37] Gave the front_contact_points, q9 and q10, name and units --- bicycledataprocessor/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 9bdf33a..e3e9044 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -851,6 +851,11 @@ def compute_front_wheel_contact_points(self): q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], p['rf']) + q9.name = 'LongitudinalFrontContact' + q9.units = 'meter' + q10.name = 'LateralFrontContact' + q10.units = 'meter' + self.taskSignals['LongitudinalFrontContact'] = q9 self.taskSignals['LateralFrontContact'] = q10 From fee18078293c5e070acc9f6bc03a066d0c57a483 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 8 Oct 2012 09:43:16 -0700 Subject: [PATCH 17/37] Seperated the importing of contact forces and front wheel rate yaw angle --- bicycledataprocessor/main.py | 2966 +++++++++++++++++----------------- 1 file changed, 1483 insertions(+), 1483 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index e3e9044..7fcf21c 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -9,11 +9,11 @@ # debugging #try: - #from IPython.core.debugger import Tracer + #from IPython.core.debugger import Tracer #except ImportError: - #pass + #pass #else: - #set_trace = Tracer() + #set_trace = Tracer() # dependencies import numpy as np @@ -25,8 +25,8 @@ import dtk.process as process from dtk.bicycle import front_contact, benchmark_to_moore -from dtk.bicycle import front_wheel_yaw_angle, front_wheel_rate, steer_torque_slip, - contact_forces_slip, contact_forces_nonslip +from dtk.bicycle import front_wheel_yaw_angle, front_wheel_rate +from dtk.bicycle import steer_torque_slip, contact_forces_slip, contact_forces_nonslip import bicycleparameters as bp @@ -39,1492 +39,1492 @@ config.read(os.path.join(os.path.dirname(__file__), '..', 'defaults.cfg')) class Signal(np.ndarray): - """ - A subclass of ndarray for collecting the data for a single signal in a run. - - Attributes - ---------- - conversions : dictionary - A mapping for unit conversions. - name : str - The name of the signal. Should be CamelCase. - runid : str - A five digit identification number associated with the - trial this signal was collected from (e.g. '00104'). - sampleRate : float - The sample rate in hertz of the signal. - source : str - The source of the data. This should be 'NI' for the - National Instruments USB-6218 and 'VN' for the VN-100 IMU. - units : str - The physcial units of the signal. These should be specified - as lowercase complete words using only multiplication and - division symbols (e.g. 'meter/second/second'). - Signal.conversions will show the avialable options. - - Methods - ------- - plot() - Plot's the signal versus time and returns the line. - frequency() - Returns the frequency spectrum of the signal. - time_derivative() - Returns the time derivative of the signal. - filter(frequency) - Returns the low passed filter of the signal. - truncate(tau) - Interpolates and truncates the signal the based on the time shift, - `tau`, and the signal source. - as_dictionary - Returns a dictionary of the metadata of the signal. - convert_units(units) - Returns a signal with different units. `conversions` specifies the - available options. - - """ - - # define some basic unit converions - conversions = {'degree->radian': pi / 180., - 'degree/second->radian/second': pi / 180., - 'degree/second/second->radian/second/second': pi / 180., - 'inch*pound->newton*meter': 25.4 / 1000. * 4.44822162, - 'pound->newton': 4.44822162, - 'feet/second->meter/second': 12. * 2.54 / 100., - 'mile/hour->meter/second': 0.00254 * 12. / 5280. / 3600.} - - def __new__(cls, inputArray, metadata): - """ - Returns an instance of the Signal class with the additional signal - data. - - Parameters - ---------- - inputArray : ndarray, shape(n,) - A one dimension array representing a single variable's time - history. - metadata : dictionary - This dictionary contains the metadata for the signal. - name : str - The name of the signal. Should be CamelCase. - runid : str - A five digit identification number associated with the - trial this experiment was collected at (e.g. '00104'). - sampleRate : float - The sample rate in hertz of the signal. - source : str - The source of the data. This should be 'NI' for the - National Instruments USB-6218 and 'VN' for the VN-100 IMU. - units : str - The physcial units of the signal. These should be specified - as lowercase complete words using only multiplication and - division symbols (e.g. 'meter/second/second'). - Signal.conversions will show the avialable options. - - Raises - ------ - ValueError - If `inputArray` is not a vector. - - """ - if len(inputArray.shape) > 1: - raise ValueError('Signals must be arrays of one dimension.') - # cast the input array into the Signal class - obj = np.asarray(inputArray).view(cls) - # add the metadata to the object - obj.name = metadata['name'] - obj.runid = metadata['runid'] - obj.sampleRate = metadata['sampleRate'] - obj.source = metadata['source'] - obj.units = metadata['units'] - return obj - - def __array_finalize__(self, obj): - if obj is None: return - self.name = getattr(obj, 'name', None) - self.runid = getattr(obj, 'runid', None) - self.sampleRate = getattr(obj, 'sampleRate', None) - self.source = getattr(obj, 'source', None) - self.units = getattr(obj, 'units', None) - - def __array_wrap__(self, outputArray, context=None): - # doesn't support these things in basic ufunc calls...maybe one day - # That means anytime you add, subtract, multiply, divide, etc, the - # following are not retained. - outputArray.name = None - outputArray.source = None - outputArray.units = None - return np.ndarray.__array_wrap__(self, outputArray, context) - - def as_dictionary(self): - '''Returns the signal metadata as a dictionary.''' - data = {'runid': self.runid, - 'name': self.name, - 'units': self.units, - 'source': self.source, - 'sampleRate': self.sampleRate} - return data - - def convert_units(self, units): - """ - Returns a signal with the specified units. - - Parameters - ---------- - units : str - The units to convert the signal to. The mapping must be in the - attribute `conversions`. - - Returns - ------- - newSig : Signal - The signal with the desired units. - - """ - if units == self.units: - return self - else: - try: - conversion = self.units + '->' + units - newSig = self * self.conversions[conversion] - except KeyError: - try: - conversion = units + '->' + self.units - newSig = self / self.conversions[conversion] - except KeyError: - raise KeyError(('Conversion from {0} to {1} is not ' + - 'possible or not defined.').format(self.units, units)) - # make the new signal - newSig = Signal(newSig, self.as_dictionary()) - newSig.units = units - - return newSig - - def filter(self, frequency): - """Returns the signal filtered by a low pass Butterworth at the given - frequency.""" - filteredArray = process.butterworth(self.spline(), frequency, self.sampleRate) - return Signal(filteredArray, self.as_dictionary()) - - def frequency(self): - """Returns the frequency content of the signal.""" - return process.freq_spectrum(self.spline(), self.sampleRate) - - def integrate(self, initialCondition=0., detrend=False): - """Integrates the signal using the trapezoidal rule.""" - time = self.time() - # integrate using trapz and adjust with the initial condition - grated = np.hstack((0., cumtrapz(self, x=time))) + initialCondition - # this tries to characterize the drift in the integrated signal. It - # works well for signals from straight line tracking but not - # necessarily for lange change. - if detrend is True: - def line(x, a, b, c): - return a * x**2 + b * x + c - popt, pcov = curve_fit(line, time, grated) - grated = grated - line(time, popt[0], popt[1], popt[2]) - grated = Signal(grated, self.as_dictionary()) - grated.units = self.units + '*second' - grated.name = self.name + 'Int' - return grated - - def plot(self, show=True): - """Plots and returns the signal versus time.""" - time = self.time() - line = plt.plot(time, self) - if show: - plt.xlabel('Time [second]') - plt.ylabel('{0} [{1}]'.format(self.name, self.units)) - plt.title('Signal plot during run {0}'.format(self.runid)) - plt.show() - return line - - def spline(self): - """Returns the signal with nans replaced by the results of a cubic - spline.""" - splined = process.spline_over_nan(self.time(), self) - return Signal(splined, self.as_dictionary()) - - def subtract_mean(self): - """Returns the mean subtracted data.""" - return Signal(process.subtract_mean(self), self.as_dictionary()) - - def time(self): - """Returns the time vector of the signal.""" - return sigpro.time_vector(len(self), self.sampleRate) - - def time_derivative(self): - """Returns the time derivative of the signal.""" - # caluculate the numerical time derivative - dsdt = process.derivative(self.time(), self, method='combination') - # map the metadata from self onto the derivative - dsdt = Signal(dsdt, self.as_dictionary()) - dsdt.name = dsdt.name + 'Dot' - dsdt.units = dsdt.units + '/second' - return dsdt - - def truncate(self, tau): - '''Returns the shifted and truncated signal based on the provided - timeshift, tau.''' - # this is now an ndarray instead of a Signal - return Signal(sigpro.truncate_data(self, tau), self.as_dictionary()) + """ + A subclass of ndarray for collecting the data for a single signal in a run. + + Attributes + ---------- + conversions : dictionary + A mapping for unit conversions. + name : str + The name of the signal. Should be CamelCase. + runid : str + A five digit identification number associated with the + trial this signal was collected from (e.g. '00104'). + sampleRate : float + The sample rate in hertz of the signal. + source : str + The source of the data. This should be 'NI' for the + National Instruments USB-6218 and 'VN' for the VN-100 IMU. + units : str + The physcial units of the signal. These should be specified + as lowercase complete words using only multiplication and + division symbols (e.g. 'meter/second/second'). + Signal.conversions will show the avialable options. + + Methods + ------- + plot() + Plot's the signal versus time and returns the line. + frequency() + Returns the frequency spectrum of the signal. + time_derivative() + Returns the time derivative of the signal. + filter(frequency) + Returns the low passed filter of the signal. + truncate(tau) + Interpolates and truncates the signal the based on the time shift, + `tau`, and the signal source. + as_dictionary + Returns a dictionary of the metadata of the signal. + convert_units(units) + Returns a signal with different units. `conversions` specifies the + available options. + + """ + + # define some basic unit converions + conversions = {'degree->radian': pi / 180., + 'degree/second->radian/second': pi / 180., + 'degree/second/second->radian/second/second': pi / 180., + 'inch*pound->newton*meter': 25.4 / 1000. * 4.44822162, + 'pound->newton': 4.44822162, + 'feet/second->meter/second': 12. * 2.54 / 100., + 'mile/hour->meter/second': 0.00254 * 12. / 5280. / 3600.} + + def __new__(cls, inputArray, metadata): + """ + Returns an instance of the Signal class with the additional signal + data. + + Parameters + ---------- + inputArray : ndarray, shape(n,) + A one dimension array representing a single variable's time + history. + metadata : dictionary + This dictionary contains the metadata for the signal. + name : str + The name of the signal. Should be CamelCase. + runid : str + A five digit identification number associated with the + trial this experiment was collected at (e.g. '00104'). + sampleRate : float + The sample rate in hertz of the signal. + source : str + The source of the data. This should be 'NI' for the + National Instruments USB-6218 and 'VN' for the VN-100 IMU. + units : str + The physcial units of the signal. These should be specified + as lowercase complete words using only multiplication and + division symbols (e.g. 'meter/second/second'). + Signal.conversions will show the avialable options. + + Raises + ------ + ValueError + If `inputArray` is not a vector. + + """ + if len(inputArray.shape) > 1: + raise ValueError('Signals must be arrays of one dimension.') + # cast the input array into the Signal class + obj = np.asarray(inputArray).view(cls) + # add the metadata to the object + obj.name = metadata['name'] + obj.runid = metadata['runid'] + obj.sampleRate = metadata['sampleRate'] + obj.source = metadata['source'] + obj.units = metadata['units'] + return obj + + def __array_finalize__(self, obj): + if obj is None: return + self.name = getattr(obj, 'name', None) + self.runid = getattr(obj, 'runid', None) + self.sampleRate = getattr(obj, 'sampleRate', None) + self.source = getattr(obj, 'source', None) + self.units = getattr(obj, 'units', None) + + def __array_wrap__(self, outputArray, context=None): + # doesn't support these things in basic ufunc calls...maybe one day + # That means anytime you add, subtract, multiply, divide, etc, the + # following are not retained. + outputArray.name = None + outputArray.source = None + outputArray.units = None + return np.ndarray.__array_wrap__(self, outputArray, context) + + def as_dictionary(self): + '''Returns the signal metadata as a dictionary.''' + data = {'runid': self.runid, + 'name': self.name, + 'units': self.units, + 'source': self.source, + 'sampleRate': self.sampleRate} + return data + + def convert_units(self, units): + """ + Returns a signal with the specified units. + + Parameters + ---------- + units : str + The units to convert the signal to. The mapping must be in the + attribute `conversions`. + + Returns + ------- + newSig : Signal + The signal with the desired units. + + """ + if units == self.units: + return self + else: + try: + conversion = self.units + '->' + units + newSig = self * self.conversions[conversion] + except KeyError: + try: + conversion = units + '->' + self.units + newSig = self / self.conversions[conversion] + except KeyError: + raise KeyError(('Conversion from {0} to {1} is not ' + + 'possible or not defined.').format(self.units, units)) + # make the new signal + newSig = Signal(newSig, self.as_dictionary()) + newSig.units = units + + return newSig + + def filter(self, frequency): + """Returns the signal filtered by a low pass Butterworth at the given + frequency.""" + filteredArray = process.butterworth(self.spline(), frequency, self.sampleRate) + return Signal(filteredArray, self.as_dictionary()) + + def frequency(self): + """Returns the frequency content of the signal.""" + return process.freq_spectrum(self.spline(), self.sampleRate) + + def integrate(self, initialCondition=0., detrend=False): + """Integrates the signal using the trapezoidal rule.""" + time = self.time() + # integrate using trapz and adjust with the initial condition + grated = np.hstack((0., cumtrapz(self, x=time))) + initialCondition + # this tries to characterize the drift in the integrated signal. It + # works well for signals from straight line tracking but not + # necessarily for lange change. + if detrend is True: + def line(x, a, b, c): + return a * x**2 + b * x + c + popt, pcov = curve_fit(line, time, grated) + grated = grated - line(time, popt[0], popt[1], popt[2]) + grated = Signal(grated, self.as_dictionary()) + grated.units = self.units + '*second' + grated.name = self.name + 'Int' + return grated + + def plot(self, show=True): + """Plots and returns the signal versus time.""" + time = self.time() + line = plt.plot(time, self) + if show: + plt.xlabel('Time [second]') + plt.ylabel('{0} [{1}]'.format(self.name, self.units)) + plt.title('Signal plot during run {0}'.format(self.runid)) + plt.show() + return line + + def spline(self): + """Returns the signal with nans replaced by the results of a cubic + spline.""" + splined = process.spline_over_nan(self.time(), self) + return Signal(splined, self.as_dictionary()) + + def subtract_mean(self): + """Returns the mean subtracted data.""" + return Signal(process.subtract_mean(self), self.as_dictionary()) + + def time(self): + """Returns the time vector of the signal.""" + return sigpro.time_vector(len(self), self.sampleRate) + + def time_derivative(self): + """Returns the time derivative of the signal.""" + # caluculate the numerical time derivative + dsdt = process.derivative(self.time(), self, method='combination') + # map the metadata from self onto the derivative + dsdt = Signal(dsdt, self.as_dictionary()) + dsdt.name = dsdt.name + 'Dot' + dsdt.units = dsdt.units + '/second' + return dsdt + + def truncate(self, tau): + '''Returns the shifted and truncated signal based on the provided + timeshift, tau.''' + # this is now an ndarray instead of a Signal + return Signal(sigpro.truncate_data(self, tau), self.as_dictionary()) class RawSignal(Signal): - """ - A subclass of Signal for collecting the data for a single raw signal in - a run. - - Attributes - ---------- - sensor : Sensor - Each raw signal has a sensor associated with it. Most sensors contain - calibration data for that sensor/signal. - calibrationType : - - Notes - ----- - This is a class for the signals that are the raw measurement outputs - collected by the BicycleDAQ software and are already stored in the pytables - database file. - - """ - - def __new__(cls, runid, signalName, database): - """ - Returns an instance of the RawSignal class with the additional signal - metadata. - - Parameters - ---------- - runid : str - A five digit - signalName : str - A CamelCase signal name that corresponds to the raw signals output - by BicycleDAQ_. - database : pytables object - The hdf5 database for the instrumented bicycle. - - .. _BicycleDAQ: https://github.com/moorepants/BicycleDAQ - - """ - - # get the tables - rTab = database.root.runTable - sTab = database.root.signalTable - cTab = database.root.calibrationTable - - # get the row number for this particular run id - rownum = get_row_num(runid, rTab) - signal = database.getNode('/rawData/' + runid, name=signalName).read() - - # cast the input array into my subclass of ndarray - obj = np.asarray(signal).view(cls) - - obj.runid = runid - obj.timeStamp = matlab_date_to_object(get_cell(rTab, 'DateTime', - rownum)) - obj.calibrationType, obj.units, obj.source = [(row['calibration'], - row['units'], row['source']) - for row in sTab.where('signal == signalName')][0] - obj.name = signalName - - try: - obj.sensor = Sensor(obj.name, cTab) - except KeyError: - pass - # This just means that there was no sensor associated with that - # signal for calibration purposes. - #print "There is no sensor named {0}.".format(signalName) - - # this assumes that the supply voltage for this signal is the same for - # all sensor calibrations - try: - supplySource = [row['runSupplyVoltageSource'] - for row in cTab.where('name == signalName')][0] - if supplySource == 'na': - obj.supply = [row['runSupplyVoltage'] - for row in cTab.where('name == signalName')][0] - else: - obj.supply = database.getNode('/rawData/' + runid, - name=supplySource).read() - except IndexError: - pass - #print "{0} does not have a supply voltage.".format(signalName) - #print "-" * 79 - - # get the appropriate sample rate - if obj.source == 'NI': - sampRateCol = 'NISampleRate' - elif obj.source == 'VN': - sampRateCol = 'VNavSampleRate' - else: - raise ValueError('{0} is not a valid source.'.format(obj.source)) - - obj.sampleRate = rTab[rownum][rTab.colnames.index(sampRateCol)] - - return obj - - def __array_finalize__(self, obj): - if obj is None: return - self.calibrationType = getattr(obj, 'calibrationType', None) - self.name = getattr(obj, 'name', None) - self.runid = getattr(obj, 'runid', None) - self.sampleRate = getattr(obj, 'sampleRate', None) - self.sensor = getattr(obj, 'sensor', None) - self.source = getattr(obj, 'source', None) - self.units = getattr(obj, 'units', None) - self.timeStamp = getattr(obj, 'timeStamp', None) - - def __array_wrap__(self, outputArray, context=None): - # doesn't support these things in basic ufunc calls...maybe one day - outputArray.calibrationType = None - outputArray.name = None - outputArray.sensor = None - outputArray.source = None - outputArray.units = None - return np.ndarray.__array_wrap__(self, outputArray, context) - - def scale(self): - """ - Returns the scaled signal based on the calibration data for the - supplied date. - - Returns - ------- - : ndarray (n,) - Scaled signal. - - """ - try: - self.calibrationType - except AttributeError: - raise AttributeError("Can't scale without the calibration type") - - # these will need to be changed once we start measuring them - doNotScale = ['LeanPotentiometer', - 'HipPotentiometer', - 'TwistPotentiometer'] - if self.calibrationType in ['none', 'matrix'] or self.name in doNotScale: - #print "Not scaling {0}".format(self.name) - return self - else: - pass - #print "Scaling {0}".format(self.name) - - # pick the largest calibration date without surpassing the run date - calibData = self.sensor.get_data_for_date(self.timeStamp) - - slope = calibData['slope'] - bias = calibData['bias'] - intercept = calibData['offset'] - calibrationSupplyVoltage = calibData['calibrationSupplyVoltage'] - - #print "slope {0}, bias {1}, intercept {2}".format(slope, bias, - #intercept) - - if self.calibrationType == 'interceptStar': - # this is for potentiometers, where the slope is ratiometric - # and zero degrees is always zero volts - calibratedSignal = (calibrationSupplyVoltage / self.supply * - slope * self + intercept) - elif self.calibrationType == 'intercept': - # this is the typical calibration that I use for all the - # sensors that I calibrate myself - calibratedSignal = (calibrationSupplyVoltage / self.supply * - (slope * self + intercept)) - elif self.calibrationType == 'bias': - # this is for the accelerometers and rate gyros that are - # "ratiometric", but I'm still not sure this is correct - calibratedSignal = (slope * (self - self.supply / - calibrationSupplyVoltage * bias)) - else: - raise StandardError("None of the calibration equations worked.") - calibratedSignal.name = calibData['signal'] - calibratedSignal.units = calibData['units'] - calibratedSignal.source = self.source - - return calibratedSignal.view(Signal) - - def plot_scaled(self, show=True): - '''Plots and returns the scaled signal versus time.''' - time = self.time() - scaled = self.scale() - line = plt.plot(time, scaled[1]) - plt.xlabel('Time [s]') - plt.ylabel(scaled[2]) - plt.title('{0} signal during run {1}'.format(scaled[0], - str(self.runid))) - if show: - plt.show() - return line + """ + A subclass of Signal for collecting the data for a single raw signal in + a run. + + Attributes + ---------- + sensor : Sensor + Each raw signal has a sensor associated with it. Most sensors contain + calibration data for that sensor/signal. + calibrationType : + + Notes + ----- + This is a class for the signals that are the raw measurement outputs + collected by the BicycleDAQ software and are already stored in the pytables + database file. + + """ + + def __new__(cls, runid, signalName, database): + """ + Returns an instance of the RawSignal class with the additional signal + metadata. + + Parameters + ---------- + runid : str + A five digit + signalName : str + A CamelCase signal name that corresponds to the raw signals output + by BicycleDAQ_. + database : pytables object + The hdf5 database for the instrumented bicycle. + + .. _BicycleDAQ: https://github.com/moorepants/BicycleDAQ + + """ + + # get the tables + rTab = database.root.runTable + sTab = database.root.signalTable + cTab = database.root.calibrationTable + + # get the row number for this particular run id + rownum = get_row_num(runid, rTab) + signal = database.getNode('/rawData/' + runid, name=signalName).read() + + # cast the input array into my subclass of ndarray + obj = np.asarray(signal).view(cls) + + obj.runid = runid + obj.timeStamp = matlab_date_to_object(get_cell(rTab, 'DateTime', + rownum)) + obj.calibrationType, obj.units, obj.source = [(row['calibration'], + row['units'], row['source']) + for row in sTab.where('signal == signalName')][0] + obj.name = signalName + + try: + obj.sensor = Sensor(obj.name, cTab) + except KeyError: + pass + # This just means that there was no sensor associated with that + # signal for calibration purposes. + #print "There is no sensor named {0}.".format(signalName) + + # this assumes that the supply voltage for this signal is the same for + # all sensor calibrations + try: + supplySource = [row['runSupplyVoltageSource'] + for row in cTab.where('name == signalName')][0] + if supplySource == 'na': + obj.supply = [row['runSupplyVoltage'] + for row in cTab.where('name == signalName')][0] + else: + obj.supply = database.getNode('/rawData/' + runid, + name=supplySource).read() + except IndexError: + pass + #print "{0} does not have a supply voltage.".format(signalName) + #print "-" * 79 + + # get the appropriate sample rate + if obj.source == 'NI': + sampRateCol = 'NISampleRate' + elif obj.source == 'VN': + sampRateCol = 'VNavSampleRate' + else: + raise ValueError('{0} is not a valid source.'.format(obj.source)) + + obj.sampleRate = rTab[rownum][rTab.colnames.index(sampRateCol)] + + return obj + + def __array_finalize__(self, obj): + if obj is None: return + self.calibrationType = getattr(obj, 'calibrationType', None) + self.name = getattr(obj, 'name', None) + self.runid = getattr(obj, 'runid', None) + self.sampleRate = getattr(obj, 'sampleRate', None) + self.sensor = getattr(obj, 'sensor', None) + self.source = getattr(obj, 'source', None) + self.units = getattr(obj, 'units', None) + self.timeStamp = getattr(obj, 'timeStamp', None) + + def __array_wrap__(self, outputArray, context=None): + # doesn't support these things in basic ufunc calls...maybe one day + outputArray.calibrationType = None + outputArray.name = None + outputArray.sensor = None + outputArray.source = None + outputArray.units = None + return np.ndarray.__array_wrap__(self, outputArray, context) + + def scale(self): + """ + Returns the scaled signal based on the calibration data for the + supplied date. + + Returns + ------- + : ndarray (n,) + Scaled signal. + + """ + try: + self.calibrationType + except AttributeError: + raise AttributeError("Can't scale without the calibration type") + + # these will need to be changed once we start measuring them + doNotScale = ['LeanPotentiometer', + 'HipPotentiometer', + 'TwistPotentiometer'] + if self.calibrationType in ['none', 'matrix'] or self.name in doNotScale: + #print "Not scaling {0}".format(self.name) + return self + else: + pass + #print "Scaling {0}".format(self.name) + + # pick the largest calibration date without surpassing the run date + calibData = self.sensor.get_data_for_date(self.timeStamp) + + slope = calibData['slope'] + bias = calibData['bias'] + intercept = calibData['offset'] + calibrationSupplyVoltage = calibData['calibrationSupplyVoltage'] + + #print "slope {0}, bias {1}, intercept {2}".format(slope, bias, + #intercept) + + if self.calibrationType == 'interceptStar': + # this is for potentiometers, where the slope is ratiometric + # and zero degrees is always zero volts + calibratedSignal = (calibrationSupplyVoltage / self.supply * + slope * self + intercept) + elif self.calibrationType == 'intercept': + # this is the typical calibration that I use for all the + # sensors that I calibrate myself + calibratedSignal = (calibrationSupplyVoltage / self.supply * + (slope * self + intercept)) + elif self.calibrationType == 'bias': + # this is for the accelerometers and rate gyros that are + # "ratiometric", but I'm still not sure this is correct + calibratedSignal = (slope * (self - self.supply / + calibrationSupplyVoltage * bias)) + else: + raise StandardError("None of the calibration equations worked.") + calibratedSignal.name = calibData['signal'] + calibratedSignal.units = calibData['units'] + calibratedSignal.source = self.source + + return calibratedSignal.view(Signal) + + def plot_scaled(self, show=True): + '''Plots and returns the scaled signal versus time.''' + time = self.time() + scaled = self.scale() + line = plt.plot(time, scaled[1]) + plt.xlabel('Time [s]') + plt.ylabel(scaled[2]) + plt.title('{0} signal during run {1}'.format(scaled[0], + str(self.runid))) + if show: + plt.show() + return line class Sensor(): - """This class is a container for calibration data for a sensor.""" - - def __init__(self, name, calibrationTable): - """ - Initializes this sensor class. - - Parameters - ---------- - name : string - The CamelCase name of the sensor (e.g. SteerTorqueSensor). - calibrationTable : pyTables table object - This is the calibration data table that contains all the data taken - during calibrations. - - """ - self.name = name - self._store_calibration_data(calibrationTable) - - def _store_calibration_data(self, calibrationTable): - """ - Stores a dictionary of calibration data for the sensor for all - calibration dates in the object. - - Parameters - ---------- - calibrationTable : pyTables table object - This is the calibration data table that contains all the data taken - during calibrations. - - """ - self.data = {} - - for row in calibrationTable.iterrows(): - if self.name == row['name']: - self.data[row['calibrationID']] = {} - for col in calibrationTable.colnames: - self.data[row['calibrationID']][col] = row[col] - - if self.data == {}: - raise KeyError(('{0} is not a valid sensor ' + - 'name').format(self.name)) - - def get_data_for_date(self, runDate): - """ - Returns the calibration data for the sensor for the most recent - calibration relative to `runDate`. - - Parameters - ---------- - runDate : datetime object - This is the date of the run that the calibration data is needed - for. - - Returns - ------- - calibData : dictionary - A dictionary containing the sensor calibration data for the - calibration closest to but not past `runDate`. - - Notes - ----- - This method will select the calibration data for the date closest to - but not past `runDate`. **All calibrations must be taken before the - runs.** - - """ - # make a list of calibration ids and time stamps - dateIdPairs = [(k, matlab_date_to_object(v['timeStamp'])) - for k, v in self.data.iteritems()] - # sort the pairs with the most recent date first - dateIdPairs.sort(key=lambda x: x[1], reverse=True) - # go through the list and return the index at which the calibration - # date is larger than the run date - for i, pair in enumerate(dateIdPairs): - if runDate >= pair[1]: - break - return self.data[dateIdPairs[i][0]] + """This class is a container for calibration data for a sensor.""" + + def __init__(self, name, calibrationTable): + """ + Initializes this sensor class. + + Parameters + ---------- + name : string + The CamelCase name of the sensor (e.g. SteerTorqueSensor). + calibrationTable : pyTables table object + This is the calibration data table that contains all the data taken + during calibrations. + + """ + self.name = name + self._store_calibration_data(calibrationTable) + + def _store_calibration_data(self, calibrationTable): + """ + Stores a dictionary of calibration data for the sensor for all + calibration dates in the object. + + Parameters + ---------- + calibrationTable : pyTables table object + This is the calibration data table that contains all the data taken + during calibrations. + + """ + self.data = {} + + for row in calibrationTable.iterrows(): + if self.name == row['name']: + self.data[row['calibrationID']] = {} + for col in calibrationTable.colnames: + self.data[row['calibrationID']][col] = row[col] + + if self.data == {}: + raise KeyError(('{0} is not a valid sensor ' + + 'name').format(self.name)) + + def get_data_for_date(self, runDate): + """ + Returns the calibration data for the sensor for the most recent + calibration relative to `runDate`. + + Parameters + ---------- + runDate : datetime object + This is the date of the run that the calibration data is needed + for. + + Returns + ------- + calibData : dictionary + A dictionary containing the sensor calibration data for the + calibration closest to but not past `runDate`. + + Notes + ----- + This method will select the calibration data for the date closest to + but not past `runDate`. **All calibrations must be taken before the + runs.** + + """ + # make a list of calibration ids and time stamps + dateIdPairs = [(k, matlab_date_to_object(v['timeStamp'])) + for k, v in self.data.iteritems()] + # sort the pairs with the most recent date first + dateIdPairs.sort(key=lambda x: x[1], reverse=True) + # go through the list and return the index at which the calibration + # date is larger than the run date + for i, pair in enumerate(dateIdPairs): + if runDate >= pair[1]: + break + return self.data[dateIdPairs[i][0]] class Run(): - """The fluppin fundamental class for a run.""" - - def __init__(self, runid, dataset, pathToParameterData=None, - forceRecalc=False, filterFreq=None, store=True): - """Loads the raw and processed data for a run if available otherwise it - generates the processed data from the raw data. - - Parameters - ---------- - runid : int or str - The run id should be an integer, e.g. 5, or a five digit string with - leading zeros, e.g. '00005'. - dataset : DataSet - A DataSet object with at least some raw data. - pathToParameterData : string, {'', None}, optional - The path to a data directory for the BicycleParameters package. It - should contain the bicycles and riders used in the experiments. - forceRecalc : boolean, optional, default = False - If true then it will force a recalculation of all the processed - data. - filterSigs : float, optional, default = None - If true all of the processed signals will be low pass filtered with - a second order Butterworth filter at the given filter frequency. - store : boolean, optional, default = True - If true the resulting task signals will be stored in the database. - - """ - - if pathToParameterData is None: - pathToParameterData = config.get('data', 'pathToParameters') - - print "Initializing the run object." - - self.filterFreq = filterFreq - - dataset.open() - dataTable = dataset.database.root.runTable - signalTable = dataset.database.root.signalTable - taskTable = dataset.database.root.taskTable - - runid = run_id_string(runid) - - # get the row number for this particular run id - rownum = get_row_num(runid, dataTable) - - # make some dictionaries to store all the data - self.metadata = {} - self.rawSignals = {} - - # make lists of the input and output signals - rawDataCols = [x['signal'] for x in - signalTable.where("isRaw == True")] - computedCols = [x['signal'] for x in - signalTable.where("isRaw == False")] - - # store the metadata for this run - print "Loading metadata from the database." - for col in dataTable.colnames: - if col not in (rawDataCols + computedCols): - self.metadata[col] = get_cell(dataTable, col, rownum) - - print "Loading the raw signals from the database." - for col in rawDataCols: - # rawDataCols includes all possible raw signals, but every run - # doesn't have all the signals, so skip the ones that aren't there - try: - self.rawSignals[col] = RawSignal(runid, col, dataset.database) - except NoSuchNodeError: - pass - - if self.metadata['Rider'] != 'None': - self.load_rider(pathToParameterData) - - self.bumpLength = 1.0 # 1 meter - - # Try to load the task signals if they've already been computed. If - # they aren't in the database, the filter frequencies don't match or - # forceRecalc is true the then compute them. This may save some time - # when repeatedly loading runs for analysis. - self.taskFromDatabase = False - try: - runGroup = dataset.database.root.taskData._f_getChild(runid) - except NoSuchNodeError: - forceRecalc = True - else: - # The filter frequency stored in the task table is either a nan - # value or a valid float. If the stored filter frequency is not the - # same as the the one passed to Run, then a recalculation should be - # forced. - taskRowNum = get_row_num(runid, taskTable) - storedFreq = taskTable.cols.FilterFrequency[taskRowNum] - self.taskSignals = {} - if filterFreq is None: - newFilterFreq = np.nan - else: - newFilterFreq = filterFreq - - if np.isnan(newFilterFreq) and np.isnan(storedFreq): - for node in runGroup._f_walkNodes(): - meta = {k : node._f_getAttr(k) for k in ['units', 'name', - 'runid', 'sampleRate', 'source']} - self.taskSignals[node.name] = Signal(node[:], meta) - self.taskFromDatabase = True - elif np.isnan(newFilterFreq) or np.isnan(storedFreq): - forceRecalc = True - else: - if abs(storedFreq - filterFreq) < 1e-10: - for node in runGroup._f_walkNodes(): - meta = {k : node._f_getAttr(k) for k in ['units', 'name', - 'runid', 'sampleRate', 'source']} - self.taskSignals[node.name] = Signal(node[:], meta) - self.taskFromDatabase = True - else: - forceRecalc = True - - dataset.close() - - if forceRecalc == True: - try: - del self.taskSignals - except AttributeError: - pass - self.process_raw_signals() - - # store the task signals in the database if they are newly computed - if (store == True and self.taskFromDatabase == False - and self.topSig == 'task'): - taskMeta = { - 'Duration' : - self.taskSignals['ForwardSpeed'].time()[-1], - 'FilterFrequency' : self.filterFreq, - 'MeanSpeed' : self.taskSignals['ForwardSpeed'].mean(), - 'RunID' : self.metadata['RunID'], - 'StdSpeed' : self.taskSignals['ForwardSpeed'].std(), - 'Tau' : self.tau, - } - dataset.add_task_signals(self.taskSignals, taskMeta) - - # tell the user about the run - print self - - def process_raw_signals(self): - """Processes the raw signals as far as possible and filters the - result if a cutoff frequency was specified.""" - - print "Computing signals from raw data." - self.calibrate_signals() - - # the following maneuvers should never be calculated beyond the - # calibrated signals - maneuver = self.metadata['Maneuver'] - con1 = maneuver != 'Steer Dynamics Test' - con2 = maneuver != 'System Test' - con3 = maneuver != 'Static Calibration' - if con1 and con2 and con3: - self.compute_time_shift() - self.check_time_shift(0.15) - self.truncate_signals() - self.compute_signals() - self.task_signals() - - if self.filterFreq is not None: - self.filter_top_signals(self.filterFreq) - - def filter_top_signals(self, filterFreq): - """Filters the top most signals with a low pass filter.""" - - if self.topSig == 'task': - print('Filtering the task signals.') - for k, v in self.taskSignals.items(): - self.taskSignals[k] = v.filter(filterFreq) - elif self.topSig == 'computed': - print('Filtering the computed signals.') - for k, v in self.computedSignals.items(): - self.computedSignals[k] = v.filter(filterFreq) - elif self.topSig == 'calibrated': - print('Filtering the calibrated signals.') - for k, v in self.calibratedSignals.items(): - self.calibratedSignals[k] = v.filter(filterFreq) - - def calibrate_signals(self): - """Calibrates the raw signals.""" - - # calibrate the signals for the run - self.calibratedSignals = {} - for sig in self.rawSignals.values(): - calibSig = sig.scale() - self.calibratedSignals[calibSig.name] = calibSig - - self.topSig = 'calibrated' - - def task_signals(self): - """Computes the task signals.""" - print('Extracting the task portion from the data.') - self.extract_task() - - # compute task specific variables* - self.compute_yaw_angle() - self.compute_rear_wheel_contact_rates() - self.compute_rear_wheel_contact_points() - self.compute_front_wheel_contact_points() - self.compute_front_wheel_yaw_angle() - self.compute_front_wheel_rate() - self.compute_front_rear_wheel_contact_forces() - - self.topSig = 'task' - - def compute_signals(self): - """Computes the task independent quantities.""" - - self.computedSignals ={} - # transfer some of the signals to computed - noChange = ['FiveVolts', - 'PushButton', - 'RearWheelRate', - 'RollAngle', - 'SteerAngle', - 'ThreeVolts'] - for sig in noChange: - if sig in ['RollAngle', 'SteerAngle']: - self.computedSignals[sig] =\ - self.truncatedSignals[sig].convert_units('radian') - else: - self.computedSignals[sig] = self.truncatedSignals[sig] - - # compute the quantities that aren't task specific - self.compute_pull_force() - self.compute_forward_speed() - self.compute_steer_rate() - self.compute_yaw_roll_pitch_rates() - self.compute_steer_torque() - - def truncate_signals(self): - """Truncates the calibrated signals based on the time shift.""" - - self.truncatedSignals = {} - for name, sig in self.calibratedSignals.items(): - self.truncatedSignals[name] = sig.truncate(self.tau).spline() - self.topSig = 'truncated' - - def compute_time_shift(self): - """Computes the time shift based on the vertical accelerometer - signals.""" - - self.tau = sigpro.find_timeshift( - self.calibratedSignals['AccelerometerAccelerationY'], - self.calibratedSignals['AccelerationZ'], - self.metadata['NISampleRate'], - self.metadata['Speed'], plotError=False) - - def check_time_shift(self, maxNRMS): - """Raises an error if the normalized root mean square of the shifted - accelerometer signals is high.""" - - # Check to make sure the signals were actually good fits by - # calculating the normalized root mean square. If it isn't very - # low, raise an error. - niAcc = self.calibratedSignals['AccelerometerAccelerationY'] - vnAcc = self.calibratedSignals['AccelerationZ'] - vnAcc = vnAcc.truncate(self.tau).spline() - niAcc = niAcc.truncate(self.tau).spline() - # todo: this should probably check the rms of the mean subtracted data - # because both accelerometers don't always give the same value, this - # may work better with a filtered signal too - # todo: this should probably be moved into the time shift code in the - # signalprocessing model - nrms = np.sqrt(np.mean((vnAcc + niAcc)**2)) / (niAcc.max() - niAcc.min()) - if nrms > maxNRMS: - raise TimeShiftError(('The normalized root mean square for this ' + - 'time shift is {}, which is greater '.format(str(nrms)) + - 'than the maximum allowed: {}'.format(str(maxNRMS)))) - - def compute_rear_wheel_contact_points(self): - """Computes the location of the wheel contact points in the ground - plane.""" - - # get the rates - try: - latRate = self.taskSignals['LateralRearContactRate'] - lonRate = self.taskSignals['LongitudinalRearContactRate'] - except AttributeError: - print('At least one of the rates are not available. ' + - 'The YawAngle was not computed.') - else: - # convert to meters per second - latRate = latRate.convert_units('meter/second') - lonRate = lonRate.convert_units('meter/second') - # integrate and try to account for the drift - lat = latRate.integrate(detrend=True) - lon = lonRate.integrate() - # set the new name and units - lat.name = 'LateralRearContact' - lat.units = 'meter' - lon.name = 'LongitudinalRearContact' - lon.units = 'meter' - # store in task signals - self.taskSignals[lat.name] = lat - self.taskSignals[lon.name] = lon - - def compute_front_wheel_contact_points(self): - """Caluculates the front wheel contact points in the ground plane.""" - - q1 = self.taskSignals['LongitudinalRearContact'] - q2 = self.taskSignals['LateralRearContact'] - q3 = self.taskSignals['YawAngle'] - q4 = self.taskSignals['RollAngle'] - q7 = self.taskSignals['SteerAngle'] - - p = benchmark_to_moore(self.bicycleRiderParameters) - - f = np.vectorize(front_contact) - q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], - p['rf']) - - q9.name = 'LongitudinalFrontContact' - q9.units = 'meter' - q10.name = 'LateralFrontContact' - q10.units = 'meter' - - self.taskSignals['LongitudinalFrontContact'] = q9 - self.taskSignals['LateralFrontContact'] = q10 - - def compute_front_wheel_yaw_angle(self): - """Calculates the yaw angle of the front wheel.""" - - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(bp) - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - f = np.vectorize(front_wheel_yaw_angle) - q1_front_wheel = f(q1, q2, q4, mp['d1'], mp['d2'], mp['d3'], - bp['lam'], mp['rr'], mp['rf']) - - q1_front_wheel.name = 'FrontWheelYawAngle' - q1_front_wheel.units = 'meter' - self.taskSignals['FrontWheelYawAngle'] = q1_front_wheel - - def compute_front_wheel_rate(self): - """Calculates the front wheel rate, based on the Jason's data - of front_wheel_contact_point. Alternatively, you can use the - sympy to get the front_wheel_contact_rate directly first.""" - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(bp) - - f = np.vectorize(front_wheel_rate) - - u6 = f(q1, q2, q4, u9, u10, mp['d1'], mp['d2'], mp['d3'], - bp['lam'], mp['rr'], mp['rf']) - - u6.name = 'FrontWheelRate' - u6.units = 'radian/second' - self.taskSignals['FrontWheelRate'] = u6 - - - def compute_front_rear_wheel_contact_forces(self): - """Calculate the contact forces for each - wheel with respect to inertial frame under - the slip and nonslip condition. Also, provide - the steer torque T4.""" - - #parameters - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(self.bicycleRiderParameters) - - l1 = mp['l1'] - l2 = mp['l2'] - mc = mp['mc'] - ic11 = mp['ic11'] - ic22 = mp['ic22'] - ic33 = mp['ic33'] - ic31 = mp['ic31'] - rf = mp['rf'] - - #signals assignment --slip condition - V = self.taskSignals['ForwardSpeed'].mean() - - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - u2 = self.taskSignals['RollRate'] - u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] - u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] - u7 = self.taskSignals['LongitudinalRearContactRate'] - u8 = self.taskSignals['LateralRearContactRate'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - u2d = u2.time_derivative() - u3d = u3.time_derivative() - u4d = u4.time_derivative() - u5d = u5.time_derivative() - u6d = u6.time_derivative() - u7d = u7.time_derivative() - u8d = u8.time_derivative() - u9d = u9.time_derivative() - u10d = u10.time_derivative() - - f_1 = np.vectorize(steer_torque_slip) - f_2 = np.vectorize(contact_forces_slip) - f_3 = np.vectorize(contact_forces_nonslip) - - #calculation - #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - - #attributes: name and units, and taskSignals - T4_slip.name = 'SteerTorque_Slip' - T4_slip.units = 'newton*meter' - self.taskSignals[T4_slip.name] = T4_slip - - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' - Fx_r_s.units = 'newton' - self.taskSignals[Fx_r_s.name] = Fx_r_s - - Fy_r_s.name = 'LateralRearContactForce_Slip' - Fy_r_s.units = 'newton' - self.taskSignals[Fy_r_s.name] = Fy_r_s - - Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' - Fx_f_s.units = 'newton' - self.taskSignals[Fx_f_s.name] = Fx_f_s - - Fy_f_s.name = 'LateralFrontContactForce_Slip' - Fy_f_s.units = 'newton' - self.taskSignals[Fy_f_s.name] = Fy_f_s - - Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' - Fx_r_ns.units = 'newton' - self.taskSignals[Fx_r_ns.name] = Fx_r_ns - - Fy_r_ns.name = 'LateralRearContactForce_Nonslip' - Fy_r_ns.units = 'newton' - self.taskSignals[Fy_r_ns.name] = Fy_r_ns - - Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' - Fx_f_ns.units = 'newton' - self.taskSignals[Fx_f_ns.name] = Fx_f_ns - - Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' - Fy_f_ns.units = 'newton' - self.taskSignals[Fy_f_ns.name] = Fy_f_ns - - - def compute_rear_wheel_contact_rates(self): - """Calculates the rates of the wheel contact points in the ground - plane.""" - - try: - yawAngle = self.taskSignals['YawAngle'] - rearWheelRate = self.taskSignals['RearWheelRate'] - rR = self.bicycleRiderParameters['rR'] # this should be in meters - except AttributeError: - print('Either the yaw angle, rear wheel rate or ' + - 'front wheel radius is not available. The ' + - 'contact rates were not computed.') - else: - yawAngle = yawAngle.convert_units('radian') - rearWheelRate = rearWheelRate.convert_units('radian/second') - - lon, lat = sigpro.rear_wheel_contact_rate(rR, rearWheelRate, yawAngle) - - lon.name = 'LongitudinalRearContactRate' - lon.units = 'meter/second' - self.taskSignals[lon.name] = lon - - lat.name = 'LateralRearContactRate' - lat.units = 'meter/second' - self.taskSignals[lat.name] = lat - - def compute_yaw_angle(self): - """Computes the yaw angle by integrating the yaw rate.""" - - # get the yaw rate - try: - yawRate = self.taskSignals['YawRate'] - except AttributeError: - print('YawRate is not available. The YawAngle was not computed.') - else: - # convert to radians per second - yawRate = yawRate.convert_units('radian/second') - # integrate and try to account for the drift - yawAngle = yawRate.integrate(detrend=True) - # set the new name and units - yawAngle.name = 'YawAngle' - yawAngle.units = 'radian' - # store in computed signals - self.taskSignals['YawAngle'] = yawAngle - - def compute_steer_torque(self, plot=False): - """Computes the rider applied steer torque. - - Parameters - ---------- - plot : boolean, optional - Default is False, but if True a plot is generated. - - """ - # steer torque - frameAngRate = np.vstack(( - self.truncatedSignals['AngularRateX'], - self.truncatedSignals['AngularRateY'], - self.truncatedSignals['AngularRateZ'])) - frameAngAccel = np.vstack(( - self.truncatedSignals['AngularRateX'].time_derivative(), - self.truncatedSignals['AngularRateY'].time_derivative(), - self.truncatedSignals['AngularRateZ'].time_derivative())) - frameAccel = np.vstack(( - self.truncatedSignals['AccelerationX'], - self.truncatedSignals['AccelerationY'], - self.truncatedSignals['AccelerationZ'])) - handlebarAngRate = self.truncatedSignals['ForkRate'] - handlebarAngAccel = self.truncatedSignals['ForkRate'].time_derivative() - steerAngle = self.truncatedSignals['SteerAngle'] - steerColumnTorque =\ - self.truncatedSignals['SteerTubeTorque'].convert_units('newton*meter') - handlebarMass = self.bicycleRiderParameters['mG'] - handlebarInertia =\ - self.bicycle.steer_assembly_moment_of_inertia(fork=False, - wheel=False, nominal=True) - # this is the distance from the handlebar center of mass to the - # steer axis - w = self.bicycleRiderParameters['w'] - c = self.bicycleRiderParameters['c'] - lam = self.bicycleRiderParameters['lam'] - xG = self.bicycleRiderParameters['xG'] - zG = self.bicycleRiderParameters['zG'] - handlebarCoM = np.array([xG, 0., zG]) - d = bp.geometry.distance_to_steer_axis(w, c, lam, handlebarCoM) - # these are the distances from the point on the steer axis which is - # aligned with the handlebar center of mass to the accelerometer on - # the frame - ds1 = self.bicycle.parameters['Measured']['ds1'] - ds3 = self.bicycle.parameters['Measured']['ds3'] - ds = np.array([ds1, 0., ds3]) # i measured these - # damping and friction values come from Peter's work, I need to verify - # them still - damping = 0.3475 - friction = 0.0861 - - components = sigpro.steer_torque_components( - frameAngRate, frameAngAccel, frameAccel, handlebarAngRate, - handlebarAngAccel, steerAngle, steerColumnTorque, - handlebarMass, handlebarInertia, damping, friction, d, ds) - steerTorque = sigpro.steer_torque(components) - - stDict = {'units':'newton*meter', - 'name':'SteerTorque', - 'runid':self.metadata['RunID'], - 'sampleRate':steerAngle.sampleRate, - 'source':'NA'} - self.computedSignals['SteerTorque'] = Signal(steerTorque, stDict) - - if plot is True: - - time = steerAngle.time() - - hdot = (components['Hdot1'] + components['Hdot2'] + - components['Hdot3'] + components['Hdot4']) - cross = (components['cross1'] + components['cross2'] + - components['cross3']) - - fig = plt.figure() - - frictionAx = fig.add_subplot(4, 1, 1) - frictionAx.plot(time, components['viscous'], - time, components['coulomb'], - time, components['viscous'] + components['coulomb']) - frictionAx.set_ylabel('Torque [N-m]') - frictionAx.legend(('Viscous Friction', 'Coulomb Friction', - 'Total Friction')) - - dynamicAx = fig.add_subplot(4, 1, 2) - dynamicAx.plot(time, hdot, time, cross, time, hdot + cross) - dynamicAx.set_ylabel('Torque [N-m]') - dynamicAx.legend((r'Torque due to $\dot{H}$', - r'Torque due to $r \times m a$', - r'Total Dynamic Torque')) - - additionalAx = fig.add_subplot(4, 1, 3) - additionalAx.plot(time, hdot + cross + components['viscous'] + - components['coulomb'], - label='Total Frictional and Dynamic Torque') - additionalAx.set_ylabel('Torque [N-m]') - additionalAx.legend() - - torqueAx = fig.add_subplot(4, 1, 4) - torqueAx.plot(time, components['steerColumn'], - time, hdot + cross + components['viscous'] + components['coulomb'], - time, steerTorque) - torqueAx.set_xlabel('Time [s]') - torqueAx.set_ylabel('Torque [N-m]') - torqueAx.legend(('Measured Torque', 'Frictional and Dynamic Torque', - 'Rider Applied Torque')) - - plt.show() - - return fig - - def compute_yaw_roll_pitch_rates(self): - """Computes the yaw, roll and pitch rates of the bicycle frame.""" - - try: - omegaX = self.truncatedSignals['AngularRateX'] - omegaY = self.truncatedSignals['AngularRateY'] - omegaZ = self.truncatedSignals['AngularRateZ'] - rollAngle = self.truncatedSignals['RollAngle'] - lam = self.bicycleRiderParameters['lam'] - except AttributeError: - print('All needed signals are not available. ' + - 'Yaw, roll and pitch rates were not computed.') - else: - omegaX = omegaX.convert_units('radian/second') - omegaY = omegaY.convert_units('radian/second') - omegaZ = omegaZ.convert_units('radian/second') - rollAngle = rollAngle.convert_units('radian') - - yr, rr, pr = sigpro.yaw_roll_pitch_rate(omegaX, omegaY, omegaZ, lam, - rollAngle=rollAngle) - yr.units = 'radian/second' - yr.name = 'YawRate' - rr.units = 'radian/second' - rr.name = 'RollRate' - pr.units = 'radian/second' - pr.name = 'PitchRate' - - self.computedSignals['YawRate'] = yr - self.computedSignals['RollRate'] = rr - self.computedSignals['PitchRate'] = pr - - def compute_steer_rate(self): - """Calculate the steer rate from the frame and fork rates.""" - try: - forkRate = self.truncatedSignals['ForkRate'] - omegaZ = self.truncatedSignals['AngularRateZ'] - except AttributeError: - print('ForkRate or AngularRateZ is not available. ' + - 'SteerRate was not computed.') - else: - forkRate = forkRate.convert_units('radian/second') - omegaZ = omegaZ.convert_units('radian/second') - - steerRate = sigpro.steer_rate(forkRate, omegaZ) - steerRate.units = 'radian/second' - steerRate.name = 'SteerRate' - self.computedSignals['SteerRate'] = steerRate - - def compute_forward_speed(self): - """Calculates the magnitude of the main component of velocity of the - center of the rear wheel.""" - - try: - rR = self.bicycleRiderParameters['rR'] - rearWheelRate = self.truncatedSignals['RearWheelRate'] - except AttributeError: - print('rR or RearWheelRate is not availabe. ' + - 'ForwardSpeed was not computed.') - else: - rearWheelRate = rearWheelRate.convert_units('radian/second') - - self.computedSignals['ForwardSpeed'] = -rR * rearWheelRate - self.computedSignals['ForwardSpeed'].units = 'meter/second' - self.computedSignals['ForwardSpeed'].name = 'ForwardSpeed' - - def compute_pull_force(self): - """ - Computes the pull force from the truncated pull force signal. - - """ - try: - pullForce = self.truncatedSignals['PullForce'] - except AttributeError: - print 'PullForce was not available. PullForce was not computed.' - else: - pullForce = pullForce.convert_units('newton') - pullForce.name = 'PullForce' - pullForce.units = 'newton' - self.computedSignals[pullForce.name] = pullForce - - def __str__(self): - '''Prints basic run information to the screen.''' - - line = "=" * 79 - info = 'Run # {0}\nEnvironment: {1}\nRider: {2}\nBicycle: {3}\nSpeed:'\ - '{4}\nManeuver: {5}\nNotes: {6}'.format( - self.metadata['RunID'], - self.metadata['Environment'], - self.metadata['Rider'], - self.metadata['Bicycle'], - self.metadata['Speed'], - self.metadata['Maneuver'], - self.metadata['Notes']) - - return line + '\n' + info + '\n' + line - - def export(self, filetype, directory='exports'): - """ - Exports the computed signals to a file. - - Parameters - ---------- - filetype : str - The type of file to export the data to. Options are 'mat', 'csv', - and 'pickle'. - - """ - - if filetype == 'mat': - if not os.path.exists(directory): - print "Creating {0}".format(directory) - os.makedirs(directory) - exportData = {} - exportData.update(self.metadata) - try: - exportData.update(self.taskSignals) - except AttributeError: - try: - exportData.update(self.truncatedSignals) - except AttributeError: - exportData.update(self.calibratedSignals) - print('Exported calibratedSignals to {}'.format(directory)) - else: - print('Exported truncatedSignals to {}'.format(directory)) - else: - print('Exported taskSignals to {}'.format(directory)) - - filename = pad_with_zeros(str(self.metadata['RunID']), 5) + '.mat' - io.savemat(os.path.join(directory, filename), exportData) - else: - raise NotImplementedError(('{0} method is not available' + - ' yet.').format(filetype)) - - def extract_task(self): - """Slices the computed signals such that data before the end of the - bump is removed and unusable trailng data is removed. - - """ - # get the z acceleration from the VN-100 - acc = -self.truncatedSignals['AccelerometerAccelerationY'].filter(30.) - # find the mean speed during the task (look at one second in the middle - # of the data) - speed = self.computedSignals['ForwardSpeed'] - meanSpeed = speed[len(speed) / 2 - 100:len(speed) / 2 + 100].mean() - wheelbase = self.bicycleRiderParameters['w'] - # find the bump - indices = sigpro.find_bump(acc, acc.sampleRate, meanSpeed, wheelbase, - self.bumpLength) - - - # if it is a pavilion run, then clip the end too - # these are the runs that the length of track method of clipping - # applies to - straight = ['Track Straight Line With Disturbance', - 'Balance With Disturbance', - 'Balance', - 'Track Straight Line'] - if (self.metadata['Environment'] == 'Pavillion Floor' and - self.metadata['Maneuver'] in straight): - - # this is based on the length of the track in the pavilion that we - # measured on September 21st, 2011 - trackLength = 32. - wheelbase - self.bumpLength - end = trackLength / meanSpeed * acc.sampleRate - - # i may need to clip the end based on the forward speed dropping - # below certain threshold around the mean - else: - # if it isn't a pavilion run, don't clip the end - end = -1 - - self.taskSignals = {} - for name, sig in self.computedSignals.items(): - self.taskSignals[name] = sig[indices[2]:end] - - def load_rider(self, pathToParameterData): - """Creates a bicycle/rider attribute which contains the physical - parameters for the bicycle and rider for this run.""" - - print("Loading the bicycle and rider data for " + - "{} on {}".format(self.metadata['Rider'], - self.metadata['Bicycle'])) - - # currently this isn't very generic, it only assumes that there was - # Luke, Jason, and Charlie riding on the instrumented bicycle. - rider = self.metadata['Rider'] - if rider == 'Charlie' or rider == 'Luke': - # Charlie and Luke rode the bike in the same configuration - bicycle = 'Rigidcl' - elif rider == 'Jason' : - bicycle = 'Rigid' - else: - raise StandardError('There are no bicycle parameters ' + - 'for {}'.format(rider)) - - # force a recalculation (but not the period calcs, they take too long) - self.bicycle = bp.Bicycle(bicycle, pathToData=pathToParameterData) - try: - self.bicycle.extras - except AttributeError: - pass - else: - self.bicycle.save_parameters() - # force a recalculation of the human parameters - self.bicycle.add_rider(rider) - if self.bicycle.human is not None: - self.bicycle.save_parameters() - - self.bicycleRiderParameters =\ - bp.io.remove_uncertainties(self.bicycle.parameters['Benchmark']) - - def plot(self, *args, **kwargs): - ''' - Returns a plot of the time series of various signals. - - Parameters - ---------- - signalName : string - These should be strings that correspond to the signals available in - the computed data. If the first character of the string is `-` then - the negative signal will be plotted. You can also scale the values - so by adding a value and an ``*`` such as: ``'-10*RollRate'. The - negative sign always has to come first. - signalType : string, optional - This allows you to plot from the other signal types. Options are - 'task', 'computed', 'truncated', 'calibrated', 'raw'. The default - is 'task'. - - ''' - if not kwargs: - kwargs = {'signalType': 'task'} - - mapping = {} - for x in ['computed', 'truncated', 'calibrated', 'raw', 'task']: - try: - mapping[x] = getattr(self, x + 'Signals') - except AttributeError: - pass - - fig = plt.figure() - ax = fig.add_axes([0.125, 0.125, 0.8, 0.7]) - - leg = [] - for i, arg in enumerate(args): - legName = arg - sign = 1. - # if a negative sign is present - if '-' in arg and arg[0] != '-': - raise ValueError('{} is incorrectly typed'.format(arg)) - elif '-' in arg and arg[0] == '-': - arg = arg[1:] - sign = -1. - # if a multiplication factor is present - if '*' in arg: - mul, arg = arg.split('*') - else: - mul = 1. - signal = sign * float(mul) * mapping[kwargs['signalType']][arg] - ax.plot(signal.time(), signal) - leg.append(legName + ' [' + mapping[kwargs['signalType']][arg].units + ']') - - ax.legend(leg) - runid = pad_with_zeros(str(self.metadata['RunID']), 5) - ax.set_title('Run: ' + runid + ', Rider: ' + self.metadata['Rider'] + - ', Speed: ' + str(self.metadata['Speed']) + 'm/s' + '\n' + - 'Maneuver: ' + self.metadata['Maneuver'] + - ', Environment: ' + self.metadata['Environment'] + '\n' + - 'Notes: ' + self.metadata['Notes']) - - ax.set_xlabel('Time [second]') - - ax.grid() - - return fig - - def plot_wheel_contact(self, show=False): - """Returns a plot of the wheel contact traces. - - Parameters - ---------- - show : boolean - If true the plot will be displayed. - - Returns - ------- - fig : matplotlib.Figure - - """ - - q1 = self.taskSignals['LongitudinalRearContact'] - q2 = self.taskSignals['LateralRearContact'] - q9 = self.taskSignals['LongitudinalFrontContact'] - q10 = self.taskSignals['LateralFrontContact'] - - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - ax.plot(q1, q2, q9, q10) - ax.set_xlabel('Distance [' + q1.units + ']') - ax.set_ylabel('Distance [' + q2.units + ']') - ax.set_ylim((-0.5, 0.5)) - rider = self.metadata['Rider'] - where = self.metadata['Environment'] - speed = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() - maneuver = self.metadata['Maneuver'] - ax.set_title(rider + ', ' + where + ', ' + maneuver + ' @ ' + speed + ' m/s') - - if show is True: - fig.show() - - return fig - - def verify_time_sync(self, show=True, saveDir=None): - """Shows a plot of the acceleration signals that were used to - synchronize the NI and VN data. If it doesn't show a good fit, then - something is wrong. - - Parameters - ---------- - show : boolean - If true, the figure will be displayed. - saveDir : str - The path to a directory in which to save the figure. - - """ - - if self.topSig == 'calibrated': - sigType = 'calibrated' - else: - sigType = 'truncated' - - fig = self.plot('-AccelerometerAccelerationY', 'AccelerationZ', - signalType=sigType) - ax = fig.axes[0] - ax.set_xlim((0, 10)) - title = ax.get_title() - ax.set_title(title + '\nSignal Type: ' + sigType) - if saveDir is not None: - if not os.path.exists(saveDir): - print "Creating {0}".format(saveDir) - os.makedirs(saveDir) - runid = run_id_string(self.metadata['RunID']) - fig.savefig(os.path.join(saveDir, runid + '.png')) - if show is True: - fig.show() - - return fig - - def video(self): - ''' - Plays the video of the run. - - ''' - # get the 5 digit string version of the run id - runid = pad_with_zeros(str(self.metadata['RunID']), 5) - viddir = os.path.join('..', 'Video') - abspath = os.path.abspath(viddir) - # check to see if there is a video for this run - if (runid + '.mp4') in os.listdir(viddir): - path = os.path.join(abspath, runid + '.mp4') - os.system('vlc "' + path + '"') - else: - print "No video for this run" + """The fluppin fundamental class for a run.""" + + def __init__(self, runid, dataset, pathToParameterData=None, + forceRecalc=False, filterFreq=None, store=True): + """Loads the raw and processed data for a run if available otherwise it + generates the processed data from the raw data. + + Parameters + ---------- + runid : int or str + The run id should be an integer, e.g. 5, or a five digit string with + leading zeros, e.g. '00005'. + dataset : DataSet + A DataSet object with at least some raw data. + pathToParameterData : string, {'', None}, optional + The path to a data directory for the BicycleParameters package. It + should contain the bicycles and riders used in the experiments. + forceRecalc : boolean, optional, default = False + If true then it will force a recalculation of all the processed + data. + filterSigs : float, optional, default = None + If true all of the processed signals will be low pass filtered with + a second order Butterworth filter at the given filter frequency. + store : boolean, optional, default = True + If true the resulting task signals will be stored in the database. + + """ + + if pathToParameterData is None: + pathToParameterData = config.get('data', 'pathToParameters') + + print "Initializing the run object." + + self.filterFreq = filterFreq + + dataset.open() + dataTable = dataset.database.root.runTable + signalTable = dataset.database.root.signalTable + taskTable = dataset.database.root.taskTable + + runid = run_id_string(runid) + + # get the row number for this particular run id + rownum = get_row_num(runid, dataTable) + + # make some dictionaries to store all the data + self.metadata = {} + self.rawSignals = {} + + # make lists of the input and output signals + rawDataCols = [x['signal'] for x in + signalTable.where("isRaw == True")] + computedCols = [x['signal'] for x in + signalTable.where("isRaw == False")] + + # store the metadata for this run + print "Loading metadata from the database." + for col in dataTable.colnames: + if col not in (rawDataCols + computedCols): + self.metadata[col] = get_cell(dataTable, col, rownum) + + print "Loading the raw signals from the database." + for col in rawDataCols: + # rawDataCols includes all possible raw signals, but every run + # doesn't have all the signals, so skip the ones that aren't there + try: + self.rawSignals[col] = RawSignal(runid, col, dataset.database) + except NoSuchNodeError: + pass + + if self.metadata['Rider'] != 'None': + self.load_rider(pathToParameterData) + + self.bumpLength = 1.0 # 1 meter + + # Try to load the task signals if they've already been computed. If + # they aren't in the database, the filter frequencies don't match or + # forceRecalc is true the then compute them. This may save some time + # when repeatedly loading runs for analysis. + self.taskFromDatabase = False + try: + runGroup = dataset.database.root.taskData._f_getChild(runid) + except NoSuchNodeError: + forceRecalc = True + else: + # The filter frequency stored in the task table is either a nan + # value or a valid float. If the stored filter frequency is not the + # same as the the one passed to Run, then a recalculation should be + # forced. + taskRowNum = get_row_num(runid, taskTable) + storedFreq = taskTable.cols.FilterFrequency[taskRowNum] + self.taskSignals = {} + if filterFreq is None: + newFilterFreq = np.nan + else: + newFilterFreq = filterFreq + + if np.isnan(newFilterFreq) and np.isnan(storedFreq): + for node in runGroup._f_walkNodes(): + meta = {k : node._f_getAttr(k) for k in ['units', 'name', + 'runid', 'sampleRate', 'source']} + self.taskSignals[node.name] = Signal(node[:], meta) + self.taskFromDatabase = True + elif np.isnan(newFilterFreq) or np.isnan(storedFreq): + forceRecalc = True + else: + if abs(storedFreq - filterFreq) < 1e-10: + for node in runGroup._f_walkNodes(): + meta = {k : node._f_getAttr(k) for k in ['units', 'name', + 'runid', 'sampleRate', 'source']} + self.taskSignals[node.name] = Signal(node[:], meta) + self.taskFromDatabase = True + else: + forceRecalc = True + + dataset.close() + + if forceRecalc == True: + try: + del self.taskSignals + except AttributeError: + pass + self.process_raw_signals() + + # store the task signals in the database if they are newly computed + if (store == True and self.taskFromDatabase == False + and self.topSig == 'task'): + taskMeta = { + 'Duration' : + self.taskSignals['ForwardSpeed'].time()[-1], + 'FilterFrequency' : self.filterFreq, + 'MeanSpeed' : self.taskSignals['ForwardSpeed'].mean(), + 'RunID' : self.metadata['RunID'], + 'StdSpeed' : self.taskSignals['ForwardSpeed'].std(), + 'Tau' : self.tau, + } + dataset.add_task_signals(self.taskSignals, taskMeta) + + # tell the user about the run + print self + + def process_raw_signals(self): + """Processes the raw signals as far as possible and filters the + result if a cutoff frequency was specified.""" + + print "Computing signals from raw data." + self.calibrate_signals() + + # the following maneuvers should never be calculated beyond the + # calibrated signals + maneuver = self.metadata['Maneuver'] + con1 = maneuver != 'Steer Dynamics Test' + con2 = maneuver != 'System Test' + con3 = maneuver != 'Static Calibration' + if con1 and con2 and con3: + self.compute_time_shift() + self.check_time_shift(0.15) + self.truncate_signals() + self.compute_signals() + self.task_signals() + + if self.filterFreq is not None: + self.filter_top_signals(self.filterFreq) + + def filter_top_signals(self, filterFreq): + """Filters the top most signals with a low pass filter.""" + + if self.topSig == 'task': + print('Filtering the task signals.') + for k, v in self.taskSignals.items(): + self.taskSignals[k] = v.filter(filterFreq) + elif self.topSig == 'computed': + print('Filtering the computed signals.') + for k, v in self.computedSignals.items(): + self.computedSignals[k] = v.filter(filterFreq) + elif self.topSig == 'calibrated': + print('Filtering the calibrated signals.') + for k, v in self.calibratedSignals.items(): + self.calibratedSignals[k] = v.filter(filterFreq) + + def calibrate_signals(self): + """Calibrates the raw signals.""" + + # calibrate the signals for the run + self.calibratedSignals = {} + for sig in self.rawSignals.values(): + calibSig = sig.scale() + self.calibratedSignals[calibSig.name] = calibSig + + self.topSig = 'calibrated' + + def task_signals(self): + """Computes the task signals.""" + print('Extracting the task portion from the data.') + self.extract_task() + + # compute task specific variables* + self.compute_yaw_angle() + self.compute_rear_wheel_contact_rates() + self.compute_rear_wheel_contact_points() + self.compute_front_wheel_contact_points() + self.compute_front_wheel_yaw_angle() + self.compute_front_wheel_rate() + self.compute_front_rear_wheel_contact_forces() + + self.topSig = 'task' + + def compute_signals(self): + """Computes the task independent quantities.""" + + self.computedSignals ={} + # transfer some of the signals to computed + noChange = ['FiveVolts', + 'PushButton', + 'RearWheelRate', + 'RollAngle', + 'SteerAngle', + 'ThreeVolts'] + for sig in noChange: + if sig in ['RollAngle', 'SteerAngle']: + self.computedSignals[sig] =\ + self.truncatedSignals[sig].convert_units('radian') + else: + self.computedSignals[sig] = self.truncatedSignals[sig] + + # compute the quantities that aren't task specific + self.compute_pull_force() + self.compute_forward_speed() + self.compute_steer_rate() + self.compute_yaw_roll_pitch_rates() + self.compute_steer_torque() + + def truncate_signals(self): + """Truncates the calibrated signals based on the time shift.""" + + self.truncatedSignals = {} + for name, sig in self.calibratedSignals.items(): + self.truncatedSignals[name] = sig.truncate(self.tau).spline() + self.topSig = 'truncated' + + def compute_time_shift(self): + """Computes the time shift based on the vertical accelerometer + signals.""" + + self.tau = sigpro.find_timeshift( + self.calibratedSignals['AccelerometerAccelerationY'], + self.calibratedSignals['AccelerationZ'], + self.metadata['NISampleRate'], + self.metadata['Speed'], plotError=False) + + def check_time_shift(self, maxNRMS): + """Raises an error if the normalized root mean square of the shifted + accelerometer signals is high.""" + + # Check to make sure the signals were actually good fits by + # calculating the normalized root mean square. If it isn't very + # low, raise an error. + niAcc = self.calibratedSignals['AccelerometerAccelerationY'] + vnAcc = self.calibratedSignals['AccelerationZ'] + vnAcc = vnAcc.truncate(self.tau).spline() + niAcc = niAcc.truncate(self.tau).spline() + # todo: this should probably check the rms of the mean subtracted data + # because both accelerometers don't always give the same value, this + # may work better with a filtered signal too + # todo: this should probably be moved into the time shift code in the + # signalprocessing model + nrms = np.sqrt(np.mean((vnAcc + niAcc)**2)) / (niAcc.max() - niAcc.min()) + if nrms > maxNRMS: + raise TimeShiftError(('The normalized root mean square for this ' + + 'time shift is {}, which is greater '.format(str(nrms)) + + 'than the maximum allowed: {}'.format(str(maxNRMS)))) + + def compute_rear_wheel_contact_points(self): + """Computes the location of the wheel contact points in the ground + plane.""" + + # get the rates + try: + latRate = self.taskSignals['LateralRearContactRate'] + lonRate = self.taskSignals['LongitudinalRearContactRate'] + except AttributeError: + print('At least one of the rates are not available. ' + + 'The YawAngle was not computed.') + else: + # convert to meters per second + latRate = latRate.convert_units('meter/second') + lonRate = lonRate.convert_units('meter/second') + # integrate and try to account for the drift + lat = latRate.integrate(detrend=True) + lon = lonRate.integrate() + # set the new name and units + lat.name = 'LateralRearContact' + lat.units = 'meter' + lon.name = 'LongitudinalRearContact' + lon.units = 'meter' + # store in task signals + self.taskSignals[lat.name] = lat + self.taskSignals[lon.name] = lon + + def compute_front_wheel_contact_points(self): + """Caluculates the front wheel contact points in the ground plane.""" + + q1 = self.taskSignals['LongitudinalRearContact'] + q2 = self.taskSignals['LateralRearContact'] + q3 = self.taskSignals['YawAngle'] + q4 = self.taskSignals['RollAngle'] + q7 = self.taskSignals['SteerAngle'] + + p = benchmark_to_moore(self.bicycleRiderParameters) + + f = np.vectorize(front_contact) + q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], + p['rf']) + + q9.name = 'LongitudinalFrontContact' + q9.units = 'meter' + q10.name = 'LateralFrontContact' + q10.units = 'meter' + + self.taskSignals['LongitudinalFrontContact'] = q9 + self.taskSignals['LateralFrontContact'] = q10 + + def compute_front_wheel_yaw_angle(self): + """Calculates the yaw angle of the front wheel.""" + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(bp) + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + f = np.vectorize(front_wheel_yaw_angle) + q1_front_wheel = f(q1, q2, q4, mp['d1'], mp['d2'], mp['d3'], + bp['lam'], mp['rr'], mp['rf']) + + q1_front_wheel.name = 'FrontWheelYawAngle' + q1_front_wheel.units = 'meter' + self.taskSignals['FrontWheelYawAngle'] = q1_front_wheel + + def compute_front_wheel_rate(self): + """Calculates the front wheel rate, based on the Jason's data + of front_wheel_contact_point. Alternatively, you can use the + sympy to get the front_wheel_contact_rate directly first.""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(bp) + + f = np.vectorize(front_wheel_rate) + + u6 = f(q1, q2, q4, u9, u10, mp['d1'], mp['d2'], mp['d3'], + bp['lam'], mp['rr'], mp['rf']) + + u6.name = 'FrontWheelRate' + u6.units = 'radian/second' + self.taskSignals['FrontWheelRate'] = u6 + + + def compute_front_rear_wheel_contact_forces(self): + """Calculate the contact forces for each + wheel with respect to inertial frame under + the slip and nonslip condition. Also, provide + the steer torque T4.""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + rf = mp['rf'] + + #signals assignment --slip condition + V = self.taskSignals['ForwardSpeed'].mean() + + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = u7.time_derivative() + u8d = u8.time_derivative() + u9d = u9.time_derivative() + u10d = u10.time_derivative() + + f_1 = np.vectorize(steer_torque_slip) + f_2 = np.vectorize(contact_forces_slip) + f_3 = np.vectorize(contact_forces_nonslip) + + #calculation + #steer torque slip + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + + #attributes: name and units, and taskSignals + T4_slip.name = 'SteerTorque_Slip' + T4_slip.units = 'newton*meter' + self.taskSignals[T4_slip.name] = T4_slip + + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s + + Fy_r_s.name = 'LateralRearContactForce_Slip' + Fy_r_s.units = 'newton' + self.taskSignals[Fy_r_s.name] = Fy_r_s + + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' + Fx_f_s.units = 'newton' + self.taskSignals[Fx_f_s.name] = Fx_f_s + + Fy_f_s.name = 'LateralFrontContactForce_Slip' + Fy_f_s.units = 'newton' + self.taskSignals[Fy_f_s.name] = Fy_f_s + + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' + Fx_r_ns.units = 'newton' + self.taskSignals[Fx_r_ns.name] = Fx_r_ns + + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' + Fy_r_ns.units = 'newton' + self.taskSignals[Fy_r_ns.name] = Fy_r_ns + + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' + Fx_f_ns.units = 'newton' + self.taskSignals[Fx_f_ns.name] = Fx_f_ns + + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' + Fy_f_ns.units = 'newton' + self.taskSignals[Fy_f_ns.name] = Fy_f_ns + + + def compute_rear_wheel_contact_rates(self): + """Calculates the rates of the wheel contact points in the ground + plane.""" + + try: + yawAngle = self.taskSignals['YawAngle'] + rearWheelRate = self.taskSignals['RearWheelRate'] + rR = self.bicycleRiderParameters['rR'] # this should be in meters + except AttributeError: + print('Either the yaw angle, rear wheel rate or ' + + 'front wheel radius is not available. The ' + + 'contact rates were not computed.') + else: + yawAngle = yawAngle.convert_units('radian') + rearWheelRate = rearWheelRate.convert_units('radian/second') + + lon, lat = sigpro.rear_wheel_contact_rate(rR, rearWheelRate, yawAngle) + + lon.name = 'LongitudinalRearContactRate' + lon.units = 'meter/second' + self.taskSignals[lon.name] = lon + + lat.name = 'LateralRearContactRate' + lat.units = 'meter/second' + self.taskSignals[lat.name] = lat + + def compute_yaw_angle(self): + """Computes the yaw angle by integrating the yaw rate.""" + + # get the yaw rate + try: + yawRate = self.taskSignals['YawRate'] + except AttributeError: + print('YawRate is not available. The YawAngle was not computed.') + else: + # convert to radians per second + yawRate = yawRate.convert_units('radian/second') + # integrate and try to account for the drift + yawAngle = yawRate.integrate(detrend=True) + # set the new name and units + yawAngle.name = 'YawAngle' + yawAngle.units = 'radian' + # store in computed signals + self.taskSignals['YawAngle'] = yawAngle + + def compute_steer_torque(self, plot=False): + """Computes the rider applied steer torque. + + Parameters + ---------- + plot : boolean, optional + Default is False, but if True a plot is generated. + + """ + # steer torque + frameAngRate = np.vstack(( + self.truncatedSignals['AngularRateX'], + self.truncatedSignals['AngularRateY'], + self.truncatedSignals['AngularRateZ'])) + frameAngAccel = np.vstack(( + self.truncatedSignals['AngularRateX'].time_derivative(), + self.truncatedSignals['AngularRateY'].time_derivative(), + self.truncatedSignals['AngularRateZ'].time_derivative())) + frameAccel = np.vstack(( + self.truncatedSignals['AccelerationX'], + self.truncatedSignals['AccelerationY'], + self.truncatedSignals['AccelerationZ'])) + handlebarAngRate = self.truncatedSignals['ForkRate'] + handlebarAngAccel = self.truncatedSignals['ForkRate'].time_derivative() + steerAngle = self.truncatedSignals['SteerAngle'] + steerColumnTorque =\ + self.truncatedSignals['SteerTubeTorque'].convert_units('newton*meter') + handlebarMass = self.bicycleRiderParameters['mG'] + handlebarInertia =\ + self.bicycle.steer_assembly_moment_of_inertia(fork=False, + wheel=False, nominal=True) + # this is the distance from the handlebar center of mass to the + # steer axis + w = self.bicycleRiderParameters['w'] + c = self.bicycleRiderParameters['c'] + lam = self.bicycleRiderParameters['lam'] + xG = self.bicycleRiderParameters['xG'] + zG = self.bicycleRiderParameters['zG'] + handlebarCoM = np.array([xG, 0., zG]) + d = bp.geometry.distance_to_steer_axis(w, c, lam, handlebarCoM) + # these are the distances from the point on the steer axis which is + # aligned with the handlebar center of mass to the accelerometer on + # the frame + ds1 = self.bicycle.parameters['Measured']['ds1'] + ds3 = self.bicycle.parameters['Measured']['ds3'] + ds = np.array([ds1, 0., ds3]) # i measured these + # damping and friction values come from Peter's work, I need to verify + # them still + damping = 0.3475 + friction = 0.0861 + + components = sigpro.steer_torque_components( + frameAngRate, frameAngAccel, frameAccel, handlebarAngRate, + handlebarAngAccel, steerAngle, steerColumnTorque, + handlebarMass, handlebarInertia, damping, friction, d, ds) + steerTorque = sigpro.steer_torque(components) + + stDict = {'units':'newton*meter', + 'name':'SteerTorque', + 'runid':self.metadata['RunID'], + 'sampleRate':steerAngle.sampleRate, + 'source':'NA'} + self.computedSignals['SteerTorque'] = Signal(steerTorque, stDict) + + if plot is True: + + time = steerAngle.time() + + hdot = (components['Hdot1'] + components['Hdot2'] + + components['Hdot3'] + components['Hdot4']) + cross = (components['cross1'] + components['cross2'] + + components['cross3']) + + fig = plt.figure() + + frictionAx = fig.add_subplot(4, 1, 1) + frictionAx.plot(time, components['viscous'], + time, components['coulomb'], + time, components['viscous'] + components['coulomb']) + frictionAx.set_ylabel('Torque [N-m]') + frictionAx.legend(('Viscous Friction', 'Coulomb Friction', + 'Total Friction')) + + dynamicAx = fig.add_subplot(4, 1, 2) + dynamicAx.plot(time, hdot, time, cross, time, hdot + cross) + dynamicAx.set_ylabel('Torque [N-m]') + dynamicAx.legend((r'Torque due to $\dot{H}$', + r'Torque due to $r \times m a$', + r'Total Dynamic Torque')) + + additionalAx = fig.add_subplot(4, 1, 3) + additionalAx.plot(time, hdot + cross + components['viscous'] + + components['coulomb'], + label='Total Frictional and Dynamic Torque') + additionalAx.set_ylabel('Torque [N-m]') + additionalAx.legend() + + torqueAx = fig.add_subplot(4, 1, 4) + torqueAx.plot(time, components['steerColumn'], + time, hdot + cross + components['viscous'] + components['coulomb'], + time, steerTorque) + torqueAx.set_xlabel('Time [s]') + torqueAx.set_ylabel('Torque [N-m]') + torqueAx.legend(('Measured Torque', 'Frictional and Dynamic Torque', + 'Rider Applied Torque')) + + plt.show() + + return fig + + def compute_yaw_roll_pitch_rates(self): + """Computes the yaw, roll and pitch rates of the bicycle frame.""" + + try: + omegaX = self.truncatedSignals['AngularRateX'] + omegaY = self.truncatedSignals['AngularRateY'] + omegaZ = self.truncatedSignals['AngularRateZ'] + rollAngle = self.truncatedSignals['RollAngle'] + lam = self.bicycleRiderParameters['lam'] + except AttributeError: + print('All needed signals are not available. ' + + 'Yaw, roll and pitch rates were not computed.') + else: + omegaX = omegaX.convert_units('radian/second') + omegaY = omegaY.convert_units('radian/second') + omegaZ = omegaZ.convert_units('radian/second') + rollAngle = rollAngle.convert_units('radian') + + yr, rr, pr = sigpro.yaw_roll_pitch_rate(omegaX, omegaY, omegaZ, lam, + rollAngle=rollAngle) + yr.units = 'radian/second' + yr.name = 'YawRate' + rr.units = 'radian/second' + rr.name = 'RollRate' + pr.units = 'radian/second' + pr.name = 'PitchRate' + + self.computedSignals['YawRate'] = yr + self.computedSignals['RollRate'] = rr + self.computedSignals['PitchRate'] = pr + + def compute_steer_rate(self): + """Calculate the steer rate from the frame and fork rates.""" + try: + forkRate = self.truncatedSignals['ForkRate'] + omegaZ = self.truncatedSignals['AngularRateZ'] + except AttributeError: + print('ForkRate or AngularRateZ is not available. ' + + 'SteerRate was not computed.') + else: + forkRate = forkRate.convert_units('radian/second') + omegaZ = omegaZ.convert_units('radian/second') + + steerRate = sigpro.steer_rate(forkRate, omegaZ) + steerRate.units = 'radian/second' + steerRate.name = 'SteerRate' + self.computedSignals['SteerRate'] = steerRate + + def compute_forward_speed(self): + """Calculates the magnitude of the main component of velocity of the + center of the rear wheel.""" + + try: + rR = self.bicycleRiderParameters['rR'] + rearWheelRate = self.truncatedSignals['RearWheelRate'] + except AttributeError: + print('rR or RearWheelRate is not availabe. ' + + 'ForwardSpeed was not computed.') + else: + rearWheelRate = rearWheelRate.convert_units('radian/second') + + self.computedSignals['ForwardSpeed'] = -rR * rearWheelRate + self.computedSignals['ForwardSpeed'].units = 'meter/second' + self.computedSignals['ForwardSpeed'].name = 'ForwardSpeed' + + def compute_pull_force(self): + """ + Computes the pull force from the truncated pull force signal. + + """ + try: + pullForce = self.truncatedSignals['PullForce'] + except AttributeError: + print 'PullForce was not available. PullForce was not computed.' + else: + pullForce = pullForce.convert_units('newton') + pullForce.name = 'PullForce' + pullForce.units = 'newton' + self.computedSignals[pullForce.name] = pullForce + + def __str__(self): + '''Prints basic run information to the screen.''' + + line = "=" * 79 + info = 'Run # {0}\nEnvironment: {1}\nRider: {2}\nBicycle: {3}\nSpeed:'\ + '{4}\nManeuver: {5}\nNotes: {6}'.format( + self.metadata['RunID'], + self.metadata['Environment'], + self.metadata['Rider'], + self.metadata['Bicycle'], + self.metadata['Speed'], + self.metadata['Maneuver'], + self.metadata['Notes']) + + return line + '\n' + info + '\n' + line + + def export(self, filetype, directory='exports'): + """ + Exports the computed signals to a file. + + Parameters + ---------- + filetype : str + The type of file to export the data to. Options are 'mat', 'csv', + and 'pickle'. + + """ + + if filetype == 'mat': + if not os.path.exists(directory): + print "Creating {0}".format(directory) + os.makedirs(directory) + exportData = {} + exportData.update(self.metadata) + try: + exportData.update(self.taskSignals) + except AttributeError: + try: + exportData.update(self.truncatedSignals) + except AttributeError: + exportData.update(self.calibratedSignals) + print('Exported calibratedSignals to {}'.format(directory)) + else: + print('Exported truncatedSignals to {}'.format(directory)) + else: + print('Exported taskSignals to {}'.format(directory)) + + filename = pad_with_zeros(str(self.metadata['RunID']), 5) + '.mat' + io.savemat(os.path.join(directory, filename), exportData) + else: + raise NotImplementedError(('{0} method is not available' + + ' yet.').format(filetype)) + + def extract_task(self): + """Slices the computed signals such that data before the end of the + bump is removed and unusable trailng data is removed. + + """ + # get the z acceleration from the VN-100 + acc = -self.truncatedSignals['AccelerometerAccelerationY'].filter(30.) + # find the mean speed during the task (look at one second in the middle + # of the data) + speed = self.computedSignals['ForwardSpeed'] + meanSpeed = speed[len(speed) / 2 - 100:len(speed) / 2 + 100].mean() + wheelbase = self.bicycleRiderParameters['w'] + # find the bump + indices = sigpro.find_bump(acc, acc.sampleRate, meanSpeed, wheelbase, + self.bumpLength) + + + # if it is a pavilion run, then clip the end too + # these are the runs that the length of track method of clipping + # applies to + straight = ['Track Straight Line With Disturbance', + 'Balance With Disturbance', + 'Balance', + 'Track Straight Line'] + if (self.metadata['Environment'] == 'Pavillion Floor' and + self.metadata['Maneuver'] in straight): + + # this is based on the length of the track in the pavilion that we + # measured on September 21st, 2011 + trackLength = 32. - wheelbase - self.bumpLength + end = trackLength / meanSpeed * acc.sampleRate + + # i may need to clip the end based on the forward speed dropping + # below certain threshold around the mean + else: + # if it isn't a pavilion run, don't clip the end + end = -1 + + self.taskSignals = {} + for name, sig in self.computedSignals.items(): + self.taskSignals[name] = sig[indices[2]:end] + + def load_rider(self, pathToParameterData): + """Creates a bicycle/rider attribute which contains the physical + parameters for the bicycle and rider for this run.""" + + print("Loading the bicycle and rider data for " + + "{} on {}".format(self.metadata['Rider'], + self.metadata['Bicycle'])) + + # currently this isn't very generic, it only assumes that there was + # Luke, Jason, and Charlie riding on the instrumented bicycle. + rider = self.metadata['Rider'] + if rider == 'Charlie' or rider == 'Luke': + # Charlie and Luke rode the bike in the same configuration + bicycle = 'Rigidcl' + elif rider == 'Jason' : + bicycle = 'Rigid' + else: + raise StandardError('There are no bicycle parameters ' + + 'for {}'.format(rider)) + + # force a recalculation (but not the period calcs, they take too long) + self.bicycle = bp.Bicycle(bicycle, pathToData=pathToParameterData) + try: + self.bicycle.extras + except AttributeError: + pass + else: + self.bicycle.save_parameters() + # force a recalculation of the human parameters + self.bicycle.add_rider(rider) + if self.bicycle.human is not None: + self.bicycle.save_parameters() + + self.bicycleRiderParameters =\ + bp.io.remove_uncertainties(self.bicycle.parameters['Benchmark']) + + def plot(self, *args, **kwargs): + ''' + Returns a plot of the time series of various signals. + + Parameters + ---------- + signalName : string + These should be strings that correspond to the signals available in + the computed data. If the first character of the string is `-` then + the negative signal will be plotted. You can also scale the values + so by adding a value and an ``*`` such as: ``'-10*RollRate'. The + negative sign always has to come first. + signalType : string, optional + This allows you to plot from the other signal types. Options are + 'task', 'computed', 'truncated', 'calibrated', 'raw'. The default + is 'task'. + + ''' + if not kwargs: + kwargs = {'signalType': 'task'} + + mapping = {} + for x in ['computed', 'truncated', 'calibrated', 'raw', 'task']: + try: + mapping[x] = getattr(self, x + 'Signals') + except AttributeError: + pass + + fig = plt.figure() + ax = fig.add_axes([0.125, 0.125, 0.8, 0.7]) + + leg = [] + for i, arg in enumerate(args): + legName = arg + sign = 1. + # if a negative sign is present + if '-' in arg and arg[0] != '-': + raise ValueError('{} is incorrectly typed'.format(arg)) + elif '-' in arg and arg[0] == '-': + arg = arg[1:] + sign = -1. + # if a multiplication factor is present + if '*' in arg: + mul, arg = arg.split('*') + else: + mul = 1. + signal = sign * float(mul) * mapping[kwargs['signalType']][arg] + ax.plot(signal.time(), signal) + leg.append(legName + ' [' + mapping[kwargs['signalType']][arg].units + ']') + + ax.legend(leg) + runid = pad_with_zeros(str(self.metadata['RunID']), 5) + ax.set_title('Run: ' + runid + ', Rider: ' + self.metadata['Rider'] + + ', Speed: ' + str(self.metadata['Speed']) + 'm/s' + '\n' + + 'Maneuver: ' + self.metadata['Maneuver'] + + ', Environment: ' + self.metadata['Environment'] + '\n' + + 'Notes: ' + self.metadata['Notes']) + + ax.set_xlabel('Time [second]') + + ax.grid() + + return fig + + def plot_wheel_contact(self, show=False): + """Returns a plot of the wheel contact traces. + + Parameters + ---------- + show : boolean + If true the plot will be displayed. + + Returns + ------- + fig : matplotlib.Figure + + """ + + q1 = self.taskSignals['LongitudinalRearContact'] + q2 = self.taskSignals['LateralRearContact'] + q9 = self.taskSignals['LongitudinalFrontContact'] + q10 = self.taskSignals['LateralFrontContact'] + + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.plot(q1, q2, q9, q10) + ax.set_xlabel('Distance [' + q1.units + ']') + ax.set_ylabel('Distance [' + q2.units + ']') + ax.set_ylim((-0.5, 0.5)) + rider = self.metadata['Rider'] + where = self.metadata['Environment'] + speed = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() + maneuver = self.metadata['Maneuver'] + ax.set_title(rider + ', ' + where + ', ' + maneuver + ' @ ' + speed + ' m/s') + + if show is True: + fig.show() + + return fig + + def verify_time_sync(self, show=True, saveDir=None): + """Shows a plot of the acceleration signals that were used to + synchronize the NI and VN data. If it doesn't show a good fit, then + something is wrong. + + Parameters + ---------- + show : boolean + If true, the figure will be displayed. + saveDir : str + The path to a directory in which to save the figure. + + """ + + if self.topSig == 'calibrated': + sigType = 'calibrated' + else: + sigType = 'truncated' + + fig = self.plot('-AccelerometerAccelerationY', 'AccelerationZ', + signalType=sigType) + ax = fig.axes[0] + ax.set_xlim((0, 10)) + title = ax.get_title() + ax.set_title(title + '\nSignal Type: ' + sigType) + if saveDir is not None: + if not os.path.exists(saveDir): + print "Creating {0}".format(saveDir) + os.makedirs(saveDir) + runid = run_id_string(self.metadata['RunID']) + fig.savefig(os.path.join(saveDir, runid + '.png')) + if show is True: + fig.show() + + return fig + + def video(self): + ''' + Plays the video of the run. + + ''' + # get the 5 digit string version of the run id + runid = pad_with_zeros(str(self.metadata['RunID']), 5) + viddir = os.path.join('..', 'Video') + abspath = os.path.abspath(viddir) + # check to see if there is a video for this run + if (runid + '.mp4') in os.listdir(viddir): + path = os.path.join(abspath, runid + '.mp4') + os.system('vlc "' + path + '"') + else: + print "No video for this run" def matlab_date_to_object(matDate): - '''Returns a date time object based on a Matlab `datestr()` output. + '''Returns a date time object based on a Matlab `datestr()` output. - Parameters - ---------- - matDate : string - String in the form '21-Mar-2011 14:45:54'. + Parameters + ---------- + matDate : string + String in the form '21-Mar-2011 14:45:54'. - Returns - ------- - python datetime object + Returns + ------- + python datetime object - ''' - return datetime.datetime.strptime(matDate, '%d-%b-%Y %H:%M:%S') + ''' + return datetime.datetime.strptime(matDate, '%d-%b-%Y %H:%M:%S') From a8b7db95b24d90602a3d7f010ea54069d8c0c2f2 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 8 Oct 2012 09:44:26 -0700 Subject: [PATCH 18/37] Added select_runs func into the database.py module --- bicycledataprocessor/database.py | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/bicycledataprocessor/database.py b/bicycledataprocessor/database.py index a05202c..0751c34 100644 --- a/bicycledataprocessor/database.py +++ b/bicycledataprocessor/database.py @@ -361,6 +361,45 @@ def create_task_table(self): pass self.close() + def select_runs(self, riders, maneuvers, environments): + """Returns a list of runs given a set of conditions. + + Parameters + ---------- + riders : list + All or a subset of ['Charlie', 'Jason', 'Luke']. + maneuvers : list + All or a subset of ['Balance', 'Balance With Disturbance', + 'Track Straight Line', 'Track Straight Line With Disturbance']. + environments : list + All or a subset of ['Horse Treadmill', 'Pavillion Floor']. + + Returns + ------- + runs : list + List of run numbers for the given conditions. + + """ + + self.open() + + table = self.database.root.runTable + + runs = [] + for row in table.iterrows(): + con = [] + con.append(row['Rider'] in riders) + con.append(row['Maneuver'] in maneuvers) + con.append(row['Environment'] in environments) + con.append(row['corrupt'] is not True) + con.append(int(row['RunID']) > 100) + if False not in con: + runs.append(row['RunID']) + + self.close() + + return runs + def sync_data(self, directory='exports/'): """Synchronizes data to the biosport website.""" user = 'biosport' From b87b5286d22f73f02ea53a80e92a500f95d6ec9a Mon Sep 17 00:00:00 2001 From: StefenYin Date: Tue, 9 Oct 2012 10:50:11 -0700 Subject: [PATCH 19/37] Added frame_acceleration in signalprocession and compute_frame_acceleration in main --- bicycledataprocessor/main.py | 2985 +++++++++++----------- bicycledataprocessor/signalprocessing.py | 1095 ++++---- 2 files changed, 2073 insertions(+), 2007 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 7fcf21c..67fb5ad 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -9,11 +9,11 @@ # debugging #try: - #from IPython.core.debugger import Tracer + #from IPython.core.debugger import Tracer #except ImportError: - #pass + #pass #else: - #set_trace = Tracer() + #set_trace = Tracer() # dependencies import numpy as np @@ -39,1492 +39,1515 @@ config.read(os.path.join(os.path.dirname(__file__), '..', 'defaults.cfg')) class Signal(np.ndarray): - """ - A subclass of ndarray for collecting the data for a single signal in a run. - - Attributes - ---------- - conversions : dictionary - A mapping for unit conversions. - name : str - The name of the signal. Should be CamelCase. - runid : str - A five digit identification number associated with the - trial this signal was collected from (e.g. '00104'). - sampleRate : float - The sample rate in hertz of the signal. - source : str - The source of the data. This should be 'NI' for the - National Instruments USB-6218 and 'VN' for the VN-100 IMU. - units : str - The physcial units of the signal. These should be specified - as lowercase complete words using only multiplication and - division symbols (e.g. 'meter/second/second'). - Signal.conversions will show the avialable options. - - Methods - ------- - plot() - Plot's the signal versus time and returns the line. - frequency() - Returns the frequency spectrum of the signal. - time_derivative() - Returns the time derivative of the signal. - filter(frequency) - Returns the low passed filter of the signal. - truncate(tau) - Interpolates and truncates the signal the based on the time shift, - `tau`, and the signal source. - as_dictionary - Returns a dictionary of the metadata of the signal. - convert_units(units) - Returns a signal with different units. `conversions` specifies the - available options. - - """ - - # define some basic unit converions - conversions = {'degree->radian': pi / 180., - 'degree/second->radian/second': pi / 180., - 'degree/second/second->radian/second/second': pi / 180., - 'inch*pound->newton*meter': 25.4 / 1000. * 4.44822162, - 'pound->newton': 4.44822162, - 'feet/second->meter/second': 12. * 2.54 / 100., - 'mile/hour->meter/second': 0.00254 * 12. / 5280. / 3600.} - - def __new__(cls, inputArray, metadata): - """ - Returns an instance of the Signal class with the additional signal - data. - - Parameters - ---------- - inputArray : ndarray, shape(n,) - A one dimension array representing a single variable's time - history. - metadata : dictionary - This dictionary contains the metadata for the signal. - name : str - The name of the signal. Should be CamelCase. - runid : str - A five digit identification number associated with the - trial this experiment was collected at (e.g. '00104'). - sampleRate : float - The sample rate in hertz of the signal. - source : str - The source of the data. This should be 'NI' for the - National Instruments USB-6218 and 'VN' for the VN-100 IMU. - units : str - The physcial units of the signal. These should be specified - as lowercase complete words using only multiplication and - division symbols (e.g. 'meter/second/second'). - Signal.conversions will show the avialable options. - - Raises - ------ - ValueError - If `inputArray` is not a vector. - - """ - if len(inputArray.shape) > 1: - raise ValueError('Signals must be arrays of one dimension.') - # cast the input array into the Signal class - obj = np.asarray(inputArray).view(cls) - # add the metadata to the object - obj.name = metadata['name'] - obj.runid = metadata['runid'] - obj.sampleRate = metadata['sampleRate'] - obj.source = metadata['source'] - obj.units = metadata['units'] - return obj - - def __array_finalize__(self, obj): - if obj is None: return - self.name = getattr(obj, 'name', None) - self.runid = getattr(obj, 'runid', None) - self.sampleRate = getattr(obj, 'sampleRate', None) - self.source = getattr(obj, 'source', None) - self.units = getattr(obj, 'units', None) - - def __array_wrap__(self, outputArray, context=None): - # doesn't support these things in basic ufunc calls...maybe one day - # That means anytime you add, subtract, multiply, divide, etc, the - # following are not retained. - outputArray.name = None - outputArray.source = None - outputArray.units = None - return np.ndarray.__array_wrap__(self, outputArray, context) - - def as_dictionary(self): - '''Returns the signal metadata as a dictionary.''' - data = {'runid': self.runid, - 'name': self.name, - 'units': self.units, - 'source': self.source, - 'sampleRate': self.sampleRate} - return data - - def convert_units(self, units): - """ - Returns a signal with the specified units. - - Parameters - ---------- - units : str - The units to convert the signal to. The mapping must be in the - attribute `conversions`. - - Returns - ------- - newSig : Signal - The signal with the desired units. - - """ - if units == self.units: - return self - else: - try: - conversion = self.units + '->' + units - newSig = self * self.conversions[conversion] - except KeyError: - try: - conversion = units + '->' + self.units - newSig = self / self.conversions[conversion] - except KeyError: - raise KeyError(('Conversion from {0} to {1} is not ' + - 'possible or not defined.').format(self.units, units)) - # make the new signal - newSig = Signal(newSig, self.as_dictionary()) - newSig.units = units - - return newSig - - def filter(self, frequency): - """Returns the signal filtered by a low pass Butterworth at the given - frequency.""" - filteredArray = process.butterworth(self.spline(), frequency, self.sampleRate) - return Signal(filteredArray, self.as_dictionary()) - - def frequency(self): - """Returns the frequency content of the signal.""" - return process.freq_spectrum(self.spline(), self.sampleRate) - - def integrate(self, initialCondition=0., detrend=False): - """Integrates the signal using the trapezoidal rule.""" - time = self.time() - # integrate using trapz and adjust with the initial condition - grated = np.hstack((0., cumtrapz(self, x=time))) + initialCondition - # this tries to characterize the drift in the integrated signal. It - # works well for signals from straight line tracking but not - # necessarily for lange change. - if detrend is True: - def line(x, a, b, c): - return a * x**2 + b * x + c - popt, pcov = curve_fit(line, time, grated) - grated = grated - line(time, popt[0], popt[1], popt[2]) - grated = Signal(grated, self.as_dictionary()) - grated.units = self.units + '*second' - grated.name = self.name + 'Int' - return grated - - def plot(self, show=True): - """Plots and returns the signal versus time.""" - time = self.time() - line = plt.plot(time, self) - if show: - plt.xlabel('Time [second]') - plt.ylabel('{0} [{1}]'.format(self.name, self.units)) - plt.title('Signal plot during run {0}'.format(self.runid)) - plt.show() - return line - - def spline(self): - """Returns the signal with nans replaced by the results of a cubic - spline.""" - splined = process.spline_over_nan(self.time(), self) - return Signal(splined, self.as_dictionary()) - - def subtract_mean(self): - """Returns the mean subtracted data.""" - return Signal(process.subtract_mean(self), self.as_dictionary()) - - def time(self): - """Returns the time vector of the signal.""" - return sigpro.time_vector(len(self), self.sampleRate) - - def time_derivative(self): - """Returns the time derivative of the signal.""" - # caluculate the numerical time derivative - dsdt = process.derivative(self.time(), self, method='combination') - # map the metadata from self onto the derivative - dsdt = Signal(dsdt, self.as_dictionary()) - dsdt.name = dsdt.name + 'Dot' - dsdt.units = dsdt.units + '/second' - return dsdt - - def truncate(self, tau): - '''Returns the shifted and truncated signal based on the provided - timeshift, tau.''' - # this is now an ndarray instead of a Signal - return Signal(sigpro.truncate_data(self, tau), self.as_dictionary()) + """ + A subclass of ndarray for collecting the data for a single signal in a run. + + Attributes + ---------- + conversions : dictionary + A mapping for unit conversions. + name : str + The name of the signal. Should be CamelCase. + runid : str + A five digit identification number associated with the + trial this signal was collected from (e.g. '00104'). + sampleRate : float + The sample rate in hertz of the signal. + source : str + The source of the data. This should be 'NI' for the + National Instruments USB-6218 and 'VN' for the VN-100 IMU. + units : str + The physcial units of the signal. These should be specified + as lowercase complete words using only multiplication and + division symbols (e.g. 'meter/second/second'). + Signal.conversions will show the avialable options. + + Methods + ------- + plot() + Plot's the signal versus time and returns the line. + frequency() + Returns the frequency spectrum of the signal. + time_derivative() + Returns the time derivative of the signal. + filter(frequency) + Returns the low passed filter of the signal. + truncate(tau) + Interpolates and truncates the signal the based on the time shift, + `tau`, and the signal source. + as_dictionary + Returns a dictionary of the metadata of the signal. + convert_units(units) + Returns a signal with different units. `conversions` specifies the + available options. + + """ + + # define some basic unit converions + conversions = {'degree->radian': pi / 180., + 'degree/second->radian/second': pi / 180., + 'degree/second/second->radian/second/second': pi / 180., + 'inch*pound->newton*meter': 25.4 / 1000. * 4.44822162, + 'pound->newton': 4.44822162, + 'feet/second->meter/second': 12. * 2.54 / 100., + 'mile/hour->meter/second': 0.00254 * 12. / 5280. / 3600.} + + def __new__(cls, inputArray, metadata): + """ + Returns an instance of the Signal class with the additional signal + data. + + Parameters + ---------- + inputArray : ndarray, shape(n,) + A one dimension array representing a single variable's time + history. + metadata : dictionary + This dictionary contains the metadata for the signal. + name : str + The name of the signal. Should be CamelCase. + runid : str + A five digit identification number associated with the + trial this experiment was collected at (e.g. '00104'). + sampleRate : float + The sample rate in hertz of the signal. + source : str + The source of the data. This should be 'NI' for the + National Instruments USB-6218 and 'VN' for the VN-100 IMU. + units : str + The physcial units of the signal. These should be specified + as lowercase complete words using only multiplication and + division symbols (e.g. 'meter/second/second'). + Signal.conversions will show the avialable options. + + Raises + ------ + ValueError + If `inputArray` is not a vector. + + """ + if len(inputArray.shape) > 1: + raise ValueError('Signals must be arrays of one dimension.') + # cast the input array into the Signal class + obj = np.asarray(inputArray).view(cls) + # add the metadata to the object + obj.name = metadata['name'] + obj.runid = metadata['runid'] + obj.sampleRate = metadata['sampleRate'] + obj.source = metadata['source'] + obj.units = metadata['units'] + return obj + + def __array_finalize__(self, obj): + if obj is None: return + self.name = getattr(obj, 'name', None) + self.runid = getattr(obj, 'runid', None) + self.sampleRate = getattr(obj, 'sampleRate', None) + self.source = getattr(obj, 'source', None) + self.units = getattr(obj, 'units', None) + + def __array_wrap__(self, outputArray, context=None): + # doesn't support these things in basic ufunc calls...maybe one day + # That means anytime you add, subtract, multiply, divide, etc, the + # following are not retained. + outputArray.name = None + outputArray.source = None + outputArray.units = None + return np.ndarray.__array_wrap__(self, outputArray, context) + + def as_dictionary(self): + '''Returns the signal metadata as a dictionary.''' + data = {'runid': self.runid, + 'name': self.name, + 'units': self.units, + 'source': self.source, + 'sampleRate': self.sampleRate} + return data + + def convert_units(self, units): + """ + Returns a signal with the specified units. + + Parameters + ---------- + units : str + The units to convert the signal to. The mapping must be in the + attribute `conversions`. + + Returns + ------- + newSig : Signal + The signal with the desired units. + + """ + if units == self.units: + return self + else: + try: + conversion = self.units + '->' + units + newSig = self * self.conversions[conversion] + except KeyError: + try: + conversion = units + '->' + self.units + newSig = self / self.conversions[conversion] + except KeyError: + raise KeyError(('Conversion from {0} to {1} is not ' + + 'possible or not defined.').format(self.units, units)) + # make the new signal + newSig = Signal(newSig, self.as_dictionary()) + newSig.units = units + + return newSig + + def filter(self, frequency): + """Returns the signal filtered by a low pass Butterworth at the given + frequency.""" + filteredArray = process.butterworth(self.spline(), frequency, self.sampleRate) + return Signal(filteredArray, self.as_dictionary()) + + def frequency(self): + """Returns the frequency content of the signal.""" + return process.freq_spectrum(self.spline(), self.sampleRate) + + def integrate(self, initialCondition=0., detrend=False): + """Integrates the signal using the trapezoidal rule.""" + time = self.time() + # integrate using trapz and adjust with the initial condition + grated = np.hstack((0., cumtrapz(self, x=time))) + initialCondition + # this tries to characterize the drift in the integrated signal. It + # works well for signals from straight line tracking but not + # necessarily for lange change. + if detrend is True: + def line(x, a, b, c): + return a * x**2 + b * x + c + popt, pcov = curve_fit(line, time, grated) + grated = grated - line(time, popt[0], popt[1], popt[2]) + grated = Signal(grated, self.as_dictionary()) + grated.units = self.units + '*second' + grated.name = self.name + 'Int' + return grated + + def plot(self, show=True): + """Plots and returns the signal versus time.""" + time = self.time() + line = plt.plot(time, self) + if show: + plt.xlabel('Time [second]') + plt.ylabel('{0} [{1}]'.format(self.name, self.units)) + plt.title('Signal plot during run {0}'.format(self.runid)) + plt.show() + return line + + def spline(self): + """Returns the signal with nans replaced by the results of a cubic + spline.""" + splined = process.spline_over_nan(self.time(), self) + return Signal(splined, self.as_dictionary()) + + def subtract_mean(self): + """Returns the mean subtracted data.""" + return Signal(process.subtract_mean(self), self.as_dictionary()) + + def time(self): + """Returns the time vector of the signal.""" + return sigpro.time_vector(len(self), self.sampleRate) + + def time_derivative(self): + """Returns the time derivative of the signal.""" + # caluculate the numerical time derivative + dsdt = process.derivative(self.time(), self, method='combination') + # map the metadata from self onto the derivative + dsdt = Signal(dsdt, self.as_dictionary()) + dsdt.name = dsdt.name + 'Dot' + dsdt.units = dsdt.units + '/second' + return dsdt + + def truncate(self, tau): + '''Returns the shifted and truncated signal based on the provided + timeshift, tau.''' + # this is now an ndarray instead of a Signal + return Signal(sigpro.truncate_data(self, tau), self.as_dictionary()) class RawSignal(Signal): - """ - A subclass of Signal for collecting the data for a single raw signal in - a run. - - Attributes - ---------- - sensor : Sensor - Each raw signal has a sensor associated with it. Most sensors contain - calibration data for that sensor/signal. - calibrationType : - - Notes - ----- - This is a class for the signals that are the raw measurement outputs - collected by the BicycleDAQ software and are already stored in the pytables - database file. - - """ - - def __new__(cls, runid, signalName, database): - """ - Returns an instance of the RawSignal class with the additional signal - metadata. - - Parameters - ---------- - runid : str - A five digit - signalName : str - A CamelCase signal name that corresponds to the raw signals output - by BicycleDAQ_. - database : pytables object - The hdf5 database for the instrumented bicycle. - - .. _BicycleDAQ: https://github.com/moorepants/BicycleDAQ - - """ - - # get the tables - rTab = database.root.runTable - sTab = database.root.signalTable - cTab = database.root.calibrationTable - - # get the row number for this particular run id - rownum = get_row_num(runid, rTab) - signal = database.getNode('/rawData/' + runid, name=signalName).read() - - # cast the input array into my subclass of ndarray - obj = np.asarray(signal).view(cls) - - obj.runid = runid - obj.timeStamp = matlab_date_to_object(get_cell(rTab, 'DateTime', - rownum)) - obj.calibrationType, obj.units, obj.source = [(row['calibration'], - row['units'], row['source']) - for row in sTab.where('signal == signalName')][0] - obj.name = signalName - - try: - obj.sensor = Sensor(obj.name, cTab) - except KeyError: - pass - # This just means that there was no sensor associated with that - # signal for calibration purposes. - #print "There is no sensor named {0}.".format(signalName) - - # this assumes that the supply voltage for this signal is the same for - # all sensor calibrations - try: - supplySource = [row['runSupplyVoltageSource'] - for row in cTab.where('name == signalName')][0] - if supplySource == 'na': - obj.supply = [row['runSupplyVoltage'] - for row in cTab.where('name == signalName')][0] - else: - obj.supply = database.getNode('/rawData/' + runid, - name=supplySource).read() - except IndexError: - pass - #print "{0} does not have a supply voltage.".format(signalName) - #print "-" * 79 - - # get the appropriate sample rate - if obj.source == 'NI': - sampRateCol = 'NISampleRate' - elif obj.source == 'VN': - sampRateCol = 'VNavSampleRate' - else: - raise ValueError('{0} is not a valid source.'.format(obj.source)) - - obj.sampleRate = rTab[rownum][rTab.colnames.index(sampRateCol)] - - return obj - - def __array_finalize__(self, obj): - if obj is None: return - self.calibrationType = getattr(obj, 'calibrationType', None) - self.name = getattr(obj, 'name', None) - self.runid = getattr(obj, 'runid', None) - self.sampleRate = getattr(obj, 'sampleRate', None) - self.sensor = getattr(obj, 'sensor', None) - self.source = getattr(obj, 'source', None) - self.units = getattr(obj, 'units', None) - self.timeStamp = getattr(obj, 'timeStamp', None) - - def __array_wrap__(self, outputArray, context=None): - # doesn't support these things in basic ufunc calls...maybe one day - outputArray.calibrationType = None - outputArray.name = None - outputArray.sensor = None - outputArray.source = None - outputArray.units = None - return np.ndarray.__array_wrap__(self, outputArray, context) - - def scale(self): - """ - Returns the scaled signal based on the calibration data for the - supplied date. - - Returns - ------- - : ndarray (n,) - Scaled signal. - - """ - try: - self.calibrationType - except AttributeError: - raise AttributeError("Can't scale without the calibration type") - - # these will need to be changed once we start measuring them - doNotScale = ['LeanPotentiometer', - 'HipPotentiometer', - 'TwistPotentiometer'] - if self.calibrationType in ['none', 'matrix'] or self.name in doNotScale: - #print "Not scaling {0}".format(self.name) - return self - else: - pass - #print "Scaling {0}".format(self.name) - - # pick the largest calibration date without surpassing the run date - calibData = self.sensor.get_data_for_date(self.timeStamp) - - slope = calibData['slope'] - bias = calibData['bias'] - intercept = calibData['offset'] - calibrationSupplyVoltage = calibData['calibrationSupplyVoltage'] - - #print "slope {0}, bias {1}, intercept {2}".format(slope, bias, - #intercept) - - if self.calibrationType == 'interceptStar': - # this is for potentiometers, where the slope is ratiometric - # and zero degrees is always zero volts - calibratedSignal = (calibrationSupplyVoltage / self.supply * - slope * self + intercept) - elif self.calibrationType == 'intercept': - # this is the typical calibration that I use for all the - # sensors that I calibrate myself - calibratedSignal = (calibrationSupplyVoltage / self.supply * - (slope * self + intercept)) - elif self.calibrationType == 'bias': - # this is for the accelerometers and rate gyros that are - # "ratiometric", but I'm still not sure this is correct - calibratedSignal = (slope * (self - self.supply / - calibrationSupplyVoltage * bias)) - else: - raise StandardError("None of the calibration equations worked.") - calibratedSignal.name = calibData['signal'] - calibratedSignal.units = calibData['units'] - calibratedSignal.source = self.source - - return calibratedSignal.view(Signal) - - def plot_scaled(self, show=True): - '''Plots and returns the scaled signal versus time.''' - time = self.time() - scaled = self.scale() - line = plt.plot(time, scaled[1]) - plt.xlabel('Time [s]') - plt.ylabel(scaled[2]) - plt.title('{0} signal during run {1}'.format(scaled[0], - str(self.runid))) - if show: - plt.show() - return line + """ + A subclass of Signal for collecting the data for a single raw signal in + a run. + + Attributes + ---------- + sensor : Sensor + Each raw signal has a sensor associated with it. Most sensors contain + calibration data for that sensor/signal. + calibrationType : + + Notes + ----- + This is a class for the signals that are the raw measurement outputs + collected by the BicycleDAQ software and are already stored in the pytables + database file. + + """ + + def __new__(cls, runid, signalName, database): + """ + Returns an instance of the RawSignal class with the additional signal + metadata. + + Parameters + ---------- + runid : str + A five digit + signalName : str + A CamelCase signal name that corresponds to the raw signals output + by BicycleDAQ_. + database : pytables object + The hdf5 database for the instrumented bicycle. + + .. _BicycleDAQ: https://github.com/moorepants/BicycleDAQ + + """ + + # get the tables + rTab = database.root.runTable + sTab = database.root.signalTable + cTab = database.root.calibrationTable + + # get the row number for this particular run id + rownum = get_row_num(runid, rTab) + signal = database.getNode('/rawData/' + runid, name=signalName).read() + + # cast the input array into my subclass of ndarray + obj = np.asarray(signal).view(cls) + + obj.runid = runid + obj.timeStamp = matlab_date_to_object(get_cell(rTab, 'DateTime', + rownum)) + obj.calibrationType, obj.units, obj.source = [(row['calibration'], + row['units'], row['source']) + for row in sTab.where('signal == signalName')][0] + obj.name = signalName + + try: + obj.sensor = Sensor(obj.name, cTab) + except KeyError: + pass + # This just means that there was no sensor associated with that + # signal for calibration purposes. + #print "There is no sensor named {0}.".format(signalName) + + # this assumes that the supply voltage for this signal is the same for + # all sensor calibrations + try: + supplySource = [row['runSupplyVoltageSource'] + for row in cTab.where('name == signalName')][0] + if supplySource == 'na': + obj.supply = [row['runSupplyVoltage'] + for row in cTab.where('name == signalName')][0] + else: + obj.supply = database.getNode('/rawData/' + runid, + name=supplySource).read() + except IndexError: + pass + #print "{0} does not have a supply voltage.".format(signalName) + #print "-" * 79 + + # get the appropriate sample rate + if obj.source == 'NI': + sampRateCol = 'NISampleRate' + elif obj.source == 'VN': + sampRateCol = 'VNavSampleRate' + else: + raise ValueError('{0} is not a valid source.'.format(obj.source)) + + obj.sampleRate = rTab[rownum][rTab.colnames.index(sampRateCol)] + + return obj + + def __array_finalize__(self, obj): + if obj is None: return + self.calibrationType = getattr(obj, 'calibrationType', None) + self.name = getattr(obj, 'name', None) + self.runid = getattr(obj, 'runid', None) + self.sampleRate = getattr(obj, 'sampleRate', None) + self.sensor = getattr(obj, 'sensor', None) + self.source = getattr(obj, 'source', None) + self.units = getattr(obj, 'units', None) + self.timeStamp = getattr(obj, 'timeStamp', None) + + def __array_wrap__(self, outputArray, context=None): + # doesn't support these things in basic ufunc calls...maybe one day + outputArray.calibrationType = None + outputArray.name = None + outputArray.sensor = None + outputArray.source = None + outputArray.units = None + return np.ndarray.__array_wrap__(self, outputArray, context) + + def scale(self): + """ + Returns the scaled signal based on the calibration data for the + supplied date. + + Returns + ------- + : ndarray (n,) + Scaled signal. + + """ + try: + self.calibrationType + except AttributeError: + raise AttributeError("Can't scale without the calibration type") + + # these will need to be changed once we start measuring them + doNotScale = ['LeanPotentiometer', + 'HipPotentiometer', + 'TwistPotentiometer'] + if self.calibrationType in ['none', 'matrix'] or self.name in doNotScale: + #print "Not scaling {0}".format(self.name) + return self + else: + pass + #print "Scaling {0}".format(self.name) + + # pick the largest calibration date without surpassing the run date + calibData = self.sensor.get_data_for_date(self.timeStamp) + + slope = calibData['slope'] + bias = calibData['bias'] + intercept = calibData['offset'] + calibrationSupplyVoltage = calibData['calibrationSupplyVoltage'] + + #print "slope {0}, bias {1}, intercept {2}".format(slope, bias, + #intercept) + + if self.calibrationType == 'interceptStar': + # this is for potentiometers, where the slope is ratiometric + # and zero degrees is always zero volts + calibratedSignal = (calibrationSupplyVoltage / self.supply * + slope * self + intercept) + elif self.calibrationType == 'intercept': + # this is the typical calibration that I use for all the + # sensors that I calibrate myself + calibratedSignal = (calibrationSupplyVoltage / self.supply * + (slope * self + intercept)) + elif self.calibrationType == 'bias': + # this is for the accelerometers and rate gyros that are + # "ratiometric", but I'm still not sure this is correct + calibratedSignal = (slope * (self - self.supply / + calibrationSupplyVoltage * bias)) + else: + raise StandardError("None of the calibration equations worked.") + calibratedSignal.name = calibData['signal'] + calibratedSignal.units = calibData['units'] + calibratedSignal.source = self.source + + return calibratedSignal.view(Signal) + + def plot_scaled(self, show=True): + '''Plots and returns the scaled signal versus time.''' + time = self.time() + scaled = self.scale() + line = plt.plot(time, scaled[1]) + plt.xlabel('Time [s]') + plt.ylabel(scaled[2]) + plt.title('{0} signal during run {1}'.format(scaled[0], + str(self.runid))) + if show: + plt.show() + return line class Sensor(): - """This class is a container for calibration data for a sensor.""" - - def __init__(self, name, calibrationTable): - """ - Initializes this sensor class. - - Parameters - ---------- - name : string - The CamelCase name of the sensor (e.g. SteerTorqueSensor). - calibrationTable : pyTables table object - This is the calibration data table that contains all the data taken - during calibrations. - - """ - self.name = name - self._store_calibration_data(calibrationTable) - - def _store_calibration_data(self, calibrationTable): - """ - Stores a dictionary of calibration data for the sensor for all - calibration dates in the object. - - Parameters - ---------- - calibrationTable : pyTables table object - This is the calibration data table that contains all the data taken - during calibrations. - - """ - self.data = {} - - for row in calibrationTable.iterrows(): - if self.name == row['name']: - self.data[row['calibrationID']] = {} - for col in calibrationTable.colnames: - self.data[row['calibrationID']][col] = row[col] - - if self.data == {}: - raise KeyError(('{0} is not a valid sensor ' + - 'name').format(self.name)) - - def get_data_for_date(self, runDate): - """ - Returns the calibration data for the sensor for the most recent - calibration relative to `runDate`. - - Parameters - ---------- - runDate : datetime object - This is the date of the run that the calibration data is needed - for. - - Returns - ------- - calibData : dictionary - A dictionary containing the sensor calibration data for the - calibration closest to but not past `runDate`. - - Notes - ----- - This method will select the calibration data for the date closest to - but not past `runDate`. **All calibrations must be taken before the - runs.** - - """ - # make a list of calibration ids and time stamps - dateIdPairs = [(k, matlab_date_to_object(v['timeStamp'])) - for k, v in self.data.iteritems()] - # sort the pairs with the most recent date first - dateIdPairs.sort(key=lambda x: x[1], reverse=True) - # go through the list and return the index at which the calibration - # date is larger than the run date - for i, pair in enumerate(dateIdPairs): - if runDate >= pair[1]: - break - return self.data[dateIdPairs[i][0]] + """This class is a container for calibration data for a sensor.""" + + def __init__(self, name, calibrationTable): + """ + Initializes this sensor class. + + Parameters + ---------- + name : string + The CamelCase name of the sensor (e.g. SteerTorqueSensor). + calibrationTable : pyTables table object + This is the calibration data table that contains all the data taken + during calibrations. + + """ + self.name = name + self._store_calibration_data(calibrationTable) + + def _store_calibration_data(self, calibrationTable): + """ + Stores a dictionary of calibration data for the sensor for all + calibration dates in the object. + + Parameters + ---------- + calibrationTable : pyTables table object + This is the calibration data table that contains all the data taken + during calibrations. + + """ + self.data = {} + + for row in calibrationTable.iterrows(): + if self.name == row['name']: + self.data[row['calibrationID']] = {} + for col in calibrationTable.colnames: + self.data[row['calibrationID']][col] = row[col] + + if self.data == {}: + raise KeyError(('{0} is not a valid sensor ' + + 'name').format(self.name)) + + def get_data_for_date(self, runDate): + """ + Returns the calibration data for the sensor for the most recent + calibration relative to `runDate`. + + Parameters + ---------- + runDate : datetime object + This is the date of the run that the calibration data is needed + for. + + Returns + ------- + calibData : dictionary + A dictionary containing the sensor calibration data for the + calibration closest to but not past `runDate`. + + Notes + ----- + This method will select the calibration data for the date closest to + but not past `runDate`. **All calibrations must be taken before the + runs.** + + """ + # make a list of calibration ids and time stamps + dateIdPairs = [(k, matlab_date_to_object(v['timeStamp'])) + for k, v in self.data.iteritems()] + # sort the pairs with the most recent date first + dateIdPairs.sort(key=lambda x: x[1], reverse=True) + # go through the list and return the index at which the calibration + # date is larger than the run date + for i, pair in enumerate(dateIdPairs): + if runDate >= pair[1]: + break + return self.data[dateIdPairs[i][0]] class Run(): - """The fluppin fundamental class for a run.""" - - def __init__(self, runid, dataset, pathToParameterData=None, - forceRecalc=False, filterFreq=None, store=True): - """Loads the raw and processed data for a run if available otherwise it - generates the processed data from the raw data. - - Parameters - ---------- - runid : int or str - The run id should be an integer, e.g. 5, or a five digit string with - leading zeros, e.g. '00005'. - dataset : DataSet - A DataSet object with at least some raw data. - pathToParameterData : string, {'', None}, optional - The path to a data directory for the BicycleParameters package. It - should contain the bicycles and riders used in the experiments. - forceRecalc : boolean, optional, default = False - If true then it will force a recalculation of all the processed - data. - filterSigs : float, optional, default = None - If true all of the processed signals will be low pass filtered with - a second order Butterworth filter at the given filter frequency. - store : boolean, optional, default = True - If true the resulting task signals will be stored in the database. - - """ - - if pathToParameterData is None: - pathToParameterData = config.get('data', 'pathToParameters') - - print "Initializing the run object." - - self.filterFreq = filterFreq - - dataset.open() - dataTable = dataset.database.root.runTable - signalTable = dataset.database.root.signalTable - taskTable = dataset.database.root.taskTable - - runid = run_id_string(runid) - - # get the row number for this particular run id - rownum = get_row_num(runid, dataTable) - - # make some dictionaries to store all the data - self.metadata = {} - self.rawSignals = {} - - # make lists of the input and output signals - rawDataCols = [x['signal'] for x in - signalTable.where("isRaw == True")] - computedCols = [x['signal'] for x in - signalTable.where("isRaw == False")] - - # store the metadata for this run - print "Loading metadata from the database." - for col in dataTable.colnames: - if col not in (rawDataCols + computedCols): - self.metadata[col] = get_cell(dataTable, col, rownum) - - print "Loading the raw signals from the database." - for col in rawDataCols: - # rawDataCols includes all possible raw signals, but every run - # doesn't have all the signals, so skip the ones that aren't there - try: - self.rawSignals[col] = RawSignal(runid, col, dataset.database) - except NoSuchNodeError: - pass - - if self.metadata['Rider'] != 'None': - self.load_rider(pathToParameterData) - - self.bumpLength = 1.0 # 1 meter - - # Try to load the task signals if they've already been computed. If - # they aren't in the database, the filter frequencies don't match or - # forceRecalc is true the then compute them. This may save some time - # when repeatedly loading runs for analysis. - self.taskFromDatabase = False - try: - runGroup = dataset.database.root.taskData._f_getChild(runid) - except NoSuchNodeError: - forceRecalc = True - else: - # The filter frequency stored in the task table is either a nan - # value or a valid float. If the stored filter frequency is not the - # same as the the one passed to Run, then a recalculation should be - # forced. - taskRowNum = get_row_num(runid, taskTable) - storedFreq = taskTable.cols.FilterFrequency[taskRowNum] - self.taskSignals = {} - if filterFreq is None: - newFilterFreq = np.nan - else: - newFilterFreq = filterFreq - - if np.isnan(newFilterFreq) and np.isnan(storedFreq): - for node in runGroup._f_walkNodes(): - meta = {k : node._f_getAttr(k) for k in ['units', 'name', - 'runid', 'sampleRate', 'source']} - self.taskSignals[node.name] = Signal(node[:], meta) - self.taskFromDatabase = True - elif np.isnan(newFilterFreq) or np.isnan(storedFreq): - forceRecalc = True - else: - if abs(storedFreq - filterFreq) < 1e-10: - for node in runGroup._f_walkNodes(): - meta = {k : node._f_getAttr(k) for k in ['units', 'name', - 'runid', 'sampleRate', 'source']} - self.taskSignals[node.name] = Signal(node[:], meta) - self.taskFromDatabase = True - else: - forceRecalc = True - - dataset.close() - - if forceRecalc == True: - try: - del self.taskSignals - except AttributeError: - pass - self.process_raw_signals() - - # store the task signals in the database if they are newly computed - if (store == True and self.taskFromDatabase == False - and self.topSig == 'task'): - taskMeta = { - 'Duration' : - self.taskSignals['ForwardSpeed'].time()[-1], - 'FilterFrequency' : self.filterFreq, - 'MeanSpeed' : self.taskSignals['ForwardSpeed'].mean(), - 'RunID' : self.metadata['RunID'], - 'StdSpeed' : self.taskSignals['ForwardSpeed'].std(), - 'Tau' : self.tau, - } - dataset.add_task_signals(self.taskSignals, taskMeta) - - # tell the user about the run - print self - - def process_raw_signals(self): - """Processes the raw signals as far as possible and filters the - result if a cutoff frequency was specified.""" - - print "Computing signals from raw data." - self.calibrate_signals() - - # the following maneuvers should never be calculated beyond the - # calibrated signals - maneuver = self.metadata['Maneuver'] - con1 = maneuver != 'Steer Dynamics Test' - con2 = maneuver != 'System Test' - con3 = maneuver != 'Static Calibration' - if con1 and con2 and con3: - self.compute_time_shift() - self.check_time_shift(0.15) - self.truncate_signals() - self.compute_signals() - self.task_signals() - - if self.filterFreq is not None: - self.filter_top_signals(self.filterFreq) - - def filter_top_signals(self, filterFreq): - """Filters the top most signals with a low pass filter.""" - - if self.topSig == 'task': - print('Filtering the task signals.') - for k, v in self.taskSignals.items(): - self.taskSignals[k] = v.filter(filterFreq) - elif self.topSig == 'computed': - print('Filtering the computed signals.') - for k, v in self.computedSignals.items(): - self.computedSignals[k] = v.filter(filterFreq) - elif self.topSig == 'calibrated': - print('Filtering the calibrated signals.') - for k, v in self.calibratedSignals.items(): - self.calibratedSignals[k] = v.filter(filterFreq) - - def calibrate_signals(self): - """Calibrates the raw signals.""" - - # calibrate the signals for the run - self.calibratedSignals = {} - for sig in self.rawSignals.values(): - calibSig = sig.scale() - self.calibratedSignals[calibSig.name] = calibSig - - self.topSig = 'calibrated' - - def task_signals(self): - """Computes the task signals.""" - print('Extracting the task portion from the data.') - self.extract_task() - - # compute task specific variables* - self.compute_yaw_angle() - self.compute_rear_wheel_contact_rates() - self.compute_rear_wheel_contact_points() - self.compute_front_wheel_contact_points() - self.compute_front_wheel_yaw_angle() - self.compute_front_wheel_rate() - self.compute_front_rear_wheel_contact_forces() - - self.topSig = 'task' - - def compute_signals(self): - """Computes the task independent quantities.""" - - self.computedSignals ={} - # transfer some of the signals to computed - noChange = ['FiveVolts', - 'PushButton', - 'RearWheelRate', - 'RollAngle', - 'SteerAngle', - 'ThreeVolts'] - for sig in noChange: - if sig in ['RollAngle', 'SteerAngle']: - self.computedSignals[sig] =\ - self.truncatedSignals[sig].convert_units('radian') - else: - self.computedSignals[sig] = self.truncatedSignals[sig] - - # compute the quantities that aren't task specific - self.compute_pull_force() - self.compute_forward_speed() - self.compute_steer_rate() - self.compute_yaw_roll_pitch_rates() - self.compute_steer_torque() - - def truncate_signals(self): - """Truncates the calibrated signals based on the time shift.""" - - self.truncatedSignals = {} - for name, sig in self.calibratedSignals.items(): - self.truncatedSignals[name] = sig.truncate(self.tau).spline() - self.topSig = 'truncated' - - def compute_time_shift(self): - """Computes the time shift based on the vertical accelerometer - signals.""" - - self.tau = sigpro.find_timeshift( - self.calibratedSignals['AccelerometerAccelerationY'], - self.calibratedSignals['AccelerationZ'], - self.metadata['NISampleRate'], - self.metadata['Speed'], plotError=False) - - def check_time_shift(self, maxNRMS): - """Raises an error if the normalized root mean square of the shifted - accelerometer signals is high.""" - - # Check to make sure the signals were actually good fits by - # calculating the normalized root mean square. If it isn't very - # low, raise an error. - niAcc = self.calibratedSignals['AccelerometerAccelerationY'] - vnAcc = self.calibratedSignals['AccelerationZ'] - vnAcc = vnAcc.truncate(self.tau).spline() - niAcc = niAcc.truncate(self.tau).spline() - # todo: this should probably check the rms of the mean subtracted data - # because both accelerometers don't always give the same value, this - # may work better with a filtered signal too - # todo: this should probably be moved into the time shift code in the - # signalprocessing model - nrms = np.sqrt(np.mean((vnAcc + niAcc)**2)) / (niAcc.max() - niAcc.min()) - if nrms > maxNRMS: - raise TimeShiftError(('The normalized root mean square for this ' + - 'time shift is {}, which is greater '.format(str(nrms)) + - 'than the maximum allowed: {}'.format(str(maxNRMS)))) - - def compute_rear_wheel_contact_points(self): - """Computes the location of the wheel contact points in the ground - plane.""" - - # get the rates - try: - latRate = self.taskSignals['LateralRearContactRate'] - lonRate = self.taskSignals['LongitudinalRearContactRate'] - except AttributeError: - print('At least one of the rates are not available. ' + - 'The YawAngle was not computed.') - else: - # convert to meters per second - latRate = latRate.convert_units('meter/second') - lonRate = lonRate.convert_units('meter/second') - # integrate and try to account for the drift - lat = latRate.integrate(detrend=True) - lon = lonRate.integrate() - # set the new name and units - lat.name = 'LateralRearContact' - lat.units = 'meter' - lon.name = 'LongitudinalRearContact' - lon.units = 'meter' - # store in task signals - self.taskSignals[lat.name] = lat - self.taskSignals[lon.name] = lon - - def compute_front_wheel_contact_points(self): - """Caluculates the front wheel contact points in the ground plane.""" - - q1 = self.taskSignals['LongitudinalRearContact'] - q2 = self.taskSignals['LateralRearContact'] - q3 = self.taskSignals['YawAngle'] - q4 = self.taskSignals['RollAngle'] - q7 = self.taskSignals['SteerAngle'] - - p = benchmark_to_moore(self.bicycleRiderParameters) - - f = np.vectorize(front_contact) - q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], - p['rf']) - - q9.name = 'LongitudinalFrontContact' - q9.units = 'meter' - q10.name = 'LateralFrontContact' - q10.units = 'meter' - - self.taskSignals['LongitudinalFrontContact'] = q9 - self.taskSignals['LateralFrontContact'] = q10 - - def compute_front_wheel_yaw_angle(self): - """Calculates the yaw angle of the front wheel.""" - - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(bp) - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - f = np.vectorize(front_wheel_yaw_angle) - q1_front_wheel = f(q1, q2, q4, mp['d1'], mp['d2'], mp['d3'], - bp['lam'], mp['rr'], mp['rf']) - - q1_front_wheel.name = 'FrontWheelYawAngle' - q1_front_wheel.units = 'meter' - self.taskSignals['FrontWheelYawAngle'] = q1_front_wheel - - def compute_front_wheel_rate(self): - """Calculates the front wheel rate, based on the Jason's data - of front_wheel_contact_point. Alternatively, you can use the - sympy to get the front_wheel_contact_rate directly first.""" - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(bp) - - f = np.vectorize(front_wheel_rate) - - u6 = f(q1, q2, q4, u9, u10, mp['d1'], mp['d2'], mp['d3'], - bp['lam'], mp['rr'], mp['rf']) - - u6.name = 'FrontWheelRate' - u6.units = 'radian/second' - self.taskSignals['FrontWheelRate'] = u6 - - - def compute_front_rear_wheel_contact_forces(self): - """Calculate the contact forces for each - wheel with respect to inertial frame under - the slip and nonslip condition. Also, provide - the steer torque T4.""" - - #parameters - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(self.bicycleRiderParameters) - - l1 = mp['l1'] - l2 = mp['l2'] - mc = mp['mc'] - ic11 = mp['ic11'] - ic22 = mp['ic22'] - ic33 = mp['ic33'] - ic31 = mp['ic31'] - rf = mp['rf'] - - #signals assignment --slip condition - V = self.taskSignals['ForwardSpeed'].mean() - - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - u2 = self.taskSignals['RollRate'] - u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] - u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] - u7 = self.taskSignals['LongitudinalRearContactRate'] - u8 = self.taskSignals['LateralRearContactRate'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - u2d = u2.time_derivative() - u3d = u3.time_derivative() - u4d = u4.time_derivative() - u5d = u5.time_derivative() - u6d = u6.time_derivative() - u7d = u7.time_derivative() - u8d = u8.time_derivative() - u9d = u9.time_derivative() - u10d = u10.time_derivative() - - f_1 = np.vectorize(steer_torque_slip) - f_2 = np.vectorize(contact_forces_slip) - f_3 = np.vectorize(contact_forces_nonslip) - - #calculation - #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - - #attributes: name and units, and taskSignals - T4_slip.name = 'SteerTorque_Slip' - T4_slip.units = 'newton*meter' - self.taskSignals[T4_slip.name] = T4_slip - - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' - Fx_r_s.units = 'newton' - self.taskSignals[Fx_r_s.name] = Fx_r_s - - Fy_r_s.name = 'LateralRearContactForce_Slip' - Fy_r_s.units = 'newton' - self.taskSignals[Fy_r_s.name] = Fy_r_s - - Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' - Fx_f_s.units = 'newton' - self.taskSignals[Fx_f_s.name] = Fx_f_s - - Fy_f_s.name = 'LateralFrontContactForce_Slip' - Fy_f_s.units = 'newton' - self.taskSignals[Fy_f_s.name] = Fy_f_s - - Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' - Fx_r_ns.units = 'newton' - self.taskSignals[Fx_r_ns.name] = Fx_r_ns - - Fy_r_ns.name = 'LateralRearContactForce_Nonslip' - Fy_r_ns.units = 'newton' - self.taskSignals[Fy_r_ns.name] = Fy_r_ns - - Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' - Fx_f_ns.units = 'newton' - self.taskSignals[Fx_f_ns.name] = Fx_f_ns - - Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' - Fy_f_ns.units = 'newton' - self.taskSignals[Fy_f_ns.name] = Fy_f_ns - - - def compute_rear_wheel_contact_rates(self): - """Calculates the rates of the wheel contact points in the ground - plane.""" - - try: - yawAngle = self.taskSignals['YawAngle'] - rearWheelRate = self.taskSignals['RearWheelRate'] - rR = self.bicycleRiderParameters['rR'] # this should be in meters - except AttributeError: - print('Either the yaw angle, rear wheel rate or ' + - 'front wheel radius is not available. The ' + - 'contact rates were not computed.') - else: - yawAngle = yawAngle.convert_units('radian') - rearWheelRate = rearWheelRate.convert_units('radian/second') - - lon, lat = sigpro.rear_wheel_contact_rate(rR, rearWheelRate, yawAngle) - - lon.name = 'LongitudinalRearContactRate' - lon.units = 'meter/second' - self.taskSignals[lon.name] = lon - - lat.name = 'LateralRearContactRate' - lat.units = 'meter/second' - self.taskSignals[lat.name] = lat - - def compute_yaw_angle(self): - """Computes the yaw angle by integrating the yaw rate.""" - - # get the yaw rate - try: - yawRate = self.taskSignals['YawRate'] - except AttributeError: - print('YawRate is not available. The YawAngle was not computed.') - else: - # convert to radians per second - yawRate = yawRate.convert_units('radian/second') - # integrate and try to account for the drift - yawAngle = yawRate.integrate(detrend=True) - # set the new name and units - yawAngle.name = 'YawAngle' - yawAngle.units = 'radian' - # store in computed signals - self.taskSignals['YawAngle'] = yawAngle - - def compute_steer_torque(self, plot=False): - """Computes the rider applied steer torque. - - Parameters - ---------- - plot : boolean, optional - Default is False, but if True a plot is generated. - - """ - # steer torque - frameAngRate = np.vstack(( - self.truncatedSignals['AngularRateX'], - self.truncatedSignals['AngularRateY'], - self.truncatedSignals['AngularRateZ'])) - frameAngAccel = np.vstack(( - self.truncatedSignals['AngularRateX'].time_derivative(), - self.truncatedSignals['AngularRateY'].time_derivative(), - self.truncatedSignals['AngularRateZ'].time_derivative())) - frameAccel = np.vstack(( - self.truncatedSignals['AccelerationX'], - self.truncatedSignals['AccelerationY'], - self.truncatedSignals['AccelerationZ'])) - handlebarAngRate = self.truncatedSignals['ForkRate'] - handlebarAngAccel = self.truncatedSignals['ForkRate'].time_derivative() - steerAngle = self.truncatedSignals['SteerAngle'] - steerColumnTorque =\ - self.truncatedSignals['SteerTubeTorque'].convert_units('newton*meter') - handlebarMass = self.bicycleRiderParameters['mG'] - handlebarInertia =\ - self.bicycle.steer_assembly_moment_of_inertia(fork=False, - wheel=False, nominal=True) - # this is the distance from the handlebar center of mass to the - # steer axis - w = self.bicycleRiderParameters['w'] - c = self.bicycleRiderParameters['c'] - lam = self.bicycleRiderParameters['lam'] - xG = self.bicycleRiderParameters['xG'] - zG = self.bicycleRiderParameters['zG'] - handlebarCoM = np.array([xG, 0., zG]) - d = bp.geometry.distance_to_steer_axis(w, c, lam, handlebarCoM) - # these are the distances from the point on the steer axis which is - # aligned with the handlebar center of mass to the accelerometer on - # the frame - ds1 = self.bicycle.parameters['Measured']['ds1'] - ds3 = self.bicycle.parameters['Measured']['ds3'] - ds = np.array([ds1, 0., ds3]) # i measured these - # damping and friction values come from Peter's work, I need to verify - # them still - damping = 0.3475 - friction = 0.0861 - - components = sigpro.steer_torque_components( - frameAngRate, frameAngAccel, frameAccel, handlebarAngRate, - handlebarAngAccel, steerAngle, steerColumnTorque, - handlebarMass, handlebarInertia, damping, friction, d, ds) - steerTorque = sigpro.steer_torque(components) - - stDict = {'units':'newton*meter', - 'name':'SteerTorque', - 'runid':self.metadata['RunID'], - 'sampleRate':steerAngle.sampleRate, - 'source':'NA'} - self.computedSignals['SteerTorque'] = Signal(steerTorque, stDict) - - if plot is True: - - time = steerAngle.time() - - hdot = (components['Hdot1'] + components['Hdot2'] + - components['Hdot3'] + components['Hdot4']) - cross = (components['cross1'] + components['cross2'] + - components['cross3']) - - fig = plt.figure() - - frictionAx = fig.add_subplot(4, 1, 1) - frictionAx.plot(time, components['viscous'], - time, components['coulomb'], - time, components['viscous'] + components['coulomb']) - frictionAx.set_ylabel('Torque [N-m]') - frictionAx.legend(('Viscous Friction', 'Coulomb Friction', - 'Total Friction')) - - dynamicAx = fig.add_subplot(4, 1, 2) - dynamicAx.plot(time, hdot, time, cross, time, hdot + cross) - dynamicAx.set_ylabel('Torque [N-m]') - dynamicAx.legend((r'Torque due to $\dot{H}$', - r'Torque due to $r \times m a$', - r'Total Dynamic Torque')) - - additionalAx = fig.add_subplot(4, 1, 3) - additionalAx.plot(time, hdot + cross + components['viscous'] + - components['coulomb'], - label='Total Frictional and Dynamic Torque') - additionalAx.set_ylabel('Torque [N-m]') - additionalAx.legend() - - torqueAx = fig.add_subplot(4, 1, 4) - torqueAx.plot(time, components['steerColumn'], - time, hdot + cross + components['viscous'] + components['coulomb'], - time, steerTorque) - torqueAx.set_xlabel('Time [s]') - torqueAx.set_ylabel('Torque [N-m]') - torqueAx.legend(('Measured Torque', 'Frictional and Dynamic Torque', - 'Rider Applied Torque')) - - plt.show() - - return fig - - def compute_yaw_roll_pitch_rates(self): - """Computes the yaw, roll and pitch rates of the bicycle frame.""" - - try: - omegaX = self.truncatedSignals['AngularRateX'] - omegaY = self.truncatedSignals['AngularRateY'] - omegaZ = self.truncatedSignals['AngularRateZ'] - rollAngle = self.truncatedSignals['RollAngle'] - lam = self.bicycleRiderParameters['lam'] - except AttributeError: - print('All needed signals are not available. ' + - 'Yaw, roll and pitch rates were not computed.') - else: - omegaX = omegaX.convert_units('radian/second') - omegaY = omegaY.convert_units('radian/second') - omegaZ = omegaZ.convert_units('radian/second') - rollAngle = rollAngle.convert_units('radian') - - yr, rr, pr = sigpro.yaw_roll_pitch_rate(omegaX, omegaY, omegaZ, lam, - rollAngle=rollAngle) - yr.units = 'radian/second' - yr.name = 'YawRate' - rr.units = 'radian/second' - rr.name = 'RollRate' - pr.units = 'radian/second' - pr.name = 'PitchRate' - - self.computedSignals['YawRate'] = yr - self.computedSignals['RollRate'] = rr - self.computedSignals['PitchRate'] = pr - - def compute_steer_rate(self): - """Calculate the steer rate from the frame and fork rates.""" - try: - forkRate = self.truncatedSignals['ForkRate'] - omegaZ = self.truncatedSignals['AngularRateZ'] - except AttributeError: - print('ForkRate or AngularRateZ is not available. ' + - 'SteerRate was not computed.') - else: - forkRate = forkRate.convert_units('radian/second') - omegaZ = omegaZ.convert_units('radian/second') - - steerRate = sigpro.steer_rate(forkRate, omegaZ) - steerRate.units = 'radian/second' - steerRate.name = 'SteerRate' - self.computedSignals['SteerRate'] = steerRate - - def compute_forward_speed(self): - """Calculates the magnitude of the main component of velocity of the - center of the rear wheel.""" - - try: - rR = self.bicycleRiderParameters['rR'] - rearWheelRate = self.truncatedSignals['RearWheelRate'] - except AttributeError: - print('rR or RearWheelRate is not availabe. ' + - 'ForwardSpeed was not computed.') - else: - rearWheelRate = rearWheelRate.convert_units('radian/second') - - self.computedSignals['ForwardSpeed'] = -rR * rearWheelRate - self.computedSignals['ForwardSpeed'].units = 'meter/second' - self.computedSignals['ForwardSpeed'].name = 'ForwardSpeed' - - def compute_pull_force(self): - """ - Computes the pull force from the truncated pull force signal. - - """ - try: - pullForce = self.truncatedSignals['PullForce'] - except AttributeError: - print 'PullForce was not available. PullForce was not computed.' - else: - pullForce = pullForce.convert_units('newton') - pullForce.name = 'PullForce' - pullForce.units = 'newton' - self.computedSignals[pullForce.name] = pullForce - - def __str__(self): - '''Prints basic run information to the screen.''' - - line = "=" * 79 - info = 'Run # {0}\nEnvironment: {1}\nRider: {2}\nBicycle: {3}\nSpeed:'\ - '{4}\nManeuver: {5}\nNotes: {6}'.format( - self.metadata['RunID'], - self.metadata['Environment'], - self.metadata['Rider'], - self.metadata['Bicycle'], - self.metadata['Speed'], - self.metadata['Maneuver'], - self.metadata['Notes']) - - return line + '\n' + info + '\n' + line - - def export(self, filetype, directory='exports'): - """ - Exports the computed signals to a file. - - Parameters - ---------- - filetype : str - The type of file to export the data to. Options are 'mat', 'csv', - and 'pickle'. - - """ - - if filetype == 'mat': - if not os.path.exists(directory): - print "Creating {0}".format(directory) - os.makedirs(directory) - exportData = {} - exportData.update(self.metadata) - try: - exportData.update(self.taskSignals) - except AttributeError: - try: - exportData.update(self.truncatedSignals) - except AttributeError: - exportData.update(self.calibratedSignals) - print('Exported calibratedSignals to {}'.format(directory)) - else: - print('Exported truncatedSignals to {}'.format(directory)) - else: - print('Exported taskSignals to {}'.format(directory)) - - filename = pad_with_zeros(str(self.metadata['RunID']), 5) + '.mat' - io.savemat(os.path.join(directory, filename), exportData) - else: - raise NotImplementedError(('{0} method is not available' + - ' yet.').format(filetype)) - - def extract_task(self): - """Slices the computed signals such that data before the end of the - bump is removed and unusable trailng data is removed. - - """ - # get the z acceleration from the VN-100 - acc = -self.truncatedSignals['AccelerometerAccelerationY'].filter(30.) - # find the mean speed during the task (look at one second in the middle - # of the data) - speed = self.computedSignals['ForwardSpeed'] - meanSpeed = speed[len(speed) / 2 - 100:len(speed) / 2 + 100].mean() - wheelbase = self.bicycleRiderParameters['w'] - # find the bump - indices = sigpro.find_bump(acc, acc.sampleRate, meanSpeed, wheelbase, - self.bumpLength) - - - # if it is a pavilion run, then clip the end too - # these are the runs that the length of track method of clipping - # applies to - straight = ['Track Straight Line With Disturbance', - 'Balance With Disturbance', - 'Balance', - 'Track Straight Line'] - if (self.metadata['Environment'] == 'Pavillion Floor' and - self.metadata['Maneuver'] in straight): - - # this is based on the length of the track in the pavilion that we - # measured on September 21st, 2011 - trackLength = 32. - wheelbase - self.bumpLength - end = trackLength / meanSpeed * acc.sampleRate - - # i may need to clip the end based on the forward speed dropping - # below certain threshold around the mean - else: - # if it isn't a pavilion run, don't clip the end - end = -1 - - self.taskSignals = {} - for name, sig in self.computedSignals.items(): - self.taskSignals[name] = sig[indices[2]:end] - - def load_rider(self, pathToParameterData): - """Creates a bicycle/rider attribute which contains the physical - parameters for the bicycle and rider for this run.""" - - print("Loading the bicycle and rider data for " + - "{} on {}".format(self.metadata['Rider'], - self.metadata['Bicycle'])) - - # currently this isn't very generic, it only assumes that there was - # Luke, Jason, and Charlie riding on the instrumented bicycle. - rider = self.metadata['Rider'] - if rider == 'Charlie' or rider == 'Luke': - # Charlie and Luke rode the bike in the same configuration - bicycle = 'Rigidcl' - elif rider == 'Jason' : - bicycle = 'Rigid' - else: - raise StandardError('There are no bicycle parameters ' + - 'for {}'.format(rider)) - - # force a recalculation (but not the period calcs, they take too long) - self.bicycle = bp.Bicycle(bicycle, pathToData=pathToParameterData) - try: - self.bicycle.extras - except AttributeError: - pass - else: - self.bicycle.save_parameters() - # force a recalculation of the human parameters - self.bicycle.add_rider(rider) - if self.bicycle.human is not None: - self.bicycle.save_parameters() - - self.bicycleRiderParameters =\ - bp.io.remove_uncertainties(self.bicycle.parameters['Benchmark']) - - def plot(self, *args, **kwargs): - ''' - Returns a plot of the time series of various signals. - - Parameters - ---------- - signalName : string - These should be strings that correspond to the signals available in - the computed data. If the first character of the string is `-` then - the negative signal will be plotted. You can also scale the values - so by adding a value and an ``*`` such as: ``'-10*RollRate'. The - negative sign always has to come first. - signalType : string, optional - This allows you to plot from the other signal types. Options are - 'task', 'computed', 'truncated', 'calibrated', 'raw'. The default - is 'task'. - - ''' - if not kwargs: - kwargs = {'signalType': 'task'} - - mapping = {} - for x in ['computed', 'truncated', 'calibrated', 'raw', 'task']: - try: - mapping[x] = getattr(self, x + 'Signals') - except AttributeError: - pass - - fig = plt.figure() - ax = fig.add_axes([0.125, 0.125, 0.8, 0.7]) - - leg = [] - for i, arg in enumerate(args): - legName = arg - sign = 1. - # if a negative sign is present - if '-' in arg and arg[0] != '-': - raise ValueError('{} is incorrectly typed'.format(arg)) - elif '-' in arg and arg[0] == '-': - arg = arg[1:] - sign = -1. - # if a multiplication factor is present - if '*' in arg: - mul, arg = arg.split('*') - else: - mul = 1. - signal = sign * float(mul) * mapping[kwargs['signalType']][arg] - ax.plot(signal.time(), signal) - leg.append(legName + ' [' + mapping[kwargs['signalType']][arg].units + ']') - - ax.legend(leg) - runid = pad_with_zeros(str(self.metadata['RunID']), 5) - ax.set_title('Run: ' + runid + ', Rider: ' + self.metadata['Rider'] + - ', Speed: ' + str(self.metadata['Speed']) + 'm/s' + '\n' + - 'Maneuver: ' + self.metadata['Maneuver'] + - ', Environment: ' + self.metadata['Environment'] + '\n' + - 'Notes: ' + self.metadata['Notes']) - - ax.set_xlabel('Time [second]') - - ax.grid() - - return fig - - def plot_wheel_contact(self, show=False): - """Returns a plot of the wheel contact traces. - - Parameters - ---------- - show : boolean - If true the plot will be displayed. - - Returns - ------- - fig : matplotlib.Figure - - """ - - q1 = self.taskSignals['LongitudinalRearContact'] - q2 = self.taskSignals['LateralRearContact'] - q9 = self.taskSignals['LongitudinalFrontContact'] - q10 = self.taskSignals['LateralFrontContact'] - - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - ax.plot(q1, q2, q9, q10) - ax.set_xlabel('Distance [' + q1.units + ']') - ax.set_ylabel('Distance [' + q2.units + ']') - ax.set_ylim((-0.5, 0.5)) - rider = self.metadata['Rider'] - where = self.metadata['Environment'] - speed = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() - maneuver = self.metadata['Maneuver'] - ax.set_title(rider + ', ' + where + ', ' + maneuver + ' @ ' + speed + ' m/s') - - if show is True: - fig.show() - - return fig - - def verify_time_sync(self, show=True, saveDir=None): - """Shows a plot of the acceleration signals that were used to - synchronize the NI and VN data. If it doesn't show a good fit, then - something is wrong. - - Parameters - ---------- - show : boolean - If true, the figure will be displayed. - saveDir : str - The path to a directory in which to save the figure. - - """ - - if self.topSig == 'calibrated': - sigType = 'calibrated' - else: - sigType = 'truncated' - - fig = self.plot('-AccelerometerAccelerationY', 'AccelerationZ', - signalType=sigType) - ax = fig.axes[0] - ax.set_xlim((0, 10)) - title = ax.get_title() - ax.set_title(title + '\nSignal Type: ' + sigType) - if saveDir is not None: - if not os.path.exists(saveDir): - print "Creating {0}".format(saveDir) - os.makedirs(saveDir) - runid = run_id_string(self.metadata['RunID']) - fig.savefig(os.path.join(saveDir, runid + '.png')) - if show is True: - fig.show() - - return fig - - def video(self): - ''' - Plays the video of the run. - - ''' - # get the 5 digit string version of the run id - runid = pad_with_zeros(str(self.metadata['RunID']), 5) - viddir = os.path.join('..', 'Video') - abspath = os.path.abspath(viddir) - # check to see if there is a video for this run - if (runid + '.mp4') in os.listdir(viddir): - path = os.path.join(abspath, runid + '.mp4') - os.system('vlc "' + path + '"') - else: - print "No video for this run" + """The fluppin fundamental class for a run.""" + + def __init__(self, runid, dataset, pathToParameterData=None, + forceRecalc=False, filterFreq=None, store=True): + """Loads the raw and processed data for a run if available otherwise it + generates the processed data from the raw data. + + Parameters + ---------- + runid : int or str + The run id should be an integer, e.g. 5, or a five digit string with + leading zeros, e.g. '00005'. + dataset : DataSet + A DataSet object with at least some raw data. + pathToParameterData : string, {'', None}, optional + The path to a data directory for the BicycleParameters package. It + should contain the bicycles and riders used in the experiments. + forceRecalc : boolean, optional, default = False + If true then it will force a recalculation of all the processed + data. + filterSigs : float, optional, default = None + If true all of the processed signals will be low pass filtered with + a second order Butterworth filter at the given filter frequency. + store : boolean, optional, default = True + If true the resulting task signals will be stored in the database. + + """ + + if pathToParameterData is None: + pathToParameterData = config.get('data', 'pathToParameters') + + print "Initializing the run object." + + self.filterFreq = filterFreq + + dataset.open() + dataTable = dataset.database.root.runTable + signalTable = dataset.database.root.signalTable + taskTable = dataset.database.root.taskTable + + runid = run_id_string(runid) + + # get the row number for this particular run id + rownum = get_row_num(runid, dataTable) + + # make some dictionaries to store all the data + self.metadata = {} + self.rawSignals = {} + + # make lists of the input and output signals + rawDataCols = [x['signal'] for x in + signalTable.where("isRaw == True")] + computedCols = [x['signal'] for x in + signalTable.where("isRaw == False")] + + # store the metadata for this run + print "Loading metadata from the database." + for col in dataTable.colnames: + if col not in (rawDataCols + computedCols): + self.metadata[col] = get_cell(dataTable, col, rownum) + + print "Loading the raw signals from the database." + for col in rawDataCols: + # rawDataCols includes all possible raw signals, but every run + # doesn't have all the signals, so skip the ones that aren't there + try: + self.rawSignals[col] = RawSignal(runid, col, dataset.database) + except NoSuchNodeError: + pass + + if self.metadata['Rider'] != 'None': + self.load_rider(pathToParameterData) + + self.bumpLength = 1.0 # 1 meter + + # Try to load the task signals if they've already been computed. If + # they aren't in the database, the filter frequencies don't match or + # forceRecalc is true the then compute them. This may save some time + # when repeatedly loading runs for analysis. + self.taskFromDatabase = False + try: + runGroup = dataset.database.root.taskData._f_getChild(runid) + except NoSuchNodeError: + forceRecalc = True + else: + # The filter frequency stored in the task table is either a nan + # value or a valid float. If the stored filter frequency is not the + # same as the the one passed to Run, then a recalculation should be + # forced. + taskRowNum = get_row_num(runid, taskTable) + storedFreq = taskTable.cols.FilterFrequency[taskRowNum] + self.taskSignals = {} + if filterFreq is None: + newFilterFreq = np.nan + else: + newFilterFreq = filterFreq + + if np.isnan(newFilterFreq) and np.isnan(storedFreq): + for node in runGroup._f_walkNodes(): + meta = {k : node._f_getAttr(k) for k in ['units', 'name', + 'runid', 'sampleRate', 'source']} + self.taskSignals[node.name] = Signal(node[:], meta) + self.taskFromDatabase = True + elif np.isnan(newFilterFreq) or np.isnan(storedFreq): + forceRecalc = True + else: + if abs(storedFreq - filterFreq) < 1e-10: + for node in runGroup._f_walkNodes(): + meta = {k : node._f_getAttr(k) for k in ['units', 'name', + 'runid', 'sampleRate', 'source']} + self.taskSignals[node.name] = Signal(node[:], meta) + self.taskFromDatabase = True + else: + forceRecalc = True + + dataset.close() + + if forceRecalc == True: + try: + del self.taskSignals + except AttributeError: + pass + self.process_raw_signals() + + # store the task signals in the database if they are newly computed + if (store == True and self.taskFromDatabase == False + and self.topSig == 'task'): + taskMeta = { + 'Duration' : + self.taskSignals['ForwardSpeed'].time()[-1], + 'FilterFrequency' : self.filterFreq, + 'MeanSpeed' : self.taskSignals['ForwardSpeed'].mean(), + 'RunID' : self.metadata['RunID'], + 'StdSpeed' : self.taskSignals['ForwardSpeed'].std(), + 'Tau' : self.tau, + } + dataset.add_task_signals(self.taskSignals, taskMeta) + + # tell the user about the run + print self + + def process_raw_signals(self): + """Processes the raw signals as far as possible and filters the + result if a cutoff frequency was specified.""" + + print "Computing signals from raw data." + self.calibrate_signals() + + # the following maneuvers should never be calculated beyond the + # calibrated signals + maneuver = self.metadata['Maneuver'] + con1 = maneuver != 'Steer Dynamics Test' + con2 = maneuver != 'System Test' + con3 = maneuver != 'Static Calibration' + if con1 and con2 and con3: + self.compute_time_shift() + self.check_time_shift(0.15) + self.truncate_signals() + self.compute_signals() + self.task_signals() + + if self.filterFreq is not None: + self.filter_top_signals(self.filterFreq) + + def filter_top_signals(self, filterFreq): + """Filters the top most signals with a low pass filter.""" + + if self.topSig == 'task': + print('Filtering the task signals.') + for k, v in self.taskSignals.items(): + self.taskSignals[k] = v.filter(filterFreq) + elif self.topSig == 'computed': + print('Filtering the computed signals.') + for k, v in self.computedSignals.items(): + self.computedSignals[k] = v.filter(filterFreq) + elif self.topSig == 'calibrated': + print('Filtering the calibrated signals.') + for k, v in self.calibratedSignals.items(): + self.calibratedSignals[k] = v.filter(filterFreq) + + def calibrate_signals(self): + """Calibrates the raw signals.""" + + # calibrate the signals for the run + self.calibratedSignals = {} + for sig in self.rawSignals.values(): + calibSig = sig.scale() + self.calibratedSignals[calibSig.name] = calibSig + + self.topSig = 'calibrated' + + def task_signals(self): + """Computes the task signals.""" + print('Extracting the task portion from the data.') + self.extract_task() + + # compute task specific variables* + self.compute_yaw_angle() + self.compute_rear_wheel_contact_rates() + self.compute_rear_wheel_contact_points() + self.compute_front_wheel_contact_points() + self.compute_front_wheel_yaw_angle() + self.compute_front_wheel_rate() + self.compute_front_rear_wheel_contact_forces() + + self.topSig = 'task' + + def compute_signals(self): + """Computes the task independent quantities.""" + + self.computedSignals ={} + # transfer some of the signals to computed + noChange = ['FiveVolts', + 'PushButton', + 'RearWheelRate', + 'RollAngle', + 'SteerAngle', + 'ThreeVolts'] + for sig in noChange: + if sig in ['RollAngle', 'SteerAngle']: + self.computedSignals[sig] =\ + self.truncatedSignals[sig].convert_units('radian') + else: + self.computedSignals[sig] = self.truncatedSignals[sig] + + # compute the quantities that aren't task specific + self.compute_pull_force() + self.compute_forward_speed() + self.compute_steer_rate() + self.compute_yaw_roll_pitch_rates() + self.compute_steer_torque() + self.compute_frame_acceleration() + + def truncate_signals(self): + """Truncates the calibrated signals based on the time shift.""" + + self.truncatedSignals = {} + for name, sig in self.calibratedSignals.items(): + self.truncatedSignals[name] = sig.truncate(self.tau).spline() + self.topSig = 'truncated' + + def compute_time_shift(self): + """Computes the time shift based on the vertical accelerometer + signals.""" + + self.tau = sigpro.find_timeshift( + self.calibratedSignals['AccelerometerAccelerationY'], + self.calibratedSignals['AccelerationZ'], + self.metadata['NISampleRate'], + self.metadata['Speed'], plotError=False) + + def check_time_shift(self, maxNRMS): + """Raises an error if the normalized root mean square of the shifted + accelerometer signals is high.""" + + # Check to make sure the signals were actually good fits by + # calculating the normalized root mean square. If it isn't very + # low, raise an error. + niAcc = self.calibratedSignals['AccelerometerAccelerationY'] + vnAcc = self.calibratedSignals['AccelerationZ'] + vnAcc = vnAcc.truncate(self.tau).spline() + niAcc = niAcc.truncate(self.tau).spline() + # todo: this should probably check the rms of the mean subtracted data + # because both accelerometers don't always give the same value, this + # may work better with a filtered signal too + # todo: this should probably be moved into the time shift code in the + # signalprocessing model + nrms = np.sqrt(np.mean((vnAcc + niAcc)**2)) / (niAcc.max() - niAcc.min()) + if nrms > maxNRMS: + raise TimeShiftError(('The normalized root mean square for this ' + + 'time shift is {}, which is greater '.format(str(nrms)) + + 'than the maximum allowed: {}'.format(str(maxNRMS)))) + + def compute_rear_wheel_contact_points(self): + """Computes the location of the wheel contact points in the ground + plane.""" + + # get the rates + try: + latRate = self.taskSignals['LateralRearContactRate'] + lonRate = self.taskSignals['LongitudinalRearContactRate'] + except AttributeError: + print('At least one of the rates are not available. ' + + 'The YawAngle was not computed.') + else: + # convert to meters per second + latRate = latRate.convert_units('meter/second') + lonRate = lonRate.convert_units('meter/second') + # integrate and try to account for the drift + lat = latRate.integrate(detrend=True) + lon = lonRate.integrate() + # set the new name and units + lat.name = 'LateralRearContact' + lat.units = 'meter' + lon.name = 'LongitudinalRearContact' + lon.units = 'meter' + # store in task signals + self.taskSignals[lat.name] = lat + self.taskSignals[lon.name] = lon + + def compute_front_wheel_contact_points(self): + """Caluculates the front wheel contact points in the ground plane.""" + + q1 = self.taskSignals['LongitudinalRearContact'] + q2 = self.taskSignals['LateralRearContact'] + q3 = self.taskSignals['YawAngle'] + q4 = self.taskSignals['RollAngle'] + q7 = self.taskSignals['SteerAngle'] + + p = benchmark_to_moore(self.bicycleRiderParameters) + + f = np.vectorize(front_contact) + q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], + p['rf']) + + q9.name = 'LongitudinalFrontContact' + q9.units = 'meter' + q10.name = 'LateralFrontContact' + q10.units = 'meter' + + self.taskSignals['LongitudinalFrontContact'] = q9 + self.taskSignals['LateralFrontContact'] = q10 + + def compute_front_wheel_yaw_angle(self): + """Calculates the yaw angle of the front wheel.""" + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(bp) + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + f = np.vectorize(front_wheel_yaw_angle) + q1_front_wheel = f(q1, q2, q4, mp['d1'], mp['d2'], mp['d3'], + bp['lam'], mp['rr'], mp['rf']) + + q1_front_wheel.name = 'FrontWheelYawAngle' + q1_front_wheel.units = 'meter' + self.taskSignals['FrontWheelYawAngle'] = q1_front_wheel + + def compute_front_wheel_rate(self): + """Calculates the front wheel rate, based on the Jason's data + of front_wheel_contact_point. Alternatively, you can use the + sympy to get the front_wheel_contact_rate directly first.""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(bp) + + f = np.vectorize(front_wheel_rate) + + u6 = f(q1, q2, q4, u9, u10, mp['d1'], mp['d2'], mp['d3'], + bp['lam'], mp['rr'], mp['rf']) + + u6.name = 'FrontWheelRate' + u6.units = 'radian/second' + self.taskSignals['FrontWheelRate'] = u6 + + + def compute_front_rear_wheel_contact_forces(self): + """Calculate the contact forces for each + wheel with respect to inertial frame under + the slip and nonslip condition. Also, provide + the steer torque T4.""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + rf = mp['rf'] + + #signals assignment --slip condition + V = self.taskSignals['ForwardSpeed'].mean() + + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = u7.time_derivative() + u8d = u8.time_derivative() + u9d = u9.time_derivative() + u10d = u10.time_derivative() + + f_1 = np.vectorize(steer_torque_slip) + f_2 = np.vectorize(contact_forces_slip) + f_3 = np.vectorize(contact_forces_nonslip) + + #calculation + #steer torque slip + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + + #attributes: name and units, and taskSignals + T4_slip.name = 'SteerTorque_Slip' + T4_slip.units = 'newton*meter' + self.taskSignals[T4_slip.name] = T4_slip + + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s + + Fy_r_s.name = 'LateralRearContactForce_Slip' + Fy_r_s.units = 'newton' + self.taskSignals[Fy_r_s.name] = Fy_r_s + + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' + Fx_f_s.units = 'newton' + self.taskSignals[Fx_f_s.name] = Fx_f_s + + Fy_f_s.name = 'LateralFrontContactForce_Slip' + Fy_f_s.units = 'newton' + self.taskSignals[Fy_f_s.name] = Fy_f_s + + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' + Fx_r_ns.units = 'newton' + self.taskSignals[Fx_r_ns.name] = Fx_r_ns + + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' + Fy_r_ns.units = 'newton' + self.taskSignals[Fy_r_ns.name] = Fy_r_ns + + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' + Fx_f_ns.units = 'newton' + self.taskSignals[Fx_f_ns.name] = Fx_f_ns + + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' + Fy_f_ns.units = 'newton' + self.taskSignals[Fy_f_ns.name] = Fy_f_ns + + + def compute_rear_wheel_contact_rates(self): + """Calculates the rates of the wheel contact points in the ground + plane.""" + + try: + yawAngle = self.taskSignals['YawAngle'] + rearWheelRate = self.taskSignals['RearWheelRate'] + rR = self.bicycleRiderParameters['rR'] # this should be in meters + except AttributeError: + print('Either the yaw angle, rear wheel rate or ' + + 'front wheel radius is not available. The ' + + 'contact rates were not computed.') + else: + yawAngle = yawAngle.convert_units('radian') + rearWheelRate = rearWheelRate.convert_units('radian/second') + + lon, lat = sigpro.rear_wheel_contact_rate(rR, rearWheelRate, yawAngle) + + lon.name = 'LongitudinalRearContactRate' + lon.units = 'meter/second' + self.taskSignals[lon.name] = lon + + lat.name = 'LateralRearContactRate' + lat.units = 'meter/second' + self.taskSignals[lat.name] = lat + + def compute_yaw_angle(self): + """Computes the yaw angle by integrating the yaw rate.""" + + # get the yaw rate + try: + yawRate = self.taskSignals['YawRate'] + except AttributeError: + print('YawRate is not available. The YawAngle was not computed.') + else: + # convert to radians per second + yawRate = yawRate.convert_units('radian/second') + # integrate and try to account for the drift + yawAngle = yawRate.integrate(detrend=True) + # set the new name and units + yawAngle.name = 'YawAngle' + yawAngle.units = 'radian' + # store in computed signals + self.taskSignals['YawAngle'] = yawAngle + + def compute_steer_torque(self, plot=False): + """Computes the rider applied steer torque. + + Parameters + ---------- + plot : boolean, optional + Default is False, but if True a plot is generated. + + """ + # steer torque + frameAngRate = np.vstack(( + self.truncatedSignals['AngularRateX'], + self.truncatedSignals['AngularRateY'], + self.truncatedSignals['AngularRateZ'])) + frameAngAccel = np.vstack(( + self.truncatedSignals['AngularRateX'].time_derivative(), + self.truncatedSignals['AngularRateY'].time_derivative(), + self.truncatedSignals['AngularRateZ'].time_derivative())) + frameAccel = np.vstack(( + self.truncatedSignals['AccelerationX'], + self.truncatedSignals['AccelerationY'], + self.truncatedSignals['AccelerationZ'])) + handlebarAngRate = self.truncatedSignals['ForkRate'] + handlebarAngAccel = self.truncatedSignals['ForkRate'].time_derivative() + steerAngle = self.truncatedSignals['SteerAngle'] + steerColumnTorque =\ + self.truncatedSignals['SteerTubeTorque'].convert_units('newton*meter') + handlebarMass = self.bicycleRiderParameters['mG'] + handlebarInertia =\ + self.bicycle.steer_assembly_moment_of_inertia(fork=False, + wheel=False, nominal=True) + # this is the distance from the handlebar center of mass to the + # steer axis + w = self.bicycleRiderParameters['w'] + c = self.bicycleRiderParameters['c'] + lam = self.bicycleRiderParameters['lam'] + xG = self.bicycleRiderParameters['xG'] + zG = self.bicycleRiderParameters['zG'] + handlebarCoM = np.array([xG, 0., zG]) + d = bp.geometry.distance_to_steer_axis(w, c, lam, handlebarCoM) + # these are the distances from the point on the steer axis which is + # aligned with the handlebar center of mass to the accelerometer on + # the frame + ds1 = self.bicycle.parameters['Measured']['ds1'] + ds3 = self.bicycle.parameters['Measured']['ds3'] + ds = np.array([ds1, 0., ds3]) # i measured these + # damping and friction values come from Peter's work, I need to verify + # them still + damping = 0.3475 + friction = 0.0861 + + components = sigpro.steer_torque_components( + frameAngRate, frameAngAccel, frameAccel, handlebarAngRate, + handlebarAngAccel, steerAngle, steerColumnTorque, + handlebarMass, handlebarInertia, damping, friction, d, ds) + steerTorque = sigpro.steer_torque(components) + + stDict = {'units':'newton*meter', + 'name':'SteerTorque', + 'runid':self.metadata['RunID'], + 'sampleRate':steerAngle.sampleRate, + 'source':'NA'} + self.computedSignals['SteerTorque'] = Signal(steerTorque, stDict) + + if plot is True: + + time = steerAngle.time() + + hdot = (components['Hdot1'] + components['Hdot2'] + + components['Hdot3'] + components['Hdot4']) + cross = (components['cross1'] + components['cross2'] + + components['cross3']) + + fig = plt.figure() + + frictionAx = fig.add_subplot(4, 1, 1) + frictionAx.plot(time, components['viscous'], + time, components['coulomb'], + time, components['viscous'] + components['coulomb']) + frictionAx.set_ylabel('Torque [N-m]') + frictionAx.legend(('Viscous Friction', 'Coulomb Friction', + 'Total Friction')) + + dynamicAx = fig.add_subplot(4, 1, 2) + dynamicAx.plot(time, hdot, time, cross, time, hdot + cross) + dynamicAx.set_ylabel('Torque [N-m]') + dynamicAx.legend((r'Torque due to $\dot{H}$', + r'Torque due to $r \times m a$', + r'Total Dynamic Torque')) + + additionalAx = fig.add_subplot(4, 1, 3) + additionalAx.plot(time, hdot + cross + components['viscous'] + + components['coulomb'], + label='Total Frictional and Dynamic Torque') + additionalAx.set_ylabel('Torque [N-m]') + additionalAx.legend() + + torqueAx = fig.add_subplot(4, 1, 4) + torqueAx.plot(time, components['steerColumn'], + time, hdot + cross + components['viscous'] + components['coulomb'], + time, steerTorque) + torqueAx.set_xlabel('Time [s]') + torqueAx.set_ylabel('Torque [N-m]') + torqueAx.legend(('Measured Torque', 'Frictional and Dynamic Torque', + 'Rider Applied Torque')) + + plt.show() + + return fig + + def compute_yaw_roll_pitch_rates(self): + """Computes the yaw, roll and pitch rates of the bicycle frame.""" + + try: + omegaX = self.truncatedSignals['AngularRateX'] + omegaY = self.truncatedSignals['AngularRateY'] + omegaZ = self.truncatedSignals['AngularRateZ'] + rollAngle = self.truncatedSignals['RollAngle'] + lam = self.bicycleRiderParameters['lam'] + except AttributeError: + print('All needed signals are not available. ' + + 'Yaw, roll and pitch rates were not computed.') + else: + omegaX = omegaX.convert_units('radian/second') + omegaY = omegaY.convert_units('radian/second') + omegaZ = omegaZ.convert_units('radian/second') + rollAngle = rollAngle.convert_units('radian') + + yr, rr, pr = sigpro.yaw_roll_pitch_rate(omegaX, omegaY, omegaZ, lam, + rollAngle=rollAngle) + yr.units = 'radian/second' + yr.name = 'YawRate' + rr.units = 'radian/second' + rr.name = 'RollRate' + pr.units = 'radian/second' + pr.name = 'PitchRate' + + self.computedSignals['YawRate'] = yr + self.computedSignals['RollRate'] = rr + self.computedSignals['PitchRate'] = pr + + def compute_frame_acceleration(self): + """Calculate the frame acceleration in inertial frame, expressed by the + the A frame coordinates which is after yaw movement.""" + AccelerationX = self.truncatedSignals['AccelerationX'] + AccelerationY = self.truncatedSignals['AccelerationY'] + AccelerationZ = self.truncatedSignals['AccelerationZ'] + rollAngle = self.truncatedSignals['RollAngle'] + lam = self.bicycleRiderParameters['lam'] + + AccX, AccY, AccZ = sigpro.frame_acceleration(AccelerationX, + AccelerationY, AccelerationZ, lam, rollAngle=rollAngle) + AccX.units = 'meter/second/second' + AccX.name = 'AccelerationX' + AccY.units = 'meter/second/second' + AccY.name = 'AccelerationY' + AccZ.units = 'meter/second/second' + AccZ.name = 'AccelerationZ' + + self.computedSignals['AccelerationX'] = AccX + self.computedSignals['AccelerationY'] = AccY + self.computedSignals['AccelerationZ'] = AccZ + + def compute_steer_rate(self): + """Calculate the steer rate from the frame and fork rates.""" + try: + forkRate = self.truncatedSignals['ForkRate'] + omegaZ = self.truncatedSignals['AngularRateZ'] + except AttributeError: + print('ForkRate or AngularRateZ is not available. ' + + 'SteerRate was not computed.') + else: + forkRate = forkRate.convert_units('radian/second') + omegaZ = omegaZ.convert_units('radian/second') + + steerRate = sigpro.steer_rate(forkRate, omegaZ) + steerRate.units = 'radian/second' + steerRate.name = 'SteerRate' + self.computedSignals['SteerRate'] = steerRate + + def compute_forward_speed(self): + """Calculates the magnitude of the main component of velocity of the + center of the rear wheel.""" + + try: + rR = self.bicycleRiderParameters['rR'] + rearWheelRate = self.truncatedSignals['RearWheelRate'] + except AttributeError: + print('rR or RearWheelRate is not availabe. ' + + 'ForwardSpeed was not computed.') + else: + rearWheelRate = rearWheelRate.convert_units('radian/second') + + self.computedSignals['ForwardSpeed'] = -rR * rearWheelRate + self.computedSignals['ForwardSpeed'].units = 'meter/second' + self.computedSignals['ForwardSpeed'].name = 'ForwardSpeed' + + def compute_pull_force(self): + """ + Computes the pull force from the truncated pull force signal. + + """ + try: + pullForce = self.truncatedSignals['PullForce'] + except AttributeError: + print 'PullForce was not available. PullForce was not computed.' + else: + pullForce = pullForce.convert_units('newton') + pullForce.name = 'PullForce' + pullForce.units = 'newton' + self.computedSignals[pullForce.name] = pullForce + + def __str__(self): + '''Prints basic run information to the screen.''' + + line = "=" * 79 + info = 'Run # {0}\nEnvironment: {1}\nRider: {2}\nBicycle: {3}\nSpeed:'\ + '{4}\nManeuver: {5}\nNotes: {6}'.format( + self.metadata['RunID'], + self.metadata['Environment'], + self.metadata['Rider'], + self.metadata['Bicycle'], + self.metadata['Speed'], + self.metadata['Maneuver'], + self.metadata['Notes']) + + return line + '\n' + info + '\n' + line + + def export(self, filetype, directory='exports'): + """ + Exports the computed signals to a file. + + Parameters + ---------- + filetype : str + The type of file to export the data to. Options are 'mat', 'csv', + and 'pickle'. + + """ + + if filetype == 'mat': + if not os.path.exists(directory): + print "Creating {0}".format(directory) + os.makedirs(directory) + exportData = {} + exportData.update(self.metadata) + try: + exportData.update(self.taskSignals) + except AttributeError: + try: + exportData.update(self.truncatedSignals) + except AttributeError: + exportData.update(self.calibratedSignals) + print('Exported calibratedSignals to {}'.format(directory)) + else: + print('Exported truncatedSignals to {}'.format(directory)) + else: + print('Exported taskSignals to {}'.format(directory)) + + filename = pad_with_zeros(str(self.metadata['RunID']), 5) + '.mat' + io.savemat(os.path.join(directory, filename), exportData) + else: + raise NotImplementedError(('{0} method is not available' + + ' yet.').format(filetype)) + + def extract_task(self): + """Slices the computed signals such that data before the end of the + bump is removed and unusable trailng data is removed. + + """ + # get the z acceleration from the VN-100 + acc = -self.truncatedSignals['AccelerometerAccelerationY'].filter(30.) + # find the mean speed during the task (look at one second in the middle + # of the data) + speed = self.computedSignals['ForwardSpeed'] + meanSpeed = speed[len(speed) / 2 - 100:len(speed) / 2 + 100].mean() + wheelbase = self.bicycleRiderParameters['w'] + # find the bump + indices = sigpro.find_bump(acc, acc.sampleRate, meanSpeed, wheelbase, + self.bumpLength) + + + # if it is a pavilion run, then clip the end too + # these are the runs that the length of track method of clipping + # applies to + straight = ['Track Straight Line With Disturbance', + 'Balance With Disturbance', + 'Balance', + 'Track Straight Line'] + if (self.metadata['Environment'] == 'Pavillion Floor' and + self.metadata['Maneuver'] in straight): + + # this is based on the length of the track in the pavilion that we + # measured on September 21st, 2011 + trackLength = 32. - wheelbase - self.bumpLength + end = trackLength / meanSpeed * acc.sampleRate + + # i may need to clip the end based on the forward speed dropping + # below certain threshold around the mean + else: + # if it isn't a pavilion run, don't clip the end + end = -1 + + self.taskSignals = {} + for name, sig in self.computedSignals.items(): + self.taskSignals[name] = sig[indices[2]:end] + + def load_rider(self, pathToParameterData): + """Creates a bicycle/rider attribute which contains the physical + parameters for the bicycle and rider for this run.""" + + print("Loading the bicycle and rider data for " + + "{} on {}".format(self.metadata['Rider'], + self.metadata['Bicycle'])) + + # currently this isn't very generic, it only assumes that there was + # Luke, Jason, and Charlie riding on the instrumented bicycle. + rider = self.metadata['Rider'] + if rider == 'Charlie' or rider == 'Luke': + # Charlie and Luke rode the bike in the same configuration + bicycle = 'Rigidcl' + elif rider == 'Jason' : + bicycle = 'Rigid' + else: + raise StandardError('There are no bicycle parameters ' + + 'for {}'.format(rider)) + + # force a recalculation (but not the period calcs, they take too long) + self.bicycle = bp.Bicycle(bicycle, pathToData=pathToParameterData) + try: + self.bicycle.extras + except AttributeError: + pass + else: + self.bicycle.save_parameters() + # force a recalculation of the human parameters + self.bicycle.add_rider(rider) + if self.bicycle.human is not None: + self.bicycle.save_parameters() + + self.bicycleRiderParameters =\ + bp.io.remove_uncertainties(self.bicycle.parameters['Benchmark']) + + def plot(self, *args, **kwargs): + ''' + Returns a plot of the time series of various signals. + + Parameters + ---------- + signalName : string + These should be strings that correspond to the signals available in + the computed data. If the first character of the string is `-` then + the negative signal will be plotted. You can also scale the values + so by adding a value and an ``*`` such as: ``'-10*RollRate'. The + negative sign always has to come first. + signalType : string, optional + This allows you to plot from the other signal types. Options are + 'task', 'computed', 'truncated', 'calibrated', 'raw'. The default + is 'task'. + + ''' + if not kwargs: + kwargs = {'signalType': 'task'} + + mapping = {} + for x in ['computed', 'truncated', 'calibrated', 'raw', 'task']: + try: + mapping[x] = getattr(self, x + 'Signals') + except AttributeError: + pass + + fig = plt.figure() + ax = fig.add_axes([0.125, 0.125, 0.8, 0.7]) + + leg = [] + for i, arg in enumerate(args): + legName = arg + sign = 1. + # if a negative sign is present + if '-' in arg and arg[0] != '-': + raise ValueError('{} is incorrectly typed'.format(arg)) + elif '-' in arg and arg[0] == '-': + arg = arg[1:] + sign = -1. + # if a multiplication factor is present + if '*' in arg: + mul, arg = arg.split('*') + else: + mul = 1. + signal = sign * float(mul) * mapping[kwargs['signalType']][arg] + ax.plot(signal.time(), signal) + leg.append(legName + ' [' + mapping[kwargs['signalType']][arg].units + ']') + + ax.legend(leg) + runid = pad_with_zeros(str(self.metadata['RunID']), 5) + ax.set_title('Run: ' + runid + ', Rider: ' + self.metadata['Rider'] + + ', Speed: ' + str(self.metadata['Speed']) + 'm/s' + '\n' + + 'Maneuver: ' + self.metadata['Maneuver'] + + ', Environment: ' + self.metadata['Environment'] + '\n' + + 'Notes: ' + self.metadata['Notes']) + + ax.set_xlabel('Time [second]') + + ax.grid() + + return fig + + def plot_wheel_contact(self, show=False): + """Returns a plot of the wheel contact traces. + + Parameters + ---------- + show : boolean + If true the plot will be displayed. + + Returns + ------- + fig : matplotlib.Figure + + """ + + q1 = self.taskSignals['LongitudinalRearContact'] + q2 = self.taskSignals['LateralRearContact'] + q9 = self.taskSignals['LongitudinalFrontContact'] + q10 = self.taskSignals['LateralFrontContact'] + + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.plot(q1, q2, q9, q10) + ax.set_xlabel('Distance [' + q1.units + ']') + ax.set_ylabel('Distance [' + q2.units + ']') + ax.set_ylim((-0.5, 0.5)) + rider = self.metadata['Rider'] + where = self.metadata['Environment'] + speed = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() + maneuver = self.metadata['Maneuver'] + ax.set_title(rider + ', ' + where + ', ' + maneuver + ' @ ' + speed + ' m/s') + + if show is True: + fig.show() + + return fig + + def verify_time_sync(self, show=True, saveDir=None): + """Shows a plot of the acceleration signals that were used to + synchronize the NI and VN data. If it doesn't show a good fit, then + something is wrong. + + Parameters + ---------- + show : boolean + If true, the figure will be displayed. + saveDir : str + The path to a directory in which to save the figure. + + """ + + if self.topSig == 'calibrated': + sigType = 'calibrated' + else: + sigType = 'truncated' + + fig = self.plot('-AccelerometerAccelerationY', 'AccelerationZ', + signalType=sigType) + ax = fig.axes[0] + ax.set_xlim((0, 10)) + title = ax.get_title() + ax.set_title(title + '\nSignal Type: ' + sigType) + if saveDir is not None: + if not os.path.exists(saveDir): + print "Creating {0}".format(saveDir) + os.makedirs(saveDir) + runid = run_id_string(self.metadata['RunID']) + fig.savefig(os.path.join(saveDir, runid + '.png')) + if show is True: + fig.show() + + return fig + + def video(self): + ''' + Plays the video of the run. + + ''' + # get the 5 digit string version of the run id + runid = pad_with_zeros(str(self.metadata['RunID']), 5) + viddir = os.path.join('..', 'Video') + abspath = os.path.abspath(viddir) + # check to see if there is a video for this run + if (runid + '.mp4') in os.listdir(viddir): + path = os.path.join(abspath, runid + '.mp4') + os.system('vlc "' + path + '"') + else: + print "No video for this run" def matlab_date_to_object(matDate): - '''Returns a date time object based on a Matlab `datestr()` output. + '''Returns a date time object based on a Matlab `datestr()` output. - Parameters - ---------- - matDate : string - String in the form '21-Mar-2011 14:45:54'. + Parameters + ---------- + matDate : string + String in the form '21-Mar-2011 14:45:54'. - Returns - ------- - python datetime object + Returns + ------- + python datetime object - ''' - return datetime.datetime.strptime(matDate, '%d-%b-%Y %H:%M:%S') + ''' + return datetime.datetime.strptime(matDate, '%d-%b-%Y %H:%M:%S') diff --git a/bicycledataprocessor/signalprocessing.py b/bicycledataprocessor/signalprocessing.py index aec7749..d481041 100644 --- a/bicycledataprocessor/signalprocessing.py +++ b/bicycledataprocessor/signalprocessing.py @@ -1,11 +1,11 @@ #!/usr/bin/env python #try: - #from IPython.core.debugger import Tracer + #from IPython.core.debugger import Tracer #except ImportError: - #pass + #pass #else: - #set_trace = Tracer() + #set_trace = Tracer() from warnings import warn @@ -22,555 +22,598 @@ #from signalprocessing import * def find_bump(accelSignal, sampleRate, speed, wheelbase, bumpLength): - '''Returns the indices that surround the bump in the acceleration signal. - - Parameters - ---------- - accelSignal : ndarray, shape(n,) - An acceleration signal with a single distinctive large acceleration - that signifies riding over the bump. - sampleRate : float - The sample rate of the signal. - speed : float - Speed of travel (or treadmill) in meters per second. - wheelbase : float - Wheelbase of the bicycle in meters. - bumpLength : float - Length of the bump in meters. - - Returns - ------- - indices : tuple - The first and last indice of the bump section. - - ''' - - # Find the indice to the maximum absolute acceleration in the provided - # signal. This is mostly likely where the bump is. Skip the first few - # points in case there are some endpoint problems with filtered data. - n = len(accelSignal) - nSkip = 5 - rectAccel = abs(subtract_mean(accelSignal[nSkip:n / 2.])) - indice = np.nanargmax(rectAccel) + nSkip - - # This calculates how many time samples it takes for the bicycle to roll - # over the bump given the speed of the bicycle, it's wheelbase, the bump - # length and the sample rate. - bumpDuration = (wheelbase + bumpLength) / speed - bumpSamples = int(bumpDuration * sampleRate) - # make the number divisible by four - bumpSamples = int(bumpSamples / 4) * 4 - - # These indices try to capture the length of the bump based on the max - # acceleration indice. - indices = (indice - bumpSamples / 4, indice, indice + 3 * bumpSamples / 4) - - # If the maximum acceleration is not greater than 0.5 m/s**2, then there was - # probably was no bump collected in the acceleration data. - maxChange = rectAccel[indice - nSkip] - if maxChange < 0.5: - warn('This run does not have a bump that is easily detectable. ' + - 'The bump only gave a {:1.2f} m/s^2 change in nominal accerelation.\n'\ - .format(maxChange) + - 'The bump indice is {} and the bump time is {:1.2f} seconds.'\ - .format(str(indice), indice / float(sampleRate))) - return None - else: - # If the bump isn't at the beginning of the run, give a warning. - if indice > n / 3.: - warn("This signal's max value is not in the first third of the data\n" - + "It is at %1.2f seconds out of %1.2f seconds" % - (indice / float(sampleRate), n / float(sampleRate))) - - # If there is a nan in the bump this maybe an issue down the line as the - # it is prefferable for the bump to be in the data when the fitting occurs, - # to get a better fit. - if np.isnan(accelSignal[indices[0]:indices[1]]).any(): - warn('There is at least one NaN in this bump') - - return indices + '''Returns the indices that surround the bump in the acceleration signal. + + Parameters + ---------- + accelSignal : ndarray, shape(n,) + An acceleration signal with a single distinctive large acceleration + that signifies riding over the bump. + sampleRate : float + The sample rate of the signal. + speed : float + Speed of travel (or treadmill) in meters per second. + wheelbase : float + Wheelbase of the bicycle in meters. + bumpLength : float + Length of the bump in meters. + + Returns + ------- + indices : tuple + The first and last indice of the bump section. + + ''' + + # Find the indice to the maximum absolute acceleration in the provided + # signal. This is mostly likely where the bump is. Skip the first few + # points in case there are some endpoint problems with filtered data. + n = len(accelSignal) + nSkip = 5 + rectAccel = abs(subtract_mean(accelSignal[nSkip:n / 2.])) + indice = np.nanargmax(rectAccel) + nSkip + + # This calculates how many time samples it takes for the bicycle to roll + # over the bump given the speed of the bicycle, it's wheelbase, the bump + # length and the sample rate. + bumpDuration = (wheelbase + bumpLength) / speed + bumpSamples = int(bumpDuration * sampleRate) + # make the number divisible by four + bumpSamples = int(bumpSamples / 4) * 4 + + # These indices try to capture the length of the bump based on the max + # acceleration indice. + indices = (indice - bumpSamples / 4, indice, indice + 3 * bumpSamples / 4) + + # If the maximum acceleration is not greater than 0.5 m/s**2, then there was + # probably was no bump collected in the acceleration data. + maxChange = rectAccel[indice - nSkip] + if maxChange < 0.5: + warn('This run does not have a bump that is easily detectable. ' + + 'The bump only gave a {:1.2f} m/s^2 change in nominal accerelation.\n'\ + .format(maxChange) + + 'The bump indice is {} and the bump time is {:1.2f} seconds.'\ + .format(str(indice), indice / float(sampleRate))) + return None + else: + # If the bump isn't at the beginning of the run, give a warning. + if indice > n / 3.: + warn("This signal's max value is not in the first third of the data\n" + + "It is at %1.2f seconds out of %1.2f seconds" % + (indice / float(sampleRate), n / float(sampleRate))) + + # If there is a nan in the bump this maybe an issue down the line as the + # it is prefferable for the bump to be in the data when the fitting occurs, + # to get a better fit. + if np.isnan(accelSignal[indices[0]:indices[1]]).any(): + warn('There is at least one NaN in this bump') + + return indices def split_around_nan(sig): - ''' - Returns the sections of an array not polluted with nans. - - Parameters - ---------- - sig : ndarray, shape(n,) - A one dimensional array that may or may not contain m nan values where - 0 <= m <= n. - - Returns - ------- - indices : list, len(indices) = k - List of tuples containing the indices for the sections of the array. - arrays : list, len(indices) = k - List of section arrays. All arrays of nan values are of dimension 1. - - k = number of non-nan sections + number of nans - - sig[indices[k][0]:indices[k][1]] == arrays[k] - - ''' - # if there are any nans then split the signal - if np.isnan(sig).any(): - firstSplit = np.split(sig, np.nonzero(np.isnan(sig))[0]) - arrays = [] - for arr in firstSplit: - # if the array has nans, then split it again - if np.isnan(arr).any(): - arrays = arrays + np.split(arr, np.nonzero(np.isnan(arr))[0] + 1) - # if it doesn't have nans, then just add it as is - else: - arrays.append(arr) - # remove any empty arrays - emptys = [i for i, arr in enumerate(arrays) if arr.shape[0] == 0] - arrays = [arr for i, arr in enumerate(arrays) if i not in emptys] - # build the indices list - indices = [] - count = 0 - for i, arr in enumerate(arrays): - count += len(arr) - if np.isnan(arr).any(): - indices.append((count - 1, count)) - else: - indices.append((count - len(arr), count)) - else: - arrays, indices = [sig], [(0, len(sig))] - - return indices, arrays + ''' + Returns the sections of an array not polluted with nans. + + Parameters + ---------- + sig : ndarray, shape(n,) + A one dimensional array that may or may not contain m nan values where + 0 <= m <= n. + + Returns + ------- + indices : list, len(indices) = k + List of tuples containing the indices for the sections of the array. + arrays : list, len(indices) = k + List of section arrays. All arrays of nan values are of dimension 1. + + k = number of non-nan sections + number of nans + + sig[indices[k][0]:indices[k][1]] == arrays[k] + + ''' + # if there are any nans then split the signal + if np.isnan(sig).any(): + firstSplit = np.split(sig, np.nonzero(np.isnan(sig))[0]) + arrays = [] + for arr in firstSplit: + # if the array has nans, then split it again + if np.isnan(arr).any(): + arrays = arrays + np.split(arr, np.nonzero(np.isnan(arr))[0] + 1) + # if it doesn't have nans, then just add it as is + else: + arrays.append(arr) + # remove any empty arrays + emptys = [i for i, arr in enumerate(arrays) if arr.shape[0] == 0] + arrays = [arr for i, arr in enumerate(arrays) if i not in emptys] + # build the indices list + indices = [] + count = 0 + for i, arr in enumerate(arrays): + count += len(arr) + if np.isnan(arr).any(): + indices.append((count - 1, count)) + else: + indices.append((count - len(arr), count)) + else: + arrays, indices = [sig], [(0, len(sig))] + + return indices, arrays def steer_rate(forkRate, angularRateZ): - '''Returns the steer rate. - - Parameters - ---------- - forkRate : ndarray, size(n,) - The rate of the fork about the steer axis relative to the Newtonian - reference frame. - angularRateZ : ndarray, size(n,) - The rate of the bicycle frame about the steer axis in the Newtonian - reference frame. - - Returns - ------- - steerRate : ndarray, size(n,) - The rate of the fork about the steer axis relative to the bicycle - frame. - - Notes - ----- - The rates are defined such that a positive rate is pointing downward along - the steer axis. - - ''' - return forkRate - angularRateZ + '''Returns the steer rate. + + Parameters + ---------- + forkRate : ndarray, size(n,) + The rate of the fork about the steer axis relative to the Newtonian + reference frame. + angularRateZ : ndarray, size(n,) + The rate of the bicycle frame about the steer axis in the Newtonian + reference frame. + + Returns + ------- + steerRate : ndarray, size(n,) + The rate of the fork about the steer axis relative to the bicycle + frame. + + Notes + ----- + The rates are defined such that a positive rate is pointing downward along + the steer axis. + + ''' + return forkRate - angularRateZ def steer_torque_components(frameAngRate, frameAngAccel, frameAccel, - handlebarAngRate, handlebarAngAccel, steerAngle, steerColumnTorque, - handlebarMass, handlebarInertia, damping, friction, d, ds): - """Returns the components of the steer torque applied by the rider. - - Parameters - ---------- - frameAngRate : ndarray, shape(3,n) - The angular velocity of the bicycle frame in the Newtonian frame - expressed in body fixed coordinates. - frameAngAccel : ndarray, shape(3,n) - The angular acceleration of the bicycle frame in the Newtonian frame - expressed in body fixed coordinates. - frameAccel : ndarray, shape(3,n) - The linear acceleration of the frame accelerometer in the Newtonian - frame expressed in body fixed coordinates. - handlebarAngRate : ndarray, shape(n,) - The component of angular rate of the handlebar in the Newtonian frame - expressed in body fixed coordinates about the steer axis. - handlebarAngAccel : ndarray, shape(n,) - The component of angular acceleration of the handlebar in the Newtonian - frame expressed in body fixed coordinates about the steer axis. - steerAngle : ndarray, shape(n,) - The angle of the steer column relative to the frame about the steer - axis. - steerColumnTorque : ndarray, shape(n,) - The torque measured on the steer column between the handlebars and the - fork and between the upper and lower bearings. - handlebarMass : float - The mass of the handlebar. - handlebarInertia : ndarray, shape(3,3) - The inertia tensor of the handlebars. Includes everything above and - including the steer tube torque sensor. This is relative to a reference - frame aligned with the steer axis and is about the center of mass. - damping : float - The damping coefficient associated with the total (upper and lower) - bearing friction. - friction : float - The columb friction associated with the total (upper and lower) bearing - friction. - d : float - The distance from the handlebar center of mass to the steer axis. The - point on the steer axis is at the projection of the mass center onto - the axis. - ds : ndarray, shape(3,) - The distance from the accelerometer to the point on the steer axis. - plot : boolean, optional - If true a plot of the components of the steer torque will be shown. - - Returns - ------- - components : dictionary - A dictionary containing the ten components of the rider applied steer - torque. - - Notes - ----- - The friction torque from the upper and lower bearings is halved because - only one bearing is associated with the handlebar rigid body. - - """ - wb1 = frameAngRate[0] - wb2 = frameAngRate[1] - wb3 = frameAngRate[2] - wb1p = frameAngAccel[0] - wb2p = frameAngAccel[1] - wb3p = frameAngAccel[2] - av1 = frameAccel[0] - av2 = frameAccel[1] - av3 = frameAccel[2] - wh3 = handlebarAngRate - wh3p = handlebarAngAccel - IH = handlebarInertia - mH = handlebarMass - delta = steerAngle - - components = {} - - components['Hdot1'] = -((IH[0, 0] * (wb1 * np.cos(delta) + - wb2 * np.sin(delta)) + - IH[2, 0] * wh3) * - (-wb1 * np.sin(delta) + wb2 * np.cos(delta))) - - components['Hdot2'] = (IH[1, 1] * - (-wb1 * np.sin(delta) + wb2 * np.cos(delta)) * - (wb1 * np.cos(delta) + wb2 * np.sin(delta))) - - components['Hdot3'] = IH[2, 2] * wh3p - - components['Hdot4'] = IH[2, 0] * (-(-wb3 + wh3) * wb1 * np.sin(delta) + - (-wb3 + wh3) * wb2 * np.cos(delta) + - np.sin(delta) * wb2p + np.cos(delta) * wb1p) - - components['cross1'] = d * mH * (d * (-wb1 * np.sin(delta) + wb2 * np.cos(delta)) * - (wb1 * np.cos(delta) + wb2 * np.sin(delta)) - + d * wh3p) - components['cross2'] = -d * mH * (-ds[0] * wb2 ** 2 + ds[2] * wb2p - - (ds[0] * wb3 - ds[2] * wb1) * wb3 + av1) * np.sin(delta) - components['cross3'] = d * mH * (ds[0] * wb1 * wb2 + ds[0] * wb3p + ds[2] * wb2 * - wb3 - ds[2] * wb1p + av2) * np.cos(delta) - components['viscous'] = (damping * (-wb3 + wh3)) / 2. - components['coulomb'] = np.sign(-wb3 + wh3) * friction / 2. - components['steerColumn'] = steerColumnTorque - - return components + handlebarAngRate, handlebarAngAccel, steerAngle, steerColumnTorque, + handlebarMass, handlebarInertia, damping, friction, d, ds): + """Returns the components of the steer torque applied by the rider. + + Parameters + ---------- + frameAngRate : ndarray, shape(3,n) + The angular velocity of the bicycle frame in the Newtonian frame + expressed in body fixed coordinates. + frameAngAccel : ndarray, shape(3,n) + The angular acceleration of the bicycle frame in the Newtonian frame + expressed in body fixed coordinates. + frameAccel : ndarray, shape(3,n) + The linear acceleration of the frame accelerometer in the Newtonian + frame expressed in body fixed coordinates. + handlebarAngRate : ndarray, shape(n,) + The component of angular rate of the handlebar in the Newtonian frame + expressed in body fixed coordinates about the steer axis. + handlebarAngAccel : ndarray, shape(n,) + The component of angular acceleration of the handlebar in the Newtonian + frame expressed in body fixed coordinates about the steer axis. + steerAngle : ndarray, shape(n,) + The angle of the steer column relative to the frame about the steer + axis. + steerColumnTorque : ndarray, shape(n,) + The torque measured on the steer column between the handlebars and the + fork and between the upper and lower bearings. + handlebarMass : float + The mass of the handlebar. + handlebarInertia : ndarray, shape(3,3) + The inertia tensor of the handlebars. Includes everything above and + including the steer tube torque sensor. This is relative to a reference + frame aligned with the steer axis and is about the center of mass. + damping : float + The damping coefficient associated with the total (upper and lower) + bearing friction. + friction : float + The columb friction associated with the total (upper and lower) bearing + friction. + d : float + The distance from the handlebar center of mass to the steer axis. The + point on the steer axis is at the projection of the mass center onto + the axis. + ds : ndarray, shape(3,) + The distance from the accelerometer to the point on the steer axis. + plot : boolean, optional + If true a plot of the components of the steer torque will be shown. + + Returns + ------- + components : dictionary + A dictionary containing the ten components of the rider applied steer + torque. + + Notes + ----- + The friction torque from the upper and lower bearings is halved because + only one bearing is associated with the handlebar rigid body. + + """ + wb1 = frameAngRate[0] + wb2 = frameAngRate[1] + wb3 = frameAngRate[2] + wb1p = frameAngAccel[0] + wb2p = frameAngAccel[1] + wb3p = frameAngAccel[2] + av1 = frameAccel[0] + av2 = frameAccel[1] + av3 = frameAccel[2] + wh3 = handlebarAngRate + wh3p = handlebarAngAccel + IH = handlebarInertia + mH = handlebarMass + delta = steerAngle + + components = {} + + components['Hdot1'] = -((IH[0, 0] * (wb1 * np.cos(delta) + + wb2 * np.sin(delta)) + + IH[2, 0] * wh3) * + (-wb1 * np.sin(delta) + wb2 * np.cos(delta))) + + components['Hdot2'] = (IH[1, 1] * + (-wb1 * np.sin(delta) + wb2 * np.cos(delta)) * + (wb1 * np.cos(delta) + wb2 * np.sin(delta))) + + components['Hdot3'] = IH[2, 2] * wh3p + + components['Hdot4'] = IH[2, 0] * (-(-wb3 + wh3) * wb1 * np.sin(delta) + + (-wb3 + wh3) * wb2 * np.cos(delta) + + np.sin(delta) * wb2p + np.cos(delta) * wb1p) + + components['cross1'] = d * mH * (d * (-wb1 * np.sin(delta) + wb2 * np.cos(delta)) * + (wb1 * np.cos(delta) + wb2 * np.sin(delta)) + + d * wh3p) + components['cross2'] = -d * mH * (-ds[0] * wb2 ** 2 + ds[2] * wb2p - + (ds[0] * wb3 - ds[2] * wb1) * wb3 + av1) * np.sin(delta) + components['cross3'] = d * mH * (ds[0] * wb1 * wb2 + ds[0] * wb3p + ds[2] * wb2 * + wb3 - ds[2] * wb1p + av2) * np.cos(delta) + components['viscous'] = (damping * (-wb3 + wh3)) / 2. + components['coulomb'] = np.sign(-wb3 + wh3) * friction / 2. + components['steerColumn'] = steerColumnTorque + + return components def steer_torque(components): - """Returns the steer torque given the components. + """Returns the steer torque given the components. - Parameters - ---------- - components : dictionary - A dictionary containing the ten components of the rider applied steer - torque. + Parameters + ---------- + components : dictionary + A dictionary containing the ten components of the rider applied steer + torque. - Returns - ------- - steerTorque : ndarray, shape(n,) - The steer torque applied by the rider. + Returns + ------- + steerTorque : ndarray, shape(n,) + The steer torque applied by the rider. - """ + """ - return np.sum(components.values(), axis=0) + return np.sum(components.values(), axis=0) def rear_wheel_contact_rate(rearRadius, rearWheelRate, yawAngle): - """Returns the longitudinal and lateral components of the velocity of the - rear wheel contact in the ground plane. - - Parameters - ---------- - rearRadius : float - The radius of the rear wheel. - rearWheelRate : ndarray, shape(n,) - The rear wheel rotation rate. - yawAngle : ndarray, shape(n,) - The yaw angle of the bicycle frame. - - Returns - ------- - longitudinal : ndarray, shape(n,) - The longitudinal deviation of the rear wheel contact point. - lateral : ndarray, shape(n,) - The lateral deviation of the rear wheel contact point. - - """ - longitudinal = -rearWheelRate * rearRadius * np.cos(yawAngle) - lateral = -rearWheelRate * rearRadius * np.sin(yawAngle) - return longitudinal, lateral + """Returns the longitudinal and lateral components of the velocity of the + rear wheel contact in the ground plane. + + Parameters + ---------- + rearRadius : float + The radius of the rear wheel. + rearWheelRate : ndarray, shape(n,) + The rear wheel rotation rate. + yawAngle : ndarray, shape(n,) + The yaw angle of the bicycle frame. + + Returns + ------- + longitudinal : ndarray, shape(n,) + The longitudinal deviation of the rear wheel contact point. + lateral : ndarray, shape(n,) + The lateral deviation of the rear wheel contact point. + + """ + longitudinal = -rearWheelRate * rearRadius * np.cos(yawAngle) + lateral = -rearWheelRate * rearRadius * np.sin(yawAngle) + return longitudinal, lateral def sync_error(tau, signal1, signal2, time): - '''Returns the error between two signal time histories. - - Parameters - ---------- - tau : float - The time shift. - signal1 : ndarray, shape(n,) - The signal that will be interpolated. This signal is - typically "cleaner" that signal2 and/or has a higher sample rate. - signal2 : ndarray, shape(n,) - The signal that will be shifted to syncronize with signal 1. - time : ndarray, shape(n,) - The time vector for the two signals - - Returns - ------- - error : float - Error between the two signals for the given tau. - - ''' - # make sure tau isn't too large - if np.abs(tau) >= time[-1]: - raise TimeShiftError(('abs(tau), {0}, must be less than or equal to ' + - '{1}').format(str(np.abs(tau)), str(time[-1]))) - - # this is the time for the second signal which is assumed to lag the first - # signal - shiftedTime = time + tau - - # create time vector where the two signals overlap - if tau > 0: - intervalTime = shiftedTime[np.nonzero(shiftedTime < time[-1])] - else: - intervalTime = shiftedTime[np.nonzero(shiftedTime > time[0])] - - # interpolate between signal 1 samples to find points that correspond in - # time to signal 2 on the shifted time - sig1OnInterval = np.interp(intervalTime, time, signal1); - - # truncate signal 2 to the time interval - if tau > 0: - sig2OnInterval = signal2[np.nonzero(shiftedTime <= intervalTime[-1])] - else: - sig2OnInterval = signal2[np.nonzero(shiftedTime >= intervalTime[0])] - - # calculate the error between the two signals - error = np.linalg.norm(sig1OnInterval - sig2OnInterval) - - return error + '''Returns the error between two signal time histories. + + Parameters + ---------- + tau : float + The time shift. + signal1 : ndarray, shape(n,) + The signal that will be interpolated. This signal is + typically "cleaner" that signal2 and/or has a higher sample rate. + signal2 : ndarray, shape(n,) + The signal that will be shifted to syncronize with signal 1. + time : ndarray, shape(n,) + The time vector for the two signals + + Returns + ------- + error : float + Error between the two signals for the given tau. + + ''' + # make sure tau isn't too large + if np.abs(tau) >= time[-1]: + raise TimeShiftError(('abs(tau), {0}, must be less than or equal to ' + + '{1}').format(str(np.abs(tau)), str(time[-1]))) + + # this is the time for the second signal which is assumed to lag the first + # signal + shiftedTime = time + tau + + # create time vector where the two signals overlap + if tau > 0: + intervalTime = shiftedTime[np.nonzero(shiftedTime < time[-1])] + else: + intervalTime = shiftedTime[np.nonzero(shiftedTime > time[0])] + + # interpolate between signal 1 samples to find points that correspond in + # time to signal 2 on the shifted time + sig1OnInterval = np.interp(intervalTime, time, signal1); + + # truncate signal 2 to the time interval + if tau > 0: + sig2OnInterval = signal2[np.nonzero(shiftedTime <= intervalTime[-1])] + else: + sig2OnInterval = signal2[np.nonzero(shiftedTime >= intervalTime[0])] + + # calculate the error between the two signals + error = np.linalg.norm(sig1OnInterval - sig2OnInterval) + + return error def find_timeshift(niAcc, vnAcc, sampleRate, speed, plotError=False): - '''Returns the timeshift, tau, of the VectorNav [VN] data relative to the - National Instruments [NI] data. - - Parameters - ---------- - niAcc : ndarray, shape(n, ) - The acceleration of the NI accelerometer in its local Y direction. - vnAcc : ndarray, shape(n, ) - The acceleration of the VN-100 in its local Z direction. Should be the - same length as NIacc and contains the same signal albiet time shifted. - The VectorNav signal should be leading the NI signal. - sampleRate : integer or float - Sample rate of the signals. This should be the same for each signal. - speed : float - The approximate forward speed of the bicycle. - - Returns - ------- - tau : float - The timeshift. - - Notes - ----- - The Z direction for `VNacc` is assumed to be aligned with the steer axis - and pointing down and the Y direction for the NI accelerometer should be - aligned with the steer axis and pointing up. - - ''' - # raise an error if the signals are not the same length - N = len(niAcc) - if N != len(vnAcc): - raise TimeShiftError('Signals are not the same length!') - - # make a time vector - time = time_vector(N, sampleRate) - - # the signals are opposite sign of each other, so fix that - niSig = -niAcc - vnSig = vnAcc - - # some constants for find_bump - wheelbase = 1.02 # this is the wheelbase of the rigid rider bike - bumpLength = 1. - cutoff = 30. - # filter the NI Signal - filNiSig = butterworth(niSig, cutoff, sampleRate) - # find the bump in the filtered NI signal - niBump = find_bump(filNiSig, sampleRate, speed, wheelbase, bumpLength) - - # remove the nan's in the VN signal and the corresponding time - v = vnSig[np.nonzero(np.isnan(vnSig) == False)] - t = time[np.nonzero(np.isnan(vnSig) == False)] - # fit a spline through the data - vn_spline = UnivariateSpline(t, v, k=3, s=0) - # and filter it - filVnSig = butterworth(vn_spline(time), cutoff, sampleRate) - # and find the bump in the filtered VN signal - vnBump = find_bump(filVnSig, sampleRate, speed, wheelbase, bumpLength) - - if vnBump is None or niBump is None: - guess = 0.3 - else: - # get an initial guess for the time shift based on the bump indice - guess = (niBump[1] - vnBump[1]) / float(sampleRate) - - # Since vnSig may have nans we should only use contiguous data around - # around the bump. The first step is to split vnSig into sections bounded - # by the nans and then selected the section in which the bump falls. Then - # we select a similar area in niSig to run the time shift algorithm on. - if vnBump is None: - bumpLocation = 800 # just a random guess so things don't crash - else: - bumpLocation = vnBump[1] - indices, arrays = split_around_nan(vnSig) - for pair in indices: - if pair[0] <= bumpLocation < pair[1]: - bSec = pair - - # subtract the mean and normalize both signals - niSig = normalize(subtract_mean(niSig, hasNans=True), hasNans=True) - vnSig = normalize(subtract_mean(vnSig, hasNans=True), hasNans=True) - - niBumpSec = niSig[bSec[0]:bSec[1]] - vnBumpSec = vnSig[bSec[0]:bSec[1]] - timeBumpSec = time[bSec[0]:bSec[1]] - - if len(niBumpSec) < 200: - warn('The bump section is only {} samples wide.'.format(str(len(niBumpSec)))) - - # set up the error landscape, error vs tau - # The NI lags the VectorNav and the time shift is typically between 0 and - # 1 seconds - tauRange = np.linspace(0., 2., num=500) - error = np.zeros_like(tauRange) - for i, val in enumerate(tauRange): - error[i] = sync_error(val, niBumpSec, vnBumpSec, timeBumpSec) - - if plotError: - plt.figure() - plt.plot(tauRange, error) - plt.xlabel('tau') - plt.ylabel('error') - plt.show() - - # find initial condition from landscape - tau0 = tauRange[np.argmin(error)] - - print "The minimun of the error landscape is %f and the provided guess is %f" % (tau0, guess) - - # Compute the minimum of the function using both the result from the error - # landscape and the bump find for initial guesses to the minimizer. Choose - # the best of the two. - tauBump, fvalBump = fmin(sync_error, guess, args=(niBumpSec, - vnBumpSec, timeBumpSec), full_output=True, disp=True)[0:2] - - tauLandscape, fvalLandscape = fmin(sync_error, tau0, args=(niBumpSec, vnBumpSec, - timeBumpSec), full_output=True, disp=True)[0:2] - - if fvalBump < fvalLandscape: - tau = tauBump - else: - tau = tauLandscape - - #### if the minimization doesn't do a good job, just use the tau0 - ###if np.abs(tau - tau0) > 0.01: - ###tau = tau0 - ###print "Bad minimizer!! Using the guess, %f, instead." % tau - - print "This is what came out of the minimization:", tau - - if not (0.05 < tau < 2.0): - raise TimeShiftError('This tau, {} s, is probably wrong'.format(str(tau))) - - return tau + '''Returns the timeshift, tau, of the VectorNav [VN] data relative to the + National Instruments [NI] data. + + Parameters + ---------- + niAcc : ndarray, shape(n, ) + The acceleration of the NI accelerometer in its local Y direction. + vnAcc : ndarray, shape(n, ) + The acceleration of the VN-100 in its local Z direction. Should be the + same length as NIacc and contains the same signal albiet time shifted. + The VectorNav signal should be leading the NI signal. + sampleRate : integer or float + Sample rate of the signals. This should be the same for each signal. + speed : float + The approximate forward speed of the bicycle. + + Returns + ------- + tau : float + The timeshift. + + Notes + ----- + The Z direction for `VNacc` is assumed to be aligned with the steer axis + and pointing down and the Y direction for the NI accelerometer should be + aligned with the steer axis and pointing up. + + ''' + # raise an error if the signals are not the same length + N = len(niAcc) + if N != len(vnAcc): + raise TimeShiftError('Signals are not the same length!') + + # make a time vector + time = time_vector(N, sampleRate) + + # the signals are opposite sign of each other, so fix that + niSig = -niAcc + vnSig = vnAcc + + # some constants for find_bump + wheelbase = 1.02 # this is the wheelbase of the rigid rider bike + bumpLength = 1. + cutoff = 30. + # filter the NI Signal + filNiSig = butterworth(niSig, cutoff, sampleRate) + # find the bump in the filtered NI signal + niBump = find_bump(filNiSig, sampleRate, speed, wheelbase, bumpLength) + + # remove the nan's in the VN signal and the corresponding time + v = vnSig[np.nonzero(np.isnan(vnSig) == False)] + t = time[np.nonzero(np.isnan(vnSig) == False)] + # fit a spline through the data + vn_spline = UnivariateSpline(t, v, k=3, s=0) + # and filter it + filVnSig = butterworth(vn_spline(time), cutoff, sampleRate) + # and find the bump in the filtered VN signal + vnBump = find_bump(filVnSig, sampleRate, speed, wheelbase, bumpLength) + + if vnBump is None or niBump is None: + guess = 0.3 + else: + # get an initial guess for the time shift based on the bump indice + guess = (niBump[1] - vnBump[1]) / float(sampleRate) + + # Since vnSig may have nans we should only use contiguous data around + # around the bump. The first step is to split vnSig into sections bounded + # by the nans and then selected the section in which the bump falls. Then + # we select a similar area in niSig to run the time shift algorithm on. + if vnBump is None: + bumpLocation = 800 # just a random guess so things don't crash + else: + bumpLocation = vnBump[1] + indices, arrays = split_around_nan(vnSig) + for pair in indices: + if pair[0] <= bumpLocation < pair[1]: + bSec = pair + + # subtract the mean and normalize both signals + niSig = normalize(subtract_mean(niSig, hasNans=True), hasNans=True) + vnSig = normalize(subtract_mean(vnSig, hasNans=True), hasNans=True) + + niBumpSec = niSig[bSec[0]:bSec[1]] + vnBumpSec = vnSig[bSec[0]:bSec[1]] + timeBumpSec = time[bSec[0]:bSec[1]] + + if len(niBumpSec) < 200: + warn('The bump section is only {} samples wide.'.format(str(len(niBumpSec)))) + + # set up the error landscape, error vs tau + # The NI lags the VectorNav and the time shift is typically between 0 and + # 1 seconds + tauRange = np.linspace(0., 2., num=500) + error = np.zeros_like(tauRange) + for i, val in enumerate(tauRange): + error[i] = sync_error(val, niBumpSec, vnBumpSec, timeBumpSec) + + if plotError: + plt.figure() + plt.plot(tauRange, error) + plt.xlabel('tau') + plt.ylabel('error') + plt.show() + + # find initial condition from landscape + tau0 = tauRange[np.argmin(error)] + + print "The minimun of the error landscape is %f and the provided guess is %f" % (tau0, guess) + + # Compute the minimum of the function using both the result from the error + # landscape and the bump find for initial guesses to the minimizer. Choose + # the best of the two. + tauBump, fvalBump = fmin(sync_error, guess, args=(niBumpSec, + vnBumpSec, timeBumpSec), full_output=True, disp=True)[0:2] + + tauLandscape, fvalLandscape = fmin(sync_error, tau0, args=(niBumpSec, vnBumpSec, + timeBumpSec), full_output=True, disp=True)[0:2] + + if fvalBump < fvalLandscape: + tau = tauBump + else: + tau = tauLandscape + + #### if the minimization doesn't do a good job, just use the tau0 + ###if np.abs(tau - tau0) > 0.01: + ###tau = tau0 + ###print "Bad minimizer!! Using the guess, %f, instead." % tau + + print "This is what came out of the minimization:", tau + + if not (0.05 < tau < 2.0): + raise TimeShiftError('This tau, {} s, is probably wrong'.format(str(tau))) + + return tau def truncate_data(signal, tau): - ''' - Returns the truncated vectors with respect to the timeshift tau. + ''' + Returns the truncated vectors with respect to the timeshift tau. - Parameters - --------- - signal : Signal(ndarray), shape(n, ) - A time signal from the NIData or the VNavData. - tau : float - The time shift. + Parameters + --------- + signal : Signal(ndarray), shape(n, ) + A time signal from the NIData or the VNavData. + tau : float + The time shift. - Returns - ------- - truncated : ndarray, shape(m, ) - The truncated time signal. + Returns + ------- + truncated : ndarray, shape(m, ) + The truncated time signal. - ''' - t = time_vector(len(signal), signal.sampleRate) + ''' + t = time_vector(len(signal), signal.sampleRate) - # shift the ni data cause it is the cleaner signal - tni = t - tau - tvn = t + # shift the ni data cause it is the cleaner signal + tni = t - tau + tvn = t - # make the common time interval - tcom = tvn[np.nonzero(tvn < tni[-1])] + # make the common time interval + tcom = tvn[np.nonzero(tvn < tni[-1])] - if signal.source == 'NI': - truncated = np.interp(tcom, tni, signal) - elif signal.source == 'VN': - truncated = signal[np.nonzero(tvn <= tcom[-1])] - else: - raise ValueError('No source was defined in this signal.') + if signal.source == 'NI': + truncated = np.interp(tcom, tni, signal) + elif signal.source == 'VN': + truncated = signal[np.nonzero(tvn <= tcom[-1])] + else: + raise ValueError('No source was defined in this signal.') - return truncated + return truncated def yaw_roll_pitch_rate(angularRateX, angularRateY, angularRateZ, - lam, rollAngle=0.): - '''Returns the bicycle frame yaw, roll and pitch rates based on the body - fixed rate data taken with the VN-100 and optionally the roll angle - measurement. - - Parameters - ---------- - angularRateX : ndarray, shape(n,) - The body fixed rate perpendicular to the headtube and pointing forward. - angularRateY : ndarray, shape(n,) - The body fixed rate perpendicular to the headtube and pointing to the - right of the bicycle. - angularRateZ : ndarray, shape(n,) - The body fixed rate aligned with the headtube and pointing downward. - lam : float - The steer axis tilt. - rollAngle : ndarray, shape(n,), optional - The roll angle of the bicycle frame. - - Returns - ------- - yawRate : ndarray, shape(n,) - The yaw rate of the bicycle frame. - rollRate : ndarray, shape(n,) - The roll rate of the bicycle frame. - pitchRate : ndarray, shape(n,) - The pitch rate of the bicycle frame. - - ''' - yawRate = -(angularRateX*np.sin(lam) - - angularRateZ * np.cos(lam)) / np.cos(rollAngle) - rollRate = angularRateX * np.cos(lam) + angularRateZ * np.sin(lam) - pitchRate = (angularRateY + angularRateX * np.sin(lam) * np.tan(rollAngle) - - angularRateZ * np.cos(lam) * np.tan(rollAngle)) - - return yawRate, rollRate, pitchRate + lam, rollAngle=0.): + '''Returns the bicycle frame yaw, roll and pitch rates based on the body + fixed rate data taken with the VN-100 and optionally the roll angle + measurement. + + Parameters + ---------- + angularRateX : ndarray, shape(n,) + The body fixed rate perpendicular to the headtube and pointing forward. + angularRateY : ndarray, shape(n,) + The body fixed rate perpendicular to the headtube and pointing to the + right of the bicycle. + angularRateZ : ndarray, shape(n,) + The body fixed rate aligned with the headtube and pointing downward. + lam : float + The steer axis tilt. + rollAngle : ndarray, shape(n,), optional + The roll angle of the bicycle frame. + + Returns + ------- + yawRate : ndarray, shape(n,) + The yaw rate of the bicycle frame. + rollRate : ndarray, shape(n,) + The roll rate of the bicycle frame. + pitchRate : ndarray, shape(n,) + The pitch rate of the bicycle frame. + + ''' + yawRate = -(angularRateX*np.sin(lam) - + angularRateZ * np.cos(lam)) / np.cos(rollAngle) + rollRate = angularRateX * np.cos(lam) + angularRateZ * np.sin(lam) + pitchRate = (angularRateY + angularRateX * np.sin(lam) * np.tan(rollAngle) - + angularRateZ * np.cos(lam) * np.tan(rollAngle)) + + return yawRate, rollRate, pitchRate + +def frame_acceleration(AccelerationX, AccelerationY, AccelerationZ, + lam, rollAngle=0.): + '''Returns the bicycle frame acceleration based on the body + fixed acceleration taken with the VN-100 and optionally the roll angle + measurement. + + Parameters + ---------- + AccelerationX : ndarray, shape(n,) + The body fixed acceleration perpendicular to the headtube and pointing forward. + AccelerationY : ndarray, shape(n,) + The body fixed acceleration perpendicular to the headtube and pointing to the + right of the bicycle. + AccelerationZ : ndarray, shape(n,) + The body fixed acceleration aligned with the headtube and pointing downward. + lam : float + The steer axis tilt. + rollAngle : ndarray, shape(n,), optional + The roll angle of the bicycle frame. + + Returns + ------- + forward_Acceleration : ndarray, shape(n,) + The forward acceleration of the bicycle frame in A['1']. + lateral_Acceleration : ndarray, shape(n,) + The lateral acceleration of the bicycle frame in A['2']. + downward_Acceleration : ndarray, shape(n,) + The downward acceleration of the bicycle frame in A['3']. + + ''' + forward_Acceleration= AccelerationX * np.cos(lam) + AccelerationZ * np.sin(lam) + + lateral_Acceleration = np.cos(rollAngle) * (AccelerationY + AccelerationX * + np.sin(lam) * np.tan(rollAngle) - + AccelerationZ * np.cos(lam) * np.tan(rollAngle)) + + downward_Acceleration = -(AccelerationX*np.sin(lam) - + AccelerationY * np.tan(rollAngle) - + AccelerationZ * np.cos(lam)) / np.cos(rollAngle) + + + return forward_Acceleration, lateral_Acceleration, downward_Acceleration From 169f589684e6eef2eb3074e7293e4755a78d4bb2 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Wed, 10 Oct 2012 09:19:20 -0700 Subject: [PATCH 20/37] Fixed an error in the func of frame_acceleration --- bicycledataprocessor/signalprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bicycledataprocessor/signalprocessing.py b/bicycledataprocessor/signalprocessing.py index d481041..fb4cf83 100644 --- a/bicycledataprocessor/signalprocessing.py +++ b/bicycledataprocessor/signalprocessing.py @@ -613,7 +613,7 @@ def frame_acceleration(AccelerationX, AccelerationY, AccelerationZ, downward_Acceleration = -(AccelerationX*np.sin(lam) - AccelerationY * np.tan(rollAngle) - - AccelerationZ * np.cos(lam)) / np.cos(rollAngle) + AccelerationZ * np.cos(lam)) * np.cos(rollAngle) return forward_Acceleration, lateral_Acceleration, downward_Acceleration From 58f00bd0215d366bfe93a0ecbc23fda4670d15bc Mon Sep 17 00:00:00 2001 From: StefenYin Date: Wed, 10 Oct 2012 18:01:49 -0700 Subject: [PATCH 21/37] Added compute_contact_points_acceleration func --- bicycledataprocessor/main.py | 80 +++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 67fb5ad..d3f7ca9 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -27,6 +27,8 @@ from dtk.bicycle import front_contact, benchmark_to_moore from dtk.bicycle import front_wheel_yaw_angle, front_wheel_rate from dtk.bicycle import steer_torque_slip, contact_forces_slip, contact_forces_nonslip +from dtk.bicycle import contact_points_acceleration + import bicycleparameters as bp @@ -741,6 +743,7 @@ def task_signals(self): self.compute_front_wheel_yaw_angle() self.compute_front_wheel_rate() self.compute_front_rear_wheel_contact_forces() + self.compute_contact_points_acceleration() self.topSig = 'task' @@ -996,6 +999,71 @@ def compute_front_rear_wheel_contact_forces(self): Fy_f_ns.units = 'newton' self.taskSignals[Fy_f_ns.name] = Fy_f_ns + def compute_contact_points_acceleration(self): + """Calculates the acceleration of the contact points of front and rear + wheels.""" + + AccX = self.taskSignals['AccelerationX'] + AccY = self.taskSignals['AccelerationY'] + AccZ = self.taskSignals['AccelerationZ'] + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + d1 = mp['d1'] + d2 = mp['d2'] + d3 = mp['d3'] + rr = mp['rr'] + rf = mp['rf'] + ds1 = self.bicycle.parameters['Measured']['ds1'] + ds3 = self.bicycle.parameters['Measured']['ds3'] + s3 = ds3 + s1 = ds1 + mp['l4'] + + f = np.vectorize(contact_points_acceleration) + + u7d, u8d, u11d, u9d, u10d, u12d = f(AccX, AccY, AccZ, q1, q2, q3, + u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d, + d1, d2, d3, rr, rf, s1, s3) + + u7d.name = 'RearWheelContactPointLongitudinalAcceleration' + u7d.units = 'meter/second/second' + u8d.name = 'RearWheelContactPointLateralAcceleration' + u8d.units = 'meter/second/second' + u11d.name = 'RearWheelContactPointDownwardAcceleration' + u11d.units = 'meter/second/second' + u9d.name = 'FrontWheelContactPointLongitudinalAcceleration' + u9d.units = 'meter/second/second' + u10d.name = 'FrontWheelContactPointLateralAcceleration' + u10d.units = 'meter/second/second' + u12d.name = 'FrontWheelContactPointDownwardAcceleration' + u12.units = 'meter/second/second' + + self.taskSignals(u7d.name) = u7d + self.taskSignals(u8d.name) = u8d + self.taskSignals(u11d.name) = u11d + self.taskSignals(u9d.name) = u9d + self.taskSignals(u10d.name) = u10d + self.taskSignals(u12d.name) = u12d + def compute_rear_wheel_contact_rates(self): """Calculates the rates of the wheel contact points in the ground @@ -1195,15 +1263,15 @@ def compute_frame_acceleration(self): AccX, AccY, AccZ = sigpro.frame_acceleration(AccelerationX, AccelerationY, AccelerationZ, lam, rollAngle=rollAngle) AccX.units = 'meter/second/second' - AccX.name = 'AccelerationX' + AccX.name = 'FrameLongitudinalAcceleration' AccY.units = 'meter/second/second' - AccY.name = 'AccelerationY' + AccY.name = 'FrameLateralAcceleration' AccZ.units = 'meter/second/second' - AccZ.name = 'AccelerationZ' + AccZ.name = 'FrameDownwardAcceleration' - self.computedSignals['AccelerationX'] = AccX - self.computedSignals['AccelerationY'] = AccY - self.computedSignals['AccelerationZ'] = AccZ + self.computedSignals['AccX.name'] = AccX + self.computedSignals['AccY.name'] = AccY + self.computedSignals['AccZ.name'] = AccZ def compute_steer_rate(self): """Calculate the steer rate from the frame and fork rates.""" From cb7684086268707dc3c88435c2a10d35aa00e2df Mon Sep 17 00:00:00 2001 From: StefenYin Date: Wed, 10 Oct 2012 18:26:27 -0700 Subject: [PATCH 22/37] Changed the source of u7d, u8d, u9d and u10d, based on contact_points_acceleration func --- bicycledataprocessor/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index d3f7ca9..fddc5f0 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -945,10 +945,10 @@ def compute_front_rear_wheel_contact_forces(self): u4d = u4.time_derivative() u5d = u5.time_derivative() u6d = u6.time_derivative() - u7d = u7.time_derivative() - u8d = u8.time_derivative() - u9d = u9.time_derivative() - u10d = u10.time_derivative() + u7d = self.taskSignals['RearWheelContactPointLongitudinalAcceleration'] + u8d = self.taskSignals['RearWheelContactPointLateralAcceleration'] + u9d = self.taskSignals['FrontWheelContactPointLongitudinalAcceleration'] + u10d = self.taskSignals['FrontWheelContactPointLateralAcceleration'] f_1 = np.vectorize(steer_torque_slip) f_2 = np.vectorize(contact_forces_slip) From e46fe622b4fcf57ecccddb41e19d908da998fa50 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Wed, 10 Oct 2012 18:29:42 -0700 Subject: [PATCH 23/37] Wrapped the variables length in contact forces func --- bicycledataprocessor/main.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index fddc5f0..8d2b387 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -956,11 +956,16 @@ def compute_front_rear_wheel_contact_forces(self): #calculation #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, + u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, + ic11, ic22, ic33, ic31, q1, q2, q4, + u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, + u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, + u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) #attributes: name and units, and taskSignals T4_slip.name = 'SteerTorque_Slip' From 0d56cc3879fe05525ed3465bb2f4eb014f0d60c1 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Wed, 10 Oct 2012 23:42:25 -0700 Subject: [PATCH 24/37] Fixed the errors after testing all new built function on IpythonNotebook --- bicycledataprocessor/main.py | 3151 +++++++++++----------- bicycledataprocessor/signalprocessing.py | 6 +- 2 files changed, 1579 insertions(+), 1578 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 8d2b387..7042fa7 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -9,11 +9,11 @@ # debugging #try: - #from IPython.core.debugger import Tracer + #from IPython.core.debugger import Tracer #except ImportError: - #pass + #pass #else: - #set_trace = Tracer() + #set_trace = Tracer() # dependencies import numpy as np @@ -41,1586 +41,1587 @@ config.read(os.path.join(os.path.dirname(__file__), '..', 'defaults.cfg')) class Signal(np.ndarray): - """ - A subclass of ndarray for collecting the data for a single signal in a run. - - Attributes - ---------- - conversions : dictionary - A mapping for unit conversions. - name : str - The name of the signal. Should be CamelCase. - runid : str - A five digit identification number associated with the - trial this signal was collected from (e.g. '00104'). - sampleRate : float - The sample rate in hertz of the signal. - source : str - The source of the data. This should be 'NI' for the - National Instruments USB-6218 and 'VN' for the VN-100 IMU. - units : str - The physcial units of the signal. These should be specified - as lowercase complete words using only multiplication and - division symbols (e.g. 'meter/second/second'). - Signal.conversions will show the avialable options. - - Methods - ------- - plot() - Plot's the signal versus time and returns the line. - frequency() - Returns the frequency spectrum of the signal. - time_derivative() - Returns the time derivative of the signal. - filter(frequency) - Returns the low passed filter of the signal. - truncate(tau) - Interpolates and truncates the signal the based on the time shift, - `tau`, and the signal source. - as_dictionary - Returns a dictionary of the metadata of the signal. - convert_units(units) - Returns a signal with different units. `conversions` specifies the - available options. - - """ - - # define some basic unit converions - conversions = {'degree->radian': pi / 180., - 'degree/second->radian/second': pi / 180., - 'degree/second/second->radian/second/second': pi / 180., - 'inch*pound->newton*meter': 25.4 / 1000. * 4.44822162, - 'pound->newton': 4.44822162, - 'feet/second->meter/second': 12. * 2.54 / 100., - 'mile/hour->meter/second': 0.00254 * 12. / 5280. / 3600.} - - def __new__(cls, inputArray, metadata): - """ - Returns an instance of the Signal class with the additional signal - data. - - Parameters - ---------- - inputArray : ndarray, shape(n,) - A one dimension array representing a single variable's time - history. - metadata : dictionary - This dictionary contains the metadata for the signal. - name : str - The name of the signal. Should be CamelCase. - runid : str - A five digit identification number associated with the - trial this experiment was collected at (e.g. '00104'). - sampleRate : float - The sample rate in hertz of the signal. - source : str - The source of the data. This should be 'NI' for the - National Instruments USB-6218 and 'VN' for the VN-100 IMU. - units : str - The physcial units of the signal. These should be specified - as lowercase complete words using only multiplication and - division symbols (e.g. 'meter/second/second'). - Signal.conversions will show the avialable options. - - Raises - ------ - ValueError - If `inputArray` is not a vector. - - """ - if len(inputArray.shape) > 1: - raise ValueError('Signals must be arrays of one dimension.') - # cast the input array into the Signal class - obj = np.asarray(inputArray).view(cls) - # add the metadata to the object - obj.name = metadata['name'] - obj.runid = metadata['runid'] - obj.sampleRate = metadata['sampleRate'] - obj.source = metadata['source'] - obj.units = metadata['units'] - return obj - - def __array_finalize__(self, obj): - if obj is None: return - self.name = getattr(obj, 'name', None) - self.runid = getattr(obj, 'runid', None) - self.sampleRate = getattr(obj, 'sampleRate', None) - self.source = getattr(obj, 'source', None) - self.units = getattr(obj, 'units', None) - - def __array_wrap__(self, outputArray, context=None): - # doesn't support these things in basic ufunc calls...maybe one day - # That means anytime you add, subtract, multiply, divide, etc, the - # following are not retained. - outputArray.name = None - outputArray.source = None - outputArray.units = None - return np.ndarray.__array_wrap__(self, outputArray, context) - - def as_dictionary(self): - '''Returns the signal metadata as a dictionary.''' - data = {'runid': self.runid, - 'name': self.name, - 'units': self.units, - 'source': self.source, - 'sampleRate': self.sampleRate} - return data - - def convert_units(self, units): - """ - Returns a signal with the specified units. - - Parameters - ---------- - units : str - The units to convert the signal to. The mapping must be in the - attribute `conversions`. - - Returns - ------- - newSig : Signal - The signal with the desired units. - - """ - if units == self.units: - return self - else: - try: - conversion = self.units + '->' + units - newSig = self * self.conversions[conversion] - except KeyError: - try: - conversion = units + '->' + self.units - newSig = self / self.conversions[conversion] - except KeyError: - raise KeyError(('Conversion from {0} to {1} is not ' + - 'possible or not defined.').format(self.units, units)) - # make the new signal - newSig = Signal(newSig, self.as_dictionary()) - newSig.units = units - - return newSig - - def filter(self, frequency): - """Returns the signal filtered by a low pass Butterworth at the given - frequency.""" - filteredArray = process.butterworth(self.spline(), frequency, self.sampleRate) - return Signal(filteredArray, self.as_dictionary()) - - def frequency(self): - """Returns the frequency content of the signal.""" - return process.freq_spectrum(self.spline(), self.sampleRate) - - def integrate(self, initialCondition=0., detrend=False): - """Integrates the signal using the trapezoidal rule.""" - time = self.time() - # integrate using trapz and adjust with the initial condition - grated = np.hstack((0., cumtrapz(self, x=time))) + initialCondition - # this tries to characterize the drift in the integrated signal. It - # works well for signals from straight line tracking but not - # necessarily for lange change. - if detrend is True: - def line(x, a, b, c): - return a * x**2 + b * x + c - popt, pcov = curve_fit(line, time, grated) - grated = grated - line(time, popt[0], popt[1], popt[2]) - grated = Signal(grated, self.as_dictionary()) - grated.units = self.units + '*second' - grated.name = self.name + 'Int' - return grated - - def plot(self, show=True): - """Plots and returns the signal versus time.""" - time = self.time() - line = plt.plot(time, self) - if show: - plt.xlabel('Time [second]') - plt.ylabel('{0} [{1}]'.format(self.name, self.units)) - plt.title('Signal plot during run {0}'.format(self.runid)) - plt.show() - return line - - def spline(self): - """Returns the signal with nans replaced by the results of a cubic - spline.""" - splined = process.spline_over_nan(self.time(), self) - return Signal(splined, self.as_dictionary()) - - def subtract_mean(self): - """Returns the mean subtracted data.""" - return Signal(process.subtract_mean(self), self.as_dictionary()) - - def time(self): - """Returns the time vector of the signal.""" - return sigpro.time_vector(len(self), self.sampleRate) - - def time_derivative(self): - """Returns the time derivative of the signal.""" - # caluculate the numerical time derivative - dsdt = process.derivative(self.time(), self, method='combination') - # map the metadata from self onto the derivative - dsdt = Signal(dsdt, self.as_dictionary()) - dsdt.name = dsdt.name + 'Dot' - dsdt.units = dsdt.units + '/second' - return dsdt - - def truncate(self, tau): - '''Returns the shifted and truncated signal based on the provided - timeshift, tau.''' - # this is now an ndarray instead of a Signal - return Signal(sigpro.truncate_data(self, tau), self.as_dictionary()) + """ + A subclass of ndarray for collecting the data for a single signal in a run. + + Attributes + ---------- + conversions : dictionary + A mapping for unit conversions. + name : str + The name of the signal. Should be CamelCase. + runid : str + A five digit identification number associated with the + trial this signal was collected from (e.g. '00104'). + sampleRate : float + The sample rate in hertz of the signal. + source : str + The source of the data. This should be 'NI' for the + National Instruments USB-6218 and 'VN' for the VN-100 IMU. + units : str + The physcial units of the signal. These should be specified + as lowercase complete words using only multiplication and + division symbols (e.g. 'meter/second/second'). + Signal.conversions will show the avialable options. + + Methods + ------- + plot() + Plot's the signal versus time and returns the line. + frequency() + Returns the frequency spectrum of the signal. + time_derivative() + Returns the time derivative of the signal. + filter(frequency) + Returns the low passed filter of the signal. + truncate(tau) + Interpolates and truncates the signal the based on the time shift, + `tau`, and the signal source. + as_dictionary + Returns a dictionary of the metadata of the signal. + convert_units(units) + Returns a signal with different units. `conversions` specifies the + available options. + + """ + + # define some basic unit converions + conversions = {'degree->radian': pi / 180., + 'degree/second->radian/second': pi / 180., + 'degree/second/second->radian/second/second': pi / 180., + 'inch*pound->newton*meter': 25.4 / 1000. * 4.44822162, + 'pound->newton': 4.44822162, + 'feet/second->meter/second': 12. * 2.54 / 100., + 'mile/hour->meter/second': 0.00254 * 12. / 5280. / 3600.} + + def __new__(cls, inputArray, metadata): + """ + Returns an instance of the Signal class with the additional signal + data. + + Parameters + ---------- + inputArray : ndarray, shape(n,) + A one dimension array representing a single variable's time + history. + metadata : dictionary + This dictionary contains the metadata for the signal. + name : str + The name of the signal. Should be CamelCase. + runid : str + A five digit identification number associated with the + trial this experiment was collected at (e.g. '00104'). + sampleRate : float + The sample rate in hertz of the signal. + source : str + The source of the data. This should be 'NI' for the + National Instruments USB-6218 and 'VN' for the VN-100 IMU. + units : str + The physcial units of the signal. These should be specified + as lowercase complete words using only multiplication and + division symbols (e.g. 'meter/second/second'). + Signal.conversions will show the avialable options. + + Raises + ------ + ValueError + If `inputArray` is not a vector. + + """ + if len(inputArray.shape) > 1: + raise ValueError('Signals must be arrays of one dimension.') + # cast the input array into the Signal class + obj = np.asarray(inputArray).view(cls) + # add the metadata to the object + obj.name = metadata['name'] + obj.runid = metadata['runid'] + obj.sampleRate = metadata['sampleRate'] + obj.source = metadata['source'] + obj.units = metadata['units'] + return obj + + def __array_finalize__(self, obj): + if obj is None: return + self.name = getattr(obj, 'name', None) + self.runid = getattr(obj, 'runid', None) + self.sampleRate = getattr(obj, 'sampleRate', None) + self.source = getattr(obj, 'source', None) + self.units = getattr(obj, 'units', None) + + def __array_wrap__(self, outputArray, context=None): + # doesn't support these things in basic ufunc calls...maybe one day + # That means anytime you add, subtract, multiply, divide, etc, the + # following are not retained. + outputArray.name = None + outputArray.source = None + outputArray.units = None + return np.ndarray.__array_wrap__(self, outputArray, context) + + def as_dictionary(self): + '''Returns the signal metadata as a dictionary.''' + data = {'runid': self.runid, + 'name': self.name, + 'units': self.units, + 'source': self.source, + 'sampleRate': self.sampleRate} + return data + + def convert_units(self, units): + """ + Returns a signal with the specified units. + + Parameters + ---------- + units : str + The units to convert the signal to. The mapping must be in the + attribute `conversions`. + + Returns + ------- + newSig : Signal + The signal with the desired units. + + """ + if units == self.units: + return self + else: + try: + conversion = self.units + '->' + units + newSig = self * self.conversions[conversion] + except KeyError: + try: + conversion = units + '->' + self.units + newSig = self / self.conversions[conversion] + except KeyError: + raise KeyError(('Conversion from {0} to {1} is not ' + + 'possible or not defined.').format(self.units, units)) + # make the new signal + newSig = Signal(newSig, self.as_dictionary()) + newSig.units = units + + return newSig + + def filter(self, frequency): + """Returns the signal filtered by a low pass Butterworth at the given + frequency.""" + filteredArray = process.butterworth(self.spline(), frequency, self.sampleRate) + return Signal(filteredArray, self.as_dictionary()) + + def frequency(self): + """Returns the frequency content of the signal.""" + return process.freq_spectrum(self.spline(), self.sampleRate) + + def integrate(self, initialCondition=0., detrend=False): + """Integrates the signal using the trapezoidal rule.""" + time = self.time() + # integrate using trapz and adjust with the initial condition + grated = np.hstack((0., cumtrapz(self, x=time))) + initialCondition + # this tries to characterize the drift in the integrated signal. It + # works well for signals from straight line tracking but not + # necessarily for lange change. + if detrend is True: + def line(x, a, b, c): + return a * x**2 + b * x + c + popt, pcov = curve_fit(line, time, grated) + grated = grated - line(time, popt[0], popt[1], popt[2]) + grated = Signal(grated, self.as_dictionary()) + grated.units = self.units + '*second' + grated.name = self.name + 'Int' + return grated + + def plot(self, show=True): + """Plots and returns the signal versus time.""" + time = self.time() + line = plt.plot(time, self) + if show: + plt.xlabel('Time [second]') + plt.ylabel('{0} [{1}]'.format(self.name, self.units)) + plt.title('Signal plot during run {0}'.format(self.runid)) + plt.show() + return line + + def spline(self): + """Returns the signal with nans replaced by the results of a cubic + spline.""" + splined = process.spline_over_nan(self.time(), self) + return Signal(splined, self.as_dictionary()) + + def subtract_mean(self): + """Returns the mean subtracted data.""" + return Signal(process.subtract_mean(self), self.as_dictionary()) + + def time(self): + """Returns the time vector of the signal.""" + return sigpro.time_vector(len(self), self.sampleRate) + + def time_derivative(self): + """Returns the time derivative of the signal.""" + # caluculate the numerical time derivative + dsdt = process.derivative(self.time(), self, method='combination') + # map the metadata from self onto the derivative + dsdt = Signal(dsdt, self.as_dictionary()) + dsdt.name = dsdt.name + 'Dot' + dsdt.units = dsdt.units + '/second' + return dsdt + + def truncate(self, tau): + '''Returns the shifted and truncated signal based on the provided + timeshift, tau.''' + # this is now an ndarray instead of a Signal + return Signal(sigpro.truncate_data(self, tau), self.as_dictionary()) class RawSignal(Signal): - """ - A subclass of Signal for collecting the data for a single raw signal in - a run. - - Attributes - ---------- - sensor : Sensor - Each raw signal has a sensor associated with it. Most sensors contain - calibration data for that sensor/signal. - calibrationType : - - Notes - ----- - This is a class for the signals that are the raw measurement outputs - collected by the BicycleDAQ software and are already stored in the pytables - database file. - - """ - - def __new__(cls, runid, signalName, database): - """ - Returns an instance of the RawSignal class with the additional signal - metadata. - - Parameters - ---------- - runid : str - A five digit - signalName : str - A CamelCase signal name that corresponds to the raw signals output - by BicycleDAQ_. - database : pytables object - The hdf5 database for the instrumented bicycle. - - .. _BicycleDAQ: https://github.com/moorepants/BicycleDAQ - - """ - - # get the tables - rTab = database.root.runTable - sTab = database.root.signalTable - cTab = database.root.calibrationTable - - # get the row number for this particular run id - rownum = get_row_num(runid, rTab) - signal = database.getNode('/rawData/' + runid, name=signalName).read() - - # cast the input array into my subclass of ndarray - obj = np.asarray(signal).view(cls) - - obj.runid = runid - obj.timeStamp = matlab_date_to_object(get_cell(rTab, 'DateTime', - rownum)) - obj.calibrationType, obj.units, obj.source = [(row['calibration'], - row['units'], row['source']) - for row in sTab.where('signal == signalName')][0] - obj.name = signalName - - try: - obj.sensor = Sensor(obj.name, cTab) - except KeyError: - pass - # This just means that there was no sensor associated with that - # signal for calibration purposes. - #print "There is no sensor named {0}.".format(signalName) - - # this assumes that the supply voltage for this signal is the same for - # all sensor calibrations - try: - supplySource = [row['runSupplyVoltageSource'] - for row in cTab.where('name == signalName')][0] - if supplySource == 'na': - obj.supply = [row['runSupplyVoltage'] - for row in cTab.where('name == signalName')][0] - else: - obj.supply = database.getNode('/rawData/' + runid, - name=supplySource).read() - except IndexError: - pass - #print "{0} does not have a supply voltage.".format(signalName) - #print "-" * 79 - - # get the appropriate sample rate - if obj.source == 'NI': - sampRateCol = 'NISampleRate' - elif obj.source == 'VN': - sampRateCol = 'VNavSampleRate' - else: - raise ValueError('{0} is not a valid source.'.format(obj.source)) - - obj.sampleRate = rTab[rownum][rTab.colnames.index(sampRateCol)] - - return obj - - def __array_finalize__(self, obj): - if obj is None: return - self.calibrationType = getattr(obj, 'calibrationType', None) - self.name = getattr(obj, 'name', None) - self.runid = getattr(obj, 'runid', None) - self.sampleRate = getattr(obj, 'sampleRate', None) - self.sensor = getattr(obj, 'sensor', None) - self.source = getattr(obj, 'source', None) - self.units = getattr(obj, 'units', None) - self.timeStamp = getattr(obj, 'timeStamp', None) - - def __array_wrap__(self, outputArray, context=None): - # doesn't support these things in basic ufunc calls...maybe one day - outputArray.calibrationType = None - outputArray.name = None - outputArray.sensor = None - outputArray.source = None - outputArray.units = None - return np.ndarray.__array_wrap__(self, outputArray, context) - - def scale(self): - """ - Returns the scaled signal based on the calibration data for the - supplied date. - - Returns - ------- - : ndarray (n,) - Scaled signal. - - """ - try: - self.calibrationType - except AttributeError: - raise AttributeError("Can't scale without the calibration type") - - # these will need to be changed once we start measuring them - doNotScale = ['LeanPotentiometer', - 'HipPotentiometer', - 'TwistPotentiometer'] - if self.calibrationType in ['none', 'matrix'] or self.name in doNotScale: - #print "Not scaling {0}".format(self.name) - return self - else: - pass - #print "Scaling {0}".format(self.name) - - # pick the largest calibration date without surpassing the run date - calibData = self.sensor.get_data_for_date(self.timeStamp) - - slope = calibData['slope'] - bias = calibData['bias'] - intercept = calibData['offset'] - calibrationSupplyVoltage = calibData['calibrationSupplyVoltage'] - - #print "slope {0}, bias {1}, intercept {2}".format(slope, bias, - #intercept) - - if self.calibrationType == 'interceptStar': - # this is for potentiometers, where the slope is ratiometric - # and zero degrees is always zero volts - calibratedSignal = (calibrationSupplyVoltage / self.supply * - slope * self + intercept) - elif self.calibrationType == 'intercept': - # this is the typical calibration that I use for all the - # sensors that I calibrate myself - calibratedSignal = (calibrationSupplyVoltage / self.supply * - (slope * self + intercept)) - elif self.calibrationType == 'bias': - # this is for the accelerometers and rate gyros that are - # "ratiometric", but I'm still not sure this is correct - calibratedSignal = (slope * (self - self.supply / - calibrationSupplyVoltage * bias)) - else: - raise StandardError("None of the calibration equations worked.") - calibratedSignal.name = calibData['signal'] - calibratedSignal.units = calibData['units'] - calibratedSignal.source = self.source - - return calibratedSignal.view(Signal) - - def plot_scaled(self, show=True): - '''Plots and returns the scaled signal versus time.''' - time = self.time() - scaled = self.scale() - line = plt.plot(time, scaled[1]) - plt.xlabel('Time [s]') - plt.ylabel(scaled[2]) - plt.title('{0} signal during run {1}'.format(scaled[0], - str(self.runid))) - if show: - plt.show() - return line + """ + A subclass of Signal for collecting the data for a single raw signal in + a run. + + Attributes + ---------- + sensor : Sensor + Each raw signal has a sensor associated with it. Most sensors contain + calibration data for that sensor/signal. + calibrationType : + + Notes + ----- + This is a class for the signals that are the raw measurement outputs + collected by the BicycleDAQ software and are already stored in the pytables + database file. + + """ + + def __new__(cls, runid, signalName, database): + """ + Returns an instance of the RawSignal class with the additional signal + metadata. + + Parameters + ---------- + runid : str + A five digit + signalName : str + A CamelCase signal name that corresponds to the raw signals output + by BicycleDAQ_. + database : pytables object + The hdf5 database for the instrumented bicycle. + + .. _BicycleDAQ: https://github.com/moorepants/BicycleDAQ + + """ + + # get the tables + rTab = database.root.runTable + sTab = database.root.signalTable + cTab = database.root.calibrationTable + + # get the row number for this particular run id + rownum = get_row_num(runid, rTab) + signal = database.getNode('/rawData/' + runid, name=signalName).read() + + # cast the input array into my subclass of ndarray + obj = np.asarray(signal).view(cls) + + obj.runid = runid + obj.timeStamp = matlab_date_to_object(get_cell(rTab, 'DateTime', + rownum)) + obj.calibrationType, obj.units, obj.source = [(row['calibration'], + row['units'], row['source']) + for row in sTab.where('signal == signalName')][0] + obj.name = signalName + + try: + obj.sensor = Sensor(obj.name, cTab) + except KeyError: + pass + # This just means that there was no sensor associated with that + # signal for calibration purposes. + #print "There is no sensor named {0}.".format(signalName) + + # this assumes that the supply voltage for this signal is the same for + # all sensor calibrations + try: + supplySource = [row['runSupplyVoltageSource'] + for row in cTab.where('name == signalName')][0] + if supplySource == 'na': + obj.supply = [row['runSupplyVoltage'] + for row in cTab.where('name == signalName')][0] + else: + obj.supply = database.getNode('/rawData/' + runid, + name=supplySource).read() + except IndexError: + pass + #print "{0} does not have a supply voltage.".format(signalName) + #print "-" * 79 + + # get the appropriate sample rate + if obj.source == 'NI': + sampRateCol = 'NISampleRate' + elif obj.source == 'VN': + sampRateCol = 'VNavSampleRate' + else: + raise ValueError('{0} is not a valid source.'.format(obj.source)) + + obj.sampleRate = rTab[rownum][rTab.colnames.index(sampRateCol)] + + return obj + + def __array_finalize__(self, obj): + if obj is None: return + self.calibrationType = getattr(obj, 'calibrationType', None) + self.name = getattr(obj, 'name', None) + self.runid = getattr(obj, 'runid', None) + self.sampleRate = getattr(obj, 'sampleRate', None) + self.sensor = getattr(obj, 'sensor', None) + self.source = getattr(obj, 'source', None) + self.units = getattr(obj, 'units', None) + self.timeStamp = getattr(obj, 'timeStamp', None) + + def __array_wrap__(self, outputArray, context=None): + # doesn't support these things in basic ufunc calls...maybe one day + outputArray.calibrationType = None + outputArray.name = None + outputArray.sensor = None + outputArray.source = None + outputArray.units = None + return np.ndarray.__array_wrap__(self, outputArray, context) + + def scale(self): + """ + Returns the scaled signal based on the calibration data for the + supplied date. + + Returns + ------- + : ndarray (n,) + Scaled signal. + + """ + try: + self.calibrationType + except AttributeError: + raise AttributeError("Can't scale without the calibration type") + + # these will need to be changed once we start measuring them + doNotScale = ['LeanPotentiometer', + 'HipPotentiometer', + 'TwistPotentiometer'] + if self.calibrationType in ['none', 'matrix'] or self.name in doNotScale: + #print "Not scaling {0}".format(self.name) + return self + else: + pass + #print "Scaling {0}".format(self.name) + + # pick the largest calibration date without surpassing the run date + calibData = self.sensor.get_data_for_date(self.timeStamp) + + slope = calibData['slope'] + bias = calibData['bias'] + intercept = calibData['offset'] + calibrationSupplyVoltage = calibData['calibrationSupplyVoltage'] + + #print "slope {0}, bias {1}, intercept {2}".format(slope, bias, + #intercept) + + if self.calibrationType == 'interceptStar': + # this is for potentiometers, where the slope is ratiometric + # and zero degrees is always zero volts + calibratedSignal = (calibrationSupplyVoltage / self.supply * + slope * self + intercept) + elif self.calibrationType == 'intercept': + # this is the typical calibration that I use for all the + # sensors that I calibrate myself + calibratedSignal = (calibrationSupplyVoltage / self.supply * + (slope * self + intercept)) + elif self.calibrationType == 'bias': + # this is for the accelerometers and rate gyros that are + # "ratiometric", but I'm still not sure this is correct + calibratedSignal = (slope * (self - self.supply / + calibrationSupplyVoltage * bias)) + else: + raise StandardError("None of the calibration equations worked.") + calibratedSignal.name = calibData['signal'] + calibratedSignal.units = calibData['units'] + calibratedSignal.source = self.source + + return calibratedSignal.view(Signal) + + def plot_scaled(self, show=True): + '''Plots and returns the scaled signal versus time.''' + time = self.time() + scaled = self.scale() + line = plt.plot(time, scaled[1]) + plt.xlabel('Time [s]') + plt.ylabel(scaled[2]) + plt.title('{0} signal during run {1}'.format(scaled[0], + str(self.runid))) + if show: + plt.show() + return line class Sensor(): - """This class is a container for calibration data for a sensor.""" - - def __init__(self, name, calibrationTable): - """ - Initializes this sensor class. - - Parameters - ---------- - name : string - The CamelCase name of the sensor (e.g. SteerTorqueSensor). - calibrationTable : pyTables table object - This is the calibration data table that contains all the data taken - during calibrations. - - """ - self.name = name - self._store_calibration_data(calibrationTable) - - def _store_calibration_data(self, calibrationTable): - """ - Stores a dictionary of calibration data for the sensor for all - calibration dates in the object. - - Parameters - ---------- - calibrationTable : pyTables table object - This is the calibration data table that contains all the data taken - during calibrations. - - """ - self.data = {} - - for row in calibrationTable.iterrows(): - if self.name == row['name']: - self.data[row['calibrationID']] = {} - for col in calibrationTable.colnames: - self.data[row['calibrationID']][col] = row[col] - - if self.data == {}: - raise KeyError(('{0} is not a valid sensor ' + - 'name').format(self.name)) - - def get_data_for_date(self, runDate): - """ - Returns the calibration data for the sensor for the most recent - calibration relative to `runDate`. - - Parameters - ---------- - runDate : datetime object - This is the date of the run that the calibration data is needed - for. - - Returns - ------- - calibData : dictionary - A dictionary containing the sensor calibration data for the - calibration closest to but not past `runDate`. - - Notes - ----- - This method will select the calibration data for the date closest to - but not past `runDate`. **All calibrations must be taken before the - runs.** - - """ - # make a list of calibration ids and time stamps - dateIdPairs = [(k, matlab_date_to_object(v['timeStamp'])) - for k, v in self.data.iteritems()] - # sort the pairs with the most recent date first - dateIdPairs.sort(key=lambda x: x[1], reverse=True) - # go through the list and return the index at which the calibration - # date is larger than the run date - for i, pair in enumerate(dateIdPairs): - if runDate >= pair[1]: - break - return self.data[dateIdPairs[i][0]] + """This class is a container for calibration data for a sensor.""" + + def __init__(self, name, calibrationTable): + """ + Initializes this sensor class. + + Parameters + ---------- + name : string + The CamelCase name of the sensor (e.g. SteerTorqueSensor). + calibrationTable : pyTables table object + This is the calibration data table that contains all the data taken + during calibrations. + + """ + self.name = name + self._store_calibration_data(calibrationTable) + + def _store_calibration_data(self, calibrationTable): + """ + Stores a dictionary of calibration data for the sensor for all + calibration dates in the object. + + Parameters + ---------- + calibrationTable : pyTables table object + This is the calibration data table that contains all the data taken + during calibrations. + + """ + self.data = {} + + for row in calibrationTable.iterrows(): + if self.name == row['name']: + self.data[row['calibrationID']] = {} + for col in calibrationTable.colnames: + self.data[row['calibrationID']][col] = row[col] + + if self.data == {}: + raise KeyError(('{0} is not a valid sensor ' + + 'name').format(self.name)) + + def get_data_for_date(self, runDate): + """ + Returns the calibration data for the sensor for the most recent + calibration relative to `runDate`. + + Parameters + ---------- + runDate : datetime object + This is the date of the run that the calibration data is needed + for. + + Returns + ------- + calibData : dictionary + A dictionary containing the sensor calibration data for the + calibration closest to but not past `runDate`. + + Notes + ----- + This method will select the calibration data for the date closest to + but not past `runDate`. **All calibrations must be taken before the + runs.** + + """ + # make a list of calibration ids and time stamps + dateIdPairs = [(k, matlab_date_to_object(v['timeStamp'])) + for k, v in self.data.iteritems()] + # sort the pairs with the most recent date first + dateIdPairs.sort(key=lambda x: x[1], reverse=True) + # go through the list and return the index at which the calibration + # date is larger than the run date + for i, pair in enumerate(dateIdPairs): + if runDate >= pair[1]: + break + return self.data[dateIdPairs[i][0]] class Run(): - """The fluppin fundamental class for a run.""" - - def __init__(self, runid, dataset, pathToParameterData=None, - forceRecalc=False, filterFreq=None, store=True): - """Loads the raw and processed data for a run if available otherwise it - generates the processed data from the raw data. - - Parameters - ---------- - runid : int or str - The run id should be an integer, e.g. 5, or a five digit string with - leading zeros, e.g. '00005'. - dataset : DataSet - A DataSet object with at least some raw data. - pathToParameterData : string, {'', None}, optional - The path to a data directory for the BicycleParameters package. It - should contain the bicycles and riders used in the experiments. - forceRecalc : boolean, optional, default = False - If true then it will force a recalculation of all the processed - data. - filterSigs : float, optional, default = None - If true all of the processed signals will be low pass filtered with - a second order Butterworth filter at the given filter frequency. - store : boolean, optional, default = True - If true the resulting task signals will be stored in the database. - - """ - - if pathToParameterData is None: - pathToParameterData = config.get('data', 'pathToParameters') - - print "Initializing the run object." - - self.filterFreq = filterFreq - - dataset.open() - dataTable = dataset.database.root.runTable - signalTable = dataset.database.root.signalTable - taskTable = dataset.database.root.taskTable - - runid = run_id_string(runid) - - # get the row number for this particular run id - rownum = get_row_num(runid, dataTable) - - # make some dictionaries to store all the data - self.metadata = {} - self.rawSignals = {} - - # make lists of the input and output signals - rawDataCols = [x['signal'] for x in - signalTable.where("isRaw == True")] - computedCols = [x['signal'] for x in - signalTable.where("isRaw == False")] - - # store the metadata for this run - print "Loading metadata from the database." - for col in dataTable.colnames: - if col not in (rawDataCols + computedCols): - self.metadata[col] = get_cell(dataTable, col, rownum) - - print "Loading the raw signals from the database." - for col in rawDataCols: - # rawDataCols includes all possible raw signals, but every run - # doesn't have all the signals, so skip the ones that aren't there - try: - self.rawSignals[col] = RawSignal(runid, col, dataset.database) - except NoSuchNodeError: - pass - - if self.metadata['Rider'] != 'None': - self.load_rider(pathToParameterData) - - self.bumpLength = 1.0 # 1 meter - - # Try to load the task signals if they've already been computed. If - # they aren't in the database, the filter frequencies don't match or - # forceRecalc is true the then compute them. This may save some time - # when repeatedly loading runs for analysis. - self.taskFromDatabase = False - try: - runGroup = dataset.database.root.taskData._f_getChild(runid) - except NoSuchNodeError: - forceRecalc = True - else: - # The filter frequency stored in the task table is either a nan - # value or a valid float. If the stored filter frequency is not the - # same as the the one passed to Run, then a recalculation should be - # forced. - taskRowNum = get_row_num(runid, taskTable) - storedFreq = taskTable.cols.FilterFrequency[taskRowNum] - self.taskSignals = {} - if filterFreq is None: - newFilterFreq = np.nan - else: - newFilterFreq = filterFreq - - if np.isnan(newFilterFreq) and np.isnan(storedFreq): - for node in runGroup._f_walkNodes(): - meta = {k : node._f_getAttr(k) for k in ['units', 'name', - 'runid', 'sampleRate', 'source']} - self.taskSignals[node.name] = Signal(node[:], meta) - self.taskFromDatabase = True - elif np.isnan(newFilterFreq) or np.isnan(storedFreq): - forceRecalc = True - else: - if abs(storedFreq - filterFreq) < 1e-10: - for node in runGroup._f_walkNodes(): - meta = {k : node._f_getAttr(k) for k in ['units', 'name', - 'runid', 'sampleRate', 'source']} - self.taskSignals[node.name] = Signal(node[:], meta) - self.taskFromDatabase = True - else: - forceRecalc = True - - dataset.close() - - if forceRecalc == True: - try: - del self.taskSignals - except AttributeError: - pass - self.process_raw_signals() - - # store the task signals in the database if they are newly computed - if (store == True and self.taskFromDatabase == False - and self.topSig == 'task'): - taskMeta = { - 'Duration' : - self.taskSignals['ForwardSpeed'].time()[-1], - 'FilterFrequency' : self.filterFreq, - 'MeanSpeed' : self.taskSignals['ForwardSpeed'].mean(), - 'RunID' : self.metadata['RunID'], - 'StdSpeed' : self.taskSignals['ForwardSpeed'].std(), - 'Tau' : self.tau, - } - dataset.add_task_signals(self.taskSignals, taskMeta) - - # tell the user about the run - print self - - def process_raw_signals(self): - """Processes the raw signals as far as possible and filters the - result if a cutoff frequency was specified.""" - - print "Computing signals from raw data." - self.calibrate_signals() - - # the following maneuvers should never be calculated beyond the - # calibrated signals - maneuver = self.metadata['Maneuver'] - con1 = maneuver != 'Steer Dynamics Test' - con2 = maneuver != 'System Test' - con3 = maneuver != 'Static Calibration' - if con1 and con2 and con3: - self.compute_time_shift() - self.check_time_shift(0.15) - self.truncate_signals() - self.compute_signals() - self.task_signals() - - if self.filterFreq is not None: - self.filter_top_signals(self.filterFreq) - - def filter_top_signals(self, filterFreq): - """Filters the top most signals with a low pass filter.""" - - if self.topSig == 'task': - print('Filtering the task signals.') - for k, v in self.taskSignals.items(): - self.taskSignals[k] = v.filter(filterFreq) - elif self.topSig == 'computed': - print('Filtering the computed signals.') - for k, v in self.computedSignals.items(): - self.computedSignals[k] = v.filter(filterFreq) - elif self.topSig == 'calibrated': - print('Filtering the calibrated signals.') - for k, v in self.calibratedSignals.items(): - self.calibratedSignals[k] = v.filter(filterFreq) - - def calibrate_signals(self): - """Calibrates the raw signals.""" - - # calibrate the signals for the run - self.calibratedSignals = {} - for sig in self.rawSignals.values(): - calibSig = sig.scale() - self.calibratedSignals[calibSig.name] = calibSig - - self.topSig = 'calibrated' - - def task_signals(self): - """Computes the task signals.""" - print('Extracting the task portion from the data.') - self.extract_task() - - # compute task specific variables* - self.compute_yaw_angle() - self.compute_rear_wheel_contact_rates() - self.compute_rear_wheel_contact_points() - self.compute_front_wheel_contact_points() - self.compute_front_wheel_yaw_angle() - self.compute_front_wheel_rate() - self.compute_front_rear_wheel_contact_forces() - self.compute_contact_points_acceleration() - - self.topSig = 'task' - - def compute_signals(self): - """Computes the task independent quantities.""" - - self.computedSignals ={} - # transfer some of the signals to computed - noChange = ['FiveVolts', - 'PushButton', - 'RearWheelRate', - 'RollAngle', - 'SteerAngle', - 'ThreeVolts'] - for sig in noChange: - if sig in ['RollAngle', 'SteerAngle']: - self.computedSignals[sig] =\ - self.truncatedSignals[sig].convert_units('radian') - else: - self.computedSignals[sig] = self.truncatedSignals[sig] - - # compute the quantities that aren't task specific - self.compute_pull_force() - self.compute_forward_speed() - self.compute_steer_rate() - self.compute_yaw_roll_pitch_rates() - self.compute_steer_torque() - self.compute_frame_acceleration() - - def truncate_signals(self): - """Truncates the calibrated signals based on the time shift.""" - - self.truncatedSignals = {} - for name, sig in self.calibratedSignals.items(): - self.truncatedSignals[name] = sig.truncate(self.tau).spline() - self.topSig = 'truncated' - - def compute_time_shift(self): - """Computes the time shift based on the vertical accelerometer - signals.""" - - self.tau = sigpro.find_timeshift( - self.calibratedSignals['AccelerometerAccelerationY'], - self.calibratedSignals['AccelerationZ'], - self.metadata['NISampleRate'], - self.metadata['Speed'], plotError=False) - - def check_time_shift(self, maxNRMS): - """Raises an error if the normalized root mean square of the shifted - accelerometer signals is high.""" - - # Check to make sure the signals were actually good fits by - # calculating the normalized root mean square. If it isn't very - # low, raise an error. - niAcc = self.calibratedSignals['AccelerometerAccelerationY'] - vnAcc = self.calibratedSignals['AccelerationZ'] - vnAcc = vnAcc.truncate(self.tau).spline() - niAcc = niAcc.truncate(self.tau).spline() - # todo: this should probably check the rms of the mean subtracted data - # because both accelerometers don't always give the same value, this - # may work better with a filtered signal too - # todo: this should probably be moved into the time shift code in the - # signalprocessing model - nrms = np.sqrt(np.mean((vnAcc + niAcc)**2)) / (niAcc.max() - niAcc.min()) - if nrms > maxNRMS: - raise TimeShiftError(('The normalized root mean square for this ' + - 'time shift is {}, which is greater '.format(str(nrms)) + - 'than the maximum allowed: {}'.format(str(maxNRMS)))) - - def compute_rear_wheel_contact_points(self): - """Computes the location of the wheel contact points in the ground - plane.""" - - # get the rates - try: - latRate = self.taskSignals['LateralRearContactRate'] - lonRate = self.taskSignals['LongitudinalRearContactRate'] - except AttributeError: - print('At least one of the rates are not available. ' + - 'The YawAngle was not computed.') - else: - # convert to meters per second - latRate = latRate.convert_units('meter/second') - lonRate = lonRate.convert_units('meter/second') - # integrate and try to account for the drift - lat = latRate.integrate(detrend=True) - lon = lonRate.integrate() - # set the new name and units - lat.name = 'LateralRearContact' - lat.units = 'meter' - lon.name = 'LongitudinalRearContact' - lon.units = 'meter' - # store in task signals - self.taskSignals[lat.name] = lat - self.taskSignals[lon.name] = lon - - def compute_front_wheel_contact_points(self): - """Caluculates the front wheel contact points in the ground plane.""" - - q1 = self.taskSignals['LongitudinalRearContact'] - q2 = self.taskSignals['LateralRearContact'] - q3 = self.taskSignals['YawAngle'] - q4 = self.taskSignals['RollAngle'] - q7 = self.taskSignals['SteerAngle'] - - p = benchmark_to_moore(self.bicycleRiderParameters) - - f = np.vectorize(front_contact) - q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], - p['rf']) - - q9.name = 'LongitudinalFrontContact' - q9.units = 'meter' - q10.name = 'LateralFrontContact' - q10.units = 'meter' - - self.taskSignals['LongitudinalFrontContact'] = q9 - self.taskSignals['LateralFrontContact'] = q10 - - def compute_front_wheel_yaw_angle(self): - """Calculates the yaw angle of the front wheel.""" - - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(bp) - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - f = np.vectorize(front_wheel_yaw_angle) - q1_front_wheel = f(q1, q2, q4, mp['d1'], mp['d2'], mp['d3'], - bp['lam'], mp['rr'], mp['rf']) - - q1_front_wheel.name = 'FrontWheelYawAngle' - q1_front_wheel.units = 'meter' - self.taskSignals['FrontWheelYawAngle'] = q1_front_wheel - - def compute_front_wheel_rate(self): - """Calculates the front wheel rate, based on the Jason's data - of front_wheel_contact_point. Alternatively, you can use the - sympy to get the front_wheel_contact_rate directly first.""" - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(bp) - - f = np.vectorize(front_wheel_rate) - - u6 = f(q1, q2, q4, u9, u10, mp['d1'], mp['d2'], mp['d3'], - bp['lam'], mp['rr'], mp['rf']) - - u6.name = 'FrontWheelRate' - u6.units = 'radian/second' - self.taskSignals['FrontWheelRate'] = u6 - - - def compute_front_rear_wheel_contact_forces(self): - """Calculate the contact forces for each - wheel with respect to inertial frame under - the slip and nonslip condition. Also, provide - the steer torque T4.""" - - #parameters - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(self.bicycleRiderParameters) - - l1 = mp['l1'] - l2 = mp['l2'] - mc = mp['mc'] - ic11 = mp['ic11'] - ic22 = mp['ic22'] - ic33 = mp['ic33'] - ic31 = mp['ic31'] - rf = mp['rf'] - - #signals assignment --slip condition - V = self.taskSignals['ForwardSpeed'].mean() - - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - u2 = self.taskSignals['RollRate'] - u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] - u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] - u7 = self.taskSignals['LongitudinalRearContactRate'] - u8 = self.taskSignals['LateralRearContactRate'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - u2d = u2.time_derivative() - u3d = u3.time_derivative() - u4d = u4.time_derivative() - u5d = u5.time_derivative() - u6d = u6.time_derivative() - u7d = self.taskSignals['RearWheelContactPointLongitudinalAcceleration'] - u8d = self.taskSignals['RearWheelContactPointLateralAcceleration'] - u9d = self.taskSignals['FrontWheelContactPointLongitudinalAcceleration'] - u10d = self.taskSignals['FrontWheelContactPointLateralAcceleration'] - - f_1 = np.vectorize(steer_torque_slip) - f_2 = np.vectorize(contact_forces_slip) - f_3 = np.vectorize(contact_forces_nonslip) - - #calculation - #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, - u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, - ic11, ic22, ic33, ic31, q1, q2, q4, - u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, - u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, - u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - - #attributes: name and units, and taskSignals - T4_slip.name = 'SteerTorque_Slip' - T4_slip.units = 'newton*meter' - self.taskSignals[T4_slip.name] = T4_slip - - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' - Fx_r_s.units = 'newton' - self.taskSignals[Fx_r_s.name] = Fx_r_s - - Fy_r_s.name = 'LateralRearContactForce_Slip' - Fy_r_s.units = 'newton' - self.taskSignals[Fy_r_s.name] = Fy_r_s - - Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' - Fx_f_s.units = 'newton' - self.taskSignals[Fx_f_s.name] = Fx_f_s - - Fy_f_s.name = 'LateralFrontContactForce_Slip' - Fy_f_s.units = 'newton' - self.taskSignals[Fy_f_s.name] = Fy_f_s - - Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' - Fx_r_ns.units = 'newton' - self.taskSignals[Fx_r_ns.name] = Fx_r_ns - - Fy_r_ns.name = 'LateralRearContactForce_Nonslip' - Fy_r_ns.units = 'newton' - self.taskSignals[Fy_r_ns.name] = Fy_r_ns - - Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' - Fx_f_ns.units = 'newton' - self.taskSignals[Fx_f_ns.name] = Fx_f_ns - - Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' - Fy_f_ns.units = 'newton' - self.taskSignals[Fy_f_ns.name] = Fy_f_ns - - def compute_contact_points_acceleration(self): - """Calculates the acceleration of the contact points of front and rear - wheels.""" - - AccX = self.taskSignals['AccelerationX'] - AccY = self.taskSignals['AccelerationY'] - AccZ = self.taskSignals['AccelerationZ'] - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - u1 = self.taskSignals['YawRate'] - u2 = self.taskSignals['RollRate'] - u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] - u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] - - u1d = u1.time_derivative() - u2d = u2.time_derivative() - u3d = u3.time_derivative() - u4d = u4.time_derivative() - u5d = u5.time_derivative() - u6d = u6.time_derivative() - - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(self.bicycleRiderParameters) - - d1 = mp['d1'] - d2 = mp['d2'] - d3 = mp['d3'] - rr = mp['rr'] - rf = mp['rf'] - ds1 = self.bicycle.parameters['Measured']['ds1'] - ds3 = self.bicycle.parameters['Measured']['ds3'] - s3 = ds3 - s1 = ds1 + mp['l4'] - - f = np.vectorize(contact_points_acceleration) - - u7d, u8d, u11d, u9d, u10d, u12d = f(AccX, AccY, AccZ, q1, q2, q3, - u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d, - d1, d2, d3, rr, rf, s1, s3) - - u7d.name = 'RearWheelContactPointLongitudinalAcceleration' - u7d.units = 'meter/second/second' - u8d.name = 'RearWheelContactPointLateralAcceleration' - u8d.units = 'meter/second/second' - u11d.name = 'RearWheelContactPointDownwardAcceleration' - u11d.units = 'meter/second/second' - u9d.name = 'FrontWheelContactPointLongitudinalAcceleration' - u9d.units = 'meter/second/second' - u10d.name = 'FrontWheelContactPointLateralAcceleration' - u10d.units = 'meter/second/second' - u12d.name = 'FrontWheelContactPointDownwardAcceleration' - u12.units = 'meter/second/second' - - self.taskSignals(u7d.name) = u7d - self.taskSignals(u8d.name) = u8d - self.taskSignals(u11d.name) = u11d - self.taskSignals(u9d.name) = u9d - self.taskSignals(u10d.name) = u10d - self.taskSignals(u12d.name) = u12d - - - def compute_rear_wheel_contact_rates(self): - """Calculates the rates of the wheel contact points in the ground - plane.""" - - try: - yawAngle = self.taskSignals['YawAngle'] - rearWheelRate = self.taskSignals['RearWheelRate'] - rR = self.bicycleRiderParameters['rR'] # this should be in meters - except AttributeError: - print('Either the yaw angle, rear wheel rate or ' + - 'front wheel radius is not available. The ' + - 'contact rates were not computed.') - else: - yawAngle = yawAngle.convert_units('radian') - rearWheelRate = rearWheelRate.convert_units('radian/second') - - lon, lat = sigpro.rear_wheel_contact_rate(rR, rearWheelRate, yawAngle) - - lon.name = 'LongitudinalRearContactRate' - lon.units = 'meter/second' - self.taskSignals[lon.name] = lon - - lat.name = 'LateralRearContactRate' - lat.units = 'meter/second' - self.taskSignals[lat.name] = lat - - def compute_yaw_angle(self): - """Computes the yaw angle by integrating the yaw rate.""" - - # get the yaw rate - try: - yawRate = self.taskSignals['YawRate'] - except AttributeError: - print('YawRate is not available. The YawAngle was not computed.') - else: - # convert to radians per second - yawRate = yawRate.convert_units('radian/second') - # integrate and try to account for the drift - yawAngle = yawRate.integrate(detrend=True) - # set the new name and units - yawAngle.name = 'YawAngle' - yawAngle.units = 'radian' - # store in computed signals - self.taskSignals['YawAngle'] = yawAngle - - def compute_steer_torque(self, plot=False): - """Computes the rider applied steer torque. - - Parameters - ---------- - plot : boolean, optional - Default is False, but if True a plot is generated. - - """ - # steer torque - frameAngRate = np.vstack(( - self.truncatedSignals['AngularRateX'], - self.truncatedSignals['AngularRateY'], - self.truncatedSignals['AngularRateZ'])) - frameAngAccel = np.vstack(( - self.truncatedSignals['AngularRateX'].time_derivative(), - self.truncatedSignals['AngularRateY'].time_derivative(), - self.truncatedSignals['AngularRateZ'].time_derivative())) - frameAccel = np.vstack(( - self.truncatedSignals['AccelerationX'], - self.truncatedSignals['AccelerationY'], - self.truncatedSignals['AccelerationZ'])) - handlebarAngRate = self.truncatedSignals['ForkRate'] - handlebarAngAccel = self.truncatedSignals['ForkRate'].time_derivative() - steerAngle = self.truncatedSignals['SteerAngle'] - steerColumnTorque =\ - self.truncatedSignals['SteerTubeTorque'].convert_units('newton*meter') - handlebarMass = self.bicycleRiderParameters['mG'] - handlebarInertia =\ - self.bicycle.steer_assembly_moment_of_inertia(fork=False, - wheel=False, nominal=True) - # this is the distance from the handlebar center of mass to the - # steer axis - w = self.bicycleRiderParameters['w'] - c = self.bicycleRiderParameters['c'] - lam = self.bicycleRiderParameters['lam'] - xG = self.bicycleRiderParameters['xG'] - zG = self.bicycleRiderParameters['zG'] - handlebarCoM = np.array([xG, 0., zG]) - d = bp.geometry.distance_to_steer_axis(w, c, lam, handlebarCoM) - # these are the distances from the point on the steer axis which is - # aligned with the handlebar center of mass to the accelerometer on - # the frame - ds1 = self.bicycle.parameters['Measured']['ds1'] - ds3 = self.bicycle.parameters['Measured']['ds3'] - ds = np.array([ds1, 0., ds3]) # i measured these - # damping and friction values come from Peter's work, I need to verify - # them still - damping = 0.3475 - friction = 0.0861 - - components = sigpro.steer_torque_components( - frameAngRate, frameAngAccel, frameAccel, handlebarAngRate, - handlebarAngAccel, steerAngle, steerColumnTorque, - handlebarMass, handlebarInertia, damping, friction, d, ds) - steerTorque = sigpro.steer_torque(components) - - stDict = {'units':'newton*meter', - 'name':'SteerTorque', - 'runid':self.metadata['RunID'], - 'sampleRate':steerAngle.sampleRate, - 'source':'NA'} - self.computedSignals['SteerTorque'] = Signal(steerTorque, stDict) - - if plot is True: - - time = steerAngle.time() - - hdot = (components['Hdot1'] + components['Hdot2'] + - components['Hdot3'] + components['Hdot4']) - cross = (components['cross1'] + components['cross2'] + - components['cross3']) - - fig = plt.figure() - - frictionAx = fig.add_subplot(4, 1, 1) - frictionAx.plot(time, components['viscous'], - time, components['coulomb'], - time, components['viscous'] + components['coulomb']) - frictionAx.set_ylabel('Torque [N-m]') - frictionAx.legend(('Viscous Friction', 'Coulomb Friction', - 'Total Friction')) - - dynamicAx = fig.add_subplot(4, 1, 2) - dynamicAx.plot(time, hdot, time, cross, time, hdot + cross) - dynamicAx.set_ylabel('Torque [N-m]') - dynamicAx.legend((r'Torque due to $\dot{H}$', - r'Torque due to $r \times m a$', - r'Total Dynamic Torque')) - - additionalAx = fig.add_subplot(4, 1, 3) - additionalAx.plot(time, hdot + cross + components['viscous'] + - components['coulomb'], - label='Total Frictional and Dynamic Torque') - additionalAx.set_ylabel('Torque [N-m]') - additionalAx.legend() - - torqueAx = fig.add_subplot(4, 1, 4) - torqueAx.plot(time, components['steerColumn'], - time, hdot + cross + components['viscous'] + components['coulomb'], - time, steerTorque) - torqueAx.set_xlabel('Time [s]') - torqueAx.set_ylabel('Torque [N-m]') - torqueAx.legend(('Measured Torque', 'Frictional and Dynamic Torque', - 'Rider Applied Torque')) - - plt.show() - - return fig - - def compute_yaw_roll_pitch_rates(self): - """Computes the yaw, roll and pitch rates of the bicycle frame.""" - - try: - omegaX = self.truncatedSignals['AngularRateX'] - omegaY = self.truncatedSignals['AngularRateY'] - omegaZ = self.truncatedSignals['AngularRateZ'] - rollAngle = self.truncatedSignals['RollAngle'] - lam = self.bicycleRiderParameters['lam'] - except AttributeError: - print('All needed signals are not available. ' + - 'Yaw, roll and pitch rates were not computed.') - else: - omegaX = omegaX.convert_units('radian/second') - omegaY = omegaY.convert_units('radian/second') - omegaZ = omegaZ.convert_units('radian/second') - rollAngle = rollAngle.convert_units('radian') - - yr, rr, pr = sigpro.yaw_roll_pitch_rate(omegaX, omegaY, omegaZ, lam, - rollAngle=rollAngle) - yr.units = 'radian/second' - yr.name = 'YawRate' - rr.units = 'radian/second' - rr.name = 'RollRate' - pr.units = 'radian/second' - pr.name = 'PitchRate' - - self.computedSignals['YawRate'] = yr - self.computedSignals['RollRate'] = rr - self.computedSignals['PitchRate'] = pr - - def compute_frame_acceleration(self): - """Calculate the frame acceleration in inertial frame, expressed by the - the A frame coordinates which is after yaw movement.""" - AccelerationX = self.truncatedSignals['AccelerationX'] - AccelerationY = self.truncatedSignals['AccelerationY'] - AccelerationZ = self.truncatedSignals['AccelerationZ'] - rollAngle = self.truncatedSignals['RollAngle'] - lam = self.bicycleRiderParameters['lam'] - - AccX, AccY, AccZ = sigpro.frame_acceleration(AccelerationX, - AccelerationY, AccelerationZ, lam, rollAngle=rollAngle) - AccX.units = 'meter/second/second' - AccX.name = 'FrameLongitudinalAcceleration' - AccY.units = 'meter/second/second' - AccY.name = 'FrameLateralAcceleration' - AccZ.units = 'meter/second/second' - AccZ.name = 'FrameDownwardAcceleration' - - self.computedSignals['AccX.name'] = AccX - self.computedSignals['AccY.name'] = AccY - self.computedSignals['AccZ.name'] = AccZ - - def compute_steer_rate(self): - """Calculate the steer rate from the frame and fork rates.""" - try: - forkRate = self.truncatedSignals['ForkRate'] - omegaZ = self.truncatedSignals['AngularRateZ'] - except AttributeError: - print('ForkRate or AngularRateZ is not available. ' + - 'SteerRate was not computed.') - else: - forkRate = forkRate.convert_units('radian/second') - omegaZ = omegaZ.convert_units('radian/second') - - steerRate = sigpro.steer_rate(forkRate, omegaZ) - steerRate.units = 'radian/second' - steerRate.name = 'SteerRate' - self.computedSignals['SteerRate'] = steerRate - - def compute_forward_speed(self): - """Calculates the magnitude of the main component of velocity of the - center of the rear wheel.""" - - try: - rR = self.bicycleRiderParameters['rR'] - rearWheelRate = self.truncatedSignals['RearWheelRate'] - except AttributeError: - print('rR or RearWheelRate is not availabe. ' + - 'ForwardSpeed was not computed.') - else: - rearWheelRate = rearWheelRate.convert_units('radian/second') - - self.computedSignals['ForwardSpeed'] = -rR * rearWheelRate - self.computedSignals['ForwardSpeed'].units = 'meter/second' - self.computedSignals['ForwardSpeed'].name = 'ForwardSpeed' - - def compute_pull_force(self): - """ - Computes the pull force from the truncated pull force signal. - - """ - try: - pullForce = self.truncatedSignals['PullForce'] - except AttributeError: - print 'PullForce was not available. PullForce was not computed.' - else: - pullForce = pullForce.convert_units('newton') - pullForce.name = 'PullForce' - pullForce.units = 'newton' - self.computedSignals[pullForce.name] = pullForce - - def __str__(self): - '''Prints basic run information to the screen.''' - - line = "=" * 79 - info = 'Run # {0}\nEnvironment: {1}\nRider: {2}\nBicycle: {3}\nSpeed:'\ - '{4}\nManeuver: {5}\nNotes: {6}'.format( - self.metadata['RunID'], - self.metadata['Environment'], - self.metadata['Rider'], - self.metadata['Bicycle'], - self.metadata['Speed'], - self.metadata['Maneuver'], - self.metadata['Notes']) - - return line + '\n' + info + '\n' + line - - def export(self, filetype, directory='exports'): - """ - Exports the computed signals to a file. - - Parameters - ---------- - filetype : str - The type of file to export the data to. Options are 'mat', 'csv', - and 'pickle'. - - """ - - if filetype == 'mat': - if not os.path.exists(directory): - print "Creating {0}".format(directory) - os.makedirs(directory) - exportData = {} - exportData.update(self.metadata) - try: - exportData.update(self.taskSignals) - except AttributeError: - try: - exportData.update(self.truncatedSignals) - except AttributeError: - exportData.update(self.calibratedSignals) - print('Exported calibratedSignals to {}'.format(directory)) - else: - print('Exported truncatedSignals to {}'.format(directory)) - else: - print('Exported taskSignals to {}'.format(directory)) - - filename = pad_with_zeros(str(self.metadata['RunID']), 5) + '.mat' - io.savemat(os.path.join(directory, filename), exportData) - else: - raise NotImplementedError(('{0} method is not available' + - ' yet.').format(filetype)) - - def extract_task(self): - """Slices the computed signals such that data before the end of the - bump is removed and unusable trailng data is removed. - - """ - # get the z acceleration from the VN-100 - acc = -self.truncatedSignals['AccelerometerAccelerationY'].filter(30.) - # find the mean speed during the task (look at one second in the middle - # of the data) - speed = self.computedSignals['ForwardSpeed'] - meanSpeed = speed[len(speed) / 2 - 100:len(speed) / 2 + 100].mean() - wheelbase = self.bicycleRiderParameters['w'] - # find the bump - indices = sigpro.find_bump(acc, acc.sampleRate, meanSpeed, wheelbase, - self.bumpLength) - - - # if it is a pavilion run, then clip the end too - # these are the runs that the length of track method of clipping - # applies to - straight = ['Track Straight Line With Disturbance', - 'Balance With Disturbance', - 'Balance', - 'Track Straight Line'] - if (self.metadata['Environment'] == 'Pavillion Floor' and - self.metadata['Maneuver'] in straight): - - # this is based on the length of the track in the pavilion that we - # measured on September 21st, 2011 - trackLength = 32. - wheelbase - self.bumpLength - end = trackLength / meanSpeed * acc.sampleRate - - # i may need to clip the end based on the forward speed dropping - # below certain threshold around the mean - else: - # if it isn't a pavilion run, don't clip the end - end = -1 - - self.taskSignals = {} - for name, sig in self.computedSignals.items(): - self.taskSignals[name] = sig[indices[2]:end] - - def load_rider(self, pathToParameterData): - """Creates a bicycle/rider attribute which contains the physical - parameters for the bicycle and rider for this run.""" - - print("Loading the bicycle and rider data for " + - "{} on {}".format(self.metadata['Rider'], - self.metadata['Bicycle'])) - - # currently this isn't very generic, it only assumes that there was - # Luke, Jason, and Charlie riding on the instrumented bicycle. - rider = self.metadata['Rider'] - if rider == 'Charlie' or rider == 'Luke': - # Charlie and Luke rode the bike in the same configuration - bicycle = 'Rigidcl' - elif rider == 'Jason' : - bicycle = 'Rigid' - else: - raise StandardError('There are no bicycle parameters ' + - 'for {}'.format(rider)) - - # force a recalculation (but not the period calcs, they take too long) - self.bicycle = bp.Bicycle(bicycle, pathToData=pathToParameterData) - try: - self.bicycle.extras - except AttributeError: - pass - else: - self.bicycle.save_parameters() - # force a recalculation of the human parameters - self.bicycle.add_rider(rider) - if self.bicycle.human is not None: - self.bicycle.save_parameters() - - self.bicycleRiderParameters =\ - bp.io.remove_uncertainties(self.bicycle.parameters['Benchmark']) - - def plot(self, *args, **kwargs): - ''' - Returns a plot of the time series of various signals. - - Parameters - ---------- - signalName : string - These should be strings that correspond to the signals available in - the computed data. If the first character of the string is `-` then - the negative signal will be plotted. You can also scale the values - so by adding a value and an ``*`` such as: ``'-10*RollRate'. The - negative sign always has to come first. - signalType : string, optional - This allows you to plot from the other signal types. Options are - 'task', 'computed', 'truncated', 'calibrated', 'raw'. The default - is 'task'. - - ''' - if not kwargs: - kwargs = {'signalType': 'task'} - - mapping = {} - for x in ['computed', 'truncated', 'calibrated', 'raw', 'task']: - try: - mapping[x] = getattr(self, x + 'Signals') - except AttributeError: - pass - - fig = plt.figure() - ax = fig.add_axes([0.125, 0.125, 0.8, 0.7]) - - leg = [] - for i, arg in enumerate(args): - legName = arg - sign = 1. - # if a negative sign is present - if '-' in arg and arg[0] != '-': - raise ValueError('{} is incorrectly typed'.format(arg)) - elif '-' in arg and arg[0] == '-': - arg = arg[1:] - sign = -1. - # if a multiplication factor is present - if '*' in arg: - mul, arg = arg.split('*') - else: - mul = 1. - signal = sign * float(mul) * mapping[kwargs['signalType']][arg] - ax.plot(signal.time(), signal) - leg.append(legName + ' [' + mapping[kwargs['signalType']][arg].units + ']') - - ax.legend(leg) - runid = pad_with_zeros(str(self.metadata['RunID']), 5) - ax.set_title('Run: ' + runid + ', Rider: ' + self.metadata['Rider'] + - ', Speed: ' + str(self.metadata['Speed']) + 'm/s' + '\n' + - 'Maneuver: ' + self.metadata['Maneuver'] + - ', Environment: ' + self.metadata['Environment'] + '\n' + - 'Notes: ' + self.metadata['Notes']) - - ax.set_xlabel('Time [second]') - - ax.grid() - - return fig - - def plot_wheel_contact(self, show=False): - """Returns a plot of the wheel contact traces. - - Parameters - ---------- - show : boolean - If true the plot will be displayed. - - Returns - ------- - fig : matplotlib.Figure - - """ - - q1 = self.taskSignals['LongitudinalRearContact'] - q2 = self.taskSignals['LateralRearContact'] - q9 = self.taskSignals['LongitudinalFrontContact'] - q10 = self.taskSignals['LateralFrontContact'] - - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - ax.plot(q1, q2, q9, q10) - ax.set_xlabel('Distance [' + q1.units + ']') - ax.set_ylabel('Distance [' + q2.units + ']') - ax.set_ylim((-0.5, 0.5)) - rider = self.metadata['Rider'] - where = self.metadata['Environment'] - speed = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() - maneuver = self.metadata['Maneuver'] - ax.set_title(rider + ', ' + where + ', ' + maneuver + ' @ ' + speed + ' m/s') - - if show is True: - fig.show() - - return fig - - def verify_time_sync(self, show=True, saveDir=None): - """Shows a plot of the acceleration signals that were used to - synchronize the NI and VN data. If it doesn't show a good fit, then - something is wrong. - - Parameters - ---------- - show : boolean - If true, the figure will be displayed. - saveDir : str - The path to a directory in which to save the figure. - - """ - - if self.topSig == 'calibrated': - sigType = 'calibrated' - else: - sigType = 'truncated' - - fig = self.plot('-AccelerometerAccelerationY', 'AccelerationZ', - signalType=sigType) - ax = fig.axes[0] - ax.set_xlim((0, 10)) - title = ax.get_title() - ax.set_title(title + '\nSignal Type: ' + sigType) - if saveDir is not None: - if not os.path.exists(saveDir): - print "Creating {0}".format(saveDir) - os.makedirs(saveDir) - runid = run_id_string(self.metadata['RunID']) - fig.savefig(os.path.join(saveDir, runid + '.png')) - if show is True: - fig.show() - - return fig - - def video(self): - ''' - Plays the video of the run. - - ''' - # get the 5 digit string version of the run id - runid = pad_with_zeros(str(self.metadata['RunID']), 5) - viddir = os.path.join('..', 'Video') - abspath = os.path.abspath(viddir) - # check to see if there is a video for this run - if (runid + '.mp4') in os.listdir(viddir): - path = os.path.join(abspath, runid + '.mp4') - os.system('vlc "' + path + '"') - else: - print "No video for this run" + """The fluppin fundamental class for a run.""" + + def __init__(self, runid, dataset, pathToParameterData=None, + forceRecalc=False, filterFreq=None, store=True): + """Loads the raw and processed data for a run if available otherwise it + generates the processed data from the raw data. + + Parameters + ---------- + runid : int or str + The run id should be an integer, e.g. 5, or a five digit string with + leading zeros, e.g. '00005'. + dataset : DataSet + A DataSet object with at least some raw data. + pathToParameterData : string, {'', None}, optional + The path to a data directory for the BicycleParameters package. It + should contain the bicycles and riders used in the experiments. + forceRecalc : boolean, optional, default = False + If true then it will force a recalculation of all the processed + data. + filterSigs : float, optional, default = None + If true all of the processed signals will be low pass filtered with + a second order Butterworth filter at the given filter frequency. + store : boolean, optional, default = True + If true the resulting task signals will be stored in the database. + + """ + + if pathToParameterData is None: + pathToParameterData = config.get('data', 'pathToParameters') + + print "Initializing the run object." + + self.filterFreq = filterFreq + + dataset.open() + dataTable = dataset.database.root.runTable + signalTable = dataset.database.root.signalTable + taskTable = dataset.database.root.taskTable + + runid = run_id_string(runid) + + # get the row number for this particular run id + rownum = get_row_num(runid, dataTable) + + # make some dictionaries to store all the data + self.metadata = {} + self.rawSignals = {} + + # make lists of the input and output signals + rawDataCols = [x['signal'] for x in + signalTable.where("isRaw == True")] + computedCols = [x['signal'] for x in + signalTable.where("isRaw == False")] + + # store the metadata for this run + print "Loading metadata from the database." + for col in dataTable.colnames: + if col not in (rawDataCols + computedCols): + self.metadata[col] = get_cell(dataTable, col, rownum) + + print "Loading the raw signals from the database." + for col in rawDataCols: + # rawDataCols includes all possible raw signals, but every run + # doesn't have all the signals, so skip the ones that aren't there + try: + self.rawSignals[col] = RawSignal(runid, col, dataset.database) + except NoSuchNodeError: + pass + + if self.metadata['Rider'] != 'None': + self.load_rider(pathToParameterData) + + self.bumpLength = 1.0 # 1 meter + + # Try to load the task signals if they've already been computed. If + # they aren't in the database, the filter frequencies don't match or + # forceRecalc is true the then compute them. This may save some time + # when repeatedly loading runs for analysis. + self.taskFromDatabase = False + try: + runGroup = dataset.database.root.taskData._f_getChild(runid) + except NoSuchNodeError: + forceRecalc = True + else: + # The filter frequency stored in the task table is either a nan + # value or a valid float. If the stored filter frequency is not the + # same as the the one passed to Run, then a recalculation should be + # forced. + taskRowNum = get_row_num(runid, taskTable) + storedFreq = taskTable.cols.FilterFrequency[taskRowNum] + self.taskSignals = {} + if filterFreq is None: + newFilterFreq = np.nan + else: + newFilterFreq = filterFreq + + if np.isnan(newFilterFreq) and np.isnan(storedFreq): + for node in runGroup._f_walkNodes(): + meta = {k : node._f_getAttr(k) for k in ['units', 'name', + 'runid', 'sampleRate', 'source']} + self.taskSignals[node.name] = Signal(node[:], meta) + self.taskFromDatabase = True + elif np.isnan(newFilterFreq) or np.isnan(storedFreq): + forceRecalc = True + else: + if abs(storedFreq - filterFreq) < 1e-10: + for node in runGroup._f_walkNodes(): + meta = {k : node._f_getAttr(k) for k in ['units', 'name', + 'runid', 'sampleRate', 'source']} + self.taskSignals[node.name] = Signal(node[:], meta) + self.taskFromDatabase = True + else: + forceRecalc = True + + dataset.close() + + if forceRecalc == True: + try: + del self.taskSignals + except AttributeError: + pass + self.process_raw_signals() + + # store the task signals in the database if they are newly computed + if (store == True and self.taskFromDatabase == False + and self.topSig == 'task'): + taskMeta = { + 'Duration' : + self.taskSignals['ForwardSpeed'].time()[-1], + 'FilterFrequency' : self.filterFreq, + 'MeanSpeed' : self.taskSignals['ForwardSpeed'].mean(), + 'RunID' : self.metadata['RunID'], + 'StdSpeed' : self.taskSignals['ForwardSpeed'].std(), + 'Tau' : self.tau, + } + dataset.add_task_signals(self.taskSignals, taskMeta) + + # tell the user about the run + print self + + def process_raw_signals(self): + """Processes the raw signals as far as possible and filters the + result if a cutoff frequency was specified.""" + + print "Computing signals from raw data." + self.calibrate_signals() + + # the following maneuvers should never be calculated beyond the + # calibrated signals + maneuver = self.metadata['Maneuver'] + con1 = maneuver != 'Steer Dynamics Test' + con2 = maneuver != 'System Test' + con3 = maneuver != 'Static Calibration' + if con1 and con2 and con3: + self.compute_time_shift() + self.check_time_shift(0.15) + self.truncate_signals() + self.compute_signals() + self.task_signals() + + if self.filterFreq is not None: + self.filter_top_signals(self.filterFreq) + + def filter_top_signals(self, filterFreq): + """Filters the top most signals with a low pass filter.""" + + if self.topSig == 'task': + print('Filtering the task signals.') + for k, v in self.taskSignals.items(): + self.taskSignals[k] = v.filter(filterFreq) + elif self.topSig == 'computed': + print('Filtering the computed signals.') + for k, v in self.computedSignals.items(): + self.computedSignals[k] = v.filter(filterFreq) + elif self.topSig == 'calibrated': + print('Filtering the calibrated signals.') + for k, v in self.calibratedSignals.items(): + self.calibratedSignals[k] = v.filter(filterFreq) + + def calibrate_signals(self): + """Calibrates the raw signals.""" + + # calibrate the signals for the run + self.calibratedSignals = {} + for sig in self.rawSignals.values(): + calibSig = sig.scale() + self.calibratedSignals[calibSig.name] = calibSig + + self.topSig = 'calibrated' + + def task_signals(self): + """Computes the task signals.""" + print('Extracting the task portion from the data.') + self.extract_task() + + # compute task specific variables* + self.compute_yaw_angle() + self.compute_rear_wheel_contact_rates() + self.compute_rear_wheel_contact_points() + self.compute_front_wheel_contact_points() + self.compute_front_wheel_yaw_angle() + self.compute_front_wheel_rate() + self.compute_front_rear_wheel_contact_forces() + self.compute_contact_points_acceleration() + + self.topSig = 'task' + + def compute_signals(self): + """Computes the task independent quantities.""" + + self.computedSignals ={} + # transfer some of the signals to computed + noChange = ['FiveVolts', + 'PushButton', + 'RearWheelRate', + 'RollAngle', + 'SteerAngle', + 'ThreeVolts'] + for sig in noChange: + if sig in ['RollAngle', 'SteerAngle']: + self.computedSignals[sig] =\ + self.truncatedSignals[sig].convert_units('radian') + else: + self.computedSignals[sig] = self.truncatedSignals[sig] + + # compute the quantities that aren't task specific + self.compute_pull_force() + self.compute_forward_speed() + self.compute_steer_rate() + self.compute_yaw_roll_pitch_rates() + self.compute_steer_torque() + self.compute_frame_acceleration() + + def truncate_signals(self): + """Truncates the calibrated signals based on the time shift.""" + + self.truncatedSignals = {} + for name, sig in self.calibratedSignals.items(): + self.truncatedSignals[name] = sig.truncate(self.tau).spline() + self.topSig = 'truncated' + + def compute_time_shift(self): + """Computes the time shift based on the vertical accelerometer + signals.""" + + self.tau = sigpro.find_timeshift( + self.calibratedSignals['AccelerometerAccelerationY'], + self.calibratedSignals['AccelerationZ'], + self.metadata['NISampleRate'], + self.metadata['Speed'], plotError=False) + + def check_time_shift(self, maxNRMS): + """Raises an error if the normalized root mean square of the shifted + accelerometer signals is high.""" + + # Check to make sure the signals were actually good fits by + # calculating the normalized root mean square. If it isn't very + # low, raise an error. + niAcc = self.calibratedSignals['AccelerometerAccelerationY'] + vnAcc = self.calibratedSignals['AccelerationZ'] + vnAcc = vnAcc.truncate(self.tau).spline() + niAcc = niAcc.truncate(self.tau).spline() + # todo: this should probably check the rms of the mean subtracted data + # because both accelerometers don't always give the same value, this + # may work better with a filtered signal too + # todo: this should probably be moved into the time shift code in the + # signalprocessing model + nrms = np.sqrt(np.mean((vnAcc + niAcc)**2)) / (niAcc.max() - niAcc.min()) + if nrms > maxNRMS: + raise TimeShiftError(('The normalized root mean square for this ' + + 'time shift is {}, which is greater '.format(str(nrms)) + + 'than the maximum allowed: {}'.format(str(maxNRMS)))) + + def compute_rear_wheel_contact_points(self): + """Computes the location of the wheel contact points in the ground + plane.""" + + # get the rates + try: + latRate = self.taskSignals['LateralRearContactRate'] + lonRate = self.taskSignals['LongitudinalRearContactRate'] + except AttributeError: + print('At least one of the rates are not available. ' + + 'The YawAngle was not computed.') + else: + # convert to meters per second + latRate = latRate.convert_units('meter/second') + lonRate = lonRate.convert_units('meter/second') + # integrate and try to account for the drift + lat = latRate.integrate(detrend=True) + lon = lonRate.integrate() + # set the new name and units + lat.name = 'LateralRearContact' + lat.units = 'meter' + lon.name = 'LongitudinalRearContact' + lon.units = 'meter' + # store in task signals + self.taskSignals[lat.name] = lat + self.taskSignals[lon.name] = lon + + def compute_front_wheel_contact_points(self): + """Caluculates the front wheel contact points in the ground plane.""" + + q1 = self.taskSignals['LongitudinalRearContact'] + q2 = self.taskSignals['LateralRearContact'] + q3 = self.taskSignals['YawAngle'] + q4 = self.taskSignals['RollAngle'] + q7 = self.taskSignals['SteerAngle'] + + p = benchmark_to_moore(self.bicycleRiderParameters) + + f = np.vectorize(front_contact) + q9, q10 = f(q1, q2, q3, q4, q7, p['d1'], p['d2'], p['d3'], p['rr'], + p['rf']) + + q9.name = 'LongitudinalFrontContact' + q9.units = 'meter' + q10.name = 'LateralFrontContact' + q10.units = 'meter' + + self.taskSignals['LongitudinalFrontContact'] = q9 + self.taskSignals['LateralFrontContact'] = q10 + + def compute_front_wheel_yaw_angle(self): + """Calculates the yaw angle of the front wheel.""" + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(bp) + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + f = np.vectorize(front_wheel_yaw_angle) + q1_front_wheel = f(q1, q2, q4, mp['d1'], mp['d2'], mp['d3'], + bp['lam'], mp['rr'], mp['rf']) + + q1_front_wheel.name = 'FrontWheelYawAngle' + q1_front_wheel.units = 'meter' + self.taskSignals['FrontWheelYawAngle'] = q1_front_wheel + + def compute_front_wheel_rate(self): + """Calculates the front wheel rate, based on the Jason's data + of front_wheel_contact_point. Alternatively, you can use the + sympy to get the front_wheel_contact_rate directly first.""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(bp) + + f = np.vectorize(front_wheel_rate) + + u6 = f(q1, q2, q4, u9, u10, mp['d1'], mp['d2'], mp['d3'], + bp['lam'], mp['rr'], mp['rf']) + + u6.name = 'FrontWheelRate' + u6.units = 'radian/second' + self.taskSignals['FrontWheelRate'] = u6 + + + def compute_front_rear_wheel_contact_forces(self): + """Calculate the contact forces for each + wheel with respect to inertial frame under + the slip and nonslip condition. Also, provide + the steer torque T4.""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + rf = mp['rf'] + + #signals assignment --slip condition + V = self.taskSignals['ForwardSpeed'].mean() + + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = self.taskSignals['RearWheelContactPointLongitudinalAcceleration'] + u8d = self.taskSignals['RearWheelContactPointLateralAcceleration'] + u9d = self.taskSignals['FrontWheelContactPointLongitudinalAcceleration'] + u10d = self.taskSignals['FrontWheelContactPointLateralAcceleration'] + + f_1 = np.vectorize(steer_torque_slip) + f_2 = np.vectorize(contact_forces_slip) + f_3 = np.vectorize(contact_forces_nonslip) + + #calculation + #steer torque slip + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, + u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, + ic11, ic22, ic33, ic31, q1, q2, q4, + u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, + u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, + u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + + #attributes: name and units, and taskSignals + T4_slip.name = 'SteerTorque_Slip' + T4_slip.units = 'newton*meter' + self.taskSignals[T4_slip.name] = T4_slip + + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s + + Fy_r_s.name = 'LateralRearContactForce_Slip' + Fy_r_s.units = 'newton' + self.taskSignals[Fy_r_s.name] = Fy_r_s + + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' + Fx_f_s.units = 'newton' + self.taskSignals[Fx_f_s.name] = Fx_f_s + + Fy_f_s.name = 'LateralFrontContactForce_Slip' + Fy_f_s.units = 'newton' + self.taskSignals[Fy_f_s.name] = Fy_f_s + + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' + Fx_r_ns.units = 'newton' + self.taskSignals[Fx_r_ns.name] = Fx_r_ns + + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' + Fy_r_ns.units = 'newton' + self.taskSignals[Fy_r_ns.name] = Fy_r_ns + + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' + Fx_f_ns.units = 'newton' + self.taskSignals[Fx_f_ns.name] = Fx_f_ns + + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' + Fy_f_ns.units = 'newton' + self.taskSignals[Fy_f_ns.name] = Fy_f_ns + + def compute_contact_points_acceleration(self): + """Calculates the acceleration of the contact points of front and rear + wheels.""" + + FLongAcc= self.taskSignals['FrameLongitudinalAcceleration'] + FLateralAcc = self.taskSignals['FrameLateralAcceleration'] + FDownAcc = self.taskSignals['FrameDownwardAcceleration'] + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + d1 = mp['d1'] + d2 = mp['d2'] + d3 = mp['d3'] + rr = mp['rr'] + rf = mp['rf'] + ds1 = self.bicycle.parameters['Measured']['ds1'] + ds3 = self.bicycle.parameters['Measured']['ds3'] + s3 = ds3 + s1 = ds1 + mp['l4'] + + f = np.vectorize(contact_points_acceleration) + + u7d, u8d, u11d, u9d, u10d, u12d = f(FLongAcc, FLateralAcc, FDownAcc, + q1, q2, q3, + u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d, + d1, d2, d3, rr, rf, s1, s3) + + u7d.name = 'RearWheelContactPointLongitudinalAcceleration' + u7d.units = 'meter/second/second' + u8d.name = 'RearWheelContactPointLateralAcceleration' + u8d.units = 'meter/second/second' + u11d.name = 'RearWheelContactPointDownwardAcceleration' + u11d.units = 'meter/second/second' + u9d.name = 'FrontWheelContactPointLongitudinalAcceleration' + u9d.units = 'meter/second/second' + u10d.name = 'FrontWheelContactPointLateralAcceleration' + u10d.units = 'meter/second/second' + u12d.name = 'FrontWheelContactPointDownwardAcceleration' + u12.units = 'meter/second/second' + + self.taskSignals[u7d.name] = u7d + self.taskSignals[u8d.name] = u8d + self.taskSignals[u11d.name] = u11d + self.taskSignals[u9d.name] = u9d + self.taskSignals[u10d.name] = u10d + self.taskSignals[u12d.name] = u12d + + + def compute_rear_wheel_contact_rates(self): + """Calculates the rates of the wheel contact points in the ground + plane.""" + + try: + yawAngle = self.taskSignals['YawAngle'] + rearWheelRate = self.taskSignals['RearWheelRate'] + rR = self.bicycleRiderParameters['rR'] # this should be in meters + except AttributeError: + print('Either the yaw angle, rear wheel rate or ' + + 'front wheel radius is not available. The ' + + 'contact rates were not computed.') + else: + yawAngle = yawAngle.convert_units('radian') + rearWheelRate = rearWheelRate.convert_units('radian/second') + + lon, lat = sigpro.rear_wheel_contact_rate(rR, rearWheelRate, yawAngle) + + lon.name = 'LongitudinalRearContactRate' + lon.units = 'meter/second' + self.taskSignals[lon.name] = lon + + lat.name = 'LateralRearContactRate' + lat.units = 'meter/second' + self.taskSignals[lat.name] = lat + + def compute_yaw_angle(self): + """Computes the yaw angle by integrating the yaw rate.""" + + # get the yaw rate + try: + yawRate = self.taskSignals['YawRate'] + except AttributeError: + print('YawRate is not available. The YawAngle was not computed.') + else: + # convert to radians per second + yawRate = yawRate.convert_units('radian/second') + # integrate and try to account for the drift + yawAngle = yawRate.integrate(detrend=True) + # set the new name and units + yawAngle.name = 'YawAngle' + yawAngle.units = 'radian' + # store in computed signals + self.taskSignals['YawAngle'] = yawAngle + + def compute_steer_torque(self, plot=False): + """Computes the rider applied steer torque. + + Parameters + ---------- + plot : boolean, optional + Default is False, but if True a plot is generated. + + """ + # steer torque + frameAngRate = np.vstack(( + self.truncatedSignals['AngularRateX'], + self.truncatedSignals['AngularRateY'], + self.truncatedSignals['AngularRateZ'])) + frameAngAccel = np.vstack(( + self.truncatedSignals['AngularRateX'].time_derivative(), + self.truncatedSignals['AngularRateY'].time_derivative(), + self.truncatedSignals['AngularRateZ'].time_derivative())) + frameAccel = np.vstack(( + self.truncatedSignals['AccelerationX'], + self.truncatedSignals['AccelerationY'], + self.truncatedSignals['AccelerationZ'])) + handlebarAngRate = self.truncatedSignals['ForkRate'] + handlebarAngAccel = self.truncatedSignals['ForkRate'].time_derivative() + steerAngle = self.truncatedSignals['SteerAngle'] + steerColumnTorque =\ + self.truncatedSignals['SteerTubeTorque'].convert_units('newton*meter') + handlebarMass = self.bicycleRiderParameters['mG'] + handlebarInertia =\ + self.bicycle.steer_assembly_moment_of_inertia(fork=False, + wheel=False, nominal=True) + # this is the distance from the handlebar center of mass to the + # steer axis + w = self.bicycleRiderParameters['w'] + c = self.bicycleRiderParameters['c'] + lam = self.bicycleRiderParameters['lam'] + xG = self.bicycleRiderParameters['xG'] + zG = self.bicycleRiderParameters['zG'] + handlebarCoM = np.array([xG, 0., zG]) + d = bp.geometry.distance_to_steer_axis(w, c, lam, handlebarCoM) + # these are the distances from the point on the steer axis which is + # aligned with the handlebar center of mass to the accelerometer on + # the frame + ds1 = self.bicycle.parameters['Measured']['ds1'] + ds3 = self.bicycle.parameters['Measured']['ds3'] + ds = np.array([ds1, 0., ds3]) # i measured these + # damping and friction values come from Peter's work, I need to verify + # them still + damping = 0.3475 + friction = 0.0861 + + components = sigpro.steer_torque_components( + frameAngRate, frameAngAccel, frameAccel, handlebarAngRate, + handlebarAngAccel, steerAngle, steerColumnTorque, + handlebarMass, handlebarInertia, damping, friction, d, ds) + steerTorque = sigpro.steer_torque(components) + + stDict = {'units':'newton*meter', + 'name':'SteerTorque', + 'runid':self.metadata['RunID'], + 'sampleRate':steerAngle.sampleRate, + 'source':'NA'} + self.computedSignals['SteerTorque'] = Signal(steerTorque, stDict) + + if plot is True: + + time = steerAngle.time() + + hdot = (components['Hdot1'] + components['Hdot2'] + + components['Hdot3'] + components['Hdot4']) + cross = (components['cross1'] + components['cross2'] + + components['cross3']) + + fig = plt.figure() + + frictionAx = fig.add_subplot(4, 1, 1) + frictionAx.plot(time, components['viscous'], + time, components['coulomb'], + time, components['viscous'] + components['coulomb']) + frictionAx.set_ylabel('Torque [N-m]') + frictionAx.legend(('Viscous Friction', 'Coulomb Friction', + 'Total Friction')) + + dynamicAx = fig.add_subplot(4, 1, 2) + dynamicAx.plot(time, hdot, time, cross, time, hdot + cross) + dynamicAx.set_ylabel('Torque [N-m]') + dynamicAx.legend((r'Torque due to $\dot{H}$', + r'Torque due to $r \times m a$', + r'Total Dynamic Torque')) + + additionalAx = fig.add_subplot(4, 1, 3) + additionalAx.plot(time, hdot + cross + components['viscous'] + + components['coulomb'], + label='Total Frictional and Dynamic Torque') + additionalAx.set_ylabel('Torque [N-m]') + additionalAx.legend() + + torqueAx = fig.add_subplot(4, 1, 4) + torqueAx.plot(time, components['steerColumn'], + time, hdot + cross + components['viscous'] + components['coulomb'], + time, steerTorque) + torqueAx.set_xlabel('Time [s]') + torqueAx.set_ylabel('Torque [N-m]') + torqueAx.legend(('Measured Torque', 'Frictional and Dynamic Torque', + 'Rider Applied Torque')) + + plt.show() + + return fig + + def compute_yaw_roll_pitch_rates(self): + """Computes the yaw, roll and pitch rates of the bicycle frame.""" + + try: + omegaX = self.truncatedSignals['AngularRateX'] + omegaY = self.truncatedSignals['AngularRateY'] + omegaZ = self.truncatedSignals['AngularRateZ'] + rollAngle = self.truncatedSignals['RollAngle'] + lam = self.bicycleRiderParameters['lam'] + except AttributeError: + print('All needed signals are not available. ' + + 'Yaw, roll and pitch rates were not computed.') + else: + omegaX = omegaX.convert_units('radian/second') + omegaY = omegaY.convert_units('radian/second') + omegaZ = omegaZ.convert_units('radian/second') + rollAngle = rollAngle.convert_units('radian') + + yr, rr, pr = sigpro.yaw_roll_pitch_rate(omegaX, omegaY, omegaZ, lam, + rollAngle=rollAngle) + yr.units = 'radian/second' + yr.name = 'YawRate' + rr.units = 'radian/second' + rr.name = 'RollRate' + pr.units = 'radian/second' + pr.name = 'PitchRate' + + self.computedSignals['YawRate'] = yr + self.computedSignals['RollRate'] = rr + self.computedSignals['PitchRate'] = pr + + def compute_frame_acceleration(self): + """Calculate the frame acceleration in inertial frame, expressed by the + the A frame coordinates which is after yaw movement.""" + AccelerationX = self.truncatedSignals['AccelerationX'] + AccelerationY = self.truncatedSignals['AccelerationY'] + AccelerationZ = self.truncatedSignals['AccelerationZ'] + rollAngle = self.truncatedSignals['RollAngle'] + lam = self.bicycleRiderParameters['lam'] + + AccX, AccY, AccZ = sigpro.frame_acceleration(AccelerationX, + AccelerationY, AccelerationZ, lam, rollAngle=rollAngle) + AccX.units = 'meter/second/second' + AccX.name = 'FrameLongitudinalAcceleration' + AccY.units = 'meter/second/second' + AccY.name = 'FrameLateralAcceleration' + AccZ.units = 'meter/second/second' + AccZ.name = 'FrameDownwardAcceleration' + + self.computedSignals[AccX.name] = AccX + self.computedSignals[AccY.name] = AccY + self.computedSignals[AccZ.name] = AccZ + + def compute_steer_rate(self): + """Calculate the steer rate from the frame and fork rates.""" + try: + forkRate = self.truncatedSignals['ForkRate'] + omegaZ = self.truncatedSignals['AngularRateZ'] + except AttributeError: + print('ForkRate or AngularRateZ is not available. ' + + 'SteerRate was not computed.') + else: + forkRate = forkRate.convert_units('radian/second') + omegaZ = omegaZ.convert_units('radian/second') + + steerRate = sigpro.steer_rate(forkRate, omegaZ) + steerRate.units = 'radian/second' + steerRate.name = 'SteerRate' + self.computedSignals['SteerRate'] = steerRate + + def compute_forward_speed(self): + """Calculates the magnitude of the main component of velocity of the + center of the rear wheel.""" + + try: + rR = self.bicycleRiderParameters['rR'] + rearWheelRate = self.truncatedSignals['RearWheelRate'] + except AttributeError: + print('rR or RearWheelRate is not availabe. ' + + 'ForwardSpeed was not computed.') + else: + rearWheelRate = rearWheelRate.convert_units('radian/second') + + self.computedSignals['ForwardSpeed'] = -rR * rearWheelRate + self.computedSignals['ForwardSpeed'].units = 'meter/second' + self.computedSignals['ForwardSpeed'].name = 'ForwardSpeed' + + def compute_pull_force(self): + """ + Computes the pull force from the truncated pull force signal. + + """ + try: + pullForce = self.truncatedSignals['PullForce'] + except AttributeError: + print 'PullForce was not available. PullForce was not computed.' + else: + pullForce = pullForce.convert_units('newton') + pullForce.name = 'PullForce' + pullForce.units = 'newton' + self.computedSignals[pullForce.name] = pullForce + + def __str__(self): + '''Prints basic run information to the screen.''' + + line = "=" * 79 + info = 'Run # {0}\nEnvironment: {1}\nRider: {2}\nBicycle: {3}\nSpeed:'\ + '{4}\nManeuver: {5}\nNotes: {6}'.format( + self.metadata['RunID'], + self.metadata['Environment'], + self.metadata['Rider'], + self.metadata['Bicycle'], + self.metadata['Speed'], + self.metadata['Maneuver'], + self.metadata['Notes']) + + return line + '\n' + info + '\n' + line + + def export(self, filetype, directory='exports'): + """ + Exports the computed signals to a file. + + Parameters + ---------- + filetype : str + The type of file to export the data to. Options are 'mat', 'csv', + and 'pickle'. + + """ + + if filetype == 'mat': + if not os.path.exists(directory): + print "Creating {0}".format(directory) + os.makedirs(directory) + exportData = {} + exportData.update(self.metadata) + try: + exportData.update(self.taskSignals) + except AttributeError: + try: + exportData.update(self.truncatedSignals) + except AttributeError: + exportData.update(self.calibratedSignals) + print('Exported calibratedSignals to {}'.format(directory)) + else: + print('Exported truncatedSignals to {}'.format(directory)) + else: + print('Exported taskSignals to {}'.format(directory)) + + filename = pad_with_zeros(str(self.metadata['RunID']), 5) + '.mat' + io.savemat(os.path.join(directory, filename), exportData) + else: + raise NotImplementedError(('{0} method is not available' + + ' yet.').format(filetype)) + + def extract_task(self): + """Slices the computed signals such that data before the end of the + bump is removed and unusable trailng data is removed. + + """ + # get the z acceleration from the VN-100 + acc = -self.truncatedSignals['AccelerometerAccelerationY'].filter(30.) + # find the mean speed during the task (look at one second in the middle + # of the data) + speed = self.computedSignals['ForwardSpeed'] + meanSpeed = speed[len(speed) / 2 - 100:len(speed) / 2 + 100].mean() + wheelbase = self.bicycleRiderParameters['w'] + # find the bump + indices = sigpro.find_bump(acc, acc.sampleRate, meanSpeed, wheelbase, + self.bumpLength) + + + # if it is a pavilion run, then clip the end too + # these are the runs that the length of track method of clipping + # applies to + straight = ['Track Straight Line With Disturbance', + 'Balance With Disturbance', + 'Balance', + 'Track Straight Line'] + if (self.metadata['Environment'] == 'Pavillion Floor' and + self.metadata['Maneuver'] in straight): + + # this is based on the length of the track in the pavilion that we + # measured on September 21st, 2011 + trackLength = 32. - wheelbase - self.bumpLength + end = trackLength / meanSpeed * acc.sampleRate + + # i may need to clip the end based on the forward speed dropping + # below certain threshold around the mean + else: + # if it isn't a pavilion run, don't clip the end + end = -1 + + self.taskSignals = {} + for name, sig in self.computedSignals.items(): + self.taskSignals[name] = sig[indices[2]:end] + + def load_rider(self, pathToParameterData): + """Creates a bicycle/rider attribute which contains the physical + parameters for the bicycle and rider for this run.""" + + print("Loading the bicycle and rider data for " + + "{} on {}".format(self.metadata['Rider'], + self.metadata['Bicycle'])) + + # currently this isn't very generic, it only assumes that there was + # Luke, Jason, and Charlie riding on the instrumented bicycle. + rider = self.metadata['Rider'] + if rider == 'Charlie' or rider == 'Luke': + # Charlie and Luke rode the bike in the same configuration + bicycle = 'Rigidcl' + elif rider == 'Jason' : + bicycle = 'Rigid' + else: + raise StandardError('There are no bicycle parameters ' + + 'for {}'.format(rider)) + + # force a recalculation (but not the period calcs, they take too long) + self.bicycle = bp.Bicycle(bicycle, pathToData=pathToParameterData) + try: + self.bicycle.extras + except AttributeError: + pass + else: + self.bicycle.save_parameters() + # force a recalculation of the human parameters + self.bicycle.add_rider(rider) + if self.bicycle.human is not None: + self.bicycle.save_parameters() + + self.bicycleRiderParameters =\ + bp.io.remove_uncertainties(self.bicycle.parameters['Benchmark']) + + def plot(self, *args, **kwargs): + ''' + Returns a plot of the time series of various signals. + + Parameters + ---------- + signalName : string + These should be strings that correspond to the signals available in + the computed data. If the first character of the string is `-` then + the negative signal will be plotted. You can also scale the values + so by adding a value and an ``*`` such as: ``'-10*RollRate'. The + negative sign always has to come first. + signalType : string, optional + This allows you to plot from the other signal types. Options are + 'task', 'computed', 'truncated', 'calibrated', 'raw'. The default + is 'task'. + + ''' + if not kwargs: + kwargs = {'signalType': 'task'} + + mapping = {} + for x in ['computed', 'truncated', 'calibrated', 'raw', 'task']: + try: + mapping[x] = getattr(self, x + 'Signals') + except AttributeError: + pass + + fig = plt.figure() + ax = fig.add_axes([0.125, 0.125, 0.8, 0.7]) + + leg = [] + for i, arg in enumerate(args): + legName = arg + sign = 1. + # if a negative sign is present + if '-' in arg and arg[0] != '-': + raise ValueError('{} is incorrectly typed'.format(arg)) + elif '-' in arg and arg[0] == '-': + arg = arg[1:] + sign = -1. + # if a multiplication factor is present + if '*' in arg: + mul, arg = arg.split('*') + else: + mul = 1. + signal = sign * float(mul) * mapping[kwargs['signalType']][arg] + ax.plot(signal.time(), signal) + leg.append(legName + ' [' + mapping[kwargs['signalType']][arg].units + ']') + + ax.legend(leg) + runid = pad_with_zeros(str(self.metadata['RunID']), 5) + ax.set_title('Run: ' + runid + ', Rider: ' + self.metadata['Rider'] + + ', Speed: ' + str(self.metadata['Speed']) + 'm/s' + '\n' + + 'Maneuver: ' + self.metadata['Maneuver'] + + ', Environment: ' + self.metadata['Environment'] + '\n' + + 'Notes: ' + self.metadata['Notes']) + + ax.set_xlabel('Time [second]') + + ax.grid() + + return fig + + def plot_wheel_contact(self, show=False): + """Returns a plot of the wheel contact traces. + + Parameters + ---------- + show : boolean + If true the plot will be displayed. + + Returns + ------- + fig : matplotlib.Figure + + """ + + q1 = self.taskSignals['LongitudinalRearContact'] + q2 = self.taskSignals['LateralRearContact'] + q9 = self.taskSignals['LongitudinalFrontContact'] + q10 = self.taskSignals['LateralFrontContact'] + + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.plot(q1, q2, q9, q10) + ax.set_xlabel('Distance [' + q1.units + ']') + ax.set_ylabel('Distance [' + q2.units + ']') + ax.set_ylim((-0.5, 0.5)) + rider = self.metadata['Rider'] + where = self.metadata['Environment'] + speed = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() + maneuver = self.metadata['Maneuver'] + ax.set_title(rider + ', ' + where + ', ' + maneuver + ' @ ' + speed + ' m/s') + + if show is True: + fig.show() + + return fig + + def verify_time_sync(self, show=True, saveDir=None): + """Shows a plot of the acceleration signals that were used to + synchronize the NI and VN data. If it doesn't show a good fit, then + something is wrong. + + Parameters + ---------- + show : boolean + If true, the figure will be displayed. + saveDir : str + The path to a directory in which to save the figure. + + """ + + if self.topSig == 'calibrated': + sigType = 'calibrated' + else: + sigType = 'truncated' + + fig = self.plot('-AccelerometerAccelerationY', 'AccelerationZ', + signalType=sigType) + ax = fig.axes[0] + ax.set_xlim((0, 10)) + title = ax.get_title() + ax.set_title(title + '\nSignal Type: ' + sigType) + if saveDir is not None: + if not os.path.exists(saveDir): + print "Creating {0}".format(saveDir) + os.makedirs(saveDir) + runid = run_id_string(self.metadata['RunID']) + fig.savefig(os.path.join(saveDir, runid + '.png')) + if show is True: + fig.show() + + return fig + + def video(self): + ''' + Plays the video of the run. + + ''' + # get the 5 digit string version of the run id + runid = pad_with_zeros(str(self.metadata['RunID']), 5) + viddir = os.path.join('..', 'Video') + abspath = os.path.abspath(viddir) + # check to see if there is a video for this run + if (runid + '.mp4') in os.listdir(viddir): + path = os.path.join(abspath, runid + '.mp4') + os.system('vlc "' + path + '"') + else: + print "No video for this run" def matlab_date_to_object(matDate): - '''Returns a date time object based on a Matlab `datestr()` output. + '''Returns a date time object based on a Matlab `datestr()` output. - Parameters - ---------- - matDate : string - String in the form '21-Mar-2011 14:45:54'. + Parameters + ---------- + matDate : string + String in the form '21-Mar-2011 14:45:54'. - Returns - ------- - python datetime object + Returns + ------- + python datetime object - ''' - return datetime.datetime.strptime(matDate, '%d-%b-%Y %H:%M:%S') + ''' + return datetime.datetime.strptime(matDate, '%d-%b-%Y %H:%M:%S') diff --git a/bicycledataprocessor/signalprocessing.py b/bicycledataprocessor/signalprocessing.py index fb4cf83..acfa366 100644 --- a/bicycledataprocessor/signalprocessing.py +++ b/bicycledataprocessor/signalprocessing.py @@ -597,7 +597,7 @@ def frame_acceleration(AccelerationX, AccelerationY, AccelerationZ, Returns ------- - forward_Acceleration : ndarray, shape(n,) + longitudinal_Acceleration : ndarray, shape(n,) The forward acceleration of the bicycle frame in A['1']. lateral_Acceleration : ndarray, shape(n,) The lateral acceleration of the bicycle frame in A['2']. @@ -605,7 +605,7 @@ def frame_acceleration(AccelerationX, AccelerationY, AccelerationZ, The downward acceleration of the bicycle frame in A['3']. ''' - forward_Acceleration= AccelerationX * np.cos(lam) + AccelerationZ * np.sin(lam) + longitudinal_Acceleration= AccelerationX * np.cos(lam) + AccelerationZ * np.sin(lam) lateral_Acceleration = np.cos(rollAngle) * (AccelerationY + AccelerationX * np.sin(lam) * np.tan(rollAngle) - @@ -616,4 +616,4 @@ def frame_acceleration(AccelerationX, AccelerationY, AccelerationZ, AccelerationZ * np.cos(lam)) * np.cos(rollAngle) - return forward_Acceleration, lateral_Acceleration, downward_Acceleration + return longitudinal_Acceleration, lateral_Acceleration, downward_Acceleration From 2008d3722a1e61f519d562629dc87154472b0f59 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Thu, 11 Oct 2012 15:51:41 -0700 Subject: [PATCH 25/37] Fixed errors when running the run481 = ... --- bicycledataprocessor/main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 7042fa7..362a3d2 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -742,8 +742,8 @@ def task_signals(self): self.compute_front_wheel_contact_points() self.compute_front_wheel_yaw_angle() self.compute_front_wheel_rate() - self.compute_front_rear_wheel_contact_forces() self.compute_contact_points_acceleration() + self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' @@ -927,9 +927,11 @@ def compute_front_rear_wheel_contact_forces(self): #signals assignment --slip condition V = self.taskSignals['ForwardSpeed'].mean() + q1 = self.taskSignals['YawAngle'] q2 = self.taskSignals['RollAngle'] q4 = self.taskSignals['SteerAngle'] + u1 = self.taskSignals['YawRate'] u2 = self.taskSignals['RollRate'] u3 = self.taskSignals['PitchRate'] u4 = self.taskSignals['SteerRate'] @@ -940,6 +942,7 @@ def compute_front_rear_wheel_contact_forces(self): u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() u10 = self.taskSignals['LateralFrontContact'].time_derivative() + u1d = u1.time_derivative() u2d = u2.time_derivative() u3d = u3.time_derivative() u4d = u4.time_derivative() @@ -1046,7 +1049,7 @@ def compute_contact_points_acceleration(self): f = np.vectorize(contact_points_acceleration) u7d, u8d, u11d, u9d, u10d, u12d = f(FLongAcc, FLateralAcc, FDownAcc, - q1, q2, q3, + q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d, d1, d2, d3, rr, rf, s1, s3) @@ -1061,7 +1064,7 @@ def compute_contact_points_acceleration(self): u10d.name = 'FrontWheelContactPointLateralAcceleration' u10d.units = 'meter/second/second' u12d.name = 'FrontWheelContactPointDownwardAcceleration' - u12.units = 'meter/second/second' + u12d.units = 'meter/second/second' self.taskSignals[u7d.name] = u7d self.taskSignals[u8d.name] = u8d From 46650e5cdfb44486cf3a4f281976d40d7c4f7d26 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Thu, 11 Oct 2012 22:09:45 -0700 Subject: [PATCH 26/37] Fixed the units of q1_front_wheel, Added signal filter(10.0) after Acceleration signal, Commented the self.compute_contact_points_acceleration() and self.compute_front_rear_wheel_contact_forces() funcs --- bicycledataprocessor/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 362a3d2..278869c 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -742,8 +742,8 @@ def task_signals(self): self.compute_front_wheel_contact_points() self.compute_front_wheel_yaw_angle() self.compute_front_wheel_rate() - self.compute_contact_points_acceleration() - self.compute_front_rear_wheel_contact_forces() + #self.compute_contact_points_acceleration() + #self.compute_front_rear_wheel_contact_forces() self.topSig = 'task' @@ -878,7 +878,7 @@ def compute_front_wheel_yaw_angle(self): bp['lam'], mp['rr'], mp['rf']) q1_front_wheel.name = 'FrontWheelYawAngle' - q1_front_wheel.units = 'meter' + q1_front_wheel.units = 'radian' self.taskSignals['FrontWheelYawAngle'] = q1_front_wheel def compute_front_wheel_rate(self): @@ -1263,10 +1263,10 @@ def compute_yaw_roll_pitch_rates(self): def compute_frame_acceleration(self): """Calculate the frame acceleration in inertial frame, expressed by the the A frame coordinates which is after yaw movement.""" - AccelerationX = self.truncatedSignals['AccelerationX'] - AccelerationY = self.truncatedSignals['AccelerationY'] - AccelerationZ = self.truncatedSignals['AccelerationZ'] - rollAngle = self.truncatedSignals['RollAngle'] + AccelerationX = self.truncatedSignals['AccelerationX'].filter(10.) + AccelerationY = self.truncatedSignals['AccelerationY'].filter(10.) + AccelerationZ = self.truncatedSignals['AccelerationZ'].filter(10.) + rollAngle = self.truncatedSignals['RollAngle'].filter(10.) lam = self.bicycleRiderParameters['lam'] AccX, AccY, AccZ = sigpro.frame_acceleration(AccelerationX, From 8c56381f3a0c5feccb8508c2794b338b8af96db9 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Fri, 12 Oct 2012 13:11:28 -0700 Subject: [PATCH 27/37] Merged the conflict parts --- bicycledataprocessor/main.py | 264 +++++++++++++++++------------------ 1 file changed, 128 insertions(+), 136 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index fbad4e0..7134640 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -744,15 +744,10 @@ def task_signals(self): self.compute_rear_wheel_contact_rates() self.compute_rear_wheel_contact_points() self.compute_front_wheel_contact_points() -<<<<<<< HEAD - self.compute_front_wheel_rate() - self.compute_front_rear_wheel_contact_forces() -======= self.compute_front_wheel_yaw_angle() self.compute_front_wheel_rate() #self.compute_contact_points_acceleration() #self.compute_front_rear_wheel_contact_forces() ->>>>>>> origin/con_force_I self.topSig = 'task' @@ -874,135 +869,134 @@ def compute_front_wheel_contact_points(self): <<<<<<< HEAD - def compute_front_wheel_rate(self): - """Calculates the front wheel rate, based on the Jason's data - of front_wheel_contact_point. Alternatively, you can use the - sympy to get the front_wheel_contact_rate directly first.""" - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - bp = self.bicycleRiderParameters - - lam = bp['lam'] - rF = bp['rF'] - - q4_wheel = q4 * cos(lam) * cos(q2) - - q4_wheel.name = 'SteerAngle_FrontWheel' - q4_wheel.units = 'radian' - self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel - - f = np.vectorize(front_wheel_rate) - - u6 = f(q1, q2, q4, u9, u10, lam, rF) - u6.name = 'FrontWheelRate' - u6.units = 'radian/second' - self.taskSignals['FrontWheelRate'] = u6 - - - def compute_front_rear_wheel_contact_forces(self): - """Calculate the contact forces for each - wheel with respect to inertial frame under - the slip and nonslip condition. Also, provide the steer torque T4.""" - - #parameters - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(self.bicycleRiderParameters) - - l1 = mp['l1'] - l2 = mp['l2'] - mc = mp['mc'] - ic11 = mp['ic11'] - ic22 = mp['ic22'] - ic33 = mp['ic33'] - ic31 = mp['ic31'] - rf = mp['rf'] - - #signals assignment --slip condition - V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() - #V = self.taskSignals['ForwardSpeed'] - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - u1 = self.taskSignals['YawRate'] - u2 = self.taskSignals['RollRate'] - u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] - u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] - u7 = self.taskSignals['LongitudinalRearContactRate'] - u8 = self.taskSignals['LateralRearContactRate'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - u1d = u1.time_derivative() - u2d = u2.time_derivative() - u3d = u3.time_derivative() - u4d = u4.time_derivative() - u5d = u5.time_derivative() - u6d = u6.time_derivative() - u7d = u7.time_derivative() - u8d = u8.time_derivative() - u9d = u9.time_derivative() - u10d = u10.time_derivative() - - #import function - f_1 = np.vectorize(steer_torque_slip) - f_2 = np.vectorize(contact_forces_slip) - - f_3 = np.vectorize(contact_forces_nonslip) - - #calculation - #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - - #attributes: name and units, and taskSignals - T4_slip.name = 'SteerTorque_Slip' - T4_slip.units = 'newton*meter' - self.taskSignals[T4_slip.name] = T4_slip - - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' - Fx_r_s.units = 'newton' - self.taskSignals[Fx_r_s.name] = Fx_r_s - - Fy_r_s.name = 'LateralRearContactForce_Slip' - Fy_r_s.units = 'newton' - self.taskSignals[Fy_r_s.name] = Fy_r_s - - Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' - Fx_f_s.units = 'newton' - self.taskSignals[Fx_f_s.name] = Fx_f_s - - Fy_f_s.name = 'LateralFrontContactForce_Slip' - Fy_f_s.units = 'newton' - self.taskSignals[Fy_f_s.name] = Fy_f_s - - Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' - Fx_r_ns.units = 'newton' - self.taskSignals[Fx_r_ns.name] = Fx_r_ns - - Fy_r_ns.name = 'LateralRearContactForce_Nonslip' - Fy_r_ns.units = 'newton' - self.taskSignals[Fy_r_ns.name] = Fy_r_ns - - Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' - Fx_f_ns.units = 'newton' - self.taskSignals[Fx_f_ns.name] = Fx_f_ns - - Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' - Fy_f_ns.units = 'newton' - self.taskSignals[Fy_f_ns.name] = Fy_f_ns -======= + def compute_front_wheel_rate(self): + """Calculates the front wheel rate, based on the Jason's data + of front_wheel_contact_point. Alternatively, you can use the + sympy to get the front_wheel_contact_rate directly first.""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + bp = self.bicycleRiderParameters + + lam = bp['lam'] + rF = bp['rF'] + + q4_wheel = q4 * cos(lam) * cos(q2) + + q4_wheel.name = 'SteerAngle_FrontWheel' + q4_wheel.units = 'radian' + self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel + + f = np.vectorize(front_wheel_rate) + + u6 = f(q1, q2, q4, u9, u10, lam, rF) + u6.name = 'FrontWheelRate' + u6.units = 'radian/second' + self.taskSignals['FrontWheelRate'] = u6 + + def compute_front_rear_wheel_contact_forces(self): + """Calculate the contact forces for each + wheel with respect to inertial frame under + the slip and nonslip condition. Also, provide the steer torque T4.""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + rf = mp['rf'] + + #signals assignment --slip condition + V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() + #V = self.taskSignals['ForwardSpeed'] + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = u7.time_derivative() + u8d = u8.time_derivative() + u9d = u9.time_derivative() + u10d = u10.time_derivative() + + #import function + f_1 = np.vectorize(steer_torque_slip) + f_2 = np.vectorize(contact_forces_slip) + + f_3 = np.vectorize(contact_forces_nonslip) + + #calculation + #steer torque slip + T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + + #attributes: name and units, and taskSignals + T4_slip.name = 'SteerTorque_Slip' + T4_slip.units = 'newton*meter' + self.taskSignals[T4_slip.name] = T4_slip + + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s + + Fy_r_s.name = 'LateralRearContactForce_Slip' + Fy_r_s.units = 'newton' + self.taskSignals[Fy_r_s.name] = Fy_r_s + + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' + Fx_f_s.units = 'newton' + self.taskSignals[Fx_f_s.name] = Fx_f_s + + Fy_f_s.name = 'LateralFrontContactForce_Slip' + Fy_f_s.units = 'newton' + self.taskSignals[Fy_f_s.name] = Fy_f_s + + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' + Fx_r_ns.units = 'newton' + self.taskSignals[Fx_r_ns.name] = Fx_r_ns + + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' + Fy_r_ns.units = 'newton' + self.taskSignals[Fy_r_ns.name] = Fy_r_ns + + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' + Fx_f_ns.units = 'newton' + self.taskSignals[Fx_f_ns.name] = Fx_f_ns + + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' + Fy_f_ns.units = 'newton' + self.taskSignals[Fy_f_ns.name] = Fy_f_ns + def compute_front_wheel_yaw_angle(self): """Calculates the yaw angle of the front wheel.""" @@ -1212,8 +1206,6 @@ def compute_contact_points_acceleration(self): self.taskSignals[u9d.name] = u9d self.taskSignals[u10d.name] = u10d self.taskSignals[u12d.name] = u12d ->>>>>>> origin/con_force_I - def compute_rear_wheel_contact_rates(self): """Calculates the rates of the wheel contact points in the ground From 71466867f3a9afc1c22dfec531aff123a4e431da Mon Sep 17 00:00:00 2001 From: StefenYin Date: Sun, 14 Oct 2012 11:55:44 -0700 Subject: [PATCH 28/37] Deleted the frame_acceleration func derived from VN-100 acceleration data, but saved it to bi_experiment directory --- bicycledataprocessor/signalprocessing.py | 1095 +++++++++++----------- 1 file changed, 526 insertions(+), 569 deletions(-) diff --git a/bicycledataprocessor/signalprocessing.py b/bicycledataprocessor/signalprocessing.py index acfa366..aec7749 100644 --- a/bicycledataprocessor/signalprocessing.py +++ b/bicycledataprocessor/signalprocessing.py @@ -1,11 +1,11 @@ #!/usr/bin/env python #try: - #from IPython.core.debugger import Tracer + #from IPython.core.debugger import Tracer #except ImportError: - #pass + #pass #else: - #set_trace = Tracer() + #set_trace = Tracer() from warnings import warn @@ -22,598 +22,555 @@ #from signalprocessing import * def find_bump(accelSignal, sampleRate, speed, wheelbase, bumpLength): - '''Returns the indices that surround the bump in the acceleration signal. - - Parameters - ---------- - accelSignal : ndarray, shape(n,) - An acceleration signal with a single distinctive large acceleration - that signifies riding over the bump. - sampleRate : float - The sample rate of the signal. - speed : float - Speed of travel (or treadmill) in meters per second. - wheelbase : float - Wheelbase of the bicycle in meters. - bumpLength : float - Length of the bump in meters. - - Returns - ------- - indices : tuple - The first and last indice of the bump section. - - ''' - - # Find the indice to the maximum absolute acceleration in the provided - # signal. This is mostly likely where the bump is. Skip the first few - # points in case there are some endpoint problems with filtered data. - n = len(accelSignal) - nSkip = 5 - rectAccel = abs(subtract_mean(accelSignal[nSkip:n / 2.])) - indice = np.nanargmax(rectAccel) + nSkip - - # This calculates how many time samples it takes for the bicycle to roll - # over the bump given the speed of the bicycle, it's wheelbase, the bump - # length and the sample rate. - bumpDuration = (wheelbase + bumpLength) / speed - bumpSamples = int(bumpDuration * sampleRate) - # make the number divisible by four - bumpSamples = int(bumpSamples / 4) * 4 - - # These indices try to capture the length of the bump based on the max - # acceleration indice. - indices = (indice - bumpSamples / 4, indice, indice + 3 * bumpSamples / 4) - - # If the maximum acceleration is not greater than 0.5 m/s**2, then there was - # probably was no bump collected in the acceleration data. - maxChange = rectAccel[indice - nSkip] - if maxChange < 0.5: - warn('This run does not have a bump that is easily detectable. ' + - 'The bump only gave a {:1.2f} m/s^2 change in nominal accerelation.\n'\ - .format(maxChange) + - 'The bump indice is {} and the bump time is {:1.2f} seconds.'\ - .format(str(indice), indice / float(sampleRate))) - return None - else: - # If the bump isn't at the beginning of the run, give a warning. - if indice > n / 3.: - warn("This signal's max value is not in the first third of the data\n" - + "It is at %1.2f seconds out of %1.2f seconds" % - (indice / float(sampleRate), n / float(sampleRate))) - - # If there is a nan in the bump this maybe an issue down the line as the - # it is prefferable for the bump to be in the data when the fitting occurs, - # to get a better fit. - if np.isnan(accelSignal[indices[0]:indices[1]]).any(): - warn('There is at least one NaN in this bump') - - return indices + '''Returns the indices that surround the bump in the acceleration signal. + + Parameters + ---------- + accelSignal : ndarray, shape(n,) + An acceleration signal with a single distinctive large acceleration + that signifies riding over the bump. + sampleRate : float + The sample rate of the signal. + speed : float + Speed of travel (or treadmill) in meters per second. + wheelbase : float + Wheelbase of the bicycle in meters. + bumpLength : float + Length of the bump in meters. + + Returns + ------- + indices : tuple + The first and last indice of the bump section. + + ''' + + # Find the indice to the maximum absolute acceleration in the provided + # signal. This is mostly likely where the bump is. Skip the first few + # points in case there are some endpoint problems with filtered data. + n = len(accelSignal) + nSkip = 5 + rectAccel = abs(subtract_mean(accelSignal[nSkip:n / 2.])) + indice = np.nanargmax(rectAccel) + nSkip + + # This calculates how many time samples it takes for the bicycle to roll + # over the bump given the speed of the bicycle, it's wheelbase, the bump + # length and the sample rate. + bumpDuration = (wheelbase + bumpLength) / speed + bumpSamples = int(bumpDuration * sampleRate) + # make the number divisible by four + bumpSamples = int(bumpSamples / 4) * 4 + + # These indices try to capture the length of the bump based on the max + # acceleration indice. + indices = (indice - bumpSamples / 4, indice, indice + 3 * bumpSamples / 4) + + # If the maximum acceleration is not greater than 0.5 m/s**2, then there was + # probably was no bump collected in the acceleration data. + maxChange = rectAccel[indice - nSkip] + if maxChange < 0.5: + warn('This run does not have a bump that is easily detectable. ' + + 'The bump only gave a {:1.2f} m/s^2 change in nominal accerelation.\n'\ + .format(maxChange) + + 'The bump indice is {} and the bump time is {:1.2f} seconds.'\ + .format(str(indice), indice / float(sampleRate))) + return None + else: + # If the bump isn't at the beginning of the run, give a warning. + if indice > n / 3.: + warn("This signal's max value is not in the first third of the data\n" + + "It is at %1.2f seconds out of %1.2f seconds" % + (indice / float(sampleRate), n / float(sampleRate))) + + # If there is a nan in the bump this maybe an issue down the line as the + # it is prefferable for the bump to be in the data when the fitting occurs, + # to get a better fit. + if np.isnan(accelSignal[indices[0]:indices[1]]).any(): + warn('There is at least one NaN in this bump') + + return indices def split_around_nan(sig): - ''' - Returns the sections of an array not polluted with nans. - - Parameters - ---------- - sig : ndarray, shape(n,) - A one dimensional array that may or may not contain m nan values where - 0 <= m <= n. - - Returns - ------- - indices : list, len(indices) = k - List of tuples containing the indices for the sections of the array. - arrays : list, len(indices) = k - List of section arrays. All arrays of nan values are of dimension 1. - - k = number of non-nan sections + number of nans - - sig[indices[k][0]:indices[k][1]] == arrays[k] - - ''' - # if there are any nans then split the signal - if np.isnan(sig).any(): - firstSplit = np.split(sig, np.nonzero(np.isnan(sig))[0]) - arrays = [] - for arr in firstSplit: - # if the array has nans, then split it again - if np.isnan(arr).any(): - arrays = arrays + np.split(arr, np.nonzero(np.isnan(arr))[0] + 1) - # if it doesn't have nans, then just add it as is - else: - arrays.append(arr) - # remove any empty arrays - emptys = [i for i, arr in enumerate(arrays) if arr.shape[0] == 0] - arrays = [arr for i, arr in enumerate(arrays) if i not in emptys] - # build the indices list - indices = [] - count = 0 - for i, arr in enumerate(arrays): - count += len(arr) - if np.isnan(arr).any(): - indices.append((count - 1, count)) - else: - indices.append((count - len(arr), count)) - else: - arrays, indices = [sig], [(0, len(sig))] - - return indices, arrays + ''' + Returns the sections of an array not polluted with nans. + + Parameters + ---------- + sig : ndarray, shape(n,) + A one dimensional array that may or may not contain m nan values where + 0 <= m <= n. + + Returns + ------- + indices : list, len(indices) = k + List of tuples containing the indices for the sections of the array. + arrays : list, len(indices) = k + List of section arrays. All arrays of nan values are of dimension 1. + + k = number of non-nan sections + number of nans + + sig[indices[k][0]:indices[k][1]] == arrays[k] + + ''' + # if there are any nans then split the signal + if np.isnan(sig).any(): + firstSplit = np.split(sig, np.nonzero(np.isnan(sig))[0]) + arrays = [] + for arr in firstSplit: + # if the array has nans, then split it again + if np.isnan(arr).any(): + arrays = arrays + np.split(arr, np.nonzero(np.isnan(arr))[0] + 1) + # if it doesn't have nans, then just add it as is + else: + arrays.append(arr) + # remove any empty arrays + emptys = [i for i, arr in enumerate(arrays) if arr.shape[0] == 0] + arrays = [arr for i, arr in enumerate(arrays) if i not in emptys] + # build the indices list + indices = [] + count = 0 + for i, arr in enumerate(arrays): + count += len(arr) + if np.isnan(arr).any(): + indices.append((count - 1, count)) + else: + indices.append((count - len(arr), count)) + else: + arrays, indices = [sig], [(0, len(sig))] + + return indices, arrays def steer_rate(forkRate, angularRateZ): - '''Returns the steer rate. - - Parameters - ---------- - forkRate : ndarray, size(n,) - The rate of the fork about the steer axis relative to the Newtonian - reference frame. - angularRateZ : ndarray, size(n,) - The rate of the bicycle frame about the steer axis in the Newtonian - reference frame. - - Returns - ------- - steerRate : ndarray, size(n,) - The rate of the fork about the steer axis relative to the bicycle - frame. - - Notes - ----- - The rates are defined such that a positive rate is pointing downward along - the steer axis. - - ''' - return forkRate - angularRateZ + '''Returns the steer rate. + + Parameters + ---------- + forkRate : ndarray, size(n,) + The rate of the fork about the steer axis relative to the Newtonian + reference frame. + angularRateZ : ndarray, size(n,) + The rate of the bicycle frame about the steer axis in the Newtonian + reference frame. + + Returns + ------- + steerRate : ndarray, size(n,) + The rate of the fork about the steer axis relative to the bicycle + frame. + + Notes + ----- + The rates are defined such that a positive rate is pointing downward along + the steer axis. + + ''' + return forkRate - angularRateZ def steer_torque_components(frameAngRate, frameAngAccel, frameAccel, - handlebarAngRate, handlebarAngAccel, steerAngle, steerColumnTorque, - handlebarMass, handlebarInertia, damping, friction, d, ds): - """Returns the components of the steer torque applied by the rider. - - Parameters - ---------- - frameAngRate : ndarray, shape(3,n) - The angular velocity of the bicycle frame in the Newtonian frame - expressed in body fixed coordinates. - frameAngAccel : ndarray, shape(3,n) - The angular acceleration of the bicycle frame in the Newtonian frame - expressed in body fixed coordinates. - frameAccel : ndarray, shape(3,n) - The linear acceleration of the frame accelerometer in the Newtonian - frame expressed in body fixed coordinates. - handlebarAngRate : ndarray, shape(n,) - The component of angular rate of the handlebar in the Newtonian frame - expressed in body fixed coordinates about the steer axis. - handlebarAngAccel : ndarray, shape(n,) - The component of angular acceleration of the handlebar in the Newtonian - frame expressed in body fixed coordinates about the steer axis. - steerAngle : ndarray, shape(n,) - The angle of the steer column relative to the frame about the steer - axis. - steerColumnTorque : ndarray, shape(n,) - The torque measured on the steer column between the handlebars and the - fork and between the upper and lower bearings. - handlebarMass : float - The mass of the handlebar. - handlebarInertia : ndarray, shape(3,3) - The inertia tensor of the handlebars. Includes everything above and - including the steer tube torque sensor. This is relative to a reference - frame aligned with the steer axis and is about the center of mass. - damping : float - The damping coefficient associated with the total (upper and lower) - bearing friction. - friction : float - The columb friction associated with the total (upper and lower) bearing - friction. - d : float - The distance from the handlebar center of mass to the steer axis. The - point on the steer axis is at the projection of the mass center onto - the axis. - ds : ndarray, shape(3,) - The distance from the accelerometer to the point on the steer axis. - plot : boolean, optional - If true a plot of the components of the steer torque will be shown. - - Returns - ------- - components : dictionary - A dictionary containing the ten components of the rider applied steer - torque. - - Notes - ----- - The friction torque from the upper and lower bearings is halved because - only one bearing is associated with the handlebar rigid body. - - """ - wb1 = frameAngRate[0] - wb2 = frameAngRate[1] - wb3 = frameAngRate[2] - wb1p = frameAngAccel[0] - wb2p = frameAngAccel[1] - wb3p = frameAngAccel[2] - av1 = frameAccel[0] - av2 = frameAccel[1] - av3 = frameAccel[2] - wh3 = handlebarAngRate - wh3p = handlebarAngAccel - IH = handlebarInertia - mH = handlebarMass - delta = steerAngle - - components = {} - - components['Hdot1'] = -((IH[0, 0] * (wb1 * np.cos(delta) + - wb2 * np.sin(delta)) + - IH[2, 0] * wh3) * - (-wb1 * np.sin(delta) + wb2 * np.cos(delta))) - - components['Hdot2'] = (IH[1, 1] * - (-wb1 * np.sin(delta) + wb2 * np.cos(delta)) * - (wb1 * np.cos(delta) + wb2 * np.sin(delta))) - - components['Hdot3'] = IH[2, 2] * wh3p - - components['Hdot4'] = IH[2, 0] * (-(-wb3 + wh3) * wb1 * np.sin(delta) + - (-wb3 + wh3) * wb2 * np.cos(delta) + - np.sin(delta) * wb2p + np.cos(delta) * wb1p) - - components['cross1'] = d * mH * (d * (-wb1 * np.sin(delta) + wb2 * np.cos(delta)) * - (wb1 * np.cos(delta) + wb2 * np.sin(delta)) - + d * wh3p) - components['cross2'] = -d * mH * (-ds[0] * wb2 ** 2 + ds[2] * wb2p - - (ds[0] * wb3 - ds[2] * wb1) * wb3 + av1) * np.sin(delta) - components['cross3'] = d * mH * (ds[0] * wb1 * wb2 + ds[0] * wb3p + ds[2] * wb2 * - wb3 - ds[2] * wb1p + av2) * np.cos(delta) - components['viscous'] = (damping * (-wb3 + wh3)) / 2. - components['coulomb'] = np.sign(-wb3 + wh3) * friction / 2. - components['steerColumn'] = steerColumnTorque - - return components + handlebarAngRate, handlebarAngAccel, steerAngle, steerColumnTorque, + handlebarMass, handlebarInertia, damping, friction, d, ds): + """Returns the components of the steer torque applied by the rider. + + Parameters + ---------- + frameAngRate : ndarray, shape(3,n) + The angular velocity of the bicycle frame in the Newtonian frame + expressed in body fixed coordinates. + frameAngAccel : ndarray, shape(3,n) + The angular acceleration of the bicycle frame in the Newtonian frame + expressed in body fixed coordinates. + frameAccel : ndarray, shape(3,n) + The linear acceleration of the frame accelerometer in the Newtonian + frame expressed in body fixed coordinates. + handlebarAngRate : ndarray, shape(n,) + The component of angular rate of the handlebar in the Newtonian frame + expressed in body fixed coordinates about the steer axis. + handlebarAngAccel : ndarray, shape(n,) + The component of angular acceleration of the handlebar in the Newtonian + frame expressed in body fixed coordinates about the steer axis. + steerAngle : ndarray, shape(n,) + The angle of the steer column relative to the frame about the steer + axis. + steerColumnTorque : ndarray, shape(n,) + The torque measured on the steer column between the handlebars and the + fork and between the upper and lower bearings. + handlebarMass : float + The mass of the handlebar. + handlebarInertia : ndarray, shape(3,3) + The inertia tensor of the handlebars. Includes everything above and + including the steer tube torque sensor. This is relative to a reference + frame aligned with the steer axis and is about the center of mass. + damping : float + The damping coefficient associated with the total (upper and lower) + bearing friction. + friction : float + The columb friction associated with the total (upper and lower) bearing + friction. + d : float + The distance from the handlebar center of mass to the steer axis. The + point on the steer axis is at the projection of the mass center onto + the axis. + ds : ndarray, shape(3,) + The distance from the accelerometer to the point on the steer axis. + plot : boolean, optional + If true a plot of the components of the steer torque will be shown. + + Returns + ------- + components : dictionary + A dictionary containing the ten components of the rider applied steer + torque. + + Notes + ----- + The friction torque from the upper and lower bearings is halved because + only one bearing is associated with the handlebar rigid body. + + """ + wb1 = frameAngRate[0] + wb2 = frameAngRate[1] + wb3 = frameAngRate[2] + wb1p = frameAngAccel[0] + wb2p = frameAngAccel[1] + wb3p = frameAngAccel[2] + av1 = frameAccel[0] + av2 = frameAccel[1] + av3 = frameAccel[2] + wh3 = handlebarAngRate + wh3p = handlebarAngAccel + IH = handlebarInertia + mH = handlebarMass + delta = steerAngle + + components = {} + + components['Hdot1'] = -((IH[0, 0] * (wb1 * np.cos(delta) + + wb2 * np.sin(delta)) + + IH[2, 0] * wh3) * + (-wb1 * np.sin(delta) + wb2 * np.cos(delta))) + + components['Hdot2'] = (IH[1, 1] * + (-wb1 * np.sin(delta) + wb2 * np.cos(delta)) * + (wb1 * np.cos(delta) + wb2 * np.sin(delta))) + + components['Hdot3'] = IH[2, 2] * wh3p + + components['Hdot4'] = IH[2, 0] * (-(-wb3 + wh3) * wb1 * np.sin(delta) + + (-wb3 + wh3) * wb2 * np.cos(delta) + + np.sin(delta) * wb2p + np.cos(delta) * wb1p) + + components['cross1'] = d * mH * (d * (-wb1 * np.sin(delta) + wb2 * np.cos(delta)) * + (wb1 * np.cos(delta) + wb2 * np.sin(delta)) + + d * wh3p) + components['cross2'] = -d * mH * (-ds[0] * wb2 ** 2 + ds[2] * wb2p - + (ds[0] * wb3 - ds[2] * wb1) * wb3 + av1) * np.sin(delta) + components['cross3'] = d * mH * (ds[0] * wb1 * wb2 + ds[0] * wb3p + ds[2] * wb2 * + wb3 - ds[2] * wb1p + av2) * np.cos(delta) + components['viscous'] = (damping * (-wb3 + wh3)) / 2. + components['coulomb'] = np.sign(-wb3 + wh3) * friction / 2. + components['steerColumn'] = steerColumnTorque + + return components def steer_torque(components): - """Returns the steer torque given the components. + """Returns the steer torque given the components. - Parameters - ---------- - components : dictionary - A dictionary containing the ten components of the rider applied steer - torque. + Parameters + ---------- + components : dictionary + A dictionary containing the ten components of the rider applied steer + torque. - Returns - ------- - steerTorque : ndarray, shape(n,) - The steer torque applied by the rider. + Returns + ------- + steerTorque : ndarray, shape(n,) + The steer torque applied by the rider. - """ + """ - return np.sum(components.values(), axis=0) + return np.sum(components.values(), axis=0) def rear_wheel_contact_rate(rearRadius, rearWheelRate, yawAngle): - """Returns the longitudinal and lateral components of the velocity of the - rear wheel contact in the ground plane. - - Parameters - ---------- - rearRadius : float - The radius of the rear wheel. - rearWheelRate : ndarray, shape(n,) - The rear wheel rotation rate. - yawAngle : ndarray, shape(n,) - The yaw angle of the bicycle frame. - - Returns - ------- - longitudinal : ndarray, shape(n,) - The longitudinal deviation of the rear wheel contact point. - lateral : ndarray, shape(n,) - The lateral deviation of the rear wheel contact point. - - """ - longitudinal = -rearWheelRate * rearRadius * np.cos(yawAngle) - lateral = -rearWheelRate * rearRadius * np.sin(yawAngle) - return longitudinal, lateral + """Returns the longitudinal and lateral components of the velocity of the + rear wheel contact in the ground plane. + + Parameters + ---------- + rearRadius : float + The radius of the rear wheel. + rearWheelRate : ndarray, shape(n,) + The rear wheel rotation rate. + yawAngle : ndarray, shape(n,) + The yaw angle of the bicycle frame. + + Returns + ------- + longitudinal : ndarray, shape(n,) + The longitudinal deviation of the rear wheel contact point. + lateral : ndarray, shape(n,) + The lateral deviation of the rear wheel contact point. + + """ + longitudinal = -rearWheelRate * rearRadius * np.cos(yawAngle) + lateral = -rearWheelRate * rearRadius * np.sin(yawAngle) + return longitudinal, lateral def sync_error(tau, signal1, signal2, time): - '''Returns the error between two signal time histories. - - Parameters - ---------- - tau : float - The time shift. - signal1 : ndarray, shape(n,) - The signal that will be interpolated. This signal is - typically "cleaner" that signal2 and/or has a higher sample rate. - signal2 : ndarray, shape(n,) - The signal that will be shifted to syncronize with signal 1. - time : ndarray, shape(n,) - The time vector for the two signals - - Returns - ------- - error : float - Error between the two signals for the given tau. - - ''' - # make sure tau isn't too large - if np.abs(tau) >= time[-1]: - raise TimeShiftError(('abs(tau), {0}, must be less than or equal to ' + - '{1}').format(str(np.abs(tau)), str(time[-1]))) - - # this is the time for the second signal which is assumed to lag the first - # signal - shiftedTime = time + tau - - # create time vector where the two signals overlap - if tau > 0: - intervalTime = shiftedTime[np.nonzero(shiftedTime < time[-1])] - else: - intervalTime = shiftedTime[np.nonzero(shiftedTime > time[0])] - - # interpolate between signal 1 samples to find points that correspond in - # time to signal 2 on the shifted time - sig1OnInterval = np.interp(intervalTime, time, signal1); - - # truncate signal 2 to the time interval - if tau > 0: - sig2OnInterval = signal2[np.nonzero(shiftedTime <= intervalTime[-1])] - else: - sig2OnInterval = signal2[np.nonzero(shiftedTime >= intervalTime[0])] - - # calculate the error between the two signals - error = np.linalg.norm(sig1OnInterval - sig2OnInterval) - - return error + '''Returns the error between two signal time histories. + + Parameters + ---------- + tau : float + The time shift. + signal1 : ndarray, shape(n,) + The signal that will be interpolated. This signal is + typically "cleaner" that signal2 and/or has a higher sample rate. + signal2 : ndarray, shape(n,) + The signal that will be shifted to syncronize with signal 1. + time : ndarray, shape(n,) + The time vector for the two signals + + Returns + ------- + error : float + Error between the two signals for the given tau. + + ''' + # make sure tau isn't too large + if np.abs(tau) >= time[-1]: + raise TimeShiftError(('abs(tau), {0}, must be less than or equal to ' + + '{1}').format(str(np.abs(tau)), str(time[-1]))) + + # this is the time for the second signal which is assumed to lag the first + # signal + shiftedTime = time + tau + + # create time vector where the two signals overlap + if tau > 0: + intervalTime = shiftedTime[np.nonzero(shiftedTime < time[-1])] + else: + intervalTime = shiftedTime[np.nonzero(shiftedTime > time[0])] + + # interpolate between signal 1 samples to find points that correspond in + # time to signal 2 on the shifted time + sig1OnInterval = np.interp(intervalTime, time, signal1); + + # truncate signal 2 to the time interval + if tau > 0: + sig2OnInterval = signal2[np.nonzero(shiftedTime <= intervalTime[-1])] + else: + sig2OnInterval = signal2[np.nonzero(shiftedTime >= intervalTime[0])] + + # calculate the error between the two signals + error = np.linalg.norm(sig1OnInterval - sig2OnInterval) + + return error def find_timeshift(niAcc, vnAcc, sampleRate, speed, plotError=False): - '''Returns the timeshift, tau, of the VectorNav [VN] data relative to the - National Instruments [NI] data. - - Parameters - ---------- - niAcc : ndarray, shape(n, ) - The acceleration of the NI accelerometer in its local Y direction. - vnAcc : ndarray, shape(n, ) - The acceleration of the VN-100 in its local Z direction. Should be the - same length as NIacc and contains the same signal albiet time shifted. - The VectorNav signal should be leading the NI signal. - sampleRate : integer or float - Sample rate of the signals. This should be the same for each signal. - speed : float - The approximate forward speed of the bicycle. - - Returns - ------- - tau : float - The timeshift. - - Notes - ----- - The Z direction for `VNacc` is assumed to be aligned with the steer axis - and pointing down and the Y direction for the NI accelerometer should be - aligned with the steer axis and pointing up. - - ''' - # raise an error if the signals are not the same length - N = len(niAcc) - if N != len(vnAcc): - raise TimeShiftError('Signals are not the same length!') - - # make a time vector - time = time_vector(N, sampleRate) - - # the signals are opposite sign of each other, so fix that - niSig = -niAcc - vnSig = vnAcc - - # some constants for find_bump - wheelbase = 1.02 # this is the wheelbase of the rigid rider bike - bumpLength = 1. - cutoff = 30. - # filter the NI Signal - filNiSig = butterworth(niSig, cutoff, sampleRate) - # find the bump in the filtered NI signal - niBump = find_bump(filNiSig, sampleRate, speed, wheelbase, bumpLength) - - # remove the nan's in the VN signal and the corresponding time - v = vnSig[np.nonzero(np.isnan(vnSig) == False)] - t = time[np.nonzero(np.isnan(vnSig) == False)] - # fit a spline through the data - vn_spline = UnivariateSpline(t, v, k=3, s=0) - # and filter it - filVnSig = butterworth(vn_spline(time), cutoff, sampleRate) - # and find the bump in the filtered VN signal - vnBump = find_bump(filVnSig, sampleRate, speed, wheelbase, bumpLength) - - if vnBump is None or niBump is None: - guess = 0.3 - else: - # get an initial guess for the time shift based on the bump indice - guess = (niBump[1] - vnBump[1]) / float(sampleRate) - - # Since vnSig may have nans we should only use contiguous data around - # around the bump. The first step is to split vnSig into sections bounded - # by the nans and then selected the section in which the bump falls. Then - # we select a similar area in niSig to run the time shift algorithm on. - if vnBump is None: - bumpLocation = 800 # just a random guess so things don't crash - else: - bumpLocation = vnBump[1] - indices, arrays = split_around_nan(vnSig) - for pair in indices: - if pair[0] <= bumpLocation < pair[1]: - bSec = pair - - # subtract the mean and normalize both signals - niSig = normalize(subtract_mean(niSig, hasNans=True), hasNans=True) - vnSig = normalize(subtract_mean(vnSig, hasNans=True), hasNans=True) - - niBumpSec = niSig[bSec[0]:bSec[1]] - vnBumpSec = vnSig[bSec[0]:bSec[1]] - timeBumpSec = time[bSec[0]:bSec[1]] - - if len(niBumpSec) < 200: - warn('The bump section is only {} samples wide.'.format(str(len(niBumpSec)))) - - # set up the error landscape, error vs tau - # The NI lags the VectorNav and the time shift is typically between 0 and - # 1 seconds - tauRange = np.linspace(0., 2., num=500) - error = np.zeros_like(tauRange) - for i, val in enumerate(tauRange): - error[i] = sync_error(val, niBumpSec, vnBumpSec, timeBumpSec) - - if plotError: - plt.figure() - plt.plot(tauRange, error) - plt.xlabel('tau') - plt.ylabel('error') - plt.show() - - # find initial condition from landscape - tau0 = tauRange[np.argmin(error)] - - print "The minimun of the error landscape is %f and the provided guess is %f" % (tau0, guess) - - # Compute the minimum of the function using both the result from the error - # landscape and the bump find for initial guesses to the minimizer. Choose - # the best of the two. - tauBump, fvalBump = fmin(sync_error, guess, args=(niBumpSec, - vnBumpSec, timeBumpSec), full_output=True, disp=True)[0:2] - - tauLandscape, fvalLandscape = fmin(sync_error, tau0, args=(niBumpSec, vnBumpSec, - timeBumpSec), full_output=True, disp=True)[0:2] - - if fvalBump < fvalLandscape: - tau = tauBump - else: - tau = tauLandscape - - #### if the minimization doesn't do a good job, just use the tau0 - ###if np.abs(tau - tau0) > 0.01: - ###tau = tau0 - ###print "Bad minimizer!! Using the guess, %f, instead." % tau - - print "This is what came out of the minimization:", tau - - if not (0.05 < tau < 2.0): - raise TimeShiftError('This tau, {} s, is probably wrong'.format(str(tau))) - - return tau + '''Returns the timeshift, tau, of the VectorNav [VN] data relative to the + National Instruments [NI] data. + + Parameters + ---------- + niAcc : ndarray, shape(n, ) + The acceleration of the NI accelerometer in its local Y direction. + vnAcc : ndarray, shape(n, ) + The acceleration of the VN-100 in its local Z direction. Should be the + same length as NIacc and contains the same signal albiet time shifted. + The VectorNav signal should be leading the NI signal. + sampleRate : integer or float + Sample rate of the signals. This should be the same for each signal. + speed : float + The approximate forward speed of the bicycle. + + Returns + ------- + tau : float + The timeshift. + + Notes + ----- + The Z direction for `VNacc` is assumed to be aligned with the steer axis + and pointing down and the Y direction for the NI accelerometer should be + aligned with the steer axis and pointing up. + + ''' + # raise an error if the signals are not the same length + N = len(niAcc) + if N != len(vnAcc): + raise TimeShiftError('Signals are not the same length!') + + # make a time vector + time = time_vector(N, sampleRate) + + # the signals are opposite sign of each other, so fix that + niSig = -niAcc + vnSig = vnAcc + + # some constants for find_bump + wheelbase = 1.02 # this is the wheelbase of the rigid rider bike + bumpLength = 1. + cutoff = 30. + # filter the NI Signal + filNiSig = butterworth(niSig, cutoff, sampleRate) + # find the bump in the filtered NI signal + niBump = find_bump(filNiSig, sampleRate, speed, wheelbase, bumpLength) + + # remove the nan's in the VN signal and the corresponding time + v = vnSig[np.nonzero(np.isnan(vnSig) == False)] + t = time[np.nonzero(np.isnan(vnSig) == False)] + # fit a spline through the data + vn_spline = UnivariateSpline(t, v, k=3, s=0) + # and filter it + filVnSig = butterworth(vn_spline(time), cutoff, sampleRate) + # and find the bump in the filtered VN signal + vnBump = find_bump(filVnSig, sampleRate, speed, wheelbase, bumpLength) + + if vnBump is None or niBump is None: + guess = 0.3 + else: + # get an initial guess for the time shift based on the bump indice + guess = (niBump[1] - vnBump[1]) / float(sampleRate) + + # Since vnSig may have nans we should only use contiguous data around + # around the bump. The first step is to split vnSig into sections bounded + # by the nans and then selected the section in which the bump falls. Then + # we select a similar area in niSig to run the time shift algorithm on. + if vnBump is None: + bumpLocation = 800 # just a random guess so things don't crash + else: + bumpLocation = vnBump[1] + indices, arrays = split_around_nan(vnSig) + for pair in indices: + if pair[0] <= bumpLocation < pair[1]: + bSec = pair + + # subtract the mean and normalize both signals + niSig = normalize(subtract_mean(niSig, hasNans=True), hasNans=True) + vnSig = normalize(subtract_mean(vnSig, hasNans=True), hasNans=True) + + niBumpSec = niSig[bSec[0]:bSec[1]] + vnBumpSec = vnSig[bSec[0]:bSec[1]] + timeBumpSec = time[bSec[0]:bSec[1]] + + if len(niBumpSec) < 200: + warn('The bump section is only {} samples wide.'.format(str(len(niBumpSec)))) + + # set up the error landscape, error vs tau + # The NI lags the VectorNav and the time shift is typically between 0 and + # 1 seconds + tauRange = np.linspace(0., 2., num=500) + error = np.zeros_like(tauRange) + for i, val in enumerate(tauRange): + error[i] = sync_error(val, niBumpSec, vnBumpSec, timeBumpSec) + + if plotError: + plt.figure() + plt.plot(tauRange, error) + plt.xlabel('tau') + plt.ylabel('error') + plt.show() + + # find initial condition from landscape + tau0 = tauRange[np.argmin(error)] + + print "The minimun of the error landscape is %f and the provided guess is %f" % (tau0, guess) + + # Compute the minimum of the function using both the result from the error + # landscape and the bump find for initial guesses to the minimizer. Choose + # the best of the two. + tauBump, fvalBump = fmin(sync_error, guess, args=(niBumpSec, + vnBumpSec, timeBumpSec), full_output=True, disp=True)[0:2] + + tauLandscape, fvalLandscape = fmin(sync_error, tau0, args=(niBumpSec, vnBumpSec, + timeBumpSec), full_output=True, disp=True)[0:2] + + if fvalBump < fvalLandscape: + tau = tauBump + else: + tau = tauLandscape + + #### if the minimization doesn't do a good job, just use the tau0 + ###if np.abs(tau - tau0) > 0.01: + ###tau = tau0 + ###print "Bad minimizer!! Using the guess, %f, instead." % tau + + print "This is what came out of the minimization:", tau + + if not (0.05 < tau < 2.0): + raise TimeShiftError('This tau, {} s, is probably wrong'.format(str(tau))) + + return tau def truncate_data(signal, tau): - ''' - Returns the truncated vectors with respect to the timeshift tau. + ''' + Returns the truncated vectors with respect to the timeshift tau. - Parameters - --------- - signal : Signal(ndarray), shape(n, ) - A time signal from the NIData or the VNavData. - tau : float - The time shift. + Parameters + --------- + signal : Signal(ndarray), shape(n, ) + A time signal from the NIData or the VNavData. + tau : float + The time shift. - Returns - ------- - truncated : ndarray, shape(m, ) - The truncated time signal. + Returns + ------- + truncated : ndarray, shape(m, ) + The truncated time signal. - ''' - t = time_vector(len(signal), signal.sampleRate) + ''' + t = time_vector(len(signal), signal.sampleRate) - # shift the ni data cause it is the cleaner signal - tni = t - tau - tvn = t + # shift the ni data cause it is the cleaner signal + tni = t - tau + tvn = t - # make the common time interval - tcom = tvn[np.nonzero(tvn < tni[-1])] + # make the common time interval + tcom = tvn[np.nonzero(tvn < tni[-1])] - if signal.source == 'NI': - truncated = np.interp(tcom, tni, signal) - elif signal.source == 'VN': - truncated = signal[np.nonzero(tvn <= tcom[-1])] - else: - raise ValueError('No source was defined in this signal.') + if signal.source == 'NI': + truncated = np.interp(tcom, tni, signal) + elif signal.source == 'VN': + truncated = signal[np.nonzero(tvn <= tcom[-1])] + else: + raise ValueError('No source was defined in this signal.') - return truncated + return truncated def yaw_roll_pitch_rate(angularRateX, angularRateY, angularRateZ, - lam, rollAngle=0.): - '''Returns the bicycle frame yaw, roll and pitch rates based on the body - fixed rate data taken with the VN-100 and optionally the roll angle - measurement. - - Parameters - ---------- - angularRateX : ndarray, shape(n,) - The body fixed rate perpendicular to the headtube and pointing forward. - angularRateY : ndarray, shape(n,) - The body fixed rate perpendicular to the headtube and pointing to the - right of the bicycle. - angularRateZ : ndarray, shape(n,) - The body fixed rate aligned with the headtube and pointing downward. - lam : float - The steer axis tilt. - rollAngle : ndarray, shape(n,), optional - The roll angle of the bicycle frame. - - Returns - ------- - yawRate : ndarray, shape(n,) - The yaw rate of the bicycle frame. - rollRate : ndarray, shape(n,) - The roll rate of the bicycle frame. - pitchRate : ndarray, shape(n,) - The pitch rate of the bicycle frame. - - ''' - yawRate = -(angularRateX*np.sin(lam) - - angularRateZ * np.cos(lam)) / np.cos(rollAngle) - rollRate = angularRateX * np.cos(lam) + angularRateZ * np.sin(lam) - pitchRate = (angularRateY + angularRateX * np.sin(lam) * np.tan(rollAngle) - - angularRateZ * np.cos(lam) * np.tan(rollAngle)) - - return yawRate, rollRate, pitchRate - -def frame_acceleration(AccelerationX, AccelerationY, AccelerationZ, - lam, rollAngle=0.): - '''Returns the bicycle frame acceleration based on the body - fixed acceleration taken with the VN-100 and optionally the roll angle - measurement. - - Parameters - ---------- - AccelerationX : ndarray, shape(n,) - The body fixed acceleration perpendicular to the headtube and pointing forward. - AccelerationY : ndarray, shape(n,) - The body fixed acceleration perpendicular to the headtube and pointing to the - right of the bicycle. - AccelerationZ : ndarray, shape(n,) - The body fixed acceleration aligned with the headtube and pointing downward. - lam : float - The steer axis tilt. - rollAngle : ndarray, shape(n,), optional - The roll angle of the bicycle frame. - - Returns - ------- - longitudinal_Acceleration : ndarray, shape(n,) - The forward acceleration of the bicycle frame in A['1']. - lateral_Acceleration : ndarray, shape(n,) - The lateral acceleration of the bicycle frame in A['2']. - downward_Acceleration : ndarray, shape(n,) - The downward acceleration of the bicycle frame in A['3']. - - ''' - longitudinal_Acceleration= AccelerationX * np.cos(lam) + AccelerationZ * np.sin(lam) - - lateral_Acceleration = np.cos(rollAngle) * (AccelerationY + AccelerationX * - np.sin(lam) * np.tan(rollAngle) - - AccelerationZ * np.cos(lam) * np.tan(rollAngle)) - - downward_Acceleration = -(AccelerationX*np.sin(lam) - - AccelerationY * np.tan(rollAngle) - - AccelerationZ * np.cos(lam)) * np.cos(rollAngle) - - - return longitudinal_Acceleration, lateral_Acceleration, downward_Acceleration + lam, rollAngle=0.): + '''Returns the bicycle frame yaw, roll and pitch rates based on the body + fixed rate data taken with the VN-100 and optionally the roll angle + measurement. + + Parameters + ---------- + angularRateX : ndarray, shape(n,) + The body fixed rate perpendicular to the headtube and pointing forward. + angularRateY : ndarray, shape(n,) + The body fixed rate perpendicular to the headtube and pointing to the + right of the bicycle. + angularRateZ : ndarray, shape(n,) + The body fixed rate aligned with the headtube and pointing downward. + lam : float + The steer axis tilt. + rollAngle : ndarray, shape(n,), optional + The roll angle of the bicycle frame. + + Returns + ------- + yawRate : ndarray, shape(n,) + The yaw rate of the bicycle frame. + rollRate : ndarray, shape(n,) + The roll rate of the bicycle frame. + pitchRate : ndarray, shape(n,) + The pitch rate of the bicycle frame. + + ''' + yawRate = -(angularRateX*np.sin(lam) - + angularRateZ * np.cos(lam)) / np.cos(rollAngle) + rollRate = angularRateX * np.cos(lam) + angularRateZ * np.sin(lam) + pitchRate = (angularRateY + angularRateX * np.sin(lam) * np.tan(rollAngle) - + angularRateZ * np.cos(lam) * np.tan(rollAngle)) + + return yawRate, rollRate, pitchRate From 7cde1477c3090f93ed9c6d74c4e4c75183ccea91 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Sun, 14 Oct 2012 11:58:19 -0700 Subject: [PATCH 29/37] Fixed still the merge conflicts --- bicycledataprocessor/main.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 7134640..381d364 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -24,15 +24,12 @@ from tables import NoSuchNodeError import dtk.process as process -<<<<<<< HEAD -from dtk.bicycle import front_contact, benchmark_to_moore, front_wheel_rate, steer_torque_slip, contact_forces_slip, contact_forces_nonslip -======= + from dtk.bicycle import front_contact, benchmark_to_moore from dtk.bicycle import front_wheel_yaw_angle, front_wheel_rate from dtk.bicycle import steer_torque_slip, contact_forces_slip, contact_forces_nonslip from dtk.bicycle import contact_points_acceleration ->>>>>>> origin/con_force_I import bicycleparameters as bp @@ -867,8 +864,6 @@ def compute_front_wheel_contact_points(self): self.taskSignals['LongitudinalFrontContact'] = q9 self.taskSignals['LateralFrontContact'] = q10 -<<<<<<< HEAD - def compute_front_wheel_rate(self): """Calculates the front wheel rate, based on the Jason's data of front_wheel_contact_point. Alternatively, you can use the From acbc4c2e937b4e9ea4465a1fee26d67d405a5fab Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 15 Oct 2012 17:15:27 -0700 Subject: [PATCH 30/37] Changed the data path to Toshiba computer --- defaults.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaults.cfg b/defaults.cfg index 793c870..b815bb2 100644 --- a/defaults.cfg +++ b/defaults.cfg @@ -1,5 +1,5 @@ [data] -base = /home/stefenyin/bicycle/bi_pypa +base = /home/stefenstudy/bicycle/bi_pypa pathToDatabase = %(base)s/BicycleDataProcessor/InstrumentedBicycleData.h5 pathToCorruption = %(base)s/BicycleDataProcessor/data-corruption.csv pathToRunMat = %(base)s/BicycleDAQ/data From f55ec5a8dcfd6c693439392976acf4805884e450 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 15 Oct 2012 17:19:28 -0700 Subject: [PATCH 31/37] Changed processedCols for new funcs --- bicycledataprocessor/database.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/bicycledataprocessor/database.py b/bicycledataprocessor/database.py index 0751c34..07bf9f7 100644 --- a/bicycledataprocessor/database.py +++ b/bicycledataprocessor/database.py @@ -93,9 +93,27 @@ def __init__(self, **kwargs): self.processedCols = ['FrameAccelerationX', 'FrameAccelerationY', 'FrameAccelerationZ', + 'LongitudinalRearContactAcceleration' + 'LateralRearContactAcceleration' + 'DownwardRearContactAcceleration' + 'LongitudinalFrontContactAcceleration' + 'LateralFrontContactAcceleration' + 'DownwardFrontContactAcceleration' + 'YawAngle' + 'FrontWheelYawAngle' + 'SteerTorque_Slip' + 'LongitudinalRearContactForce_Slip' + 'LateralRearContactForce_Slip' + 'LongitudinalFrontContactForce_Slip' + 'LateralFrontContactForce_Slip' + 'LongitudinalRearContactForce_Nonslip' + 'LateralRearContactForce_Nonslip' + 'LongitudinalFrontContactForce_Nonslip' + 'LateralFrontContactForce_Nonslip' 'PitchRate', 'PullForce', 'RearWheelRate', + 'FrontWheelRate', 'RollAngle', 'RollRate', 'ForwardSpeed', From 6cba40687898573ac34f2030eb8ea67119d52413 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 15 Oct 2012 17:30:27 -0700 Subject: [PATCH 32/37] Deleted two same funcs --- bicycledataprocessor/main.py | 128 ----------------------------------- 1 file changed, 128 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 381d364..960bfc3 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -864,134 +864,6 @@ def compute_front_wheel_contact_points(self): self.taskSignals['LongitudinalFrontContact'] = q9 self.taskSignals['LateralFrontContact'] = q10 - def compute_front_wheel_rate(self): - """Calculates the front wheel rate, based on the Jason's data - of front_wheel_contact_point. Alternatively, you can use the - sympy to get the front_wheel_contact_rate directly first.""" - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - bp = self.bicycleRiderParameters - - lam = bp['lam'] - rF = bp['rF'] - - q4_wheel = q4 * cos(lam) * cos(q2) - - q4_wheel.name = 'SteerAngle_FrontWheel' - q4_wheel.units = 'radian' - self.taskSignals['SteerAngle_FrontWheel'] = q4_wheel - - f = np.vectorize(front_wheel_rate) - - u6 = f(q1, q2, q4, u9, u10, lam, rF) - u6.name = 'FrontWheelRate' - u6.units = 'radian/second' - self.taskSignals['FrontWheelRate'] = u6 - - def compute_front_rear_wheel_contact_forces(self): - """Calculate the contact forces for each - wheel with respect to inertial frame under - the slip and nonslip condition. Also, provide the steer torque T4.""" - - #parameters - bp = self.bicycleRiderParameters - mp = benchmark_to_moore(self.bicycleRiderParameters) - - l1 = mp['l1'] - l2 = mp['l2'] - mc = mp['mc'] - ic11 = mp['ic11'] - ic22 = mp['ic22'] - ic33 = mp['ic33'] - ic31 = mp['ic31'] - rf = mp['rf'] - - #signals assignment --slip condition - V = '%1.2f' % self.taskSignals['ForwardSpeed'].mean() - #V = self.taskSignals['ForwardSpeed'] - - q1 = self.taskSignals['YawAngle'] - q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] - - u1 = self.taskSignals['YawRate'] - u2 = self.taskSignals['RollRate'] - u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] - u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] - u7 = self.taskSignals['LongitudinalRearContactRate'] - u8 = self.taskSignals['LateralRearContactRate'] - u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() - u10 = self.taskSignals['LateralFrontContact'].time_derivative() - - u1d = u1.time_derivative() - u2d = u2.time_derivative() - u3d = u3.time_derivative() - u4d = u4.time_derivative() - u5d = u5.time_derivative() - u6d = u6.time_derivative() - u7d = u7.time_derivative() - u8d = u8.time_derivative() - u9d = u9.time_derivative() - u10d = u10.time_derivative() - - #import function - f_1 = np.vectorize(steer_torque_slip) - f_2 = np.vectorize(contact_forces_slip) - - f_3 = np.vectorize(contact_forces_nonslip) - - #calculation - #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) - - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - - #attributes: name and units, and taskSignals - T4_slip.name = 'SteerTorque_Slip' - T4_slip.units = 'newton*meter' - self.taskSignals[T4_slip.name] = T4_slip - - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' - Fx_r_s.units = 'newton' - self.taskSignals[Fx_r_s.name] = Fx_r_s - - Fy_r_s.name = 'LateralRearContactForce_Slip' - Fy_r_s.units = 'newton' - self.taskSignals[Fy_r_s.name] = Fy_r_s - - Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' - Fx_f_s.units = 'newton' - self.taskSignals[Fx_f_s.name] = Fx_f_s - - Fy_f_s.name = 'LateralFrontContactForce_Slip' - Fy_f_s.units = 'newton' - self.taskSignals[Fy_f_s.name] = Fy_f_s - - Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' - Fx_r_ns.units = 'newton' - self.taskSignals[Fx_r_ns.name] = Fx_r_ns - - Fy_r_ns.name = 'LateralRearContactForce_Nonslip' - Fy_r_ns.units = 'newton' - self.taskSignals[Fy_r_ns.name] = Fy_r_ns - - Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' - Fx_f_ns.units = 'newton' - self.taskSignals[Fx_f_ns.name] = Fx_f_ns - - Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' - Fy_f_ns.units = 'newton' - self.taskSignals[Fy_f_ns.name] = Fy_f_ns - def compute_front_wheel_yaw_angle(self): """Calculates the yaw angle of the front wheel.""" From ed9e1a98bda1025f5c8a29cc17a0dfa0a24c7d42 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 15 Oct 2012 17:41:37 -0700 Subject: [PATCH 33/37] Changed the compute_frame_acceleration func into the one directly derived from truncatedSignals --- bicycledataprocessor/main.py | 67 ++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 960bfc3..35167d3 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -744,7 +744,7 @@ def task_signals(self): self.compute_front_wheel_yaw_angle() self.compute_front_wheel_rate() #self.compute_contact_points_acceleration() - #self.compute_front_rear_wheel_contact_forces() + #self.compute_contact_forces() self.topSig = 'task' @@ -768,11 +768,12 @@ def compute_signals(self): # compute the quantities that aren't task specific self.compute_pull_force() + self.compute_frame_acceleration() self.compute_forward_speed() self.compute_steer_rate() self.compute_yaw_roll_pitch_rates() self.compute_steer_torque() - self.compute_frame_acceleration() + def truncate_signals(self): """Truncates the calibrated signals based on the time shift.""" @@ -906,7 +907,7 @@ def compute_front_wheel_rate(self): self.taskSignals['FrontWheelRate'] = u6 - def compute_front_rear_wheel_contact_forces(self): + def compute_contact_forces(self): """Calculate the contact forces for each wheel with respect to inertial frame under the slip and nonslip condition. Also, provide @@ -1012,9 +1013,9 @@ def compute_contact_points_acceleration(self): """Calculates the acceleration of the contact points of front and rear wheels.""" - FLongAcc= self.taskSignals['FrameLongitudinalAcceleration'] - FLateralAcc = self.taskSignals['FrameLateralAcceleration'] - FDownAcc = self.taskSignals['FrameDownwardAcceleration'] + AccX= self.taskSignals['FrameAccelerationX'] + AccY = self.taskSignals['FrameAccelerationY'] + AccZ = self.taskSignals['FrameAccelerationZ'] q1 = self.taskSignals['YawAngle'] q2 = self.taskSignals['RollAngle'] @@ -1049,7 +1050,7 @@ def compute_contact_points_acceleration(self): f = np.vectorize(contact_points_acceleration) - u7d, u8d, u11d, u9d, u10d, u12d = f(FLongAcc, FLateralAcc, FDownAcc, + u7d, u8d, u11d, u9d, u10d, u12d = f(AccX, AccY, AccZ, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d, d1, d2, d3, rr, rf, s1, s3) @@ -1260,28 +1261,6 @@ def compute_yaw_roll_pitch_rates(self): self.computedSignals['RollRate'] = rr self.computedSignals['PitchRate'] = pr - def compute_frame_acceleration(self): - """Calculate the frame acceleration in inertial frame, expressed by the - the A frame coordinates which is after yaw movement.""" - AccelerationX = self.truncatedSignals['AccelerationX'].filter(10.) - AccelerationY = self.truncatedSignals['AccelerationY'].filter(10.) - AccelerationZ = self.truncatedSignals['AccelerationZ'].filter(10.) - rollAngle = self.truncatedSignals['RollAngle'].filter(10.) - lam = self.bicycleRiderParameters['lam'] - - AccX, AccY, AccZ = sigpro.frame_acceleration(AccelerationX, - AccelerationY, AccelerationZ, lam, rollAngle=rollAngle) - AccX.units = 'meter/second/second' - AccX.name = 'FrameLongitudinalAcceleration' - AccY.units = 'meter/second/second' - AccY.name = 'FrameLateralAcceleration' - AccZ.units = 'meter/second/second' - AccZ.name = 'FrameDownwardAcceleration' - - self.computedSignals[AccX.name] = AccX - self.computedSignals[AccY.name] = AccY - self.computedSignals[AccZ.name] = AccZ - def compute_steer_rate(self): """Calculate the steer rate from the frame and fork rates.""" try: @@ -1331,6 +1310,36 @@ def compute_pull_force(self): pullForce.units = 'newton' self.computedSignals[pullForce.name] = pullForce + def compute_frame_acceleration(self): + """ + Computes the frame acceleration in inertial frame about body-fixed + coordinates, taken from the NV-100signal. + + """ + try: + AccX = self.truncatedSignals['AccelerationX'] + AccY = self.truncatedSignals['AccelerationY'] + AccZ = self.truncatedSignals['AccelerationZ'] + + except AttributeError: + print 'Accelerations from truncatedSignals were not available,'\ + ' Accelerations for computedSignals are not computed' + else: + AccX = AccX.convert_units('meter/second/second') + AccX.name = 'FrameAccelerationX' + AccX.units = 'meter/second/second' + self.computedSignals[AccX.name] = AccX + + AccY = AccY.convert_units('meter/second/second') + AccY.name = 'FrameAccelerationY' + AccY.units = 'meter/second/second' + self.computedSignals[AccY.name] = AccY + + AccZ = AccZ.convert_units('meter/second/second') + AccZ.name = 'FrameAccelerationZ' + AccZ.units = 'meter/second/second' + self.computedSignals[AccZ.name] = AccZ + def __str__(self): '''Prints basic run information to the screen.''' From 96176ff6398db99fe97443a523f7c4ca83a0866d Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 15 Oct 2012 17:45:31 -0700 Subject: [PATCH 34/37] Changed the names of contact points acceleration --- bicycledataprocessor/main.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 35167d3..cb059e1 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -950,10 +950,10 @@ def compute_contact_forces(self): u4d = u4.time_derivative() u5d = u5.time_derivative() u6d = u6.time_derivative() - u7d = self.taskSignals['RearWheelContactPointLongitudinalAcceleration'] - u8d = self.taskSignals['RearWheelContactPointLateralAcceleration'] - u9d = self.taskSignals['FrontWheelContactPointLongitudinalAcceleration'] - u10d = self.taskSignals['FrontWheelContactPointLateralAcceleration'] + u7d = self.taskSignals['LongitudinalRearContactAcceleration'] + u8d = self.taskSignals['LateralRearContactAcceleration'] + u9d = self.taskSignals['LongitudinalFrontContactAcceleration'] + u10d = self.taskSignals['LateralFrontContactAcceleration'] f_1 = np.vectorize(steer_torque_slip) f_2 = np.vectorize(contact_forces_slip) @@ -1055,17 +1055,17 @@ def compute_contact_points_acceleration(self): u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d, d1, d2, d3, rr, rf, s1, s3) - u7d.name = 'RearWheelContactPointLongitudinalAcceleration' + u7d.name = 'LongitudinalRearContactAcceleration' u7d.units = 'meter/second/second' - u8d.name = 'RearWheelContactPointLateralAcceleration' + u8d.name = 'LateralRearContactAcceleration' u8d.units = 'meter/second/second' - u11d.name = 'RearWheelContactPointDownwardAcceleration' + u11d.name = 'DownwardRearContactAcceleration' u11d.units = 'meter/second/second' - u9d.name = 'FrontWheelContactPointLongitudinalAcceleration' + u9d.name = 'LongitudinalFrontContactAcceleration' u9d.units = 'meter/second/second' - u10d.name = 'FrontWheelContactPointLateralAcceleration' + u10d.name = 'LateralFrontContactAcceleration' u10d.units = 'meter/second/second' - u12d.name = 'FrontWheelContactPointDownwardAcceleration' + u12d.name = 'DownwardFrontContactAcceleration' u12d.units = 'meter/second/second' self.taskSignals[u7d.name] = u7d From af47613baa0909f3cefaaf2166509c0911132cae Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 15 Oct 2012 22:46:49 -0700 Subject: [PATCH 35/37] Import pdb and set_trace() for debug the program, especially the compute_contact_forces() --- bicycledataprocessor/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index cb059e1..241018f 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -7,6 +7,7 @@ from warnings import warn from ConfigParser import SafeConfigParser +import pdb # debugging #try: #from IPython.core.debugger import Tracer @@ -743,8 +744,8 @@ def task_signals(self): self.compute_front_wheel_contact_points() self.compute_front_wheel_yaw_angle() self.compute_front_wheel_rate() - #self.compute_contact_points_acceleration() - #self.compute_contact_forces() + self.compute_contact_points_acceleration() + self.compute_contact_forces() self.topSig = 'task' @@ -969,6 +970,8 @@ def compute_contact_forces(self): u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + pdb.set_trace() + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) From 101bf97134fbed2df76b953c897e1f5bbea3856e Mon Sep 17 00:00:00 2001 From: StefenYin Date: Mon, 15 Oct 2012 23:06:56 -0700 Subject: [PATCH 36/37] Seperated the compute_contact_forces into three funcs, compute_steer_torque_slip, compute_contact_forces_slip, and compute_contact_forces_nonslip --- bicycledataprocessor/main.py | 113 ++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 27 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 241018f..8a093d1 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -745,7 +745,9 @@ def task_signals(self): self.compute_front_wheel_yaw_angle() self.compute_front_wheel_rate() self.compute_contact_points_acceleration() - self.compute_contact_forces() + self.compute_steer_torque_slip() + self.compute_contact_forces_slip() + self.compute_contact_forces_nonslip() self.topSig = 'task' @@ -907,12 +909,53 @@ def compute_front_wheel_rate(self): u6.units = 'radian/second' self.taskSignals['FrontWheelRate'] = u6 + def compute_steer_torque_slip(self): + """Calculate the steer torque on the front frame and back on the rear + frame, in E['3'] direction, under the slip condition""" - def compute_contact_forces(self): - """Calculate the contact forces for each - wheel with respect to inertial frame under - the slip and nonslip condition. Also, provide - the steer torque T4.""" + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + + V = self.taskSignals['ForwardSpeed'].mean() + + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u4 = self.taskSignals['SteerRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u4d = u4.time_derivative() + u8d = self.taskSignals['LateralRearContactAcceleration'] + u10d = self.taskSignals['LateralFrontContactAcceleration'] + + f = np.vectorize(steer_torque_slip) + + #calculation + #steer torque slip + steerTorque_slip = f(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, + u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + + steerTorque_slip.name = 'SteerTorque_Slip' + steerTorque_slip.units = 'newton*meter' + self.taskSignals[steerTorque_slip.name] = steerTorque_slip + + def compute_contact_forces_slip(self): + """Calculate the contact forces of rear and front wheels with respect + to inertial frame under the slip condition.""" #parameters bp = self.bicycleRiderParameters @@ -925,9 +968,7 @@ def compute_contact_forces(self): ic22 = mp['ic22'] ic33 = mp['ic33'] ic31 = mp['ic31'] - rf = mp['rf'] - #signals assignment --slip condition V = self.taskSignals['ForwardSpeed'].mean() q1 = self.taskSignals['YawAngle'] @@ -956,30 +997,13 @@ def compute_contact_forces(self): u9d = self.taskSignals['LongitudinalFrontContactAcceleration'] u10d = self.taskSignals['LateralFrontContactAcceleration'] - f_1 = np.vectorize(steer_torque_slip) - f_2 = np.vectorize(contact_forces_slip) - f_3 = np.vectorize(contact_forces_nonslip) - - #calculation - #steer torque slip - T4_slip = f_1(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, - u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + f = np.vectorize(contact_forces_slip) - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f_2(V, l1, l2, mc, + Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f(V, l1, l2, mc, ic11, ic22, ic33, ic31, q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) - pdb.set_trace() - - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f_3(l1, l2, mc, q1, q2, q4, - u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) - - #attributes: name and units, and taskSignals - T4_slip.name = 'SteerTorque_Slip' - T4_slip.units = 'newton*meter' - self.taskSignals[T4_slip.name] = T4_slip - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' Fx_r_s.units = 'newton' self.taskSignals[Fx_r_s.name] = Fx_r_s @@ -996,6 +1020,41 @@ def compute_contact_forces(self): Fy_f_s.units = 'newton' self.taskSignals[Fy_f_s.name] = Fy_f_s + def compute_contact_forces_nonslip(self): + """Calculate the contact forces of rear and front wheels with respect + to inertial frame under the nonlip condition.""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + + f = np.vectorize(contact_forces_nonslip) + + Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f(l1, l2, mc, q1, q2, q4, + u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' Fx_r_ns.units = 'newton' self.taskSignals[Fx_r_ns.name] = Fx_r_ns From 63e1a21f3ed31492265e1fb9c7bae2e52a489df2 Mon Sep 17 00:00:00 2001 From: StefenYin Date: Tue, 16 Oct 2012 10:34:47 -0700 Subject: [PATCH 37/37] Seperated the compute_contact_forces_nonslip, compute_contact_forces_slip func into four funcs, respectively; Imported the pdb for debugging --- bicycledataprocessor/main.py | 261 +++++++++++++++++++++++++++++++---- 1 file changed, 233 insertions(+), 28 deletions(-) diff --git a/bicycledataprocessor/main.py b/bicycledataprocessor/main.py index 8a093d1..76487d7 100644 --- a/bicycledataprocessor/main.py +++ b/bicycledataprocessor/main.py @@ -28,7 +28,18 @@ from dtk.bicycle import front_contact, benchmark_to_moore from dtk.bicycle import front_wheel_yaw_angle, front_wheel_rate -from dtk.bicycle import steer_torque_slip, contact_forces_slip, contact_forces_nonslip + +from dtk.bicycle import steer_torque_slip +from dtk.bicycle import contact_force_rear_longitudinal_slip +from dtk.bicycle import contact_force_rear_lateral_slip +from dtk.bicycle import contact_force_front_longitudinal_slip +from dtk.bicycle import contact_force_front_lateral_slip + +from dtk.bicycle import contact_force_rear_longitudinal_nonslip +from dtk.bicycle import contact_force_rear_lateral_nonslip +from dtk.bicycle import contact_force_front_longitudinal_nonslip +from dtk.bicycle import contact_force_front_lateral_nonslip + from dtk.bicycle import contact_points_acceleration import bicycleparameters as bp @@ -746,8 +757,14 @@ def task_signals(self): self.compute_front_wheel_rate() self.compute_contact_points_acceleration() self.compute_steer_torque_slip() - self.compute_contact_forces_slip() - self.compute_contact_forces_nonslip() + self.compute_contact_force_rear_longitudinal_slip() + self.compute_contact_force_rear_lateral_slip() + self.compute_contact_force_front_longitudinal_slip() + self.compute_contact_force_front_lateral_slip() + self.compute_contact_force_rear_longitudinal_nonslip() + self.compute_contact_force_rear_lateral_nonslip() + self.compute_contact_force_front_longitudinal_nonslip() + self.compute_contact_force_front_lateral_nonslip() self.topSig = 'task' @@ -944,8 +961,6 @@ def compute_steer_torque_slip(self): f = np.vectorize(steer_torque_slip) - #calculation - #steer torque slip steerTorque_slip = f(V, l1, l2, mc, ic11, ic33, ic31, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) @@ -953,9 +968,42 @@ def compute_steer_torque_slip(self): steerTorque_slip.units = 'newton*meter' self.taskSignals[steerTorque_slip.name] = steerTorque_slip - def compute_contact_forces_slip(self): - """Calculate the contact forces of rear and front wheels with respect - to inertial frame under the slip condition.""" + def compute_contact_force_rear_longitudinal_slip(self): + """Calculate the longitudinal contact force of rear wheel under + the slip condition""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic22 = mp['ic22'] + + u3 = self.taskSignals['PitchRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + u7 = self.taskSignals['LongitudinalRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + + u3d = u3.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = self.taskSignals['LongitudinalRearContactAcceleration'] + u9d = self.taskSignals['LongitudinalFrontContactAcceleration'] + + f = np.vectorize(contact_force_rear_longitudinal_slip) + + Fx_r_s = f(l1, l2, mc, ic22, u3, u5, u6, u7, u9, u3d, u5d, u6d, u7d, u9d) + + Fx_r_s.name = 'LongitudinalRearContactForce_Slip' + Fx_r_s.units = 'newton' + self.taskSignals[Fx_r_s.name] = Fx_r_s + + def compute_contact_force_rear_lateral_slip(self): + """Calculate the lateral contact force of rear wheel under + the slip condition""" #parameters bp = self.bicycleRiderParameters @@ -997,32 +1045,115 @@ def compute_contact_forces_slip(self): u9d = self.taskSignals['LongitudinalFrontContactAcceleration'] u10d = self.taskSignals['LateralFrontContactAcceleration'] - f = np.vectorize(contact_forces_slip) - - Fx_r_s, Fy_r_s, Fx_f_s, Fy_f_s = f(V, l1, l2, mc, - ic11, ic22, ic33, ic31, q1, q2, q4, - u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, - u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + f = np.vectorize(contact_force_rear_lateral_slip) - Fx_r_s.name = 'LongitudinalRearContactForce_Slip' - Fx_r_s.units = 'newton' - self.taskSignals[Fx_r_s.name] = Fx_r_s + Fy_r_s = f(V, l1, l2, mc, ic11, ic22, ic33, ic31, + q1, q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, + u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) Fy_r_s.name = 'LateralRearContactForce_Slip' Fy_r_s.units = 'newton' self.taskSignals[Fy_r_s.name] = Fy_r_s + def compute_contact_force_front_longitudinal_slip(self): + """Calculate the longitudinal contact force of front wheel under + the slip condition""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic22 = mp['ic22'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + + V = self.taskSignals['ForwardSpeed'].mean() + + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u5 = self.taskSignals['RearWheelRate'] + u6 = self.taskSignals['FrontWheelRate'] + u7 = self.taskSignals['LongitudinalRearContactRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u5d = u5.time_derivative() + u6d = u6.time_derivative() + u7d = self.taskSignals['LongitudinalRearContactAcceleration'] + u8d = self.taskSignals['LateralRearContactAcceleration'] + u9d = self.taskSignals['LongitudinalFrontContactAcceleration'] + u10d = self.taskSignals['LateralFrontContactAcceleration'] + + f = np.vectorize(contact_force_front_longitudinal_slip) + + Fx_f_s = f(V, l1, l2, mc, ic11, ic22, ic33, ic31, + q2, q4, u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, + u1d, u2d, u3d, u4d, u5d, u6d, u7d, u8d, u9d, u10d) + Fx_f_s.name = 'LongitudinalFrontContactForce_Slip' Fx_f_s.units = 'newton' self.taskSignals[Fx_f_s.name] = Fx_f_s + def compute_contact_force_front_lateral_slip(self): + """Calculate the lateral contact force of front wheel under + the slip condition""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + ic11 = mp['ic11'] + ic33 = mp['ic33'] + ic31 = mp['ic31'] + + V = self.taskSignals['ForwardSpeed'].mean() + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u4 = self.taskSignals['SteerRate'] + u8 = self.taskSignals['LateralRearContactRate'] + u9 = self.taskSignals['LongitudinalFrontContact'].time_derivative() + u10 = self.taskSignals['LateralFrontContact'].time_derivative() + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u4d = u4.time_derivative() + u8d = self.taskSignals['LateralRearContactAcceleration'] + u10d = self.taskSignals['LateralFrontContactAcceleration'] + + f = np.vectorize(contact_force_front_lateral_slip) + + Fy_f_s = f(V, l1, l2, mc, ic11, ic33, ic31, + q1, q2, q4, u1, u2, u4, u8, u9, u10, u1d, u2d, u4d, u8d, u10d) + Fy_f_s.name = 'LateralFrontContactForce_Slip' Fy_f_s.units = 'newton' self.taskSignals[Fy_f_s.name] = Fy_f_s - def compute_contact_forces_nonslip(self): - """Calculate the contact forces of rear and front wheels with respect - to inertial frame under the nonlip condition.""" + def compute_contact_force_rear_longitudinal_nonslip(self): + """Calculate the longitudinal contact force of rear wheel under + the constraint condition""" #parameters bp = self.bicycleRiderParameters @@ -1034,39 +1165,113 @@ def compute_contact_forces_nonslip(self): q1 = self.taskSignals['YawAngle'] q2 = self.taskSignals['RollAngle'] - q4 = self.taskSignals['SteerAngle'] u1 = self.taskSignals['YawRate'] u2 = self.taskSignals['RollRate'] u3 = self.taskSignals['PitchRate'] - u4 = self.taskSignals['SteerRate'] u5 = self.taskSignals['RearWheelRate'] - u6 = self.taskSignals['FrontWheelRate'] u1d = u1.time_derivative() u2d = u2.time_derivative() u3d = u3.time_derivative() - u4d = u4.time_derivative() u5d = u5.time_derivative() - u6d = u6.time_derivative() - f = np.vectorize(contact_forces_nonslip) + f = np.vectorize(contact_force_rear_longitudinal_nonslip) - Fx_r_ns, Fy_r_ns, Fx_f_ns, Fy_f_ns = f(l1, l2, mc, q1, q2, q4, - u1, u2, u3, u4, u5, u6, u1d, u2d, u3d, u4d, u5d, u6d) + Fx_r_ns = f(l1, l2, mc, q1, q2, u1, u2, u3, u5, u1d, u2d, u3d, u5d) Fx_r_ns.name = 'LongitudinalRearContactForce_Nonslip' Fx_r_ns.units = 'newton' self.taskSignals[Fx_r_ns.name] = Fx_r_ns + def compute_contact_force_rear_lateral_nonslip(self): + """Calculate the lateral contact force of rear wheel under + the constraint condition""" + + #parameters + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + bp = self.bicycleRiderParameters + mp = benchmark_to_moore(self.bicycleRiderParameters) + + l1 = mp['l1'] + l2 = mp['l2'] + mc = mp['mc'] + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u5 = self.taskSignals['RearWheelRate'] + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u5d = u5.time_derivative() + + f = np.vectorize(contact_force_rear_lateral_nonslip) + + Fy_r_ns = f(l1, l2, mc, q1, q2, u1, u2, u3, u5, u1d, u2d, u3d, u5d) + Fy_r_ns.name = 'LateralRearContactForce_Nonslip' Fy_r_ns.units = 'newton' self.taskSignals[Fy_r_ns.name] = Fy_r_ns + def compute_contact_force_front_longitudinal_nonslip(self): + """Calculate the longitudinal contact force of front wheel under + the constraint condition""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u6 = self.taskSignals['FrontWheelRate'] + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u6d = u6.time_derivative() + + f = np.vectorize(contact_force_front_longitudinal_nonslip) + + Fx_f_ns = f(q1, q2, q4, u1, u2, u3, u4, u6, u1d, u2d, u3d, u4d, u6d) + Fx_f_ns.name = 'LongitudinalFrontContactForce_Nonslip' Fx_f_ns.units = 'newton' self.taskSignals[Fx_f_ns.name] = Fx_f_ns + def compute_contact_force_front_lateral_nonslip(self): + """Calculate the lateral contact force of front wheel under + the constraint condition""" + + q1 = self.taskSignals['YawAngle'] + q2 = self.taskSignals['RollAngle'] + q4 = self.taskSignals['SteerAngle'] + + u1 = self.taskSignals['YawRate'] + u2 = self.taskSignals['RollRate'] + u3 = self.taskSignals['PitchRate'] + u4 = self.taskSignals['SteerRate'] + u6 = self.taskSignals['FrontWheelRate'] + + u1d = u1.time_derivative() + u2d = u2.time_derivative() + u3d = u3.time_derivative() + u4d = u4.time_derivative() + u6d = u6.time_derivative() + + f = np.vectorize(contact_force_front_lateral_nonslip) + + Fy_f_ns = f(q1, q2, q4, u1, u2, u3, u4, u6, u1d, u2d, u3d, u4d, u6d) + Fy_f_ns.name = 'LateralFrontContactForce_Nonslip' Fy_f_ns.units = 'newton' self.taskSignals[Fy_f_ns.name] = Fy_f_ns