Module:Climate profile/testcases
From Landrace.wiki
More actions
Documentation for this module may be created at Module:Climate profile/testcases/doc
-- Module:Climate profile/testcases
-- Fixtures for Module:Climate profile. The expected values of the named places are copied from the
-- computation's own tables (profiles, top-5 matches); the whole-data checks recompute every
-- similarity from the stored variables with the method's distance (Help:Climate matches) and test the
-- shape of every entry. {{#invoke:Climate profile/testcases|table}} shows the results.
local M = require('Module:Climate profile')
local tc = {}
local results = {}
local function check(group, case, expected, got)
local e, g = tostring(expected), tostring(got)
results[#results + 1] = { group = group, case = case, expected = e, got = g, ok = (e == g) }
end
local function frameFor(key, parentArgs)
local parent = mw.getCurrentFrame():newChild{ title = 'Template:Infobox Appellation', args = parentArgs or {} }
return parent:newChild{ title = 'Module:Climate profile', args = key and { key = key } or {} }
end
local GLO = 'Copernicus GLO-90 (90 m)'
local GMTED = 'GMTED2010 (1 km)'
local function sources(dem)
return 'Computed from CHELSA V2.1 (1981–2010), the Köppen–Geiger map of Beck et al. (1991–2020) and '
.. dem .. ' · [[Help:Climate matches|About these figures]]'
end
local function sim(n) return '<span title="Similarity, out of 100">' .. n .. '</span>' end
-- Named places -------------------------------------------------------------------------------------
local function named()
-- A small polygon on the 90 m DEM (sourced elevation 2,350–3,200 m stays in the article's own row).
local f = frameFor('Malana (appellation)')
check('Malana (appellation)', 'has', 'yes', M.has(f))
check('Malana (appellation)', 'koppen', 'Dfc', M.koppen(f))
check('Malana (appellation)', 'elevation', '3171', M.elevation(f))
check('Malana (appellation)', 'matches',
'Nirang (appellation);Mejigh (appellation);Waichin (appellation);Gohar (appellation);Thal (appellation)', M.matches(f))
local e = M._profile('Malana (appellation)')
check('Malana (appellation)', 'Köppen text', 'Dfc (cold, no dry season, cold summer)', M._koppenText(e))
check('Malana (appellation)', 'elevation text', '3,171 m (2,672–3,589 m)', M._elevationText(e))
check('Malana (appellation)', 'sources', sources(GLO), M._sourcesText(e))
-- A point place: one pixel, so one figure, no range. Sourced 1,638 m disagrees by 90 m.
e = M._profile('Saur Valley (appellation)')
check('Saur Valley (appellation)', 'elevation text', '1,548 m', M._elevationText(e))
check('Saur Valley (appellation)', 'Köppen text', 'Cwa (temperate, dry winter, hot summer)', M._koppenText(e))
check('Saur Valley (appellation)', 'matches text', table.concat({
'[[Patkai Bum]] – ' .. sim(75), '[[Kali Valley]] – ' .. sim(74), '[[Liti (appellation)]] – ' .. sim(73),
'[[Naga Hills]] – ' .. sim(72), '[[Upper Jaldhaka Valley]] – ' .. sim(71) }, '<br>'),
M._matchesText('Saur Valley (appellation)'))
-- A coastal polygon on the 1 km DEM. Sourced 218 m disagrees; the computed figure is shown as computed.
e = M._profile('Tha Sala (appellation)')
check('Tha Sala (appellation)', 'elevation text', '14 m (5–35 m)', M._elevationText(e))
check('Tha Sala (appellation)', 'Köppen text', 'Af (tropical, rainforest)', M._koppenText(e))
check('Tha Sala (appellation)', 'sources', sources(GMTED), M._sourcesText(e))
-- The whole block, byte for byte.
check('Abdelghaya Souahel (appellation)', 'rows', table.concat({
'! class="lw-infobox__label" | Köppen class (computed)\n| class="lw-infobox__value" | Csa (temperate, dry summer, hot summer)\n|-',
'! class="lw-infobox__label" | Elevation (computed)\n| class="lw-infobox__value" | 1,061 m (681–1,536 m)\n|-',
'! class="lw-infobox__label" | Closest climate matches\n| class="lw-infobox__value" | '
.. '[[Beni Bouchibet (appellation)]] – ' .. sim(91) .. '<br>[[Ketama (appellation)]] – ' .. sim(89)
.. '<br>[[Chefchaouen]] – ' .. sim(87) .. '<br>[[Beni Ahmed Imoukzan (appellation)]] – ' .. sim(86)
.. '<br>[[Beqaa Valley]] – ' .. sim(77) .. '\n|-',
'| colspan="2" class="lw-infobox__caption" | ' .. sources(GMTED) .. '\n|-',
}, '\n'), M.rows(frameFor('Abdelghaya Souahel (appellation)')))
-- A growing area whose title has a comma.
check('Yu County, Hebei', 'koppen', 'BSk', M.koppen(frameFor('Yu County, Hebei')))
-- climate_key on the calling template picks the entry when key= is not given.
check('climate_key', 'parent argument', 'Dfc', M.koppen(frameFor(nil, { climate_key = 'Malana (appellation)' })))
-- A page with no profile shows and stores nothing.
f = frameFor('No such place')
check('No profile', 'has', '', M.has(f))
check('No profile', 'rows', '', M.rows(f))
check('No profile', 'koppen', '', M.koppen(f))
check('No profile', 'elevation', '', M.elevation(f))
check('No profile', 'matches', '', M.matches(f))
end
-- Whole data -----------------------------------------------------------------------------------
local function wholeData()
local data = mw.loadData(M.DATA)
local normals = mw.loadData(M.NORMALS)
local meta = data.meta
local sd, w = meta.sd, meta.weights
local function vec(e)
return { bio5 = e.bio5, bio6 = e.bio6, ln_bio12 = math.log(e.bio12), summer_share = e.ss,
longest_day_h = e.ld, elev_median_m = e.e }
end
local VARS = { 'bio5', 'bio6', 'ln_bio12', 'summer_share', 'longest_day_h', 'elev_median_m' }
local wsum = 0
for _, v in ipairs(VARS) do wsum = wsum + w[v] end
local n, shape, missing, selfm, order, koppen, elev, simOff1, simOffMore, worst = 0, 0, 0, 0, 0, 0, 0, 0, 0, ''
local normOk = 0
for key, e in pairs(data.places) do
n = n + 1
local cnt = 0
local prev = 101
local a = vec(e)
for i = 1, 6 do
local m = e.m[i]
if not m then break end
cnt = cnt + 1
local other = data.places[m[1]]
if not other then missing = missing + 1 end
if m[1] == key then selfm = selfm + 1 end
if m[2] > prev then order = order + 1 end
prev = m[2]
if other then
local b, s = vec(other), 0
for _, v in ipairs(VARS) do s = s + w[v] * ((a[v] - b[v]) / sd[v]) ^ 2 end
local got = math.floor(100 * math.exp(-math.sqrt(s / wsum)) + 0.5)
local d = math.abs(got - m[2])
if d == 1 then simOff1 = simOff1 + 1 elseif d > 1 then
simOffMore = simOffMore + 1; worst = key .. ' → ' .. m[1]
end
end
end
if cnt == 5 then shape = shape + 1 end
if M.KOPPEN[e.k] then koppen = koppen + 1 end
if e.e10 <= e.e and e.e <= e.e90 then elev = elev + 1 end
local nm = normals.places[key]
if nm then
local ok = true
for _, v in ipairs({ 'tmax', 'tmin', 'pr', 'vpd', 'rsds' }) do
if not nm[v] or nm[v][12] == nil or nm[v][13] ~= nil then ok = false end
end
if ok then normOk = normOk + 1 end
end
end
check('Whole data', 'places', 377, n)
check('Whole data', 'places with exactly five matches', n, shape)
check('Whole data', 'matches naming a place with no profile', 0, missing)
check('Whole data', 'places listed as their own match', 0, selfm)
check('Whole data', 'similarities rising down a list', 0, order)
check('Whole data', 'Köppen classes in the legend', n, koppen)
check('Whole data', 'medians inside their 10th–90th range', n, elev)
check('Whole data', 'places with twelve months of all five normals', n, normOk)
-- The stored variables are rounded, so a recomputed figure may land one point away.
check('Whole data', 'similarities more than 1 point off when recomputed', 0, simOffMore .. (worst ~= '' and (' (' .. worst .. ')') or ''))
results[#results + 1] = { group = 'Whole data', case = 'similarities 1 point off (rounding; shown, not failed)',
expected = '–', got = tostring(simOff1), ok = true }
check('Whole data', 'count()', tostring(n), M.count())
end
function tc.run()
results = {}
named()
wholeData()
return results
end
function tc.summary(res)
local pass, fail = 0, 0
for _, r in ipairs(res) do if r.ok then pass = pass + 1 else fail = fail + 1 end end
return pass, fail
end
-- Plain text, one line per case.
function tc.plain()
local res = tc.run()
local out = {}
for _, r in ipairs(res) do
out[#out + 1] = table.concat({ r.ok and 'PASS' or 'FAIL', r.group, r.case, r.expected, r.got }, '\t')
end
local pass, fail = tc.summary(res)
out[#out + 1] = string.format('TOTAL\t%d passed\t%d failed', pass, fail)
return table.concat(out, '\n')
end
-- {{#invoke:Climate profile/testcases|table}}
function tc.table()
local res = tc.run()
local pass, fail = tc.summary(res)
local out = { string.format("'''%d of %d cases pass%s.'''", pass, pass + fail,
fail > 0 and string.format(", '''%d fail'''", fail) or ''),
'{| class="wikitable sortable"', '! Group !! Case !! Expected !! Module !! Result' }
for _, r in ipairs(res) do
out[#out + 1] = '|-'
out[#out + 1] = '| ' .. table.concat({ r.group, '<nowiki>' .. r.case .. '</nowiki>',
'<nowiki>' .. r.expected .. '</nowiki>', '<nowiki>' .. r.got .. '</nowiki>',
r.ok and 'pass' or "'''fail'''" }, ' || ')
end
out[#out + 1] = '|}'
return table.concat(out, '\n')
end
return tc