climb_factor_gem 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3de080785de7f1f7833f2d69a8323197e026bb3e621dfd395539f152ce9966ca
4
+ data.tar.gz: 4b697e0f6cea23b5c2fbdb5b650ac90e7912b5367b87d0d96a3cce7efda9e1c7
5
+ SHA512:
6
+ metadata.gz: 7f3b2ff8ce4b33182e3407b7f214b711404f888ee8d6dae3d06f5d07c543c2d86afe38015f1315a9480f32ad9d7217079b8043bed3e2e3de14974f30933ad47c
7
+ data.tar.gz: 3fac167ac1c40cc6d30f2ae4639cd5eb40f1adefdaf00eb3fba6723abca53539679778af68d0963931b230966484a7b59b72c9723f9f27ba296ded124933a388
data/LICENSE ADDED
@@ -0,0 +1,2 @@
1
+ copyright (c) 2017 Benjamin Crowell
2
+ GPL v2
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ Climb Factor
2
+ =====
3
+ This is a Ruby gem adaptation of the software authored by Ben Crowell, the original source for which you can find [here](https://bitbucket.org/ben-crowell/kcals).
4
+
5
+ ## Description
6
+
7
+ This software estimates your energy expenditure from running or walking,
8
+ based on a GPS track or a track generated by an application such as
9
+ google maps or [onthegomap](https://onthegomap.com).
10
+
11
+ The model used to calculate the results is described in this paper:
12
+
13
+ B. Crowell, "From treadmill to trails: predicting performance of runners,"
14
+ https://www.biorxiv.org/content/10.1101/2021.04.03.438339v2 , doi: 10.1101/2021.04.03.438339
15
+
16
+ The output from this software is most useful if you want to compare
17
+ one run to another, e.g., if I want to know how a mountain run with lots of elevation gain
18
+ compares with a flat run at a longer distance, or if I want to project whether doing a certain
19
+ trail as a run is feasible for me.
20
+
21
+ ## Use through the web interface
22
+
23
+ The web interface is available [here](https://www.lightandmatter.com/cf)
24
+
25
+ ## Use
26
+ Add this to your Gemfile
27
+ ```
28
+ gem 'climb_factor_gem'
29
+ ```
30
+ To invoke it, you must provide an array of horizontal/vertical pairs (in meters!):
31
+ ```
32
+ hv = [[0, 1], [1.1, 4], [3.4, 6], ...]
33
+ ClimbFactor.estimate(hv)
34
+ ```
35
+
36
+ See `climb_factor.rb` for more details and additional options.
37
+
38
+ ## Filtering
39
+
40
+ The parameters filtering and xy_filtering both represent horizontal distances
41
+ in units of meters. Their defaults values are 200 and 30, respectively. These are meant to get rid of bogus
42
+ oscillations in the data. Any elevation (z) changes that occur over horizontal distances less
43
+ than the value of "filtering" will tend to get filtered out, and likewise any horizontal motion that occurs
44
+ over horizontal distances less than the value of "xy_filtering."
45
+ To turn off filtering, set the relevant parameter to 0.
46
+
47
+ The choice of the vertical filtering parameter
48
+ can have a huge effect on the total elevation gain, but the
49
+ effect on the calorie expenditure is usually fairly small.
50
+ There are several reasons why it may be a good idea to set a fairly large value of the vertical
51
+ ("filtering") parameter:
52
+
53
+ 1. If the resolution of the horizontal track is poor, then it may appear to go up and down steep hillsides,
54
+ when in fact the real road or trail contours around them.
55
+
56
+ 2. If elevations from GPS are being used (which is a bad idea), then random fluctuations in the GPS
57
+ elevations can cause large errors.
58
+
59
+ 3. If the elevations are being taken from a digital elevation model (DEM), which is generally a good
60
+ idea, then there may still be certain types of errors.
61
+ Trails and roads are intentionally constructed so as not to go up and down steep hills, but
62
+ the DEM may not accurately reflect this. The most common situation seems to be one in which
63
+ a trail or road takes a detour into a narrow gully in order to maintain a steady grade.
64
+ The DEM currently used by this software has a horizontal resolution of 30 meters.
65
+ If the gully is narrower than this, then the DEM doesn't know about the the gully, and
66
+ the detour appears to be a steep excursion up and then back down the prevailing slope.
67
+ I have found empirically that setting filtering=60 m is roughly the minimum that is required
68
+ in order to eliminate this type of artifact, which makes sense because a detour into a 30 meter
69
+ gully probably does involve about 60 meters of horizontal travel.
70
+
71
+ My rules of thumb for setting the filtering are as follows:
72
+
73
+ * For most runs with relatively short and not insanely steep hills, the default vertical filtering
74
+ parameter of 60 m works well. Using a higher filtering value leads to wrong results, because
75
+ the hills get smoothed out entirely.
76
+
77
+ * For very steep runs with a lot of elevation gain, in rugged terrain, it's necessary to use a
78
+ larger filtering value of about 200 m. Otherwise the energy estimates are much too high.
79
+ This is the software's default.
80
+
81
+ The mileage derived from a GPS track can vary quite a bit depending on the resolution of the GPS data.
82
+ Higher resolution increases the mileage, because small wiggles get counted in. This has a big effect on
83
+ the energy calculation, because the energy is mostly sensitive to mileage, not gain.
@@ -0,0 +1,124 @@
1
+ require_relative 'low_level_math'
2
+
3
+ module ClimbFactor
4
+ module Filtering
5
+ def self.resample_and_filter_hv(hv,filtering,target_resampling_resolution:30.0)
6
+ # The default for target_resampling_resolution is chosen because typically public DEM data isn't any better than this horizontal resolution anyway.
7
+ if filtering>target_resampling_resolution then filtering=target_resampling_resolution end
8
+ k = (filtering/target_resampling_resolution).floor # Set resampling resolution so that it divides filtering.
9
+ while target_resampling_resolution*k<filtering do k+=1 end
10
+ if k%2==1 then k+=1 end # make it even
11
+ resampling_resolution = filtering/k
12
+ resampled_v = resample_hv(hv,resampling_resolution)
13
+ result = []
14
+ 0.upto(resampled_v.length-1) { |i|
15
+ result.push([resampling_resolution*i,resampled_v[i]])
16
+ }
17
+ return result
18
+ end
19
+
20
+ def self.resample_hv(hv,desired_h_resolution)
21
+ # Inputs a list of [h,v] points, returns a list of v values interpolated to achieve the given constant horizontal resolution (or slightly more).
22
+ # This is similar to add_resolution_and_check_size_limit(), but that routine kludgingly does multiple things.
23
+ tot_h = hv.last[0]-hv[0][0]
24
+ n = (tot_h/desired_h_resolution).ceil+1
25
+ dh = tot_h/(n-1) # actual resolution, which will be a tiny bit better
26
+ result = []
27
+ m = 0 # index into hv
28
+ 0.upto(n-1) { |i| # i is an index into resampled output
29
+ h = hv[0][0]+dh*i
30
+ while m<hv.length-1 && hv[m+1][0]<h do m += 1 end
31
+ s = (h-hv[m][0])/(hv[m+1][0]-hv[m][0]) # interpolation fraction ranging from 0 to 1
32
+ result.push(CfMath.linear_interp(hv[m][1],hv[m+1][1],s))
33
+ }
34
+ return result
35
+ end
36
+
37
+ def self.do(v0,w)
38
+ # inputs:
39
+ # v0 is a list of floating-point numbers, representing the value of a function at equally spaced points on the x axis
40
+ # w = width of rectangular window to convolve with; will be made even if it isn't
41
+ # output:
42
+ # a fresh array in the same format as v0, doesn't modify v0
43
+ # v0's length should be a power of 2 and >=2
44
+
45
+ if w<=1 then return v0 end
46
+
47
+ if w%2==1 then w=w+1 end
48
+
49
+ # remove DC and detrend, so that start and end are both at 0
50
+ # -- https://www.dsprelated.com/showthread/comp.dsp/175408-1.php
51
+ # After filtering, we put these back in.
52
+ # v0 = original, which we don't touch
53
+ # v = detrended
54
+ # v1 = filtered
55
+ # For the current method, it's only necessary that n is even, not a power of 2, and detrending
56
+ # isn't actually needed.
57
+ v = v0.dup
58
+ n = v.length
59
+ slope = (v.last-v[0])/(n.to_f-1.0)
60
+ c = -v[0]
61
+ 0.upto(n-1) { |i|
62
+ v[i] = v[i] - (c + slope*i)
63
+ }
64
+
65
+ # Copy the unfiltered data over as a default. On the initial and final portions, where part of the
66
+ # rectangular kernel hangs over the end, we don't attempt to do any filtering. Using the filter
67
+ # on those portions, even with appropriate normalization, would bias the (x,y) points, effectively
68
+ # moving the start and finish line inward.
69
+ v1 = v.dup
70
+
71
+ # convolve with a rectangle of width w:
72
+ sum = 0
73
+ count = 0
74
+ # Sum the initial portion for use in the average for the first filtered data point:
75
+ sum_left = 0.0
76
+ 0.upto(w-1) { |i|
77
+ break if i>n/2-1 # this happens in the unusual case where w isn't less than n; we're guaranteed that n is even
78
+ sum_left = sum_left+v[i]
79
+ }
80
+ # the filter is applied to the middle portion, from w to n-w:
81
+ if w<n then
82
+ sum = sum_left
83
+ w.upto(n) { |i|
84
+ if i>=v.length then break end # wasn't part of original algorithm, but needed for some data sets...?
85
+ if v[i].nil?
86
+ raise("coordinate #{i} of #{w}..#{n} is nil, length of vector is #{v.length}, for v0=#{v0.length}, w=#{w}")
87
+ end
88
+ sum = sum + v[i]-v[i-w]
89
+ j = i-w/2
90
+ break if j>n-w
91
+ if j>=w && j<=n-w then
92
+ v1[j] = sum/w
93
+ end
94
+ }
95
+ end
96
+
97
+ # To avoid a huge discontinuity in the elevation when the filter turns on, turn it on gradually
98
+ # in the initial and final segments of length w:
99
+ # FIXME: leaves a small discontinuity
100
+ sum_left = 0.0
101
+ sum_right = 0.0
102
+ nn = 0
103
+ 0.upto(2*w+1) { |i|
104
+ break if i>n/2-1 # unusual case, see above
105
+ j = n-i-1
106
+ sum_left = sum_left+v[i]
107
+ sum_right = sum_right+v[j]
108
+ nn = nn+1
109
+ if i%2==0 then
110
+ ii = i/2
111
+ jj = n-i/2-1
112
+ v1[ii] = sum_left/nn
113
+ v1[jj] = sum_right/nn
114
+ end
115
+ }
116
+
117
+ # put DC and trend back in:
118
+ 0.upto(n-1) { |i|
119
+ v1[i] = v1[i] + (c + slope*i)
120
+ }
121
+ return v1
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,84 @@
1
+ require_relative 'low_level_math'
2
+
3
+ module ClimbFactor
4
+ module Geom
5
+ def self.earth_radius(lat)
6
+ # https://en.wikipedia.org/wiki/Earth_radius#Geocentric_radius
7
+ a = 6378137.0 # earth's equatorial radius, in meters
8
+ b = 6356752.3 # polar radius
9
+ slat = Math::sin(CfMath.deg_to_rad(lat))
10
+ clat = Math::cos(CfMath.deg_to_rad(lat))
11
+ return Math::sqrt( ((a*a*clat)**2+(b*b*slat)**2) / ((a*clat)**2+(b*slat)**2)) # radius in meters
12
+ end
13
+
14
+ def self.cartesian_to_spherical(x,yy,z,lat0,lon0)
15
+ # returns [lat,lon,altitude], in units of degrees, degrees, and meters
16
+ # see Geom.spherical_to_cartesian() for description of coordinate systems used and the transformations.
17
+ # Calculate a first-order approximation to the inverse of the polyconic projection:
18
+ r0 = earth_radius(lat0)
19
+ zz = z+r0
20
+ slat0 = Math::sin(CfMath.deg_to_rad(lat0))
21
+ clat0 = Math::cos(CfMath.deg_to_rad(lat0))
22
+ r = Math::sqrt(x*x+yy*yy+zz*zz)
23
+ y = clat0*yy+slat0*zz
24
+ zzz = -slat0*yy+clat0*zz
25
+ lat = CfMath.rad_to_deg(Math::asin(y/r))
26
+ lon = CfMath.rad_to_deg(Math::atan2(x,zzz))+lon0
27
+ 1.upto(10) { |i| # more iterations to improve the result
28
+ x2,y2,z2 = spherical_to_cartesian(lat,lon,z,lat0,lon0)
29
+ dx = x-x2
30
+ dy = yy-y2
31
+ break if dx.abs<1.0e-8 and dy.abs<1.0e-8
32
+ lat = lat + CfMath.rad_to_deg(dy/r0)
33
+ lon = lon + CfMath.rad_to_deg(dx/(r0*clat0)) if clat0!=0.0
34
+ }
35
+ return [lat,lon,z]
36
+ end
37
+
38
+ def self.spherical_to_cartesian(lat,lon,alt,lat0,lon0)
39
+ # Inputs are in degrees, except for alt, which is in meters.
40
+ # Returns [x,y,z] in meters.
41
+ # The "cartesian" coordinates are not actually cartesian. They're coordinates in which
42
+ # (x,y) are from a polyconic projection https://en.wikipedia.org/wiki/Polyconic_projection
43
+ # centered on (lat0,lon0), and z is altitude.
44
+ # (In older versions of the software, z was distance from center of earth.)
45
+ # Outputs are in meters. The metric for the projection is not exactly euclidean, so later
46
+ # calculations that treat these as cartesian coordinates are making an approximation. The error
47
+ # should be tiny on the scales we normally deal with. The important thing for our purposes is
48
+ # that the gradient of z is vertical.
49
+ lam = CfMath.deg_to_rad(lon)
50
+ lam0 = CfMath.deg_to_rad(lon0)
51
+ phi = CfMath.deg_to_rad(lat)
52
+ phi0 = CfMath.deg_to_rad(lat0)
53
+ cotphi = 1/Math::tan(phi)
54
+ u = (lam-lam0)*Math::sin(phi) # is typically on the order of 10^-3 (half the width of a USGS topo)
55
+ if u.abs<0.01
56
+ # use taylor series to avoid excessive rounding in calculation of 1-cos(u)
57
+ u2 = u*u
58
+ u4 = u2*u2
59
+ one_minus_cosu = 0.5*u2-(0.0416666666666667)*u4+(1.38888888888889e-3)*u2*u4-(2.48015873015873e-5)*u4*u4
60
+ # max error is about 10^-27, which is a relative error of about 10^-23
61
+ else
62
+ one_minus_cosu = 1-Math::cos(u)
63
+ end
64
+ r0 = earth_radius(lat0)
65
+ # Use initial latitude and keep r0 constant. If we let r0 vary, then we also need to figure
66
+ # out the direction of the g vector in this model.
67
+ x = r0*cotphi*Math::sin(u)
68
+ y = r0*((phi-phi0)+cotphi*one_minus_cosu)
69
+ z = alt
70
+ return [x,y,z]
71
+ end
72
+
73
+ def self.interpolate_raster(z,x,y)
74
+ # z = array[iy][ix]
75
+ # x,y = floating point, in array-index units
76
+ ix = x.to_i
77
+ iy = y.to_i
78
+ fx = x-ix # fractional part
79
+ fy = y-iy
80
+ z = CfMath.interpolate_square(fx,fy,z[iy][ix],z[iy][ix+1],z[iy+1][ix],z[iy+1][ix+1])
81
+ return z
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,59 @@
1
+ module ClimbFactor
2
+ module CfMath
3
+ def self.deg_to_rad(x)
4
+ return 0.0174532925199433*x
5
+ end
6
+
7
+ def self.rad_to_deg(x)
8
+ return x/0.0174532925199433
9
+ end
10
+
11
+ def self.pythag(x,y)
12
+ return Math::sqrt(x*x+y*y)
13
+ end
14
+
15
+ def self.interpolate_square(x,y,z00,z10,z01,z11)
16
+ # https://en.wikipedia.org/wiki/BiClimbFactorMath.linear_interpolation#Unit_Square
17
+ # The crucial thing is that this give results that are continuous across boundaries of squares.
18
+ w00 = (1.0-x)*(1.0-y)
19
+ w10 = x*(1.0-y)
20
+ w01 = (1.0-x)*y
21
+ w11 = x*y
22
+ norm = w00+w10+w01+w11
23
+ z = (z00*w00+z10*w10+z01*w01+z11*w11)/norm
24
+ return z
25
+ end
26
+
27
+ def self.linear_interp(x1,x2,s)
28
+ return x1+s*(x2-x1)
29
+ end
30
+
31
+ def self.add2d(p,q)
32
+ return [p[0]+q[0],p[1]+q[1]]
33
+ end
34
+
35
+ def self.sub2d(p,q)
36
+ return [p[0]-q[0],p[1]-q[1]]
37
+ end
38
+
39
+ def self.dot2d(p,q)
40
+ return p[0]*q[0]+p[1]*q[1]
41
+ end
42
+
43
+ def self.scalar_mul2d(p,s)
44
+ return [s*p[0],s*p[1]]
45
+ end
46
+
47
+ def self.normalize2d(p)
48
+ return scalar_mul2d(p,1.0/mag2d(p))
49
+ end
50
+
51
+ def self.dist2d(p,q)
52
+ return mag2d(sub2d(p,q))
53
+ end
54
+
55
+ def self.mag2d(p)
56
+ return Math::sqrt(dot2d(p,p))
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,104 @@
1
+ #=========================================================================
2
+ # @@ physiological model
3
+ #=========================================================================
4
+
5
+ # For the cr and cw functions, see Minetti, http://jap.physiology.org/content/93/3/1039.full
6
+
7
+ module ClimbFactor
8
+ module Phys
9
+ def self.minetti(i)
10
+ # cost of running or walking, in J/kg.m
11
+ if $running then
12
+ a,b,c,d,p = [26.073730183424228, 0.031038121935618928, 1.3809948743424785, -0.06547207947176657, 2.181405714691871]
13
+ else
14
+ a,b,c,d,p = [22.911633035337864, 0.02621471025436344, 1.3154310892336223, -0.08317260964525384, 2.208584834633906]
15
+ end
16
+ cost = (a*((i**p+b)**(1/p)+i/c+d)).abs
17
+ if true then
18
+ # "recreational" version
19
+ cutoff_i = -0.03
20
+ if i<cutoff_i then cost=[cost,minetti(cutoff_i)].max end
21
+ end
22
+ return cost
23
+ end
24
+ # Five-parameter fit to the following data:
25
+ # c is minimized at imin, and has the correct value cmin there (see comments in ClimbFactorPhys.i_to_iota())
26
+ # slopes at +-infty are ClimbFactorPhys.minetti's values: sp=9.8/0.218; sm=9.8/-1.062 for running,
27
+ # sp=9.8/0.243; sm=9.8/-1.215 for walking
28
+ # match ClimbFactorPhys.minetti's value at i=0.0
29
+ # original analytic work, with p=2 and slightly different values of sp and sm:
30
+ # calc -e "x0=-0.181355; y0=1.781269; sp=9.8/.23; sm=9.8/-1.2; a=(sp-sm)/2; c=a/[(sp+sm)/2]; b=x0^2(c^2-1); d=(1/a)*{y0-a*[sqrt(x0^2+b)+x0/c]}; a(1-1/c)"
31
+ # a = 25.3876811594203
32
+ # c = 1.47422680412371
33
+ # b = 0.0385908791280687
34
+ # d = -0.0741786448190981
35
+ # I then optimized the parameters further, including p, numerically, to fit the above criteria.
36
+ # Also checked that it agrees well with the polynomial for a reasonable range of i values.
37
+
38
+ def self.minetti_original(i) # their 5th-order polynomial fits; these won't work well at extremes of i
39
+ if $running then return minetti_cr(i) else return minetti_cw(i) end
40
+ end
41
+
42
+ def self.i_to_iota(i)
43
+ # convert i to a linearized scale iota, where iota^2=[C(i)-C(imin)]/c2 and sign(iota)=sign(i-imin)
44
+ # The following are the minima of the Minetti functions.
45
+ if $running then
46
+ imin = -0.181355
47
+ cmin = 1.781269
48
+ c2=66.0 # see comments at ClimbFactorPhys.minetti_quadratic_coeffs()
49
+ else
50
+ imin = -0.152526
51
+ cmin= 0.935493
52
+ c2=94.0 # see comments at ClimbFactorPhys.minetti_quadratic_coeffs()
53
+ end
54
+ if i.class != Float then i=to_f(i) end
55
+ if i.infinite? then return i end
56
+ c=minetti(i)
57
+ if c<cmin then
58
+ # happens sometimes due to rounding
59
+ c=cmin
60
+ end
61
+ result = Math::sqrt((c-cmin)/c2)
62
+ if i<imin then result= -result end
63
+ return result
64
+ end
65
+
66
+ def self.minetti_quadratic_coeffs()
67
+ # my rough approximation to Minetti, optimized to fit the range that's most common
68
+ if $running then
69
+ i0=-0.15
70
+ c0=1.84
71
+ c2=66.0
72
+ else
73
+ i0=-0.1
74
+ c0=1.13
75
+ c2=94.0
76
+ end
77
+ b0=c0+c2*i0*i0
78
+ b1=-2*c2*i0
79
+ b2=c2
80
+ return [i0,c0,c2,b0,b1,b2]
81
+ end
82
+
83
+ def self.minetti_cr(i) # no longer used
84
+ # i = gradient
85
+ # cr = cost of running, in J/kg.m
86
+ if i>0.5 || i<-0.5 then return minetti_steep(i) end
87
+ return 155.4*i**5-30.4*i**4-43.3*i**3+46.3*i**2+19.5*i+3.6
88
+ # note that the 3.6 is different from their best value of 3.4 on the flats, i.e., the polynomial isn't a perfect fit
89
+ end
90
+
91
+ def self.minetti_cw(i) # no longer used
92
+ # i = gradient
93
+ # cr = cost of walking, in J/kg.m
94
+ if i>0.5 || i<-0.5 then return minetti_steep(i) end
95
+ return 280.5*i**5-58.7*i**4-76.8*i**3+51.9*i**2+19.6*i+2.5
96
+ end
97
+
98
+ def self.minetti_steep(i) # no longer used
99
+ g=9.8 # m/s2=J/kg.m
100
+ if i>0 then eff=0.23 else eff=-1.2 end
101
+ return g*i/eff
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,86 @@
1
+ require_relative "climb_factor/filtering"
2
+ require_relative "climb_factor/physiology"
3
+ require_relative "climb_factor/low_level_math"
4
+
5
+ module ClimbFactor
6
+ def self.estimate(hv,nominal_distance:nil,filtering:200.0)
7
+ # inputs:
8
+ # All inputs are in units of meters.
9
+ # hv = list of [horizontal,vertical] coordinate pairs
10
+ # nominal_distance = if supplied, scale horizontal length of course to equal this value
11
+ # filtering = get rid of bogus fluctuations in vertical data that occur on horizontal scales less than this
12
+ # output:
13
+ # cf = a floating-point number representing a climb factor, expressed as a percentage
14
+ hv = Filtering.resample_and_filter_hv(hv,filtering)
15
+ rescale = 1.0
16
+ if !nominal_distance.nil? then
17
+ rescale = nominal_distance/(hv.last[0]-hv[0][0])
18
+ end
19
+ stats,hv = integrate_gain_and_energy(hv,rescale)
20
+ c,h = stats['c'],stats['h'] # energy cost in joules and (possibly rescaled) horiz distance in meters
21
+ cf = 100.0*(c-h*Phys.minetti(0.0))/c
22
+ return cf
23
+ end
24
+
25
+ def self.integrate_gain_and_energy(hv,rescale,body_mass:1.0)
26
+ # integrate to find total gain, slope distance, and energy burned
27
+ # returns [stats,hv], where:
28
+ # stats = {'c'=>c,'d'=>d,'gain'=>gain,'i_rms'=>i_rms,...}
29
+ # hv = modified copy of input hv, with predicted times added, if we have the necessary data
30
+ v = 0 # total vertical distance (=0 at end of a loop)
31
+ d = 0 # total distance along the slope
32
+ gain = 0 # total gain
33
+ c = 0 # cost in joules
34
+ first = true
35
+ old_h = 0
36
+ old_v = 0
37
+ i_sum = 0.0
38
+ i_sum_sq = 0.0
39
+ iota_sum = 0.0
40
+ iota_sum_sq = 0.0
41
+ baumel_si = 0.0 # compute this directly as a check
42
+ t = 0.0 # integrated time, in seconds
43
+ h_reintegrated = 0.0 # if rescale!=1, this should be the same as nominal_h
44
+ k = 0
45
+ hv.each { |a|
46
+ h,v = a
47
+ if !first then
48
+ dh = (h-old_h)*rescale
49
+ dv = v-old_v
50
+ dd = Math::sqrt(dh*dh+dv*dv)
51
+ h_reintegrated = h_reintegrated+dh
52
+ d = d+dd
53
+ if dv>0 then gain=gain+dv end
54
+ i=0
55
+ if dh>0 then i=dv/dh end
56
+ if dh>0 then baumel_si=baumel_si+dv**2/dh end
57
+ # In the following, weight by dh, although normally this doesn't matter because we make the
58
+ # h intervals constant before this point.
59
+ i_sum = i_sum + i*dh
60
+ i_sum_sq = i_sum_sq + i*i*dh
61
+ dc = dd*body_mass*Phys.minetti(i)
62
+ # in theory it matters whether we use dd or dh here; I think from Minetti's math it's dd
63
+ c = c+dc
64
+ #if not ($split_energy_at.nil?) and d-dd<$split_energy_at_m and d>$split_energy_at_m then
65
+ # $stderr.print "at d=#{$split_energy_at}, energy=#{(c*0.0002388459).round} kcals\n"
66
+ # # fixme -- implement this in a better way
67
+ #end
68
+ k=k+1
69
+ end
70
+ old_h = h
71
+ old_v = v
72
+ first = false
73
+ }
74
+ n = hv.length-1.0
75
+ h = h_reintegrated # should equal $nominal_h; may differ from hv.last[0]-hv[0][0] if rescale!=1
76
+ i_rms = Math::sqrt(i_sum_sq/h - (i_sum/h)**2)
77
+ i_mean = (hv.last[1]-hv[0][1])/h
78
+ i0,c0,c2,b0,b1,b2 = Phys.minetti_quadratic_coeffs()
79
+ e_q = h*body_mass*(b0+b1*i_mean+b2*i_rms)
80
+ cf = (c-h*body_mass*Phys.minetti(0.0))/c
81
+ stats = {'c'=>c,'h'=>h,'d'=>d,'gain'=>gain,'i_rms'=>i_rms,'i_mean'=>i_mean,'e_q'=>e_q,
82
+ 'cf'=>cf,'baumel_si'=>baumel_si,
83
+ 't'=>t}
84
+ return [stats,hv]
85
+ end
86
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: climb_factor_gem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Crowell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-08-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - README.md
21
+ - lib/climb_factor.rb
22
+ - lib/climb_factor/filtering.rb
23
+ - lib/climb_factor/geometry.rb
24
+ - lib/climb_factor/low_level_math.rb
25
+ - lib/climb_factor/physiology.rb
26
+ homepage: https://bitbucket.org/hello-drifter/climb-factor-gem
27
+ licenses:
28
+ - GPL-2.0-or-later
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 1.9.0
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.0.3.1
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: A library that uses a scientifically validated model to determine the energetic
49
+ cost of running up and down hills.
50
+ test_files: []