git-fit 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1036c14a867417be89ed1c100aa71de434f38695c49540ef56bf2b7e7d197f9
4
- data.tar.gz: 66b405c18d393b552ac0e1c0000b92a66f695c6939432af2228f7a4230fd38fe
3
+ metadata.gz: a0433b30c8c6ae304f3cca0d0bdc83d32579566265383eff605e8cee4b4db0f9
4
+ data.tar.gz: 88056dd48a9c6bfc81853d72c61d1783816921ff546875321de26d2f2241c154
5
5
  SHA512:
6
- metadata.gz: d2b3c47c8b27144718d11303a7127ed6a7734e7e6015cca0c9c40ab52ec6ea08c07b1f9d5df7b9f491eeb5928f6034bc6a94e56cf5616954b0298dbbfae3e46f
7
- data.tar.gz: 7f022ddad0be9ff9709310aef97f3ec4016224d719747cb0a5e6c5f7f484390dafa5257633161c187b5f8d51efaaafcc86f6e7480ff5d5f6e905db1287885932
6
+ metadata.gz: c07db6a65c33503843c0c39ee17affff523ba946f9bc6993dceacbce4e926f2358bc7424bb2e892ff3732e4848f66a7a9bddd9a78f5ad54f49605cd75d987916
7
+ data.tar.gz: dd0fffe383ce5a329d52a49f6225d325768fcf59a0f3752e27edb460a81e0913bf1a42615c2eea9ddecae723d82f8edbb7657535e7a4d31485e0929728e59671
@@ -57,16 +57,16 @@ module GitFit
57
57
  r = range_per_km.to_f
58
58
  s = seg_density.to_f
59
59
 
60
- m = GitFit::Elevation::TerrainCoeff.terrain_M(
60
+ m = GitFit::Elevation::TerrainCoeff.terrain_m(
61
61
  gain_per_km: g,
62
62
  range_per_km: r,
63
- seg_density: s
63
+ seg_density: s,
64
64
  )
65
65
  h = GitFit::Elevation::TerrainCoeff.hysteresis_threshold(m)
66
66
  spacing = GitFit::Elevation::TerrainCoeff.dem_spacing(
67
67
  gain_per_km: g,
68
68
  range_per_km: r,
69
- seg_density: s
69
+ seg_density: s,
70
70
  )
71
71
 
72
72
  say "terrain_M: #{format('%.3f', m)}"
@@ -36,7 +36,7 @@ module GitFit
36
36
  @model = nil
37
37
  end
38
38
 
39
- def terrain_M(gain_per_km:, range_per_km:, seg_density:)
39
+ def terrain_m(gain_per_km:, range_per_km:, seg_density:)
40
40
  g = gain_per_km.to_f
41
41
  r = range_per_km.to_f
42
42
  s = seg_density.to_f
@@ -48,21 +48,21 @@ module GitFit
48
48
  g_n * w[0] + r_n * w[1] + s_n * w[2]
49
49
  end
50
50
 
51
- def hysteresis_threshold(m)
52
- return 2.0 if m <= 0
51
+ def hysteresis_threshold(m_val)
52
+ return 2.0 if m_val <= 0
53
53
  pav = model['pav_h']
54
- return 2.0 if m <= pav[0][0]
55
- return pav[-1][1] if m >= pav[-1][0]
56
- pav.each_with_index do |(xk, yk), i|
54
+ return 2.0 if m_val <= pav[0][0]
55
+ return pav[-1][1] if m_val >= pav[-1][0]
56
+ pav.each_with_index do |(_xk, yk), i|
57
57
  next unless i + 1 < pav.size
58
- return yk if m <= pav[i + 1][0]
58
+ return yk if m_val <= pav[i + 1][0]
59
59
  end
60
60
  pav[-1][1]
61
61
  end
62
62
 
63
63
  def dem_spacing(gain_per_km:, range_per_km:, seg_density:)
64
64
  nearest_neighbor_spacing(gain_per_km, range_per_km, seg_density)
65
- rescue => e
65
+ rescue StandardError => e
66
66
  warn "TerrainCoeff: dem_spacing fallback to 181m (#{e.message})"
67
67
  181
68
68
  end
@@ -75,7 +75,7 @@ module GitFit
75
75
  elevs = {}
76
76
  uniq_vals.each_slice(100).with_index do |slice, bi|
77
77
  qs = slice.map { |la, ln| "#{la},#{ln}" }.join('|')
78
- uri = URI("#{DEM_URL}?locations=#{URI.encode_www_form_component(qs)}")
78
+ uri = URI("#{DEM_URL}?locations=#{qs.gsub(',', '%2C').gsub('|', '%7C')}")
79
79
  begin
80
80
  res = Net::HTTP.get_response(uri)
81
81
  if res.code == '200'
@@ -112,7 +112,7 @@ module GitFit
112
112
  d = Math.sqrt(
113
113
  (gain_per_km.to_f - ref['gk'])**2 +
114
114
  (range_per_km.to_f - ref['rk'])**2 +
115
- (seg_density.to_f - ref['sd'])**2
115
+ (seg_density.to_f - ref['sd'])**2,
116
116
  )
117
117
  next unless d < best_dist
118
118
  best_dist = d
@@ -25,11 +25,9 @@ module GitFit
25
25
  def each_source_sample(&block)
26
26
  adapters = @sources ? resolve_sources(@sources) : all_adapters
27
27
  adapters.each do |adapter|
28
- begin
29
- adapter.training_samples.each(&block)
30
- rescue StandardError => e
31
- warn "Collector: #{adapter.name} failed: #{e.message}"
32
- end
28
+ adapter.training_samples.each(&block)
29
+ rescue StandardError => e
30
+ warn "Collector: #{adapter.name} failed: #{e.message}"
33
31
  end
34
32
  end
35
33
 
@@ -69,7 +67,7 @@ module GitFit
69
67
  SPACINGS.each do |sp|
70
68
  sel = sample_indices(sm, segs, distances, sp)
71
69
  lmin, lmax, npts = layered_estimate(dem, sel)
72
- err = [ (lmin - truth_min).abs, (lmax - truth_max).abs ].max
70
+ err = [(lmin - truth_min).abs, (lmax - truth_max).abs].max
73
71
  if err <= BUDGET
74
72
  chosen = { h: h, spacing: sp, err: err, npts: npts }
75
73
  break
@@ -78,7 +76,7 @@ module GitFit
78
76
  unless chosen
79
77
  sel = sample_indices(sm, segs, distances, SPACINGS.last)
80
78
  lmin, lmax, npts = layered_estimate(dem, sel)
81
- err = [ (lmin - truth_min).abs, (lmax - truth_max).abs ].max
79
+ err = [(lmin - truth_min).abs, (lmax - truth_max).abs].max
82
80
  chosen = { h: h, spacing: SPACINGS.last, err: err, npts: npts }
83
81
  end
84
82
  best = chosen if best.nil? || chosen[:npts] < best[:npts]
@@ -98,11 +96,11 @@ module GitFit
98
96
  npts: best[:npts],
99
97
  gain_per_km: f[:gain_per_km],
100
98
  range_per_km: f[:range_per_km],
101
- seg_density: f[:seg_density]
99
+ seg_density: f[:seg_density],
102
100
  }
103
101
  end
104
102
 
105
- def features(altitudes, distances, locations)
103
+ def features(altitudes, distances, _locations)
106
104
  return nil if altitudes.empty? || distances.empty?
107
105
 
108
106
  dm = distances.last
@@ -118,7 +116,7 @@ module GitFit
118
116
  range_per_km: range_per_km,
119
117
  seg_density: seg_density,
120
118
  distance_m: dm,
121
- npts: altitudes.size
119
+ npts: altitudes.size,
122
120
  }
123
121
  end
124
122
 
@@ -135,8 +133,8 @@ module GitFit
135
133
  moving_avg(moving_median(vals, 9), 9)
136
134
  end
137
135
 
138
- def moving_median(vals, w)
139
- half = w / 2
136
+ def moving_median(vals, window)
137
+ half = window / 2
140
138
  (0...vals.size).map do |i|
141
139
  lo = [0, i - half].max
142
140
  hi = [vals.size - 1, i + half].min
@@ -144,8 +142,8 @@ module GitFit
144
142
  end
145
143
  end
146
144
 
147
- def moving_avg(vals, w)
148
- half = w / 2
145
+ def moving_avg(vals, window)
146
+ half = window / 2
149
147
  (0...vals.size).map do |i|
150
148
  lo = [0, i - half].max
151
149
  hi = [vals.size - 1, i + half].min
@@ -153,7 +151,7 @@ module GitFit
153
151
  end
154
152
  end
155
153
 
156
- def segments(smooth, h, min_len: 20)
154
+ def segments(smooth, threshold, min_len: 20)
157
155
  n = smooth.size
158
156
  return [[0, n - 1]] if n < 2
159
157
 
@@ -166,28 +164,34 @@ module GitFit
166
164
  v = smooth[i]
167
165
  if rising.nil?
168
166
  if v > ext_val
169
- ext_val = v; ext_idx = i; rising = true
167
+ ext_val = v
168
+ ext_idx = i
169
+ rising = true
170
170
  elsif v < ext_val
171
- ext_val = v; ext_idx = i; rising = false
171
+ ext_val = v
172
+ ext_idx = i
173
+ rising = false
172
174
  end
173
175
  elsif rising
174
176
  if v > ext_val
175
- ext_val = v; ext_idx = i
176
- elsif ext_val - v >= h
177
+ ext_val = v
178
+ ext_idx = i
179
+ elsif ext_val - v >= threshold
177
180
  segs << [seg_start, ext_idx] if ext_idx - seg_start >= min_len
178
181
  seg_start = ext_idx if ext_idx - seg_start >= min_len
179
182
  rising = false
180
- ext_val = v; ext_idx = i
181
- end
182
- else
183
- if v < ext_val
184
- ext_val = v; ext_idx = i
185
- elsif v - ext_val >= h
186
- segs << [seg_start, ext_idx] if ext_idx - seg_start >= min_len
187
- seg_start = ext_idx if ext_idx - seg_start >= min_len
188
- rising = true
189
- ext_val = v; ext_idx = i
183
+ ext_val = v
184
+ ext_idx = i
190
185
  end
186
+ elsif v < ext_val
187
+ ext_val = v
188
+ ext_idx = i
189
+ elsif v - ext_val >= threshold
190
+ segs << [seg_start, ext_idx] if ext_idx - seg_start >= min_len
191
+ seg_start = ext_idx if ext_idx - seg_start >= min_len
192
+ rising = true
193
+ ext_val = v
194
+ ext_idx = i
191
195
  end
192
196
  end
193
197
  segs << [seg_start, n - 1] if n - 1 - seg_start >= min_len
@@ -196,8 +200,9 @@ module GitFit
196
200
 
197
201
  def sample_indices(smooth, segs, dist, spacing)
198
202
  sel = Set.new
199
- segs.each { |a, b| sel << a; sel << b }
200
203
  segs.each do |a, b|
204
+ sel << a
205
+ sel << b
201
206
  next if b - a < 2
202
207
 
203
208
  d0 = dist[a].to_f
@@ -25,7 +25,7 @@ module GitFit
25
25
  norms: norms,
26
26
  pav_h: pav_h,
27
27
  lookup: lookup,
28
- meta: meta
28
+ meta: meta,
29
29
  }
30
30
  end
31
31
 
@@ -101,7 +101,7 @@ module GitFit
101
101
  end
102
102
 
103
103
  def fit_pav_h
104
- m_values = @samples.map { |s| terrain_M(s) }
104
+ m_values = @samples.map { |s| terrain_m(s) }
105
105
  h_values = @samples.map { |s| s[:h_opt] }
106
106
 
107
107
  sorted = @samples.map.with_index { |_, i| [m_values[i], h_values[i]] }.sort_by(&:first)
@@ -113,7 +113,7 @@ module GitFit
113
113
 
114
114
  sorted.each do |m, h|
115
115
  if current_m.nil? || (m - current_m).abs < 0.01
116
- current_m = m unless current_m
116
+ current_m ||= m
117
117
  current_h ||= h
118
118
  current_h = [current_h, h].min
119
119
  count += 1
@@ -129,7 +129,7 @@ module GitFit
129
129
  pav
130
130
  end
131
131
 
132
- def terrain_M(sample)
132
+ def terrain_m(sample)
133
133
  g = sample[:gain_per_km].to_f
134
134
  r = sample[:range_per_km].to_f
135
135
  s = sample[:seg_density].to_f
@@ -148,7 +148,7 @@ module GitFit
148
148
  'rk' => s[:range_per_km],
149
149
  'sd' => s[:seg_density],
150
150
  'h' => s[:h_opt],
151
- 'sp' => s[:spacing_opt]
151
+ 'sp' => s[:spacing_opt],
152
152
  }
153
153
  end
154
154
  end
@@ -157,7 +157,7 @@ module GitFit
157
157
  {
158
158
  'n_samples' => @samples.size,
159
159
  'trained_at' => Time.now.strftime('%Y-%m-%d'),
160
- 'sources' => @samples.map { |s| s[:source] }.uniq
160
+ 'sources' => @samples.map { |s| s[:source] }.uniq,
161
161
  }
162
162
  end
163
163
  end
@@ -17,33 +17,33 @@ module GitFit
17
17
  end
18
18
 
19
19
  def run
20
- puts "Collecting samples..."
20
+ puts 'Collecting samples...'
21
21
  collector = Collector.new(sources: @sources)
22
22
  samples = collector.collect
23
23
 
24
24
  if samples.empty?
25
- warn "No samples collected"
25
+ warn 'No samples collected'
26
26
  return false
27
27
  end
28
28
 
29
29
  puts "Collected #{samples.size} samples"
30
30
 
31
- puts "Fitting model..."
31
+ puts 'Fitting model...'
32
32
  fitter = Fitter.new(samples)
33
33
  model = fitter.fit
34
34
 
35
35
  unless model
36
- warn "Fitting failed"
36
+ warn 'Fitting failed'
37
37
  return false
38
38
  end
39
39
 
40
40
  puts "Writing model to #{@output}..."
41
41
  unless fitter.write_msgpack(@output)
42
- warn "Failed to write model"
42
+ warn 'Failed to write model'
43
43
  return false
44
44
  end
45
45
 
46
- puts "Model trained successfully!"
46
+ puts 'Model trained successfully!'
47
47
  puts " n_samples: #{model[:meta]['n_samples']}"
48
48
  puts " sources: #{model[:meta]['sources'].join(', ')}"
49
49
  puts " ridge_h: #{model[:ridge_h].map { |w| format('%.2f', w) }.join(', ')}"
@@ -122,6 +122,61 @@ module GitFit
122
122
  def camelcase_to_snakecase(str)
123
123
  str.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
124
124
  end
125
+
126
+ def training_samples
127
+ std = File.join('data', 'std', config_key)
128
+ return [] unless Dir.exist?(std)
129
+
130
+ Dir.children(std).filter_map do |f|
131
+ next unless f.end_with?('.json')
132
+
133
+ pts = JSON.parse(File.read(File.join(std, f))) rescue next
134
+ next unless pts.is_a?(Array) && pts.any?
135
+
136
+ altitudes = pts.filter_map { |p| p['altitude']&.to_f }
137
+ next if altitudes.size < 2
138
+
139
+ locations = pts.filter_map { |p| extract_location(p) }
140
+ next if locations.size < 2
141
+
142
+ distances = cumulative_distances(locations)
143
+
144
+ {
145
+ altitudes: altitudes,
146
+ distances: distances,
147
+ locations: locations,
148
+ source: config_key,
149
+ id: File.basename(f, '.json'),
150
+ }
151
+ end
152
+ end
153
+
154
+ def extract_location(point)
155
+ lat = point['positionLat'] || point['latitude'] || point['lat']
156
+ lng = point['positionLong'] || point['longitude'] || point['lng']
157
+ return nil unless lat && lng
158
+ [lng.to_f, lat.to_f]
159
+ end
160
+
161
+ def cumulative_distances(locations)
162
+ return [] if locations.size < 2
163
+
164
+ dists = [0.0]
165
+ (1...locations.size).each do |i|
166
+ dists << dists.last + haversine(locations[i - 1], locations[i])
167
+ end
168
+ dists
169
+ end
170
+
171
+ def haversine(loc1, loc2)
172
+ r = 6_371_000.0
173
+ dlat = (loc2[0] - loc1[0]) * Math::PI / 180
174
+ dlon = (loc2[1] - loc1[1]) * Math::PI / 180
175
+ la1 = loc1[0] * Math::PI / 180
176
+ la2 = loc2[0] * Math::PI / 180
177
+ h = Math.sin(dlat / 2)**2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dlon / 2)**2
178
+ 2 * r * Math.asin(Math.sqrt(h))
179
+ end
125
180
  end
126
181
 
127
182
  protected
@@ -208,61 +263,6 @@ module GitFit
208
263
  raise NotImplementedError, "#{self.class} must implement #raw_path"
209
264
  end
210
265
 
211
- def self.training_samples
212
- std = File.join('data', 'std', config_key)
213
- return [] unless Dir.exist?(std)
214
-
215
- Dir.children(std).filter_map do |f|
216
- next unless f.end_with?('.json')
217
-
218
- pts = JSON.parse(File.read(File.join(std, f))) rescue next
219
- next unless pts.is_a?(Array) && pts.any?
220
-
221
- altitudes = pts.filter_map { |p| p['altitude']&.to_f }
222
- next if altitudes.size < 2
223
-
224
- locations = pts.filter_map { |p| extract_location(p) }
225
- next if locations.size < 2
226
-
227
- distances = cumulative_distances(locations)
228
-
229
- {
230
- altitudes: altitudes,
231
- distances: distances,
232
- locations: locations,
233
- source: config_key,
234
- id: File.basename(f, '.json')
235
- }
236
- end
237
- end
238
-
239
- def self.extract_location(p)
240
- lat = p['positionLat'] || p['latitude'] || p['lat']
241
- lng = p['positionLong'] || p['longitude'] || p['lng']
242
- return nil unless lat && lng
243
- [lng.to_f, lat.to_f]
244
- end
245
-
246
- def self.cumulative_distances(locations)
247
- return [] if locations.size < 2
248
-
249
- dists = [0.0]
250
- (1...locations.size).each do |i|
251
- dists << dists.last + haversine(locations[i - 1], locations[i])
252
- end
253
- dists
254
- end
255
-
256
- def self.haversine(a, b)
257
- r = 6_371_000.0
258
- dlat = (b[0] - a[0]) * Math::PI / 180
259
- dlon = (b[1] - a[1]) * Math::PI / 180
260
- la1 = a[0] * Math::PI / 180
261
- la2 = b[0] * Math::PI / 180
262
- h = Math.sin(dlat / 2)**2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dlon / 2)**2
263
- 2 * r * Math.asin(Math.sqrt(h))
264
- end
265
-
266
266
  def write_source_archive(data, platform_id, ext = 'json')
267
267
  dir = raw_dir
268
268
  FileUtils.mkdir_p(dir)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitFit
4
- VERSION = '0.18.0'
4
+ VERSION = '0.18.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-fit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.18.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lax