Aero Magic

raam-018 img_2771 2010-06-04-17-10-15

VE - A Perl Script for Virtual Elevation Calculations

In trying to create a prototype for Golden Cheetah, I put together this perl script that I submit for your perusal.   The script comes with the sample data used by Robert Chung in his recent post to the Wattage Forum, so it may look familiar to many of you.

My aim was to match a known profile by using the exact 2nd-law equations of motion.  All the details are contained within, as well as a full set of data.  The code is submitted “warts and all”  for your healthy criticism.  As shown in the header file, the code is fully GPL’d, as it is meant to be used for Golden Cheetah.

Features

  • computes Crr and CdA from power file and elevation profile
  • stably tries to match the profiles using a global L2 norm
  • full equations used for Newton’s 2nd law (no small-angle approximations):
    F – m g sin(theta) – Crr m g cos(theta) – CdA rho v2 / 2 = m a
  • regular iteration solver for slope to cope with nonlinear trig equation
  • full Newton-Raphson using numerical derivatives for outer solver
  • GPL license

To Do

  • recheck numerics over and over to make sure they’re right
  • implement Chung method for use when no elevation profile is available
  • provide unit tests to bolt-down the validation of the code
  • add calculation for rho, air density

Perl dependencies

require Math::Trig;

Inputs
A PowerTap CSV file, pt_data.csv, attached.
An associated elevation file, elevation.csv, attached.
Mass of rider + bike, m.  ( Here, 89kg)
Time-sampling interval, dt. ( Here, 1.26s )
Density of air, rho ( Here 1.236kg/m3)
Conversion factor from speed in file to m/s, v_factor (Here, 3.600 )
Starting elevation, elev_start. (Here, 7.59m)

Command line usage
% ve.pl <power_tap_file> <elevation_file> <m> <rho> <dt> <v_factor>  <elev_start>


Example
% ve.pl pt_data.csv elevation.csv 89 1.236 1.26 3.6 7.59

crr= -0.00896848, cda=  0.71414735     RESIDUALS:    0.051821      121720394637.221100

crr= -0.01887355, cda=  0.74727538     RESIDUALS:    0.043221      24240094726.978828

crr= -0.06140901, cda=  1.32495998     RESIDUALS:    0.724061      4847607668.734711

crr=  0.04881965, cda= -0.23143669     RESIDUALS:    1.950369      970123080.834633

crr=  0.01233395, cda=  0.28146843     RESIDUALS:    0.642751      195195026.279830

crr=  0.00621468, cda=  0.36746316     RESIDUALS:    0.107765      39145221.731429

crr=  0.00499893, cda=  0.38454789     RESIDUALS:    0.021410      7829464.327135

crr=  0.00475554, cda=  0.38796827     RESIDUALS:    0.004286      1565869.723166

crr=  0.00470688, cda=  0.38865210     RESIDUALS:    0.000857      313175.156131

crr=  0.00469715, cda=  0.38878888     RESIDUALS:    0.000171      62634.927414

crr=  0.00469520, cda=  0.38881621     RESIDUALS:    0.000034      12527.026148

crr=  0.00469481, cda=  0.38882167     RESIDUALS:    0.000007       2505.412917

crr=  0.00469473, cda=  0.38882276     RESIDUALS:    0.000001        501.082018

crr=  0.00469472, cda=  0.38882298     RESIDUALS:    0.000000        100.216611

crr=  0.00469472, cda=  0.38882303     RESIDUALS:    0.000000         20.043254

crr=  0.00469471, cda=  0.38882303     RESIDUALS:    0.000000          4.008642

crr=  0.00469471, cda=  0.38882304     RESIDUALS:    0.000000          0.801726

crr=  0.00469471, cda=  0.38882304     RESIDUALS:    0.000000          0.160310

crr=  0.00469471, cda=  0.38882304     RESIDUALS:    0.000000          0.032088

crr=  0.00469471, cda=  0.38882304     RESIDUALS:    0.000000          0.006452

crr=  0.00469471, cda=  0.38882304     RESIDUALS:    0.000000          0.001279

crr=  0.00469471, cda=  0.38882304     RESIDUALS:    0.000000          0.000221

Results

Shown below is the convergence of the virtual elevation profile to the actual elevation profile.   The 10th iteration of the Newton-Raphson solver pretty much nails the elevation correctly.

convergence

To see the final results a little better, here is the actual vs. 10th iteration profile.  As you can see, the results are virtually identical, indicating the quality of the convergence.

convergence2

Executable


#!/usr/bin/perl -I .
#
# ve - a perl script to perform virtual elevation calculations
#
# Copyright (C) 2009 Andy M. Froncioni
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
#
# Description:
# A program to determine CdA and Crr by matching virtual
# elevation with actual elevation.
#
# Commandline Usage:
# % ve power_tap_file elevation_file m rho dt v_factor elev_start
#
# Example:
# % ./ve pt_data.csv elevation.csv 89 1.236 1.26 3.6 7.59
# crr= -0.00896848, cda= 0.71414735 RESIDUALS: 0.051821 121720394637.221100
# crr= -0.01887355, cda= 0.74727538 RESIDUALS: 0.043221 24240094726.978828
# crr= -0.06140901, cda= 1.32495998 RESIDUALS: 0.724061 4847607668.734711
# crr= 0.04881965, cda= -0.23143669 RESIDUALS: 1.950369 970123080.834633
# crr= 0.01233395, cda= 0.28146843 RESIDUALS: 0.642751 195195026.279830
# crr= 0.00621468, cda= 0.36746316 RESIDUALS: 0.107765 39145221.731429
# crr= 0.00499893, cda= 0.38454789 RESIDUALS: 0.021410 7829464.327135
# crr= 0.00475554, cda= 0.38796827 RESIDUALS: 0.004286 1565869.723166
# crr= 0.00470688, cda= 0.38865210 RESIDUALS: 0.000857 313175.156131
# crr= 0.00469715, cda= 0.38878888 RESIDUALS: 0.000171 62634.927414
# crr= 0.00469520, cda= 0.38881621 RESIDUALS: 0.000034 12527.026148
# crr= 0.00469481, cda= 0.38882167 RESIDUALS: 0.000007 2505.412917
# crr= 0.00469473, cda= 0.38882276 RESIDUALS: 0.000001 501.082018
# crr= 0.00469472, cda= 0.38882298 RESIDUALS: 0.000000 100.216611
# crr= 0.00469472, cda= 0.38882303 RESIDUALS: 0.000000 20.043254
# crr= 0.00469471, cda= 0.38882303 RESIDUALS: 0.000000 4.008642
# crr= 0.00469471, cda= 0.38882304 RESIDUALS: 0.000000 0.801726
# crr= 0.00469471, cda= 0.38882304 RESIDUALS: 0.000000 0.160310
# crr= 0.00469471, cda= 0.38882304 RESIDUALS: 0.000000 0.032088
# crr= 0.00469471, cda= 0.38882304 RESIDUALS: 0.000000 0.006452
# crr= 0.00469471, cda= 0.38882304 RESIDUALS: 0.000000 0.001279
# crr= 0.00469471, cda= 0.38882304 RESIDUALS: 0.000000 0.000221
#
#
use strict;
use Math::Trig;

#
# These commandline parameters assume file-scope. I need this to
# simplify the f, fx, fy, fxx, etc... functions that are derived
# from compute_virtual_elevation():

my $power_file = shift;
my $elev_file = shift;
my $m = shift;
my $rho = shift;
my $dt = shift;
my $v_factor = shift;
my $start_elev = shift;

#
# read the input files:
my $power_data = read_power_data( $power_file );
my $actual_elevation = read_elevation_data( $elev_file );
print_elev( "000.dat", $actual_elevation );

# Use a nonlinear solver to determine CdA and Crr.
#
# The idea is that we have a function F(x,y) which we want to
# minimize. F is the L2-norm of the difference between our
# actual and computed elevation profiles. (x,y) is the ( crr, cda )
# input vector.
#
# The problem: find (x,y) that minimizes F.
#
# Solution:
#
# Set dF/dx and DF/dy to zero and get 2 equations in 2 unknowns.
# Follow the gradient downhill to get x*,y* that sets F(x*,y*) to zero.
#

#
# Seed:
my $cda = 0.700;
my $crr = 0.030;

#
# Outer loop for (crr,cda) solve:
my $error_norm = 10000000000;
my $tolerance = 0.001;
my $delta = 0.0000001;
my $x = $crr;
my $y = $cda;
my $alpha = 0.8;
my $correction_norm = 100000000;
my $i = 0;
while( ($error_norm > $tolerance) || ($correction_norm > $tolerance) )
{
# RHS vector:
my $b0 = fx($x,$y,$delta);
my $b1 = fy($x,$y,$delta);
my @b = ( -1.0 * $b0 , -1.0 * $b1 );

# Hessian:
my @A;
$A[0][0] = fxx( $x, $y, $delta );
$A[0][1] = fxy( $x, $y, $delta );
$A[1][0] = fyx( $x, $y, $delta );
$A[1][1] = fyy( $x, $y, $delta );

# Solve for new (dx,dy) vector:
#
# A * dx = b
my $dx = solve_2x2_matrix( \@A, \@b );

#
# Apply correction to (x,y):
$x += $alpha * $dx->[0];
$y += $alpha * $dx->[1];

my $ve = compute_virtual_elevation( $x, $y );
print_elev( sprintf("%02d\.dat", $i ), $ve );

#
# Compute both correction norm and rhs norm.
# This ensures that ill-conditioned problems converge completely:
$error_norm = sqrt( $b0*$b0 + $b1*$b1 );
$correction_norm = sqrt( $dx->[0]*$dx->[0] + $dx->[1]*$dx->[1] );

print sprintf("crr=%12.8f, cda=%12.8f", $x, $y ) . " RESIDUALS:" . sprintf( "%12.6f %12.6f\n", $correction_norm, $error_norm );

$i++;
}

#
# dummy function for l2-norm of actual vs virtual elevation error:
sub f
{
my $x = shift;
my $y = shift;

my $ve = compute_virtual_elevation( $x, $y);

return l2_error( $ve, $actual_elevation);
}

#
# dummy function dfdx for l2-norm of actual vs virtual elevation error:
sub fx
{
my $x = shift;
my $y = shift;
my $delta = shift;

my $f1 = f( $x+$delta, $y);
my $f0 = f( $x, $y);

return ( $f1 - $f0 ) / $delta;
}

#
# dummy function dfdx for l2-norm of actual vs virtual elevation error:
sub fy
{
my $x = shift;
my $y = shift;
my $delta = shift;

my $f1 = f( $x, $y+$delta);
my $f0 = f( $x, $y);

return ( $f1 - $f0 ) / $delta;
}

#
# dummy function fxx for l2-norm of actual vs virtual elevation error:
sub fxx
{
my $x = shift;
my $y = shift;
my $delta = shift;

my $f1 = fx( $x+$delta, $y, $delta);
my $f0 = fx( $x, $y, $delta);

return ( $f1 - $f0 )/ $delta;
}

#
# dummy function fxy for l2-norm of actual vs virtual elevation error:
sub fxy
{
my $x = shift;
my $y = shift;
my $delta = shift;

my $f1 = fx( $x, $y+$delta, $delta);
my $f0 = fx( $x, $y, $delta);

return ( $f1 - $f0 )/ $delta;
}

#
# dummy function fyx for l2-norm of actual vs virtual elevation error:
sub fyx
{
my $x = shift;
my $y = shift;
my $delta = shift;

my $f1 = fy( $x+$delta, $y, $delta);
my $f0 = fy( $x, $y, $delta);

return ( $f1 - $f0 )/ $delta;
}

#
# dummy function fyy for l2-norm of actual vs virtual elevation error:
sub fyy
{
my $x = shift;
my $y = shift;
my $delta = shift;

my $f1 = fy( $x, $y+$delta, $delta);
my $f0 = fy( $x, $y, $delta);

return ( $f1 - $f0 )/ $delta;
}

#
# 2x2 Matrix solver
#
# Solves the system A x = b using Cramer's rule:
sub solve_2x2_matrix
{
my $A = shift; #reference to 2x2 matrix
my $b = shift; # column vector rhs

my $det_A = ($A->[0][0] * $A->[1][1]) - ($A->[1][0] * $A->[0][1]);

if( $det_A == 0 )
{
die( "Determinant of matrix system is zero!\n");
}
my $cramer_0 = $b->[0] * $A->[1][1] - $b->[1] * $A->[0][1];
my $cramer_1 = $A->[0][0] * $b->[1] - $A->[1][0] * $b->[0];

my @solution = ( $cramer_0 / $det_A , $cramer_1 / $det_A );

return \@solution;
}

#
# Determine elevation vector:
sub compute_virtual_elevation
{
my $crr = shift;
my $cda = shift;
# my $power_data = shift;
# my $m = shift;
# my $rho = shift;
# my $dt = shift;
# my $v_factor = shift;
# my $start_elev = shift;

#
# Unpack:
my $p = $power_data->{p};
my $d = $power_data->{d};
my $v = $power_data->{v};

#
# With the input crr and cda, compute the virtual elevation
# profile:
my @ve;

#
# Unroll first row of calculation, to avoid if-statement in loop:
my $v1 = $v->[ 0 ]/$v_factor;
my $v2 = $v->[ 0 ]/$v_factor;
my $elev = $start_elev;
my @ve;
$ve[ 0 ] = 0; # CAUTION: assume ZERO starting elevation
#
# Now compute the acceleration, slope, and elevation change:
for( my $i=1; $i<$power_data->{n}; $i++ )
{
#
# unpack:
$v2 = $v->[ $i ]/$v_factor;

# use v2^2 - v1^2 = 2 * a * d to solve for a:
my $a = ( ($v2*$v2) - ($v1*$v1) ) / ( 2 * $dt * $v2 );
my $slope1 = slope( 0, $p->[$i]/$v2, $a, $m, $crr, $cda, $rho, $v2 );

#
my $delta_elev = $slope1 * $v2 * $dt;
$elev += $delta_elev;
#print "DATA:". join( ",", ( $i+1, $v->[$i], $p->[$i], $d->[$i], $v2, $a, $slope1, $delta_elev, $elev ) ) . "\n";
$ve[ $i ] = $elev;

$v1 = $v2;
}

return \@ve;

}

#
# Compute the L^2 error of 2 vectors:
sub l2_error
{
my $a = shift;
my $b = shift;

#
# are the 2 vectors the same length?
# no: die
if( scalar( @$a ) != scalar( @$b ) )
{
die "Cannot compute errors: " .
"vector a has length " . scalar( @$a ) . " and " .
"vector b has length " . scalar( @$b );
}

#
my $l2 = 0;
for( my $i=0; $i[ $i ] - $b->[ $i ];
$l2 += $delta * $delta;
}

return $l2;
}

#
# Given the formula
#
# F_drive - m g sin(theta) - Crr m g cos(theta) - CdA rho v^2 = m a
#
# compute theta (slope angle) given all the other variables, using
# regular iteration.
#
# NOTE:
# This equation is the exact form of the equations of motion, with
# no small-angle approximation as in the original Chung formulation.
# Whether this is justified or not, I don't personally believe in
# introducing more noise than necessary in the equations of motion.
# Only during the solution process should one decide on the
# tolerances.
#
#
# Test with:
# my $p = 321;
# my $m = 89;
# my $v_factor = 1.0;
# my $v = 7.111111;
# my $a = -0.665251;
# my $cda = 0.3845;
# my $crr = 0.005;
# my $rho = 1.236;
#
# slope( $p/$v, $a, $m, $crr, $cda, $rho, $v );
#
# should give: a return value of 0.039771537080168
#
# (Presently the slope is over-toleranced, which leads to
# a slow calculation. I will determine the tolerance that
# works.)
#
sub slope
{
my $compute_mode = shift;
my $f = shift;
my $a = shift;
my $m = shift;
my $crr = shift;
my $cda = shift;
my $rho = shift;
my $v = shift;

my $g = 9.80665;
#print "slope: " . join( ",", ( $f, $a, $m, $crr, $cda, $rho, $v ) ) . "\n";;

if( $compute_mode == 0 )
{
return ( ($f/($m*$g)) - $crr - ($cda* $rho*$v*$v/2)/($m*$g) - ($a/$g) );
}

my $tol = 0.000001;

my $error = 100000000;
my $theta = 0.01;
my $u = sin( $theta );
my $u_last = 0.01;
my $n_iters = 0;
while( ($error > $tol) && ($n_iters < 10) )
{
#print " " . tan( asin( $u ) ) . ":\n";
my $numerator = ( $f - $m * $a - $crr * $m * $g * sqrt( 1 - ($u*$u) ) - ($cda* $rho*$v*$v)/2);
my $denominator = ( $m * $g );

$u = $numerator / $denominator;

$error = abs( $u - $u_last );
$u_last = $u;

$n_iters++;
}
$theta = asin( $u );

return tan( $theta );
}

#
# read the power data from a CSV file:
# Minutes, Torq (N-m), Km/h, Watts, Km, Cadence, Hrate, ID

sub read_power_data
{
my $file = shift;

open POWER, $file;
my $i = 0;
my @p;
my @v;
my @d;
foreach my $line (
)
{
chomp $line;
my ( $t, $torque, $vel, $power, $dist, @others ) = split( /,/, $line );
$p[ $i ] = $power;
$d[ $i ] = $dist;
$v[ $i ] = $vel;

$i++;
}
close POWER;

my %power_data;
$power_data{n} = $i;
$power_data{p} = \@p;
$power_data{v} = \@v;
$power_data{d} = \@d;

return \%power_data;
}

#
# read elevation data:
sub read_elevation_data
{
my $elev_file = shift;

open ELEV, $elev_file;
my @e;
my $i = 0;
foreach my $line ( )
{
chomp $line;
$e[ $i ] = $line;
$i++;
}
close ELEV;

return \@e;
}

#
# print the elevation datai to file:
sub print_elev
{
my $file = shift;
my $vec = shift;

open ELEV, ">$file";
foreach my $v ( @$vec )
{
print ELEV $v . "\n";
}
close ELEV;

return 1;
}

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>