Module:Grow-out priority
More actions
This is a documentation subpage for Grow-out priority. It contains usage information, categories, and other content that is not part of the original template.
This module works out the readings of the grow-out priority method, version 0.1: the seed lot grade, the holdings measure of a population and the ranks used to order populations for grow-out and collecting. The field grade itself is worked out by Module:Population on each population page; this module reads the stored grade. The rules are the method's own. The same code reads every holder's lots, and every reading shows the date it was worked out and the method version.
The module reads stored properties through #ask, works out each reading when the page is displayed, and can store the result with #set so that pages higher up can count readings. A reading that depends on the date, such as a test falling overdue, changes without an edit, so each one shows the date it was worked out ("as of").
Readings
| Reading | Values |
|---|---|
| Seed lot | Urgent · At risk · Secure · Data deficient |
| Holdings | Urgent · At risk · Secure · Data deficient |
| Field grade (read from the population page) | Very high · High · Moderate · Low · Very low · Data deficient |
Every reading is shown with its own label ("Seed lot: Data deficient", "Field grade: Data deficient"), never a bare word.
Functions
All functions are called with {{#invoke:Grow-out priority|function|…}}. Parameters not given on the call are read from the calling template.
| Function | Parameters | Returns |
|---|---|---|
lot |
lot (the lot's page or subobject; default the current page), asof (YYYY-MM-DD; default today), set=yes |
The lot's reading: its step, the factors behind it, the next actions and the next test date. With set=yes it also stores the values listed under Stored values on the current page.
|
population |
population (default the current page), asof, set=yes |
The holdings reading, with copies and collections counted, the places reading and any collection whose lots are all Urgent. |
countTable |
sizes (default 25,50,100,200), firsts (first-test percentages, default 90,95) |
A table of the seedlings needed to pass, and the most that is clearly below, for each test size and line. |
version |
none | The method version, v0.1.
|
Lua callers can use the engine directly: evaluateLot(id, data, ctx), evaluatePopulation(id, data, ctx), readHoldings(entries), lapse(grade, asof), walk(accession, data), passesAt(n, p), clearlyBelowAt(n, p) and ranks(holdings, band, bestLot). data holds three tables keyed by page name: lots, accessions and populations; ctx.today fixes the date.
The seed lot grade
A lot is judged on eight factors, each by name: germination, seeds left, storage, backup copy, replaceability, mother plants, handed out and grow-out history. Each factor passes, reaches a warning, crosses a hard line or is not known. Nothing is added or weighted:
- any hard line: Urgent
- otherwise any warning: At risk, or Urgent for an irreplaceable lot
- every factor known and passing: Secure
- no known problem but gaps: Data deficient, with the missing factors listed
Among the reasons for Urgent, germination is listed before seeds left. An empty field always means not known and never counts for safety.
| Factor | Hard line | Warning | Not known |
|---|---|---|---|
| Germination | latest test clearly below 70%, or below 85% of the first test (exact binomial, one-sided, 95%) | between the lines; or a percentage with no count, below a line | never tested; a percentage with no count at or above the lines |
| Seeds left | fewer than three sowings (3 × 150 ÷ germination, rounded up); under 450 when germination is not known | under 3,000; 450 to 2,999 when germination is not known | no count; 3,000 or more with germination not known |
| Storage | room temperature with moisture over 5% or not known | a test overdue; never tested once the first test was due | storage not reported |
| Backup copy | the holder states there is no backup | backup under 500 seeds expected to germinate, or at the same site or in the same town | not reported |
| Replaceability | none: an irreplaceable lot's warnings become hard lines | none | population not graded, reach not known, or change since collection not known |
| Mother plants | under 30 plants at any step back to the collection | none | any step without a count; a lot pooled from several collections |
| Handed out | handed out with fewer than six sowings left | handed out | not reported |
| Grow-out history | more than three grow-outs, or a grow-out not isolated or with isolation not recorded | none | lineage not traceable |
Details:
- Germination. A result passes a line when the count of seedlings is at least the smallest number that would come up less than one time in twenty if the true germination were only at the line; it is clearly below when the count is at most the highest such number from below. The relative line applies once a lot has two tests. A clearly low result from seed tested within 42 days of harvest, or from feral seed, reads as a warning until a tetrazolium test or a dormancy-breaking retest confirms it, on lots that can spare the seed. A lot below the seeds-left hard line is not tested; its germination comes from the grow-out. Untested seed at room temperature, with moisture over 5% or not known, held for more than nine months is predicted to have fallen, and the prediction is labelled. No prediction ever passes.
- Seeds left. One sowing is enough seed for 150 seedlings at the lot's latest germination. Seeds set aside from the original sample are not counted.
- Storage. Frozen seed is retested every five years, dry seed in a fridge or cool room (7% moisture or less) every year, wetter or unmeasured seed in a fridge or cool room every four months, and dry seed at room temperature (5% or less) every year. With two tests showing a fall, the next test is brought forward to the date the fall would reach the line, if that is sooner. An untested lot's first test is due twelve months after it arrived.
- Replaceability. A lot is irreplaceable when its population has changed since the collection (introgression now reported in the population or seen in its plants, having been absent or reported only nearby at collection; or a reported change in the plants' traits since then), is extinct in the field or extinct on-farm, can no longer be reached, or is graded High or Very high. A known change comes first. A lot with a warning and replaceability not known stays At risk and is listed as "replaceability unknown, may be Urgent".
- Mother plants and grow-out history walk
Has parent accessionfrom the lot's accession back to its collection, at most six steps. - A lot with no report of any kind for five years is shown as not known to still exist and stops counting in holdings. A lot marked gone counts for nothing.
Actions and the sowing queue
Each lot lists its next actions in this order: isolation concerns; pooling or collecting again; sowing; retests and first tests; storage; backup; handing out. A lot is queued for sowing only when germination is clearly below a line or seeds left is at a warning or the hard line, and never when it was grown out without isolation or with isolation not recorded. A queued lot gets one full sowing (150 seedlings' worth, or 215 seeds when germination is not known) and the rest stays in store; a lot smaller than one sowing is sown whole and flagged as a small sowing. A lot grown out without recorded isolation that is the only seed held of its population is flagged for review as a preservation grow-out. A backup is split off only when the seed left after it stays above the seeds-left hard line; otherwise the regrown seed is backed up.
The holdings measure
Only verified copies count: lots collected from the field, or grown out with isolation documented at every step, that are not gone. Lots grown out without isolation, with isolation not recorded, or crossed between populations are shown and not counted. Two copies are in separate places only when both give a storage site, a country and a distance class, and either gives "other town" or "other country" from the other; if the two disagree, or either is missing, no separate place is credited.
- Urgent if no verified copy is held, or every verified copy is Urgent.
- At risk if every verified copy is in one town or site, only one collection is held, or the best lot is At risk.
- Data deficient if the best lot is Data deficient or the places are not known.
- Secure only with a Secure best lot and two collections whose seed has produced seedlings, held at separate places.
Lots rank from best to worst as Secure, At risk, Data deficient, Urgent. A collection whose lots are all Urgent is listed under its population whatever the population's reading.
Ranks
Lower numbers sort first. A population without a reading gets a rank that places it in its own group, never an empty value.
| Rank | Values |
|---|---|
Has holdings rank |
Urgent 1 · At risk 2 · Secure 3 · Data deficient 8 · none 9 |
Has field grade rank |
Extinct on-farm 0 · Very high 1 · High 2 · Moderate 3 · Low 4 · Very low 5 · Near threatened 6 · Least concern 7 · Data deficient 8 · none 9 · Extinct 10 |
Has best lot rank |
Urgent 1 · Data deficient 2 · At risk 3 · Secure 4 · none 9 |
Grades from fields and from feral stands sort together on the bands.
Stored values
| Property | Set by | Value |
|---|---|---|
Has seed lot step |
lot |
the lot's reading |
Has seed lot factors |
lot |
the factors behind the reading (the missing factors for Data deficient) |
Has lot action |
lot |
the action codes, in order |
Is queued for sowing, Is small sowing, Is flagged for preservation grow-out |
lot |
true or false |
Has holdings step, Has live verified copy |
population |
the holdings reading; whether any verified copy is held |
Has holdings rank, Has field grade rank, Has best lot rank |
population |
numbers (see Ranks) |
Has method version |
both | v0.1
|
Properties read
Every property name the module reads is in one table, PROPS, near the end of the module.
- Seed lot:
Is lot of,Has lot holder,Has lot role,Is safety duplicate of,Has storage site,Has storage country,Has backup distance,Has distance measured from,Has lot received date,Has original sample kept,Is handed out,Has backup status,Has lot status,Has lot status date,Has gone reason - Inventory and germination reports on a lot:
Is report on,Has report date,Has seed count,Has seed count unit,Has thousand seed weight,Has storage regime,Has seed moisture,Has test date,Has seeds tested,Has seedlings counted,Has germination percent,Has germination basis,Has dormancy check,Has days after harvest - Accession:
Has parent accession,Has collection,Has regrow generation,Has regrow isolation,Has parent female count,Has parent male count,Has parent plant count,Has population,Has copy status,Has collection date,Has perceived introgression level,Has classification - Population:
Has field grade,Has field grade evidence year,Has field item set,Has reachability,Has field status; introgression assessments (Has introgression grade,Has introgression year) and trait change reports (Has trait change date) as subobjects of the population page
Constants
The method's figures are in the table C at the top of the module: the 95% confidence level, the 70% floor and the 85% relative line, 150 seedlings to a sowing, three sowings for the seeds-left hard line and six for handing out, the 3,000-seed warning, the 70% planning figure, the 500-seed backup, 30 mother plants, three grow-outs, the moisture limits (5% at room temperature, 7% in a fridge or cool room), nine months for the room-temperature prediction, twelve months to the first test, the retest intervals, five years without a report, the ten-year lapse of a field grade and the 42-day dormancy window. A stored field grade whose evidence is ten or more years old is read as Data deficient. A revision of the method changes them there and the version with them.
Test cases
Module:Grow-out priority/testcases runs the method's own cases against this module: the five invented lots of section 8.1, the holdings cases of Table 3, the germination counts of Table 4 and the default actions of Table 5, with a few further rules. A change to the module that alters any of these figures shows here as a difference.
148 of 148 cases reproduced (method v0.1).
| Source | Case | Paper | Module | Result |
|---|---|---|---|---|
| Section 8.1 | Lot 1: reading | Secure | Secure | match |
| Section 8.1 | Lot 1: germination (88 of 100; the floor needs 78, the relative line 84) | pass | pass | match |
| Section 8.1 | Lot 1: actions | 0 | 0 | match |
| Section 8.1 | Lot 1: next test due (in three years) | 2029-10-15 | 2029-10-15 | match |
| Section 8.1 | Lot 1, population Very High: reading | Secure | Secure | match |
| Section 8.1 | Lot 1, population Very High: irreplaceable | true | true | match |
| Section 8.1 | Lot 2: three sowings at 88% | 512 | 512 | match |
| Section 8.1 | Lot 2: seeds left before raising | warning | warning | match |
| Section 8.1 | Lot 2: reading | Urgent | Urgent | match |
| Section 8.1 | Lot 2: reasons | seeds left | seeds left | match |
| Section 8.1 | Lot 2: queued for sowing | true | true | match |
| Section 8.1 | Lot 2: actions | includes sow | includes sow | match |
| Section 8.1 | Lot 3: three sowings at 88% | 512 | 512 | match |
| Section 8.1 | Lot 3: reading | Urgent | Urgent | match |
| Section 8.1 | Lot 3: reasons | seeds left, mother plants | seeds left, mother plants | match |
| Section 8.1 | Lot 3: queued for sowing | true | true | match |
| Section 8.1 | Lot 3: one full sowing at 88% | 171 | 171 | match |
| Section 8.1 | Lot 3: seeds kept in store | 309 | 309 | match |
| Section 8.1 | Lot 3: actions | includes sow | includes sow | match |
| Section 8.1 | Lot 3: actions | includes pool or re-collect | includes pool or re-collect | match |
| Section 8.1 | Lot 3: no further test | excludes first test | excludes first test | match |
| Section 8.1 | Lot 3: no further test | excludes retest sooner | excludes retest sooner | match |
| Section 8.1 | Lot 4: reading | Urgent | Urgent | match |
| Section 8.1 | Lot 4: reasons | includes grow-out history | includes grow-out history | match |
| Section 8.1 | Lot 4: verified copy | isolation not recorded | isolation not recorded | match |
| Section 8.1 | Lot 4: queued for sowing | false | false | match |
| Section 8.1 | Lot 4: flagged for a preservation grow-out | true | true | match |
| Section 8.1 | Lot 4: holdings | Urgent | Urgent | match |
| Section 8.1 | Lot 4: live verified copy (collecting list) | false | false | match |
| Section 8.1 | Lot 4 with 300 seeds: queued for sowing | false | false | match |
| Section 8.1 | Lot 5a: reading | Urgent | Urgent | match |
| Section 8.1 | Lot 5a: reasons | includes seeds left | includes seeds left | match |
| Section 8.1 | Lot 5a: actions | includes sow | includes sow | match |
| Section 8.1 | Lot 5b: reading | Data deficient | Data deficient | match |
| Section 8.1 | Lot 5b: not known | includes germination | includes germination | match |
| Section 8.1 | Lot 5b: not known | includes backup copy | includes backup copy | match |
| Section 8.1 | Lot 5b: not known | includes mother plants | includes mother plants | match |
| Section 8.1 | Lot 5b: storage (frozen, low risk) | pass | pass | match |
| Section 8.1 | Lot 5b: actions | includes first test | includes first test | match |
| Section 8.1 | Lots 5a and 5b: holdings | Data deficient | Data deficient | match |
| Section 8.1 | Lots 5a and 5b: collections listed with every lot Urgent | A5a | A5a | match |
| Table 3 | No verified copy that is not confirmed dead or used up | Urgent | Urgent | match |
| Table 3 | No lot at all | Urgent | Urgent | match |
| Table 3 | Every verified copy is Urgent | Urgent | Urgent | match |
| Table 3 | One collection only, whatever its lots (two Secure lots) | At risk | At risk | match |
| Table 3 | One collection only, whatever its lots (two Data deficient lots) | At risk | At risk | match |
| Table 3 | All verified copies in one town | At risk | At risk | match |
| Table 3 | Two collections in two towns: one Urgent, one At risk | At risk | At risk | match |
| Table 3 | Two collections in two towns: one Urgent, one Data deficient | Data deficient | Data deficient | match |
| Table 3 | Two collections in two towns: one Urgent, one Data deficient: collection listed | C1 | C1 | match |
| Table 3 | Two collections in two towns: both At risk | At risk | At risk | match |
| Table 3 | Two collections in two towns: one Urgent, one Secure | Secure | Secure | match |
| Table 3 | Two collections in two towns: one Urgent, one Secure: collection listed | C1 | C1 | match |
| Table 3 | Two collections in two towns: one Secure, one Data deficient because its viability is unknown | Data deficient | Data deficient | match |
| Table 3 | Two collections in two towns: one Secure, one Data deficient for another missing fact, with evidenced live seed | Secure | Secure | match |
| Table 3 | Two collections in two towns: both Data deficient | Data deficient | Data deficient | match |
| Table 3 | Section 4: two Secure collections, sites not recorded | Data deficient | Data deficient | match |
| Table 3 | Section 4: two Secure collections, the two distance records disagree | Data deficient | Data deficient | match |
| Table 4 | 25 seeds, 70% floor: passes | 22 | 22 | match |
| Table 4 | 25 seeds, 70% floor: clearly below | 13 | 13 | match |
| Table 4 | 25 seeds, 76.5% (85% of 90%): passes | 23 | 23 | match |
| Table 4 | 25 seeds, 76.5% (85% of 90%): clearly below | 15 | 15 | match |
| Table 4 | 25 seeds, 80.75% (85% of 95%): passes | 24 | 24 | match |
| Table 4 | 25 seeds, 80.75% (85% of 95%): clearly below | 16 | 16 | match |
| Table 4 | 50 seeds, 70% floor: passes | 41 | 41 | match |
| Table 4 | 50 seeds, 70% floor: clearly below | 29 | 29 | match |
| Table 4 | 50 seeds, 76.5% (85% of 90%): passes | 44 | 44 | match |
| Table 4 | 50 seeds, 76.5% (85% of 90%): clearly below | 32 | 32 | match |
| Table 4 | 50 seeds, 80.75% (85% of 95%): passes | 46 | 46 | match |
| Table 4 | 50 seeds, 80.75% (85% of 95%): clearly below | 35 | 35 | match |
| Table 4 | 100 seeds, 70% floor: passes | 78 | 78 | match |
| Table 4 | 100 seeds, 70% floor: clearly below | 61 | 61 | match |
| Table 4 | 100 seeds, 76.5% (85% of 90%): passes | 84 | 84 | match |
| Table 4 | 100 seeds, 76.5% (85% of 90%): clearly below | 68 | 68 | match |
| Table 4 | 100 seeds, 80.75% (85% of 95%): passes | 88 | 88 | match |
| Table 4 | 100 seeds, 80.75% (85% of 95%): clearly below | 73 | 73 | match |
| Table 4 | 200 seeds, 70% floor: passes | 152 | 152 | match |
| Table 4 | 200 seeds, 70% floor: clearly below | 128 | 128 | match |
| Table 4 | 200 seeds, 76.5% (85% of 90%): passes | 164 | 164 | match |
| Table 4 | 200 seeds, 76.5% (85% of 90%): clearly below | 142 | 142 | match |
| Table 4 | 200 seeds, 80.75% (85% of 95%): passes | 171 | 171 | match |
| Table 4 | 200 seeds, 80.75% (85% of 95%): clearly below | 151 | 151 | match |
| Count table, method notes | 25 seeds, 70% floor: passes | 22 | 22 | match |
| Count table, method notes | 25 seeds, 70% floor: clearly below | 13 | 13 | match |
| Count table, method notes | 25 seeds, 76.5% (85% of 90%): passes | 23 | 23 | match |
| Count table, method notes | 25 seeds, 76.5% (85% of 90%): clearly below | 15 | 15 | match |
| Count table, method notes | 25 seeds, 80.75% (85% of 95%): passes | 24 | 24 | match |
| Count table, method notes | 25 seeds, 80.75% (85% of 95%): clearly below | 16 | 16 | match |
| Count table, method notes | 50 seeds, 70% floor: passes | 41 | 41 | match |
| Count table, method notes | 50 seeds, 70% floor: clearly below | 29 | 29 | match |
| Count table, method notes | 50 seeds, 76.5% (85% of 90%): passes | 44 | 44 | match |
| Count table, method notes | 50 seeds, 76.5% (85% of 90%): clearly below | 32 | 32 | match |
| Count table, method notes | 50 seeds, 80.75% (85% of 95%): passes | 46 | 46 | match |
| Count table, method notes | 50 seeds, 80.75% (85% of 95%): clearly below | 35 | 35 | match |
| Count table, method notes | 100 seeds, 70% floor: passes | 78 | 78 | match |
| Count table, method notes | 100 seeds, 70% floor: clearly below | 61 | 61 | match |
| Count table, method notes | 100 seeds, 76.5% (85% of 90%): passes | 84 | 84 | match |
| Count table, method notes | 100 seeds, 76.5% (85% of 90%): clearly below | 68 | 68 | match |
| Count table, method notes | 100 seeds, 80.75% (85% of 95%): passes | 88 | 88 | match |
| Count table, method notes | 100 seeds, 80.75% (85% of 95%): clearly below | 73 | 73 | match |
| Count table, method notes | 200 seeds, 70% floor: passes | 152 | 152 | match |
| Count table, method notes | 200 seeds, 70% floor: clearly below | 128 | 128 | match |
| Count table, method notes | 200 seeds, 76.5% (85% of 90%): passes | 164 | 164 | match |
| Count table, method notes | 200 seeds, 76.5% (85% of 90%): clearly below | 142 | 142 | match |
| Count table, method notes | 200 seeds, 80.75% (85% of 95%): passes | 171 | 171 | match |
| Count table, method notes | 200 seeds, 80.75% (85% of 95%): clearly below | 151 | 151 | match |
| Table 5 | Germination clearly below a line: queued | true | true | match |
| Table 5 | Germination clearly below a line | includes sow | includes sow | match |
| Table 5 | Germination test between the lines | includes retest sooner | includes retest sooner | match |
| Table 5 | Germination test between the lines: queued | false | false | match |
| Table 5 | A percentage below a line, with no count | includes retest with counts | includes retest with counts | match |
| Table 5 | A percentage below a line: germination | warning | warning | match |
| Table 5 | A percentage above the lines (section 5.1): germination | unknown | unknown | match |
| Table 5 | Never tested, kept where a fall is predicted | includes test at once | includes test at once | match |
| Table 5 | Never tested, kept where a fall is predicted | includes cold storage | includes cold storage | match |
| Table 5 | Never tested, kept where a fall is predicted: queued | false | false | match |
| Table 5 | Never tested, kept elsewhere | includes first test | includes first test | match |
| Table 5 | Seeds left below the warning line | includes sow | includes sow | match |
| Table 5 | Seeds left below the warning line: one full sowing at 90% | 167 | 167 | match |
| Table 5 | Seeds left below the warning line: rest in store | 1833 | 1833 | match |
| Table 5 | Seeds left below the hard line, untested: queued | true | true | match |
| Table 5 | Seeds left below the hard line, untested: sowing at the 70% planning figure | 215 | 215 | match |
| Table 5 | Seeds left below the hard line, untested: rest in store | 185 | 185 | match |
| Table 5 | Seeds left below the hard line: not tested first | excludes first test | excludes first test | match |
| Table 5 | Whole lot too small for one full sowing: small sowing | true | true | match |
| Table 5 | Whole lot too small for one full sowing | includes sow | includes sow | match |
| Table 5 | Storage at the hard line | includes cold storage | includes cold storage | match |
| Table 5 | The holder confirms there is no backup | includes assess spare seed for a backup | includes assess spare seed for a backup | match |
| Table 5 | The holder confirms there is no backup: backup copy | hard | hard | match |
| Table 5 | Backup status not reported | includes confirm whether a backup exists | includes confirm whether a backup exists | match |
| Table 5 | Backup status not reported: backup copy | unknown | unknown | match |
| Table 5 | Handed out with little left | includes stop handing out | includes stop handing out | match |
| Table 5 | Too few mother plants | includes pool or re-collect | includes pool or re-collect | match |
| Table 5 | Too many grow-outs (4) | includes pool or re-collect | includes pool or re-collect | match |
| Table 5 | Isolation not recorded | includes do not queue | includes do not queue | match |
| Table 5 | Isolation not recorded: queued, even with 300 seeds | false | false | match |
| Table 5 | Isolation not recorded, only live seed: preservation review | true | true | match |
| Table 5 | Isolation not recorded, another lot held: preservation review | false | false | match |
| Other rules | Seeds left, germination unknown: 449 | hard | hard | match |
| Other rules | Seeds left, germination unknown: 450 | warning | warning | match |
| Other rules | Seeds left, germination unknown: 2,999 | warning | warning | match |
| Other rules | Seeds left, germination unknown: 3,000 | unknown | unknown | match |
| Other rules | Stored field grade with 2014 evidence has lapsed by 2026 | Data deficient | Data deficient | match |
| Other rules | Stored field grade with 2022 evidence stands | High | High | match |
| Other rules | Stored band read without regard to case (very high): irreplaceable | Urgent | Urgent | match |
| Other rules | Changed population: a seeds-left warning is raised | Urgent | Urgent | match |
| Other rules | Old level scale at collection: change not known, so replaceability is not known | unknown | unknown | match |
| Other rules | Warning with replaceability not known: At risk, may be Urgent | At risk true | At risk true | match |
Version history
- v0.1: first version, following version 0.1 of the method.
-- Module:Grow-out priority
-- Works out the readings of the grow-out priority method, version 0.1:
-- the seed lot grade, the holdings measure, the field grade band and the ranks used by
-- the priorities lists. Every rule constant is in the tables at the top, so a revision of
-- the method changes them in one place and the version stamp with them.
--
-- The rules engine works on plain Lua tables (see the documentation for their fields).
-- The SMW reader near the end builds those tables from stored properties through #ask,
-- and the entry points render a reading or store it with #set.
local p = {}
------------------------------------------------------------------------------------------
-- Rule tables
------------------------------------------------------------------------------------------
p.VERSION = 'v0.1'
local C = {
confidence = 0.05, -- exact binomial, one-sided, 95%
floor = 0.70, -- germination floor, whatever the first test
relative = 0.85, -- latest test against 85% of the first test
sowingSeedlings = 150, -- one sowing raises 150 seedlings
sowingsHard = 3, -- seeds left: fewer than three sowings is the hard line
sowingsHandOut = 6, -- handed out: fewer than six sowings left is the hard line
seedsWarning = 3000, -- seeds left warning line
planningGermination = 0.70, -- a sowing for a lot with no germination figure (215 seeds)
backupViable = 500, -- a backup needs 500 seeds expected to germinate
motherPlants = 30,
maxGrowouts = 3,
walkCap = 6, -- lineage walk stops after this many steps
roomMoistureMax = 5, -- room temperature: dry seed is 5% moisture or less
coolMoistureMax = 7, -- fridge or cool room: dry seed is 7% or less
predictionMonths = 9, -- untested room-temperature seed predicted to fail after nine months
firstTestMonths = 12, -- first test due twelve months after the lot arrives
retestMonths = { freezer = 60, cooldry = 12, coolwet = 4, roomdry = 12 },
noReportYears = 5, -- no report for five years: not known whether the lot still exists
gradeLapseYears = 10, -- a field grade lapses ten years after its evidence
dormancyWindowDays = 42, -- fresh seed tested within 42 days of harvest may be dormant
}
p.C = C
-- Reading names, from one table.
local STEP = { urgent = 'Urgent', atrisk = 'At risk', secure = 'Secure', dd = 'Data deficient' }
p.STEP = STEP
-- Factors are keyed by name, never by number.
local FACTORS = { 'germination', 'seeds left', 'storage', 'backup copy', 'replaceability',
'mother plants', 'handed out', 'grow-out history' }
p.FACTORS = FACTORS
-- Lots from best to worst, for the holdings measure.
local LOT_ORDER = { [STEP.secure] = 1, [STEP.atrisk] = 2, [STEP.dd] = 3, [STEP.urgent] = 4 }
-- Actions, in the order they are listed on a lot.
local ACTIONS = {
{ 'do not queue', 'Do not queue for routine sowing: isolation not recorded or pollen known to have got in' },
{ 'preservation review', 'Review for a preservation grow-out: the only live seed of its population' },
{ 'pool or re-collect', 'Consider pooling with another collection of the population after review, or collecting again' },
{ 'sow', 'Queue for sowing in isolation' },
{ 'retest sooner', 'Retest sooner' },
{ 'retest with counts', 'Retest and record the number of seeds tested and seedlings' },
{ 'test at once', 'Test at once and correct the storage' },
{ 'first test', 'Test for the first time' },
{ 'retest', 'Retest: the scheduled test is overdue' },
{ 'cold storage', 'Dry the seed and move it to cold storage' },
{ 'assess spare seed for a backup', 'Assess whether enough seed can be spared, and arrange a backup at another site' },
{ 'split a backup', 'Split off a backup of at least 500 viable seeds and keep it at another site' },
{ 'back up the regrow', 'Sow first, and back up the regrown seed' },
{ 'confirm whether a backup exists', 'Confirm whether a backup exists' },
{ 'stop handing out', 'Stop handing it out' },
}
local ACTION_RANK, ACTION_TEXT = {}, {}
for i, a in ipairs(ACTIONS) do ACTION_RANK[a[1]] = i; ACTION_TEXT[a[1]] = a[2] end
p.ACTION_TEXT = ACTION_TEXT
-- Field grade bands, as stored on population pages (the grade itself is worked out there).
-- Stored values are read without regard to case.
local GRADE_DD = 'Data deficient'
local BAND_NAMES = { 'Very high', 'High', 'Moderate', 'Low', 'Very low', 'Near threatened', 'Least concern', GRADE_DD }
local BAND = {}
for _, b in ipairs(BAND_NAMES) do BAND[b:lower()] = b end
local function band(v)
if v == nil then return nil end
return BAND[tostring(v):gsub('^%s+', ''):gsub('%s+$', ''):lower()]
end
p.band = band
-- Introgression evidence scale, lowest to highest.
local INTRO_LOW = { ['None seen or reported'] = true, ['Reported nearby'] = true }
local INTRO_HIGH = { ['Reported in this population'] = true, ['Seen in the plants'] = true }
-- Ranks stored for sorting (lower sorts first). A population with no reading gets a rank
-- that puts it in its own group, never an empty value.
local RANK = {
holdings = { [STEP.urgent] = 1, [STEP.atrisk] = 2, [STEP.secure] = 3, [STEP.dd] = 8, none = 9 },
-- Cultivated and feral grades sort together on the bands (provisional order).
field = { ['Extinct on-farm'] = 0, ['Very high'] = 1, High = 2, Moderate = 3, Low = 4,
['Very low'] = 5, ['Near threatened'] = 6, ['Least concern'] = 7, [GRADE_DD] = 8, none = 9, Extinct = 10 },
bestLot = { [STEP.urgent] = 1, [STEP.dd] = 2, [STEP.atrisk] = 3, [STEP.secure] = 4, none = 9 },
}
p.RANK = RANK
------------------------------------------------------------------------------------------
-- Small helpers
------------------------------------------------------------------------------------------
local function trim(s) return (tostring(s or ''):gsub('^%s+', ''):gsub('%s+$', '')) end
local function blank(v) return v == nil or (type(v) == 'string' and trim(v) == '') end
local function num(v)
if type(v) == 'number' then return v end
if blank(v) then return nil end
return tonumber((trim(v):gsub(',', '')))
end
local function list(v)
if v == nil then return {} end
if type(v) == 'table' then return v end
if blank(v) then return {} end
return { trim(v) }
end
local function lower(v)
if blank(v) then return nil end
return trim(v):lower()
end
local function split(s, sep)
local out, i = {}, 1
if s == '' then return out end
while true do
local a, b = string.find(s, sep, i, true)
if not a then out[#out + 1] = string.sub(s, i); break end
out[#out + 1] = string.sub(s, i, a - 1)
i = b + 1
end
return out
end
-- Dates as {y, m, d}; arithmetic on day numbers, so years before 1970 work.
local function parseDate(s)
if type(s) == 'table' then return s end
if blank(s) then return nil end
local y, rest = trim(s):match('^(%d%d%d%d)(.*)$')
if not y then return nil end
local m, d = rest:match('^%-(%d%d?)%-(%d%d?)')
if not m then m = rest:match('^%-(%d%d?)') end
return { y = tonumber(y), m = tonumber(m) or 1, d = tonumber(d) or 1 }
end
p.parseDate = parseDate
local function dayNumber(dt) -- days from civil (proleptic Gregorian)
local y, m, d = dt.y, dt.m, dt.d
if m <= 2 then y = y - 1 end
local era = math.floor(y / 400)
local yoe = y - era * 400
local mp = (m + 9) % 12
local doy = math.floor((153 * mp + 2) / 5) + d - 1
local doe = yoe * 365 + math.floor(yoe / 4) - math.floor(yoe / 100) + doy
return era * 146097 + doe
end
local MONTH_DAYS = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
local function addMonths(dt, n)
local total = dt.y * 12 + (dt.m - 1) + n
local y, m = math.floor(total / 12), total % 12 + 1
local leap = (y % 4 == 0 and y % 100 ~= 0) or y % 400 == 0
local last = MONTH_DAYS[m] + ((m == 2 and leap) and 1 or 0)
return { y = y, m = m, d = math.min(dt.d, last) }
end
local function addDays(dt, n)
local z = dayNumber(dt) + n + 719468
local era = math.floor(z / 146097)
local doe = z - era * 146097
local yoe = math.floor((doe - math.floor(doe / 1460) + math.floor(doe / 36524) - math.floor(doe / 146096)) / 365)
local y = yoe + era * 400
local doy = doe - (365 * yoe + math.floor(yoe / 4) - math.floor(yoe / 100))
local mp = math.floor((5 * doy + 2) / 153)
local d = doy - math.floor((153 * mp + 2) / 5) + 1
local m = mp < 10 and mp + 3 or mp - 9
if m <= 2 then y = y + 1 end
return { y = y, m = m, d = d }
end
local function fmtDate(dt) return dt and string.format('%04d-%02d-%02d', dt.y, dt.m, dt.d) or nil end
local function before(a, b) return dayNumber(a) < dayNumber(b) end
p.fmtDate = fmtDate
local function today(ctx) return ctx.today or parseDate(os.date('!%Y-%m-%d')) end
local function fmtInt(n)
local s = tostring(math.floor(n))
local out = s:reverse():gsub('(%d%d%d)', '%1,'):reverse()
return (out:gsub('^,', ''))
end
------------------------------------------------------------------------------------------
-- Exact binomial (germination results)
------------------------------------------------------------------------------------------
local logfact = { [0] = 0 }
local function lf(n)
for i = #logfact + 1, n do logfact[i] = logfact[i - 1] + math.log(i) end
return logfact[n]
end
local function pmf(n, k, q)
if q <= 0 then return k == 0 and 1 or 0 end
if q >= 1 then return k == n and 1 or 0 end
return math.exp(lf(n) - lf(k) - lf(n - k) + k * math.log(q) + (n - k) * math.log(1 - q))
end
-- Smallest number of seedlings that passes the line q: P(X >= k | n, q) <= 5%.
-- Returns n + 1 when no result can pass.
function p.passesAt(n, q)
local tail = 0
for k = n, 0, -1 do
tail = tail + pmf(n, k, q)
if tail > C.confidence then return k + 1 end
end
return 0
end
-- Largest number of seedlings that is clearly below the line q: P(X <= k | n, q) <= 5%.
-- Returns -1 when no result is clearly below.
function p.clearlyBelowAt(n, q)
local tail = 0
for k = 0, n do
tail = tail + pmf(n, k, q)
if tail > C.confidence then return k - 1 end
end
return n
end
-- 'pass', 'between' or 'below' for one counted result against one line.
function p.judge(seedlings, n, q)
if seedlings >= p.passesAt(n, q) then return 'pass' end
if seedlings <= p.clearlyBelowAt(n, q) then return 'below' end
return 'between'
end
------------------------------------------------------------------------------------------
-- Field grade lapse (the grade is worked out by Module:Population; a stored grade goes stale
-- with time, so the lapse is applied again when it is read)
------------------------------------------------------------------------------------------
-- A grade whose evidence is ten or more years old reads Data deficient until reassessed.
function p.lapse(r, asof)
local now = parseDate(asof)
r.band = band(r.band) or r.band
if r.band and r.band ~= GRADE_DD and r.evidenceYear and now
and now.y >= r.evidenceYear + C.gradeLapseYears then
r.lapsedFrom, r.lapsedYear = r.band, r.evidenceYear + C.gradeLapseYears
r.band = GRADE_DD
end
return r
end
------------------------------------------------------------------------------------------
-- Lineage
------------------------------------------------------------------------------------------
-- Walks Has parent accession from an accession back to its collection.
-- Returns {steps = {accession, parent, ..., root}, complete, pooled, capped, gap}.
function p.walk(accId, data)
local w = { steps = {} }
local seen, cur = {}, accId
for _ = 1, C.walkCap + 1 do
local a = cur and data.accessions[cur]
if not a then w.gap = cur or 'accession'; return w end
if seen[cur] then w.gap = cur; return w end
seen[cur] = true
w.steps[#w.steps + 1] = a
local parents = list(a.parents)
if #parents > 1 or #list(a.collection) > 1 then w.pooled = true; return w end
if #parents == 0 then
if (num(a.generation) or 0) > 0 then w.gap = a.id; return w end
w.complete, w.root = true, a
return w
end
cur = parents[1]
end
w.capped = true
return w
end
local function plantCount(a)
local t = num(a.parent_total)
if t then return t end
local f, m = num(a.parent_females), num(a.parent_males)
if f and m then return f + m end
return nil
end
------------------------------------------------------------------------------------------
-- Lot inputs
------------------------------------------------------------------------------------------
local function byDate(t)
local dated = {}
for i, r in ipairs(t or {}) do dated[#dated + 1] = { r = r, d = parseDate(r.date), i = i } end
table.sort(dated, function(a, b)
if a.d and b.d then
local x, y = dayNumber(a.d), dayNumber(b.d)
if x ~= y then return x < y end
return a.i < b.i
end
if a.d or b.d then return b.d ~= nil end -- undated first
return a.i < b.i
end)
local out = {}
for _, x in ipairs(dated) do out[#out + 1] = x.r end
return out
end
-- Germination figure of one test: proportion, and the counts when given.
local function testFigure(t)
local n, s = num(t.seeds_tested), num(t.seedlings)
if n and s and n > 0 then return s / n, n, s end
local pc = num(t.percent)
if pc then return pc / 100 end
return nil
end
local function realTests(lot)
local out = {}
for _, t in ipairs(byDate(lot.tests)) do
if lower(t.basis) ~= 'prediction' and testFigure(t) then out[#out + 1] = t end
end
return out
end
local function storageKey(inv)
local s = lower(inv and inv.storage)
if not s then return nil end
if s:find('freez') then return 'freezer' end
if s:find('fridge') or s:find('cool') then return 'cool' end
if s:find('room') then return 'room' end
return 'other'
end
-- Seeds needed for `seedlings` seedlings at the lot's germination (rounded up).
local function seedsFor(seedlings, st)
if st.gn and st.gs then
if st.gs == 0 then return math.huge end
return math.floor((seedlings * st.gn + st.gs - 1) / st.gs)
end
local g = st.g or C.planningGermination
if g <= 0 then return math.huge end
return math.ceil(seedlings / g - 1e-9)
end
p.seedsFor = seedsFor
local function lotInputs(lot, data, ctx)
local st = {}
st.tests = realTests(lot)
local latest = st.tests[#st.tests]
if latest then
st.g, st.gn, st.gs = testFigure(latest)
st.latestTest = latest
end
local inv = byDate(lot.inventory)
st.inv = inv[#inv]
if st.inv then
local q = num(st.inv.quantity)
local unit = lower(st.inv.quantity_unit) or 'seeds'
if q and unit == 'g' then
local tsw = num(st.inv.thousand_seed_weight)
q = (tsw and tsw > 0) and math.floor(q * 1000 / tsw) or nil
end
if q then
st.count = q
st.usable = q - (num(lot.original_kept) or 0)
end
end
-- Last report of any kind.
local last
for _, r in ipairs(lot.inventory or {}) do
local d = parseDate(r.date); if d and (not last or before(last, d)) then last = d end
end
for _, r in ipairs(lot.tests or {}) do
local d = parseDate(r.date); if d and (not last or before(last, d)) then last = d end
end
local rec = parseDate(lot.received_date)
if rec and (not last or before(last, rec)) then last = rec end
st.lastReport = last
st.walk = p.walk(lot.accession, data)
st.accession = data.accessions[lot.accession]
return st
end
------------------------------------------------------------------------------------------
-- Factors. Each returns {state = 'pass' | 'warning' | 'hard' | 'unknown', text = ...}.
------------------------------------------------------------------------------------------
local F = {}
F['seeds left'] = function(lot, st)
if not st.count then return { state = 'unknown', text = 'seed count not reported' } end
local n = st.usable
if st.g then
local hard = seedsFor(C.sowingSeedlings * C.sowingsHard, st)
st.threeSowings = hard
if n < hard then
return { state = 'hard', text = string.format('%s seeds left, under three sowings (%s at %d%% germination)',
fmtInt(n), fmtInt(hard), math.floor(st.g * 100 + 0.5)) }
elseif n < C.seedsWarning then
return { state = 'warning', text = string.format('%s seeds left, under %s', fmtInt(n), fmtInt(C.seedsWarning)) }
end
return { state = 'pass', text = fmtInt(n) .. ' seeds left' }
end
local atFull = C.sowingSeedlings * C.sowingsHard
if n < atFull then
return { state = 'hard', text = string.format('%s seeds left, under three sowings even at full germination', fmtInt(n)) }
elseif n < C.seedsWarning then
return { state = 'warning', hardUnknown = true,
text = string.format('%s seeds left, under %s; germination not known', fmtInt(n), fmtInt(C.seedsWarning)) }
end
return { state = 'unknown', text = fmtInt(n) .. ' seeds left; germination not known' }
end
local function predictedFall(lot, st, ctx)
if storageKey(st.inv) ~= 'room' then return false end
local m = num(st.inv.seed_moisture)
if m and m <= C.roomMoistureMax then return false end
local since = parseDate(lot.received_date)
if not since then
for _, r in ipairs(byDate(lot.inventory)) do since = parseDate(r.date); if since then break end end
end
if not since then return false end
return before(addMonths(since, C.predictionMonths), today(ctx))
end
F['germination'] = function(lot, st, ctx, data)
local tests = st.tests
if #tests == 0 then
if st.seedsHard then
st.untestable = true
return { state = 'unknown', text = 'not tested: too few seeds to spare; germination is read from the grow-out' }
end
if predictedFall(lot, st, ctx) then
st.germEvidence = 'predicted'
return { state = 'hard', predicted = true,
text = 'never tested; predicted to have fallen (room temperature, moisture over 5% or not known, held over nine months)' }
end
st.germEvidence = 'never tested'
return { state = 'unknown', text = 'never tested' }
end
local latest, first = tests[#tests], tests[1]
local lines = { { q = C.floor, label = '70% floor' } }
if #tests >= 2 then
local fq = testFigure(first)
lines[#lines + 1] = { q = C.relative * fq, label = string.format('85%% of the first test (%s%%)',
tostring(math.floor(C.relative * fq * 10000 + 0.5) / 100)) }
end
local q, n, s = testFigure(latest)
if not n then
for _, l in ipairs(lines) do
if q < l.q then
st.germEvidence = 'percentage below'
return { state = 'warning', text = string.format('%s%% (percentage only), below the %s; counts needed',
tostring(math.floor(q * 1000 + 0.5) / 10), l.label) }
end
end
st.germEvidence = 'percentage only'
return { state = 'unknown', text = 'percentage only; counts needed' }
end
local worst, worstLine = 'pass', nil
local order = { pass = 0, between = 1, below = 2 }
for _, l in ipairs(lines) do
local j = p.judge(s, n, l.q)
if order[j] > order[worst] then worst, worstLine = j, l end
end
local what = string.format('%d seedlings from %d seeds', s, n)
if worst == 'below' then
-- Fresh or feral seed can be dormant: a low result counts once a tetrazolium test or a
-- dormancy-breaking retest confirms it, on lots that can spare the seed.
local dah = num(latest.days_after_harvest)
local feral = st.accession and lower(st.accession.classification) == 'feral'
local checked = lower(latest.dormancy_check)
local confirmed = checked and checked ~= 'none'
if (feral or (dah and dah <= C.dormancyWindowDays)) and not confirmed and not st.seedsHard then
st.germEvidence = 'between'
return { state = 'warning', text = what .. ', below the ' .. worstLine.label .. '; not yet checked for dormancy' }
end
st.germEvidence = 'clearly below'
return { state = 'hard', text = what .. ', clearly below the ' .. worstLine.label }
elseif worst == 'between' then
st.germEvidence = 'between'
return { state = 'warning', text = what .. ', between the lines for the ' .. worstLine.label }
end
st.germEvidence = 'passes'
st.evidencedLive = true
return { state = 'pass', text = what .. ', passes' }
end
F['storage'] = function(lot, st, ctx)
local key = storageKey(st.inv)
if not key or key == 'other' then return { state = 'unknown', text = 'storage not reported' } end
local m = num(st.inv.seed_moisture)
local months
if key == 'freezer' then months = C.retestMonths.freezer
elseif key == 'cool' then months = (m and m <= C.coolMoistureMax) and C.retestMonths.cooldry or C.retestMonths.coolwet
else
if not (m and m <= C.roomMoistureMax) then
return { state = 'hard', text = 'room temperature with moisture over 5% or not known' }
end
months = C.retestMonths.roomdry
end
local now = today(ctx)
local due
if #st.tests > 0 then
local last = parseDate(st.tests[#st.tests].date)
if last then
due = addMonths(last, months)
-- After two tests the lot's own rate of fall can bring the date forward.
if #st.tests >= 2 then
local f1, l1 = st.tests[1], st.tests[#st.tests]
local d1, d2 = parseDate(f1.date), last
local q1, q2 = testFigure(f1), testFigure(l1)
if d1 and q2 < q1 and dayNumber(d2) > dayNumber(d1) then
local rate = (q1 - q2) / (dayNumber(d2) - dayNumber(d1))
local line = math.max(C.floor, C.relative * q1)
local days = math.max(0, math.floor((q2 - line) / rate))
local cross = addDays(d2, days)
if before(cross, due) then due = cross end
end
end
end
elseif not st.seedsHard then
local rec = parseDate(lot.received_date)
if rec then due = addMonths(rec, C.firstTestMonths) end
end
st.nextTest = due
local regime = (key == 'freezer') and 'frozen' or (key == 'cool' and 'fridge or cool room' or 'room temperature, dry')
if #st.tests == 0 and st.seedsHard then
return { state = 'pass', text = regime .. '; not tested (too few seeds to spare)' }
end
if not due then return { state = 'unknown', text = regime .. '; test date not known' } end
if before(due, now) then
if #st.tests == 0 then
st.overdueFirst = true
return { state = 'warning', text = regime .. '; never tested, first test was due ' .. fmtDate(due) }
end
st.overdue = true
return { state = 'warning', text = regime .. '; test overdue since ' .. fmtDate(due) }
end
if #st.tests == 0 then
return { state = 'pass', text = regime .. '; low risk, first test due ' .. fmtDate(due) }
end
return { state = 'pass', text = regime .. '; next test due ' .. fmtDate(due) }
end
local function copyReading(other, data, ctx)
-- What a copy of this lot is worth as a backup: 'pass', 'warning' or 'unknown'.
local ost = lotInputs(other, data, ctx)
local dist = lower(other.backup_distance)
local where
if dist == 'same site' or dist == 'same town' then where = 'warning'
elseif dist == 'other town' or dist == 'other country' then where = 'pass'
else where = 'unknown' end
local viable
if ost.usable and ost.g then viable = ost.usable * ost.g end
local size
if viable then size = viable >= C.backupViable and 'pass' or 'warning'
elseif ost.usable and ost.usable < C.backupViable then size = 'warning'
else size = 'unknown' end
local state = 'pass'
if where == 'warning' or size == 'warning' then state = 'warning'
elseif where == 'unknown' or size == 'unknown' then state = 'unknown' end
local parts = {}
parts[#parts + 1] = viable and (fmtInt(math.floor(viable)) .. ' seeds expected to germinate')
or (ost.usable and (fmtInt(ost.usable) .. ' seeds, germination not known') or 'size not reported')
parts[#parts + 1] = dist or 'distance not recorded'
return state, table.concat(parts, ', ')
end
F['backup copy'] = function(lot, st, ctx, data)
local copies = {}
for _, other in pairs(data.lots) do
if other ~= lot and lower(other.lot_status) ~= 'gone' then
if lower(other.lot_role) == 'safety duplicate' and other.duplicate_of == lot.id then
copies[#copies + 1] = other
end
end
end
-- A safety duplicate's own copy is the lot it backs up.
if lower(lot.lot_role) == 'safety duplicate' and lot.duplicate_of and data.lots[lot.duplicate_of] then
copies[#copies + 1] = data.lots[lot.duplicate_of]
end
if #copies > 0 then
local best, bestText, anyWarn, warnText = nil, nil, false, nil
for _, c in ipairs(copies) do
local s, t = copyReading(c, data, ctx)
if s == 'pass' then best, bestText = 'pass', t end
if s == 'warning' then anyWarn, warnText = true, t end
if s == 'unknown' and not best then bestText = bestText or t end
end
if best == 'pass' then return { state = 'pass', text = 'backup: ' .. bestText } end
if anyWarn then return { state = 'warning', text = 'backup: ' .. warnText } end
return { state = 'unknown', text = 'backup: ' .. (bestText or 'not reported') }
end
if lower(lot.backup_status) == 'confirmed none' then
st.backupNone = true
return { state = 'hard', text = 'no backup copy' }
end
st.backupUnreported = true
return { state = 'unknown', text = 'backup not reported' }
end
-- Whether the population has changed since the lot's collection: true, false or nil (not known).
local function changedSinceCollection(pop, root)
if not pop then return false end
local result = false
local intro = byDate(pop.introgression)
local current = intro[#intro]
if current and INTRO_HIGH[trim(current.grade)] then
local at = root and trim(root.introgression_at_collection or '')
if INTRO_LOW[at] then return true end
if not INTRO_HIGH[at] then result = nil end
end
local cdate = root and parseDate(root.collection_date)
for _, tc in ipairs(pop.trait_changes or {}) do
local d = parseDate(tc.date)
if d and cdate then
if before(cdate, d) then return true end
else
result = nil
end
end
return result
end
F['replaceability'] = function(lot, st, ctx, data)
local acc = st.accession
local root = st.walk.root or acc
local popId = (acc and acc.population) or (root and root.population)
local pop = popId and data.populations[popId]
if not pop then
st.replaceability = 'unknown'
return { state = 'unknown', text = 'population not known' }
end
local changed = changedSinceCollection(pop, root)
local function irr(why) st.irreplaceable = why; st.replaceability = 'irreplaceable'
return { state = 'pass', irreplaceable = true, text = 'irreplaceable: ' .. why } end
if changed == true then return irr('population changed since collection') end
local fs = trim(pop.field_status or '')
if fs == 'Extinct' or fs == 'Extinct on-farm' then return irr(fs:lower()) end
local reach = lower(pop.reachability)
if reach == 'not reachable' then return irr('can no longer be reached') end
local b = band(pop.band)
if b == 'Very high' or b == 'High' then return irr('population graded ' .. b) end
st.replaceability = 'unknown'
if changed == nil then return { state = 'unknown', text = 'whether the population has changed is not known' } end
if not b or b == GRADE_DD then return { state = 'unknown', text = 'population not graded' } end
if reach ~= 'reachable' and reach ~= 'with difficulty' then
return { state = 'unknown', text = 'whether the population can be reached is not known' }
end
st.replaceability = 'replaceable'
return { state = 'pass', text = 'replaceable (population ' .. b .. (reach == 'with difficulty' and ', reached with difficulty' or '') .. ')' }
end
F['mother plants'] = function(lot, st)
local w = st.walk
if w.pooled then return { state = 'unknown', text = 'pooled from several collections; mother plants not settled' } end
local min, minGen, missing = nil, nil, false
local depth = #w.steps
for i, a in ipairs(w.steps) do
local c = plantCount(a)
local gen = num(a.generation) or (w.complete and (depth - i)) or nil
if c then
if not min or c < min then min, minGen = c, gen end
else
missing = true
end
end
if min and min < C.motherPlants then
st.cureByPooling = true
local where = minGen and (minGen == 0 and ' (the collection)' or (' (grow-out ' .. minGen .. ')')) or ''
local tail = (missing or not w.complete) and '; earlier steps not recorded' or ''
return { state = 'hard', text = 'collected from ' .. min .. ' plants' .. where .. tail }
end
if missing or not w.complete or not min then return { state = 'unknown', text = 'mother plants not recorded' } end
return { state = 'pass', text = min .. ' plants at the smallest step' }
end
F['handed out'] = function(lot, st)
local h = list(lot.handed_out)
if #h == 0 then return { state = 'unknown', text = 'not reported whether it is handed out' } end
local out = false
for _, v in ipairs(h) do if lower(v) ~= 'no' then out = true end end
if not out then return { state = 'pass', text = 'not handed out' } end
if st.usable then
local six = st.g and seedsFor(C.sowingSeedlings * C.sowingsHandOut, st) or (C.sowingSeedlings * C.sowingsHandOut)
if st.usable < six then
return { state = 'hard', text = string.format('handed out with %s seeds left, under six sowings', fmtInt(st.usable)) }
end
end
return { state = 'warning', text = 'handed out (' .. table.concat(h, ', ') .. ')' }
end
F['grow-out history'] = function(lot, st)
local w = st.walk
local isolation = 'documented'
for i, a in ipairs(w.steps) do
local isRegrow = #list(a.parents) > 0 or (num(a.generation) or 0) > 0
if isRegrow then
local iso = lower(a.regrow_isolation)
if iso == 'not isolated' then isolation = 'pollen known in'
elseif not iso and isolation == 'documented' then isolation = 'not recorded' end
end
end
local gens
if w.complete then gens = #w.steps - 1 else gens = st.accession and num(st.accession.generation) end
st.generations = gens
if lower(st.accession and st.accession.copy_status) == 'cross between populations' then st.verified = 'cross'
elseif isolation == 'pollen known in' then st.verified = 'pollen known in'
elseif isolation == 'not recorded' then st.verified = 'isolation not recorded'
elseif w.complete or w.pooled then st.verified = gens == 0 and 'field collection' or 'documented isolation'
else st.verified = 'lineage not traceable' end
if isolation == 'pollen known in' then
st.isolationProblem = true
return { state = 'hard', text = 'grown out without isolation' }
elseif isolation == 'not recorded' then
st.isolationProblem = true
return { state = 'hard', text = 'grown out; isolation not recorded' }
end
if gens and gens > C.maxGrowouts then
st.cureByPooling = true
return { state = 'hard', text = gens .. ' grow-outs from the collection, more than three' }
end
if gens == 0 then return { state = 'pass', text = 'collected from the field' } end
if gens then
local ord = ({ '1st', '2nd', '3rd' })[gens] or (gens .. 'th')
return { state = 'pass', text = ord .. ' isolated grow-out' }
end
return { state = 'unknown', text = 'grow-out history not traceable' }
end
------------------------------------------------------------------------------------------
-- The seed lot grade
------------------------------------------------------------------------------------------
local EVAL_ORDER = { 'seeds left', 'germination', 'storage', 'backup copy', 'replaceability',
'mother plants', 'handed out', 'grow-out history' }
-- Lots of one population, for the preservation check.
local function populationOf(lot, data)
local a = data.accessions[lot.accession]
return a and a.population
end
function p.evaluateLot(lotId, data, ctx)
ctx = ctx or {}
local lot = data.lots[lotId]
local r = { id = lotId, lot = lot, version = p.VERSION, asof = fmtDate(today(ctx)), factors = {} }
if not lot then r.error = 'lot not found'; return r end
if lower(lot.lot_status) == 'gone' then
r.gone = true
r.goneText = 'Gone' .. (lot.gone_reason and (' (' .. lot.gone_reason .. ')') or '')
.. (lot.lot_status_date and (', ' .. lot.lot_status_date) or '')
return r
end
local st = lotInputs(lot, data, ctx)
r.st = st
-- Seeds left first: it decides whether the lot is tested at all.
r.factors['seeds left'] = F['seeds left'](lot, st, ctx, data)
st.seedsHard = r.factors['seeds left'].state == 'hard'
for _, name in ipairs(EVAL_ORDER) do
if name ~= 'seeds left' then r.factors[name] = F[name](lot, st, ctx, data) end
end
-- Not known whether it still exists.
if st.lastReport and before(addMonths(st.lastReport, 12 * C.noReportYears), today(ctx)) then
r.existsUnknown = true
r.lastReport = fmtDate(st.lastReport)
end
-- Combine. The replaceability factor raises warnings; it never sets a reading by itself.
local hard, warn, unknown = {}, {}, {}
for _, name in ipairs(FACTORS) do
local f = r.factors[name]
local state = f.state
if state == 'warning' and st.irreplaceable then
state = 'hard'; f.raised = true
end
f.effective = state
if state == 'hard' then hard[#hard + 1] = name
elseif state == 'warning' then warn[#warn + 1] = name
elseif state == 'unknown' then unknown[#unknown + 1] = name end
end
r.irreplaceable = st.irreplaceable
r.reasons, r.missing = {}, unknown
if #hard > 0 then
r.step = STEP.urgent
-- Among Urgent lots, a germination problem comes before a low seed count.
table.sort(hard, function(a, b)
local ia, ib
for i, n in ipairs(FACTORS) do if n == a then ia = i end; if n == b then ib = i end end
return ia < ib
end)
r.reasons = hard
elseif #warn > 0 then
r.step = STEP.atrisk
r.reasons = warn
if st.replaceability == 'unknown' then r.mayBeUrgent = true end
elseif #unknown > 0 then
r.step = STEP.dd
else
r.step = STEP.secure
end
r.verified = st.verified
r.counting = (st.verified == 'field collection' or st.verified == 'documented isolation') and not r.existsUnknown
r.evidencedLive = st.evidencedLive or false
r.nextTest = fmtDate(st.nextTest)
r.threeSowings = st.threeSowings
r.generations = st.generations
p.actions(r, lot, st, data)
return r
end
-- Actions and the sowing queue (manuscript Table 5).
function p.actions(r, lot, st, data)
local acts, seen = {}, {}
local function add(code, detail)
if seen[code] then return end
seen[code] = true
acts[#acts + 1] = { code = code, text = ACTION_TEXT[code], detail = detail }
end
local f = r.factors
if st.isolationProblem then
add('do not queue')
-- The only live seed of its population: no other lot of it is held.
local pop = populationOf(lot, data)
if pop then
local other = false
for id, o in pairs(data.lots) do
if id ~= lot.id and lower(o.lot_status) ~= 'gone' and populationOf(o, data) == pop then other = true end
end
r.preservation = not other
if r.preservation then add('preservation review') end
end
end
if st.cureByPooling then add('pool or re-collect') end
-- Queue for sowing: only a clear germination failure or a low seed count, never an
-- unisolated grow-out.
local seedsReason = f['seeds left'].state == 'hard' or f['seeds left'].state == 'warning'
local germReason = st.germEvidence == 'clearly below'
r.queued = (seedsReason or germReason) and not st.isolationProblem
if r.queued then
local sowing = seedsFor(C.sowingSeedlings, st)
r.sowing = sowing
local total = st.count or 0
if st.count and total < sowing then
r.smallSowing = true
add('sow', string.format('small sowing: sow the whole lot (%s seeds) in isolation; pooling with another lot of the same population can be considered after review, each parent recorded separately', fmtInt(total)))
else
local g = st.g and (string.format('%d%%', math.floor(st.g * 100 + 0.5))) or 'the 70% planning figure'
local detail = string.format('one full sowing, %s seeds at %s', fmtInt(sowing), g)
if st.usable then
r.rest = math.max(0, st.usable - sowing)
detail = detail .. string.format('; keep the other %s in store', fmtInt(r.rest))
end
if st.seedsHard then detail = detail .. '; no test first' end
add('sow', detail)
end
end
r.smallSowing = r.smallSowing or false
if not st.untestable then
local ev = st.germEvidence
if ev == 'between' then add('retest sooner')
elseif ev == 'percentage below' then add('retest with counts')
elseif ev == 'predicted' then add('test at once')
elseif ev == 'never tested' then add('first test')
end
if st.overdue then add('retest') end
end
if f['storage'].state == 'hard' then add('cold storage') end
-- A backup is split off only if the seed left stays above the hard line.
local canSplit = not st.seedsHard
if canSplit and st.usable and st.threeSowings and st.g then
canSplit = st.usable - seedsFor(C.backupViable, st) >= st.threeSowings
end
if st.backupNone then add(canSplit and 'assess spare seed for a backup' or 'back up the regrow')
elseif f['backup copy'].state == 'warning' then add(canSplit and 'split a backup' or 'back up the regrow')
elseif st.backupUnreported then add('confirm whether a backup exists') end
if f['handed out'].effective == 'hard' then add('stop handing out') end
table.sort(acts, function(a, b) return ACTION_RANK[a.code] < ACTION_RANK[b.code] end)
r.actions = acts
return acts
end
------------------------------------------------------------------------------------------
-- Holdings
------------------------------------------------------------------------------------------
local function siteRecorded(e)
return not blank(e.site) and not blank(e.country) and not blank(e.distance)
end
-- How two copies stand: 'separate', 'same' or nil (not known).
local function pairPlace(a, b)
if not (siteRecorded(a) and siteRecorded(b)) then return nil end
if trim(a.site) == trim(b.site) then return 'same' end
local function rec(x, y)
if x.distanceFrom ~= y.id then return nil end
local d = lower(x.distance)
if d == 'other town' or d == 'other country' then return 'separate' end
if d == 'same site' or d == 'same town' then return 'same' end
end
local ab, ba = rec(a, b), rec(b, a)
if ab and ba then return ab == ba and ab or nil end
return ab or ba
end
p.pairPlace = pairPlace
-- entries: one per lot of the population, {id, step, collection, counting, evidencedLive,
-- site, country, distance, distanceFrom}. Returns the holdings reading.
function p.readHoldings(entries)
local h = { collections = {}, counting = {}, listed = {} }
for _, e in ipairs(entries) do
if e.counting then h.counting[#h.counting + 1] = e end
end
h.liveVerifiedCopy = #h.counting > 0
-- Collections, and the best lot of each.
local colls, nColl = {}, 0
for _, e in ipairs(h.counting) do
local c = e.collection or e.id
if not colls[c] then colls[c] = { id = c, lots = {} }; nColl = nColl + 1 end
table.insert(colls[c].lots, e)
end
for _, c in pairs(colls) do
local best, allUrgent = nil, true
for _, e in ipairs(c.lots) do
if not best or LOT_ORDER[e.step] < LOT_ORDER[best] then best = e.step end
if e.step ~= STEP.urgent then allUrgent = false end
end
c.step = best
h.collections[#h.collections + 1] = c
if allUrgent then h.listed[#h.listed + 1] = c end
end
table.sort(h.collections, function(a, b) return tostring(a.id) < tostring(b.id) end)
table.sort(h.listed, function(a, b) return tostring(a.id) < tostring(b.id) end)
h.nCollections = nColl
if #h.counting == 0 then
h.step, h.why = STEP.urgent, 'no verified copy held'
return h
end
local best, allUrgent = nil, true
for _, e in ipairs(h.counting) do
if not best or LOT_ORDER[e.step] < LOT_ORDER[best] then best = e.step end
if e.step ~= STEP.urgent then allUrgent = false end
end
h.best = best
if allUrgent then h.step, h.why = STEP.urgent, 'every verified copy is Urgent'; return h end
-- Places.
local anySeparate, allSame = false, true
local securePair = false
for i = 1, #h.counting do
for j = i + 1, #h.counting do
local a, b = h.counting[i], h.counting[j]
local rel = pairPlace(a, b)
if rel == 'separate' then
anySeparate = true
if a.evidencedLive and b.evidencedLive and (a.collection or a.id) ~= (b.collection or b.id) then
securePair = true
end
end
if rel ~= 'same' then allSame = false end
end
end
if #h.counting == 1 then allSame = siteRecorded(h.counting[1]) end
h.places = anySeparate and 'separate' or (allSame and 'one' or 'not known')
if allSame then h.step, h.why = STEP.atrisk, 'every verified copy in one town or site'; return h end
if nColl == 1 then h.step, h.why = STEP.atrisk, 'one collection only'; return h end
if best == STEP.atrisk then h.step, h.why = STEP.atrisk, 'best lot At risk'; return h end
if best == STEP.dd then h.step, h.why = STEP.dd, 'best lot Data deficient'; return h end
if not anySeparate then h.step, h.why = STEP.dd, 'storage sites not recorded'; return h end
if not securePair then
h.step, h.why = STEP.dd, 'not two collections with evidenced live seed at separate sites'
return h
end
h.step, h.why = STEP.secure, nil
return h
end
-- The holdings reading of a population from the lots in `data`.
function p.evaluatePopulation(popId, data, ctx)
ctx = ctx or {}
local out = { id = popId, lots = {}, entries = {}, version = p.VERSION, asof = fmtDate(today(ctx)) }
local ids = {}
for id, lot in pairs(data.lots) do
if populationOf(lot, data) == popId then ids[#ids + 1] = id end
end
table.sort(ids)
for _, id in ipairs(ids) do
local r = p.evaluateLot(id, data, ctx)
out.lots[#out.lots + 1] = r
if not r.gone then
local lot = r.lot
local w = r.st and r.st.walk
local root = w and w.root
out.entries[#out.entries + 1] = {
id = id, step = r.step, counting = r.counting, evidencedLive = r.evidencedLive,
collection = root and root.id or lot.accession,
site = lot.storage_site, country = lot.storage_country, distance = lot.backup_distance,
distanceFrom = lot.distance_measured_from,
}
end
end
out.holdings = p.readHoldings(out.entries)
local pop = data.populations[popId] or {}
out.band = pop.band
out.ranks = p.ranks(out.holdings.step, (pop.field_status == 'Extinct' or pop.field_status == 'Extinct on-farm')
and pop.field_status or pop.band, out.holdings.best)
return out
end
function p.ranks(holdingsStep, fieldBand, bestLot)
return {
holdings = RANK.holdings[holdingsStep or 'none'] or RANK.holdings.none,
field = RANK.field[band(fieldBand) or fieldBand or 'none'] or RANK.field.none,
bestLot = RANK.bestLot[bestLot or 'none'] or RANK.bestLot.none,
}
end
------------------------------------------------------------------------------------------
-- SMW reader: builds the engine's tables from stored properties through #ask.
------------------------------------------------------------------------------------------
-- Every property the module reads, in one table.
local P = {
-- seed lot (page or subobject)
lot_of = 'Is lot of', holder = 'Has lot holder', lot_role = 'Has lot role',
duplicate_of = 'Is safety duplicate of', storage_site = 'Has storage site',
storage_country = 'Has storage country', backup_distance = 'Has backup distance',
distance_measured_from = 'Has distance measured from', received_date = 'Has lot received date',
original_kept = 'Has original sample kept', handed_out = 'Is handed out',
backup_status = 'Has backup status', lot_status = 'Has lot status',
lot_status_date = 'Has lot status date', gone_reason = 'Has gone reason',
-- reports on a lot (subobjects)
report_on = 'Is report on',
report_date = 'Has report date', seed_count = 'Has seed count', seed_count_unit = 'Has seed count unit',
thousand_seed_weight = 'Has thousand seed weight', storage_regime = 'Has storage regime',
seed_moisture = 'Has seed moisture',
test_date = 'Has test date', seeds_tested = 'Has seeds tested', seedlings = 'Has seedlings counted',
germination_percent = 'Has germination percent', germination_basis = 'Has germination basis',
dormancy_check = 'Has dormancy check', days_after_harvest = 'Has days after harvest',
-- accession
parent = 'Has parent accession', collection = 'Has collection', generation = 'Has regrow generation',
isolation = 'Has regrow isolation', females = 'Has parent female count', males = 'Has parent male count',
total = 'Has parent plant count', population = 'Has population', copy_status = 'Has copy status',
collection_date = 'Has collection date', introgression = 'Has perceived introgression level',
classification = 'Has classification',
-- population
field_grade = 'Has field grade', grade_year = 'Has field grade evidence year',
item_set = 'Has field item set', reachability = 'Has reachability', field_status = 'Has field status',
intro_grade = 'Has introgression grade', intro_year = 'Has introgression year',
trait_date = 'Has trait change date',
}
p.PROPS = P
local SEP_R, SEP_P, SEP_V = '⟦R⟧', '⟦P⟧', '⟦V⟧'
-- printouts: list of {key, property, format}; returns list of {subject, fields = {key = {values}}}.
local function ask(frame, cond, printouts, limit)
local parts = { '{{#ask:', cond }
for _, po in ipairs(printouts) do
parts[#parts + 1] = '|?' .. po[2] .. '#' .. (po[3] or '-') .. '=⟪' .. po[1] .. '⟫'
end
parts[#parts + 1] = '|format=plainlist|headers=plain|link=none|searchlabel=|default='
parts[#parts + 1] = '|sep=' .. SEP_R .. '|propsep=' .. SEP_P .. '|valuesep=' .. SEP_V
parts[#parts + 1] = '|limit=' .. (limit or 500) .. '}}'
local out = trim(frame:preprocess(table.concat(parts)))
local rows = {}
if out == '' then return rows end
for _, row in ipairs(split(out, SEP_R)) do
local subject, rest = row:match('^(.-) %((⟪.*)%)%s*$')
if not subject then subject, rest = trim(row), '' end
local fields = {}
for _, part in ipairs(split(rest, SEP_P)) do
local k, v = part:match('^⟪([%w_]+)⟫: (.*)$')
if k then fields[k] = split(v, SEP_V) end
end
rows[#rows + 1] = { subject = trim(subject), fields = fields }
end
return rows
end
p._ask = ask
local function one(fields, k) local v = fields[k]; return v and trim(v[1]) or nil end
local LOT_PRINTOUTS = {
{ 'accession', P.lot_of }, { 'holder', P.holder }, { 'lot_role', P.lot_role },
{ 'duplicate_of', P.duplicate_of }, { 'storage_site', P.storage_site },
{ 'storage_country', P.storage_country }, { 'backup_distance', P.backup_distance },
{ 'distance_measured_from', P.distance_measured_from }, { 'received_date', P.received_date, 'ISO' },
{ 'original_kept', P.original_kept }, { 'handed_out', P.handed_out },
{ 'backup_status', P.backup_status }, { 'lot_status', P.lot_status },
{ 'lot_status_date', P.lot_status_date, 'ISO' }, { 'gone_reason', P.gone_reason },
}
local MULTI = { handed_out = true }
local function lotFromRow(row)
local lot = { id = row.subject, inventory = {}, tests = {} }
for _, po in ipairs(LOT_PRINTOUTS) do
lot[po[1]] = MULTI[po[1]] and row.fields[po[1]] or one(row.fields, po[1])
end
return lot
end
local ACC_PRINTOUTS = {
{ 'parents', P.parent }, { 'collection', P.collection }, { 'generation', P.generation },
{ 'regrow_isolation', P.isolation }, { 'parent_females', P.females }, { 'parent_males', P.males },
{ 'parent_total', P.total }, { 'population', P.population }, { 'copy_status', P.copy_status },
{ 'collection_date', P.collection_date, 'ISO' }, { 'introgression_at_collection', P.introgression },
{ 'classification', P.classification },
}
local function accFromRow(row)
local a = { id = row.subject }
for _, po in ipairs(ACC_PRINTOUTS) do
if po[1] == 'parents' or po[1] == 'collection' then a[po[1]] = row.fields[po[1]] or {}
else a[po[1]] = one(row.fields, po[1]) end
end
return a
end
local function loadAccessions(frame, data, cond)
for _, row in ipairs(ask(frame, cond, ACC_PRINTOUTS)) do
data.accessions[row.subject] = accFromRow(row)
end
end
-- Fills in ancestors that are not yet loaded (one query per missing step, capped).
local function loadAncestors(frame, data, accId)
local cur = accId
for _ = 1, C.walkCap + 1 do
if not cur then return end
if not data.accessions[cur] then loadAccessions(frame, data, '[[' .. cur .. ']]') end
local a = data.accessions[cur]
if not a or #list(a.parents) ~= 1 then return end
cur = a.parents[1]
end
end
local function loadReports(frame, data, cond)
local inv = ask(frame, cond .. '[[' .. P.report_date .. '::+]]', {
{ 'lot', P.report_on }, { 'date', P.report_date, 'ISO' }, { 'quantity', P.seed_count },
{ 'quantity_unit', P.seed_count_unit }, { 'thousand_seed_weight', P.thousand_seed_weight },
{ 'storage', P.storage_regime }, { 'seed_moisture', P.seed_moisture } })
for _, row in ipairs(inv) do
local lot = data.lots[one(row.fields, 'lot') or '']
if lot then
local r = {}
for k in pairs(row.fields) do r[k] = one(row.fields, k) end
table.insert(lot.inventory, r)
end
end
local tests = ask(frame, cond .. '[[' .. P.test_date .. '::+]]', {
{ 'lot', P.report_on }, { 'date', P.test_date, 'ISO' }, { 'seeds_tested', P.seeds_tested },
{ 'seedlings', P.seedlings }, { 'percent', P.germination_percent },
{ 'basis', P.germination_basis }, { 'dormancy_check', P.dormancy_check },
{ 'days_after_harvest', P.days_after_harvest } })
for _, row in ipairs(tests) do
local lot = data.lots[one(row.fields, 'lot') or '']
if lot then
local r = {}
for k in pairs(row.fields) do r[k] = one(row.fields, k) end
table.insert(lot.tests, r)
end
end
end
local function loadPopulation(frame, data, popId, asof)
local rows = ask(frame, '[[' .. popId .. ']]', {
{ 'band', P.field_grade }, { 'grade_year', P.grade_year }, { 'item_set', P.item_set },
{ 'reachability', P.reachability }, { 'field_status', P.field_status } })
local pop = { id = popId, introgression = {}, trait_changes = {} }
if rows[1] then
for k in pairs(rows[1].fields) do pop[k] = one(rows[1].fields, k) end
end
if pop.band then
local g = p.lapse({ band = pop.band, evidenceYear = num(pop.grade_year) }, asof)
pop.band, pop.lapsedFrom = g.band, g.lapsedFrom
end
for _, row in ipairs(ask(frame, '[[-Has subobject::' .. popId .. ']][[' .. P.intro_grade .. '::+]]', {
{ 'grade', P.intro_grade }, { 'date', P.intro_year } })) do
table.insert(pop.introgression, { grade = one(row.fields, 'grade'), date = one(row.fields, 'date') })
end
for _, row in ipairs(ask(frame, '[[-Has subobject::' .. popId .. ']][[' .. P.trait_date .. '::+]]', {
{ 'date', P.trait_date, 'ISO' } })) do
table.insert(pop.trait_changes, { date = one(row.fields, 'date') })
end
data.populations[popId] = pop
-- Every accession and lot of the population, with their reports.
loadAccessions(frame, data, '[[' .. P.population .. '::' .. popId .. ']]')
for _, row in ipairs(ask(frame, '[[' .. P.lot_of .. '.' .. P.population .. '::' .. popId .. ']]', LOT_PRINTOUTS)) do
data.lots[row.subject] = data.lots[row.subject] or lotFromRow(row)
end
loadReports(frame, data, '[[' .. P.report_on .. '.' .. P.lot_of .. '.' .. P.population .. '::' .. popId .. ']]')
for id in pairs(data.accessions) do loadAncestors(frame, data, id) end
end
-- Data for one lot: the lot, its lineage, its population and the population's other lots.
function p.loadLot(frame, lotId, asof)
local data = { lots = {}, accessions = {}, populations = {} }
local rows = ask(frame, '[[' .. lotId .. ']]', LOT_PRINTOUTS)
if not rows[1] then return data end
local lot = lotFromRow(rows[1])
lot.id = lotId
data.lots[lotId] = lot
if lot.accession then
loadAncestors(frame, data, lot.accession)
local a = data.accessions[lot.accession]
if a and a.population then
loadPopulation(frame, data, a.population, asof)
if rows[1].subject ~= lotId then data.lots[rows[1].subject] = nil end
data.lots[lotId] = data.lots[lotId] or lot
end
end
if #data.lots[lotId].inventory == 0 and #data.lots[lotId].tests == 0 then
loadReports(frame, data, '[[' .. P.report_on .. '::' .. lotId .. ']]')
end
return data
end
function p.loadPopulation(frame, popId, asof)
local data = { lots = {}, accessions = {}, populations = {} }
loadPopulation(frame, data, popId, asof)
return data
end
------------------------------------------------------------------------------------------
-- Display
------------------------------------------------------------------------------------------
local function stepClass(step)
return 'gop-step gop-step--' .. (step or 'none'):lower():gsub('%s+', '-')
end
local function asofLine(r)
return string.format('<span class="gop-asof">as of %s · method %s</span>', r.asof, r.version)
end
function p.renderLot(r)
if r.error then return '<span class="gop-error">Seed lot: ' .. r.error .. '</span>' end
if r.gone then return 'Seed lot: ' .. r.goneText end
local out = {}
out[#out + 1] = string.format('Seed lot: <span class="%s">%s</span>', stepClass(r.step), r.step)
if r.existsUnknown then
out[#out + 1] = ' · not known whether it still exists (last report ' .. r.lastReport .. ')'
end
local why = {}
for _, name in ipairs(r.reasons) do
local f = r.factors[name]
why[#why + 1] = name .. ': ' .. f.text .. (f.raised and ' (irreplaceable)' or '')
end
if #why > 0 then out[#out + 1] = '<br />' .. table.concat(why, '; ') end
if r.mayBeUrgent then out[#out + 1] = '<br />replaceability unknown, may be Urgent' end
if r.step == STEP.dd and #r.missing > 0 then
out[#out + 1] = '<br />not known: ' .. table.concat(r.missing, ', ')
end
if r.actions and #r.actions > 0 then
local a = {}
for _, x in ipairs(r.actions) do a[#a + 1] = x.text .. (x.detail and (' (' .. x.detail .. ')') or '') end
out[#out + 1] = '<br />Next: ' .. table.concat(a, '; ')
end
if r.nextTest then out[#out + 1] = '<br />Next test due ' .. r.nextTest end
out[#out + 1] = '<br />' .. asofLine(r)
return table.concat(out)
end
function p.renderHoldings(pr)
local h = pr.holdings
local out = { string.format('Holdings: <span class="%s">%s</span>', stepClass(h.step), h.step) }
if h.why then out[#out + 1] = ' · ' .. h.why end
out[#out + 1] = string.format('<br />%d verified %s from %d %s; places: %s',
#h.counting, #h.counting == 1 and 'copy' or 'copies', h.nCollections,
h.nCollections == 1 and 'collection' or 'collections', h.places or 'not known')
for _, c in ipairs(h.listed) do
out[#out + 1] = '<br />Every lot Urgent: collection ' .. c.id
end
out[#out + 1] = '<br />' .. asofLine(pr)
return table.concat(out)
end
------------------------------------------------------------------------------------------
-- Entry points
------------------------------------------------------------------------------------------
local function args(frame)
local a = {}
for k, v in pairs(frame.args) do a[k] = v end
if frame.getParent and frame:getParent() then
for k, v in pairs(frame:getParent().args) do if blank(a[k]) then a[k] = v end end
end
return a
end
local function factorNames(r)
local out = {}
for _, name in ipairs(r.reasons or {}) do out[#out + 1] = name end
if r.step == STEP.dd then for _, name in ipairs(r.missing or {}) do out[#out + 1] = name end end
return out
end
-- Values to store for a lot reading (property = value or list of values).
function p.lotValues(r)
if r.gone or r.error then return {} end
local acts = {}
for _, a in ipairs(r.actions or {}) do acts[#acts + 1] = a.code end
return {
['Has seed lot step'] = r.step,
['Has seed lot factors'] = factorNames(r),
['Has lot action'] = acts,
['Is queued for sowing'] = r.queued and 'true' or 'false',
['Is small sowing'] = r.smallSowing and 'true' or 'false',
['Is flagged for preservation grow-out'] = r.preservation and 'true' or 'false',
['Has method version'] = r.version,
}
end
function p.populationValues(pr)
local h = pr.holdings
return {
['Has holdings step'] = h.step,
['Has live verified copy'] = h.liveVerifiedCopy and 'true' or 'false',
['Has holdings rank'] = tostring(pr.ranks.holdings),
['Has field grade rank'] = tostring(pr.ranks.field),
['Has best lot rank'] = tostring(pr.ranks.bestLot),
['Has method version'] = pr.version,
}
end
local function store(frame, values)
local props = {}
for prop in pairs(values) do props[#props + 1] = prop end
table.sort(props)
local parts = {}
for _, prop in ipairs(props) do
local v = values[prop]
if type(v) == 'table' then
if #v > 0 then
parts[#parts + 1] = prop .. '=' .. table.concat(v, ';')
parts[#parts + 1] = '+sep=;' -- a multi-valued property needs its own separator
end
elseif not blank(v) then
parts[#parts + 1] = prop .. '=' .. v
end
end
return frame:preprocess('{{#set:' .. table.concat(parts, '|') .. '}}')
end
-- {{#invoke:Grow-out priority|lot|lot=<lot page or subobject>|asof=|set=yes}}
function p.lot(frame)
local a = args(frame)
local lotId = trim(a.lot or a[1] or mw.title.getCurrentTitle().prefixedText)
local ctx = { today = parseDate(a.asof) }
local data = p.loadLot(frame, lotId, fmtDate(today(ctx)))
local r = p.evaluateLot(lotId, data, ctx)
local out = p.renderLot(r)
if lower(a.set) == 'yes' then out = store(frame, p.lotValues(r)) .. out end
return out
end
-- {{#invoke:Grow-out priority|population|population=<page>|asof=|set=yes}}
function p.population(frame)
local a = args(frame)
local popId = trim(a.population or a[1] or mw.title.getCurrentTitle().prefixedText)
local ctx = { today = parseDate(a.asof) }
local data = p.loadPopulation(frame, popId, fmtDate(today(ctx)))
local pr = p.evaluatePopulation(popId, data, ctx)
local out = p.renderHoldings(pr)
if lower(a.set) == 'yes' then out = store(frame, p.populationValues(pr)) .. out end
return out
end
-- The germination count table: {{#invoke:Grow-out priority|countTable|sizes=25,50,100,200|firsts=90,95}}
function p.countTable(frame)
local a = args(frame)
local sizes, firsts = {}, {}
for s in tostring(a.sizes or '25,50,100,200'):gmatch('%d+') do sizes[#sizes + 1] = tonumber(s) end
for s in tostring(a.firsts or '90,95'):gmatch('[%d%.]+') do firsts[#firsts + 1] = tonumber(s) end
local lines = { { q = C.floor, label = '70% floor' } }
for _, f in ipairs(firsts) do
local q = C.relative * f / 100
lines[#lines + 1] = { q = q, label = string.format('85%% of a first test of %s%% (%s%%)', f, math.floor(q * 10000 + 0.5) / 100) }
end
local t = { '{| class="wikitable"', '! Seeds tested' }
for _, l in ipairs(lines) do t[#t + 1] = '! ' .. l.label .. ': passes !! clearly below' end
for _, n in ipairs(sizes) do
t[#t + 1] = '|-'
local row = { '| ' .. n }
for _, l in ipairs(lines) do
local b = p.clearlyBelowAt(n, l.q)
row[#row + 1] = p.passesAt(n, l.q) .. ' or more || ' .. (b >= 0 and (b .. ' or fewer') or 'none')
end
t[#t + 1] = table.concat(row, ' || ')
end
t[#t + 1] = '|}'
return table.concat(t, '\n')
end
function p.version() return p.VERSION end
return p