git-fit 0.18.3 → 0.18.4
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 +4 -4
- data/lib/git_fit/elevation/dem_cache.rb +51 -0
- data/lib/git_fit/elevation/dem_fetch.rb +46 -0
- data/lib/git_fit/elevation/terrain_coeff.rb +15 -46
- data/lib/git_fit/elevation/train/collector.rb +19 -5
- data/lib/git_fit/elevation/train/runner.rb +49 -12
- data/lib/git_fit/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b68548ae0905b7374d85979f27053683d604ab26c8ae0e66011cd6cf776bf7ad
|
|
4
|
+
data.tar.gz: 863da16427ca424985a8f6ef62237b9a53e2c782fc390950323fd04ad2cb6929
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa7450b9a823e2b8dca5d5a81d6663caba96fdc9f43444cb53619b5f0cf3b4afc2016b3e69c74e03a4a53250cd4f6a7f79b38862a87a98dc1fb4a538f014117f
|
|
7
|
+
data.tar.gz: 1d7a706ea4b63ccf6f2607a236d73bbbcdeee9a49fb809846030667f8dec6b2b6f43eec3019891d00fbcb9d8b71198c3eec72243bc4a0d9e650bb012dfaff481
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module GitFit
|
|
7
|
+
module Elevation
|
|
8
|
+
module DemCache
|
|
9
|
+
DEM_CACHE_DIR = ENV.fetch('GIT_FIT_DEM_CACHE_DIR', 'data/cache/dem')
|
|
10
|
+
CACHE_FILE = File.join(DEM_CACHE_DIR, 'dem.json')
|
|
11
|
+
GRID = 0.0003
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def grid_key(lat, lng)
|
|
15
|
+
"#{(lat / GRID).round}/#{(lng / GRID).round}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load_all
|
|
19
|
+
return {} unless File.exist?(CACHE_FILE)
|
|
20
|
+
JSON.parse(File.read(CACHE_FILE))
|
|
21
|
+
rescue StandardError
|
|
22
|
+
{}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def load_batch(slice)
|
|
26
|
+
all = load_all
|
|
27
|
+
results = {}
|
|
28
|
+
slice.each do |lat, lng|
|
|
29
|
+
gk = grid_key(lat, lng)
|
|
30
|
+
results[gk] = all[gk] if all.key?(gk)
|
|
31
|
+
end
|
|
32
|
+
results
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def missing_keys(slice, cached)
|
|
36
|
+
slice.reject { |lat, lng| cached.key?(grid_key(lat, lng)) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def save_batch(elevs_by_gk)
|
|
40
|
+
return if elevs_by_gk.empty?
|
|
41
|
+
FileUtils.mkdir_p(DEM_CACHE_DIR)
|
|
42
|
+
existing = load_all
|
|
43
|
+
elevs_by_gk.each { |gk, elev| existing[gk] = elev }
|
|
44
|
+
File.write(CACHE_FILE, JSON.generate(existing))
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
warn "DEM cache write error: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module GitFit
|
|
7
|
+
module Elevation
|
|
8
|
+
module DemFetch
|
|
9
|
+
DEM_URL = 'https://api.opentopodata.org/v1/srtm30m'
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def fetch_batch(slice, batch_idx, elevs, grid_key_fn)
|
|
13
|
+
qs = slice.map { |la, ln| "#{la},#{ln}" }.join('|')
|
|
14
|
+
uri = URI("#{DEM_URL}?locations=#{qs.gsub(',', '%2C').gsub('|', '%7C')}")
|
|
15
|
+
attempt = 0
|
|
16
|
+
loop do
|
|
17
|
+
res = Net::HTTP.get_response(uri)
|
|
18
|
+
if res.code == '200'
|
|
19
|
+
results = JSON.parse(res.body)['results']
|
|
20
|
+
slice.each_with_index do |(lat, lng), i|
|
|
21
|
+
elevs[grid_key_fn.call(lat, lng)] = results[i]['elevation'] if results[i]
|
|
22
|
+
end
|
|
23
|
+
break
|
|
24
|
+
elsif res.code == '429'
|
|
25
|
+
attempt += 1
|
|
26
|
+
if attempt < 5
|
|
27
|
+
delay = 2**attempt
|
|
28
|
+
warn "DEM batch #{batch_idx}: rate limited, retrying in #{delay}s (attempt #{attempt})"
|
|
29
|
+
sleep(delay)
|
|
30
|
+
else
|
|
31
|
+
warn "DEM batch #{batch_idx}: rate limited, giving up after #{attempt} attempts"
|
|
32
|
+
break
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
warn "DEM batch #{batch_idx} HTTP #{res.code}"
|
|
36
|
+
break
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
warn "DEM batch #{batch_idx}: #{e.message}"
|
|
40
|
+
break
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'msgpack'
|
|
4
|
-
require 'net/http'
|
|
5
|
-
require 'uri'
|
|
6
4
|
|
|
5
|
+
require_relative 'dem_cache'
|
|
6
|
+
require_relative 'dem_fetch'
|
|
7
7
|
require_relative 'terrain_lookup'
|
|
8
8
|
|
|
9
9
|
module GitFit
|
|
10
10
|
module Elevation
|
|
11
11
|
module TerrainCoeff
|
|
12
|
-
|
|
13
|
-
DEM_CACHE_DIR = File.join(CACHE_DIR, 'cache')
|
|
12
|
+
MODEL_CACHE_DIR = ENV.fetch('GIT_FIT_DEM_MODEL_DIR', 'data/cache/dem_fit')
|
|
14
13
|
BUNDLED_MODEL_DIR = File.expand_path('model', __dir__)
|
|
15
14
|
BUNDLED_MODEL = File.join(BUNDLED_MODEL_DIR, 'dem_model.msgpack')
|
|
16
15
|
MODEL_FILE = 'dem_model.msgpack'
|
|
17
|
-
DEM_URL = 'https://api.opentopodata.org/v1/srtm30m'
|
|
18
16
|
GRID = 0.0003
|
|
19
17
|
|
|
20
18
|
class << self
|
|
@@ -27,10 +25,10 @@ module GitFit
|
|
|
27
25
|
end
|
|
28
26
|
|
|
29
27
|
def find_model
|
|
30
|
-
cache_path = File.join(
|
|
28
|
+
cache_path = File.join(MODEL_CACHE_DIR, MODEL_FILE)
|
|
31
29
|
return cache_path if File.exist?(cache_path)
|
|
32
30
|
return BUNDLED_MODEL if File.exist?(BUNDLED_MODEL)
|
|
33
|
-
raise "No #{MODEL_FILE} found in cache (#{
|
|
31
|
+
raise "No #{MODEL_FILE} found in cache (#{MODEL_CACHE_DIR}) or bundled (#{BUNDLED_MODEL})"
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
def reset!
|
|
@@ -65,7 +63,16 @@ module GitFit
|
|
|
65
63
|
uniq_vals = uniquify_locations(locations)
|
|
66
64
|
elevs = {}
|
|
67
65
|
uniq_vals.each_slice(100).with_index do |slice, bi|
|
|
68
|
-
|
|
66
|
+
cached = DemCache.load_batch(slice)
|
|
67
|
+
elevs.merge!(cached)
|
|
68
|
+
missing = DemCache.missing_keys(slice, cached)
|
|
69
|
+
if missing.any?
|
|
70
|
+
missing_locations = missing.map { |lat, lng| [lat, lng] }
|
|
71
|
+
batch = {}
|
|
72
|
+
DemFetch.fetch_batch(missing_locations, bi, batch, method(:grid_key))
|
|
73
|
+
elevs.merge!(batch)
|
|
74
|
+
DemCache.save_batch(batch)
|
|
75
|
+
end
|
|
69
76
|
sleep 0.15
|
|
70
77
|
end
|
|
71
78
|
locations.map { |lat, lng| elevs[grid_key(lat, lng)] }
|
|
@@ -77,44 +84,6 @@ module GitFit
|
|
|
77
84
|
uniq.values
|
|
78
85
|
end
|
|
79
86
|
|
|
80
|
-
def fetch_dem_batch(slice, batch_idx)
|
|
81
|
-
elevs = {}
|
|
82
|
-
qs = slice.map { |la, ln| "#{la},#{ln}" }.join('|')
|
|
83
|
-
uri = URI("#{DEM_URL}?locations=#{qs.gsub(',', '%2C').gsub('|', '%7C')}")
|
|
84
|
-
attempt = 0
|
|
85
|
-
loop do
|
|
86
|
-
# rubocop:disable Style/RedundantBegin
|
|
87
|
-
begin
|
|
88
|
-
res = Net::HTTP.get_response(uri)
|
|
89
|
-
if res.code == '200'
|
|
90
|
-
results = JSON.parse(res.body)['results']
|
|
91
|
-
slice.each_with_index do |(lat, lng), i|
|
|
92
|
-
elevs[grid_key(lat, lng)] = results[i]['elevation'] if results[i]
|
|
93
|
-
end
|
|
94
|
-
break
|
|
95
|
-
elsif res.code == '429'
|
|
96
|
-
attempt += 1
|
|
97
|
-
if attempt < 5
|
|
98
|
-
delay = 2**attempt
|
|
99
|
-
warn "DEM batch #{batch_idx}: rate limited, retrying in #{delay}s (attempt #{attempt})"
|
|
100
|
-
sleep(delay)
|
|
101
|
-
else
|
|
102
|
-
warn "DEM batch #{batch_idx}: rate limited, giving up after #{attempt} attempts"
|
|
103
|
-
break
|
|
104
|
-
end
|
|
105
|
-
else
|
|
106
|
-
warn "DEM batch #{batch_idx} HTTP #{res.code}"
|
|
107
|
-
break
|
|
108
|
-
end
|
|
109
|
-
rescue StandardError => e
|
|
110
|
-
warn "DEM batch #{batch_idx}: #{e.message}"
|
|
111
|
-
break
|
|
112
|
-
end
|
|
113
|
-
# rubocop:enable Style/RedundantBegin
|
|
114
|
-
end
|
|
115
|
-
elevs
|
|
116
|
-
end
|
|
117
|
-
|
|
118
87
|
def grid_key(lat, lng)
|
|
119
88
|
"#{(lat / GRID).round}/#{(lng / GRID).round}"
|
|
120
89
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../dem_cache'
|
|
3
4
|
require_relative '../terrain_coeff'
|
|
4
5
|
|
|
5
6
|
module GitFit
|
|
@@ -11,15 +12,25 @@ module GitFit
|
|
|
11
12
|
|
|
12
13
|
def initialize(sources: nil)
|
|
13
14
|
@sources = sources
|
|
15
|
+
@samples = []
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
attr_reader :samples
|
|
19
|
+
|
|
20
|
+
def count_samples
|
|
21
|
+
total = 0
|
|
22
|
+
each_source_sample do |_sample|
|
|
23
|
+
total += 1
|
|
24
|
+
end
|
|
25
|
+
total
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def each_sample
|
|
18
29
|
each_source_sample do |sample|
|
|
19
30
|
result = evaluate_sample(sample)
|
|
20
|
-
samples << result if result
|
|
31
|
+
@samples << result if result
|
|
32
|
+
yield result
|
|
21
33
|
end
|
|
22
|
-
samples
|
|
23
34
|
end
|
|
24
35
|
|
|
25
36
|
def each_source_sample(&block)
|
|
@@ -50,11 +61,13 @@ module GitFit
|
|
|
50
61
|
|
|
51
62
|
return nil if altitudes.size < 2
|
|
52
63
|
return nil if distances.empty? || distances.last < 1000
|
|
53
|
-
return nil if sample[:altitudes].size < 2
|
|
54
64
|
|
|
65
|
+
cache_before = DemCache.load_batch(locations)
|
|
55
66
|
dem = TerrainCoeff.elevations_for(locations)
|
|
56
67
|
return nil if dem.empty? || dem.compact.size < 2
|
|
57
68
|
|
|
69
|
+
cache_hit = !cache_before.empty? && cache_before.values.all? { |v| !v.nil? }
|
|
70
|
+
|
|
58
71
|
truth_min = dem.compact.min
|
|
59
72
|
truth_max = dem.compact.max
|
|
60
73
|
sm = smooth(altitudes)
|
|
@@ -97,6 +110,7 @@ module GitFit
|
|
|
97
110
|
gain_per_km: f[:gain_per_km],
|
|
98
111
|
range_per_km: f[:range_per_km],
|
|
99
112
|
seg_density: f[:seg_density],
|
|
113
|
+
cache_hit: cache_hit,
|
|
100
114
|
}
|
|
101
115
|
end
|
|
102
116
|
|
|
@@ -17,19 +17,39 @@ module GitFit
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def run
|
|
20
|
-
puts 'Collecting samples...'
|
|
21
20
|
collector = Collector.new(sources: @sources)
|
|
22
|
-
|
|
21
|
+
total = collector.count_samples
|
|
22
|
+
puts "Collecting samples (#{total} total)..."
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
collected = 0
|
|
25
|
+
cached = 0
|
|
26
|
+
missed = 0
|
|
27
|
+
skipped = 0
|
|
28
|
+
|
|
29
|
+
collector.each_sample do |result|
|
|
30
|
+
if result
|
|
31
|
+
collected += 1
|
|
32
|
+
if result[:cache_hit]
|
|
33
|
+
cached += 1
|
|
34
|
+
else
|
|
35
|
+
missed += 1
|
|
36
|
+
end
|
|
37
|
+
print_progress(collected, cached, missed, total)
|
|
38
|
+
else
|
|
39
|
+
skipped += 1
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
|
|
29
|
-
puts
|
|
43
|
+
puts
|
|
44
|
+
puts "Collected: #{collected} samples (#{cached} hit / #{missed} miss / #{skipped} skipped)"
|
|
45
|
+
|
|
46
|
+
if collected.zero?
|
|
47
|
+
warn 'No valid samples collected'
|
|
48
|
+
return false
|
|
49
|
+
end
|
|
30
50
|
|
|
31
51
|
puts 'Fitting model...'
|
|
32
|
-
fitter = Fitter.new(samples)
|
|
52
|
+
fitter = Fitter.new(collector.samples)
|
|
33
53
|
model = fitter.fit
|
|
34
54
|
|
|
35
55
|
unless model
|
|
@@ -43,15 +63,32 @@ module GitFit
|
|
|
43
63
|
return false
|
|
44
64
|
end
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
puts " n_samples: #{model[:meta]['n_samples']}"
|
|
48
|
-
puts " sources: #{model[:meta]['sources'].join(', ')}"
|
|
49
|
-
puts " ridge_h: #{model[:ridge_h].map { |w| format('%.2f', w) }.join(', ')}"
|
|
50
|
-
puts " output: #{@output}"
|
|
66
|
+
print_summary(model)
|
|
51
67
|
|
|
52
68
|
true
|
|
53
69
|
end
|
|
54
70
|
|
|
71
|
+
def print_progress(collected, cached, missed, total)
|
|
72
|
+
pct = (collected.to_f / total * 100).round(1)
|
|
73
|
+
bar_len = 30
|
|
74
|
+
filled = (collected.to_f / total * bar_len).round
|
|
75
|
+
bar = '=' * filled + '-' * (bar_len - filled)
|
|
76
|
+
$stdout.write "\r [#{bar}] #{pct}% #{collected}/#{total} (h:#{cached} m:#{missed})"
|
|
77
|
+
$stdout.flush
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def print_summary(model)
|
|
81
|
+
meta = model[:meta]
|
|
82
|
+
puts
|
|
83
|
+
puts '=== Model Trained ==='
|
|
84
|
+
puts " samples: #{meta['n_samples']}"
|
|
85
|
+
puts " sources: #{meta['sources'].join(', ')}"
|
|
86
|
+
puts " trained_at: #{meta['trained_at']}"
|
|
87
|
+
puts " ridge_h: #{model[:ridge_h].map { |w| format('%.3f', w) }.join(', ')}"
|
|
88
|
+
puts " norms: #{model[:norms].map { |n| format('%.2f', n) }.join(', ')}"
|
|
89
|
+
puts " output: #{@output}"
|
|
90
|
+
end
|
|
91
|
+
|
|
55
92
|
def default_output_path
|
|
56
93
|
File.join(CACHE_DIR, MODEL_FILE)
|
|
57
94
|
end
|
data/lib/git_fit/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.18.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lax
|
|
@@ -295,6 +295,8 @@ files:
|
|
|
295
295
|
- lib/git_fit/elevation/backfill.rb
|
|
296
296
|
- lib/git_fit/elevation/data/dem_coeff.json
|
|
297
297
|
- lib/git_fit/elevation/data/dem_lookup.json
|
|
298
|
+
- lib/git_fit/elevation/dem_cache.rb
|
|
299
|
+
- lib/git_fit/elevation/dem_fetch.rb
|
|
298
300
|
- lib/git_fit/elevation/terrain_coeff.rb
|
|
299
301
|
- lib/git_fit/elevation/terrain_lookup.rb
|
|
300
302
|
- lib/git_fit/elevation/train/collector.rb
|