Skip to contents

Vignette 1 produced one call per curated GIFT for one genome. This one turns those calls into numbers you can compare between genomes — and shows why a number of that kind is meaningless until you say what it was counted over.

We reuse the illustrative gut anaerobe from vignette 1.

gut_anaerobe <- data.frame(
  gene_id = paste0("g", sprintf("%04d", 1:33)),
  namespace = c(rep("CAZY", 3), rep("KO", 30)),
  accession = c(
    "GH11", "GH39", "CE1",
    "K10543", "K10544", "K10545", "K01805", "K00854",
    "K00764", "K01945", "K00601", "K01952", "K01587", "K01756", "K00602",
    "K03412", "K00575", "K03407", "K03408", "K03776", "K03413", "K03410",
    "K03427", "K01153", "K01154",
    "K00812", "K01915", "K00600", "K00826", "K00259",
    "K02945", "K02863", "K01873"
  ),
  stringsAsFactors = FALSE
)

result <- evaluate_gifts(gut_anaerobe)
sum(result$gifts$complete)
#> [1] 10

The problem: five is not a result

This genome supports ten GIFTs. Is that a lot?

The question has no answer until you say ten out of what. Out of the catalogue curated today? Out of all metabolic ones? Out of the amino-acid biosynthetic capabilities, of which this genome was tested for none? And when the next curation campaign adds forty GIFTs, does “ten” still mean the same thing?

gifter’s answer is that every quantitative trait names its reference frame, and carries that name in the output beside the number.

Step 1: define a frame

reference_frame() builds one from curated metadata — the GIFT type, the metabolic mode, the registered facet vocabulary, the derived profile view. You never write a list of GIFT identifiers yourself; that would move biological content out of the database, where it can be validated and versioned, and into your script, where it cannot.

Common questions already have named, versioned presets. Inspect them before writing a custom filter:

presets <- list_reference_frames()
presets[, c("frame_id", "bounded", "member_count")]
#> # A tibble: 19 × 3
#>    frame_id                       bounded member_count
#>    <chr>                          <lgl>          <int>
#>  1 amino_acid_autonomy            TRUE              22
#>  2 aromatic_catabolism            FALSE             12
#>  3 biomass_essential_anabolism    TRUE              44
#>  4 carbohydrate_degradation       FALSE             18
#>  5 carbon_acquisition             FALSE             36
#>  6 chemical_detoxification        FALSE              5
#>  7 cofactor_autonomy              TRUE              15
#>  8 extracellular_public_goods     FALSE              3
#>  9 fermentation_products          FALSE             11
#> 10 host_glycan_foraging           FALSE              7
#> 11 nitrogen_acquisition           FALSE             20
#> 12 nucleotide_autonomy            TRUE               4
#> 13 nutrient_uptake                FALSE              3
#> 14 plant_fibre_utilisation        FALSE             13
#> 15 released_bioactive_products    FALSE              5
#> 16 shikimate_derived_biosynthesis FALSE             10
#> 17 short_chain_fatty_acids        FALSE              4
#> 18 sulfur_acquisition             FALSE              4
#> 19 vitamin_biosynthesis           FALSE             20

carbohydrate <- reference_frame(preset = "carbohydrate_degradation")
carbohydrate
#> <gifter_frame> Carbohydrate degradation 
#>   preset:   carbohydrate_degradation 
#>   GIFTs:   18 
#>   bounded: FALSE 
#>   database version: 2026.39.1

Choose a preset from your research question

The searchable Frames view in the Atlas is the quickest chooser: search for a substrate, process or metric, then narrow the cards to genome, community, network or bounded-coverage analyses. The full registry is reproduced below so that the choice remains visible in this tutorial too. “Current GIFTs” describes this database release, not a fixed list stored in the preset.

Research question Preset Use it for Current GIFTs Recommended starting metrics Denominator
Amino-acid autonomy amino_acid_autonomy Biomass-essential anabolic GIFTs in the amino-acid substrate class. 22 community: community_coverage, provider_count; genome: assessable_fraction, supported_fraction Bounded: coverage fractions are valid
Aromatic catabolism aromatic_catabolism GIFTs that activate, cleave or funnel aromatic rings toward central metabolism. 12 community: community_richness, provider_count; genome: gift_richness; network: distributed_chain_links Open: use counts, breadth or distribution
Biomass-essential anabolic GIFTs biomass_essential_anabolism Anabolic GIFTs whose declared output is a curated biomass-essential anchor. 44 community: community_coverage, provider_count; genome: assessable_fraction, supported_fraction Bounded: coverage fractions are valid
Carbohydrate degradation carbohydrate_degradation Catabolic GIFTs acting on polysaccharides, neutral monosaccharides, amino sugars or uronates. 18 community: abundance_coverage, community_richness, provider_count; genome: breadth_substrate_class, gift_richness Open: use counts, breadth or distribution
Carbon acquisition carbon_acquisition GIFTs that convert an acquired substrate into central carbon metabolism. 36 community: community_richness, provider_count; genome: breadth_substrate_class, gift_richness Open: use counts, breadth or distribution
Chemical detoxification chemical_detoxification Defense GIFTs implementing a complete curated chemical-detoxification mechanism. 5 community: community_richness, provider_count; genome: gift_richness Open: use counts, breadth or distribution
Cofactor autonomy cofactor_autonomy Biomass-essential anabolic GIFTs in the cofactor substrate class. 15 community: community_coverage, provider_count; genome: assessable_fraction, supported_fraction Bounded: coverage fractions are valid
Extracellular processing outputs extracellular_public_goods Metabolic GIFTs derived as consuming and releasing extracellular anchors. 3 community: abundance_coverage, provider_count; genome: gift_richness; network: handoff_edges Open: use counts, breadth or distribution
Fermentation products fermentation_products GIFTs that form a curated terminal fermentation product. 11 community: community_richness, provider_count; genome: breadth_substrate_class, gift_richness Open: use counts, breadth or distribution
Host glycan foraging host_glycan_foraging GIFTs acting on host-derived glycans or the sugars released from them. 7 community: abundance_coverage, provider_count; genome: gift_richness Open: use counts, breadth or distribution
Nitrogen acquisition nitrogen_acquisition GIFTs whose curated role makes nitrogen available from inorganic or organic sources. 20 community: community_richness, provider_count; genome: breadth_substrate_class, gift_richness Open: use counts, breadth or distribution
Nucleotide autonomy nucleotide_autonomy Biomass-essential anabolic GIFTs in the nucleotide substrate class. 4 community: community_coverage, provider_count; genome: assessable_fraction, supported_fraction Bounded: coverage fractions are valid
Nutrient uptake nutrient_uptake Metabolic GIFTs derived as moving an extracellular anchor to a cytoplasmic anchor. 3 community: abundance_coverage, provider_count; genome: gift_richness; network: distributed_chain_links Open: use counts, breadth or distribution
Plant fibre utilisation plant_fibre_utilisation GIFTs curated with a fibre-degradation role, including polymer processing, uptake and intracellular catabolism of released sugars. 13 community: community_richness, provider_count; genome: gift_richness; network: distributed_chain_links Open: use counts, breadth or distribution
Released bioactive products released_bioactive_products GIFTs whose chemistry forms a product curated as potentially bioactive when released. 5 community: abundance_coverage, provider_count; genome: gift_richness Open: use counts, breadth or distribution
Shikimate-derived biosynthesis shikimate_derived_biosynthesis Biosynthetic GIFTs whose product carbon skeleton is derived from the shikimate pathway. 10 community: community_richness, provider_count; genome: gift_richness Open: use counts, breadth or distribution
Short-chain fatty acids short_chain_fatty_acids GIFTs whose curated substrate or product class is a short-chain fatty acid. 4 community: abundance_coverage, community_richness, provider_count; genome: gift_richness Open: use counts, breadth or distribution
Sulfur acquisition sulfur_acquisition GIFTs whose curated role makes sulfur available from inorganic or organic sources. 4 community: community_richness, provider_count; genome: gift_richness Open: use counts, breadth or distribution
Vitamin biosynthesis vitamin_biosynthesis GIFTs participating in curated vitamin and cofactor biosynthetic capabilities. 20 community: community_richness, provider_count; genome: gift_richness Open: use counts, breadth or distribution

Choose the frame from the biological question first, then use its scope-specific metric recommendations:

  • for one genome, start with gift_richness or the recommended breadth metric;
  • for a community, use community_richness to count represented capabilities and provider_count for redundancy, keeping abundance separate;
  • use a network metric only when the question depends on compatible, extracellular GIFT boundaries;
  • use supported_fraction or community_coverage only where the table marks the frame bounded, and always report assessable_fraction alongside a genome fraction.

Each preset carries its description, interpretation limits and recommended genome, community or network metrics. Its members are still resolved from the current release’s metadata: the registry stores a query, never a list of GIFT identifiers. An open preset cannot be promoted to bounded = TRUE at runtime.

For a question that has no preset, build a custom frame directly:

reference_frame(type = "metabolic")
#> <gifter_frame> GIFTs where type = metabolic 
#>   GIFTs:   143 
#>   bounded: FALSE 
#>   database version: 2026.39.1

reference_frame(mode = "catabolic")
#> <gifter_frame> GIFTs where mode = catabolic 
#>   GIFTs:   69 
#>   bounded: FALSE 
#>   database version: 2026.39.1

reference_frame(facet = "substrate_class", value = "polysaccharide")
#> <gifter_frame> GIFTs where facet = substrate_class, value = polysaccharide 
#>   GIFTs:   9 
#>   bounded: FALSE 
#>   database version: 2026.39.1

Filters combine with AND:

reference_frame(mode = "catabolic", resource_strategy = "uptake")
#> <gifter_frame> GIFTs where mode = catabolic, resource_strategy = uptake 
#>   GIFTs:   0 
#>   bounded: FALSE 
#>   database version: 2026.39.1

Note that mode partitions this database more finely than gift_type does: most current GIFTs are metabolic, while mode separates those capabilities into anabolic, catabolic, transport and interconversion questions. The current counts are returned by list_gifts() rather than fixed in the tutorial.

Step 2: compute traits

traits <- genome_traits(result, genome_id = "MAG_001")
traits
#> <gifter_traits> MAG_001 
#>   metrics:  274 rows across 14 reference frames
#>   database version: 2026.39.1 
#> 
#>   all curated GIFTs                               10 / 163 supported
#>   defense GIFTs                                    1 /   7 supported
#>   metabolic GIFTs                                  8 / 143 supported
#>   regulatory GIFTs                                 1 /   4 supported
#>   structural GIFTs                                 0 /   9 supported
#>   anabolic GIFTs                                   5 /  67 supported
#>   catabolic GIFTs                                  2 /  69 supported
#>   interconversion GIFTs                            0 /   4 supported
#>   transport GIFTs                                  1 /   3 supported
#>   GIFTs with resource strategy private             6 / 130 supported
#>   GIFTs with resource strategy public_good         1 /   3 supported
#>   GIFTs with resource strategy unresolved          0 /   7 supported
#>   GIFTs with resource strategy uptake              1 /   3 supported
#>   biomass-essential anabolic GIFTs                 5 /  44 supported

With no frames supplied you get a default set that partitions the catalogue by type, by mode, and by resource strategy. The result is a long-form table:

names(traits$metrics)
#>  [1] "target_type"       "target_id"         "metric_id"        
#>  [4] "value"             "unit"              "numerator"        
#>  [7] "denominator"       "assessable"        "reference_frame"  
#> [10] "database_version"  "derivation_method"

Every row carries its numerator, its denominator, how many members of the frame were assessable, the frame itself, and the database version. That is what lets a number be taken apart six months later.

richness <- traits$metrics[traits$metrics$metric_id == "gift_richness", ]
richness[, c("reference_frame", "value", "assessable")]
#> # A tibble: 14 × 3
#>    reference_frame                          value assessable
#>    <chr>                                    <dbl>      <int>
#>  1 all curated GIFTs                           10        163
#>  2 defense GIFTs                                1          7
#>  3 metabolic GIFTs                              8        143
#>  4 regulatory GIFTs                             1          4
#>  5 structural GIFTs                             0          9
#>  6 anabolic GIFTs                               5         67
#>  7 catabolic GIFTs                              2         69
#>  8 interconversion GIFTs                        0          4
#>  9 transport GIFTs                              1          3
#> 10 GIFTs with resource strategy private         6        130
#> 11 GIFTs with resource strategy public_good     1          3
#> 12 GIFTs with resource strategy unresolved      0          7
#> 13 GIFTs with resource strategy uptake          1          3
#> 14 biomass-essential anabolic GIFTs             5         44

Step 3: ask why a number is what it is

Every metric keeps the GIFTs that produced it.

trace <- traits$trace[
  traits$trace$metric_id == "gift_richness" &
    traits$trace$reference_frame == "catabolic GIFTs",
]
trace[, c("metric_id", "gift_id")]
#> # A tibble: 2 × 2
#>   metric_id     gift_id                     
#>   <chr>         <chr>                       
#> 1 gift_richness xylan_degradation           
#> 2 gift_richness xylose_degradation_isomerase

This is the point of the long-form shape. A trait you cannot decompose is a trait you cannot defend.

Step 4: breadth is not richness

Counting GIFTs overcounts wherever the ontology happens to be finely subdivided. Breadth counts distinct classifications instead:

breadth <- traits$metrics[
  startsWith(traits$metrics$metric_id, "breadth_") &
    traits$metrics$reference_frame == "all curated GIFTs",
]
breadth[, c("metric_id", "value", "denominator")]
#> # A tibble: 13 × 3
#>    metric_id                      value denominator
#>    <chr>                          <dbl>       <int>
#>  1 breadth_biosynthetic_family        0           1
#>  2 breadth_challenge_class            0           4
#>  3 breadth_defense_class              1           3
#>  4 breadth_input_resource_origin      3           7
#>  5 breadth_metabolic_cycle            0           2
#>  6 breadth_network_position           2           4
#>  7 breadth_output_resource_origin     2           6
#>  8 breadth_physiological_role         4          15
#>  9 breadth_regulatory_class           1           2
#> 10 breadth_resource_strategy          3           4
#> 11 breadth_structural_class           0           3
#> 12 breadth_substrate_class            3          19
#> 13 breadth_substrate_tier             3           3

breadth_substrate_class counts how many curated substrate classes this genome can act on, out of the classes present in the frame. Two GIFTs acting on the same class contribute one class, not two.

Step 5: fractions, and when gifter refuses to give you one

Here is a metric that is not in the output above:

"supported_fraction" %in% richness$metric_id
#> [1] FALSE

gifter will not tell you what fraction of the metabolic catalogue this genome supports. Supporting eight curated metabolic GIFTs does not mean a genome lacks every other metabolic capability; the open catalogue will grow. A fraction of an open, growing set reads as “the share of microbial function this organism carries”, which is not what it measures.

A fraction is offered only for a frame you explicitly declare bounded — a claim that curation intends to cover that set completely.

autonomy <- reference_frame(preset = "biomass_essential_anabolism")
autonomy
#> <gifter_frame> Biomass-essential anabolic GIFTs 
#>   preset:   biomass_essential_anabolism 
#>   GIFTs:   44 
#>   bounded: TRUE 
#>   database version: 2026.39.1

autonomy_traits <- genome_traits(
  result, frames = list(autonomy), genome_id = "MAG_001"
)
autonomy_traits$metrics[
  autonomy_traits$metrics$metric_id %in%
    c("gift_richness", "supported_fraction"),
  c("metric_id", "value", "numerator", "denominator")
]
#> # A tibble: 2 × 4
#>   metric_id          value numerator denominator
#>   <chr>              <dbl>     <int>       <int>
#> 1 gift_richness      5             5          NA
#> 2 supported_fraction 0.114         5          44

Over that frame the fraction is meaningful: it is genomic coverage of the curated biomass-essential anabolic capabilities. It still does not mean the organism grows without supplementation — it means the genes for those capabilities are or are not there.

Step 6: MAG completeness, and the honesty of a denominator

Everything above assumed that a negative call means absence. For a closed isolate genome that is reasonable. For a 55%-complete metagenome-assembled genome it is not: the organism was never fully observed, so it has not been shown to lack anything.

By default gifter makes the optimistic assumption and tells you it did:

assessable <- traits$metrics[
  traits$metrics$metric_id == "assessable_fraction" &
    traits$metrics$reference_frame == "all curated GIFTs",
]
assessable[, c("value", "numerator", "denominator")]
#> # A tibble: 1 × 3
#>   value numerator denominator
#>   <dbl>     <int>       <int>
#> 1     1       163         163

Supply a completeness estimate and a threshold, and negative calls on a genome below that threshold leave every denominator. Both may be stated as proportions or as the percentages CheckM and BUSCO report — a proportion cannot exceed 1, so gifter reads the scale from the values themselves, and quality = c(MAG_001 = 55), threshold = 90 is the same analysis as the call below:

fragmented <- genome_traits(
  result, frames = list(autonomy), genome_id = "MAG_001",
  quality = c(MAG_001 = 0.55), policy = "completeness", threshold = 0.9
)
#> Warning: Less than half of the reference frame was assessable for 1 of 1
#> frames, including "Biomass-essential anabolic GIFTs". Proportions over them
#> rest on very few GIFTs; read them beside assessable_fraction.
fragmented$metrics[
  fragmented$metrics$metric_id %in%
    c("gift_richness", "supported_fraction", "assessable_fraction"),
  c("metric_id", "value", "numerator", "denominator")
]
#> # A tibble: 3 × 4
#>   metric_id           value numerator denominator
#>   <chr>               <dbl>     <int>       <int>
#> 1 gift_richness       5             5          NA
#> 2 assessable_fraction 0.114         5          44
#> 3 supported_fraction  1             5           5

Read that carefully, because it is the most misusable output in the package. supported_fraction went from 0.125 to 1.0, and the genome did not gain a single capability: gift_richness is 5 in both runs. All that happened is that the 35 negative calls left the denominator, because a 55%-complete genome has not been shown to lack them. The number now means “of the five capabilities we could assess, five are supported”, which is nearly content-free. assessable_fraction is what says so — 5 of 40 — and the warning gifter emitted points at exactly that.

The honest reading of a fragmented genome is gift_richness beside assessable_fraction, not the fraction on its own.

If the frame contains no supported GIFTs on a genome below the quality threshold, none of its members is assessable under this policy. gifter still reports gift_richness = 0 and assessable_fraction = 0, but it emits no supported_fraction row: there is no denominator. An absent row is not a measured zero or an NA value to include in an average.

Two guarantees hold whatever policy you choose:

  • no policy can make an unsupported GIFT supported. Genome quality changes how absence is read and nothing else. The complete calls are identical in both runs above.
  • there is no default threshold. How complete a genome must be before its silence is informative is your declared choice, and a package default would be read as a recommendation.

The incomplete-genome case study measures how calls and assessable denominators change when genes are removed from reference genomes.

What these numbers may not say

  • richness counts encoded capabilities in the current gifter ontology within a stated frame. It is not a measure of biological complexity or of metabolic versatility in any particular environment.
  • a supported fraction is genomic coverage, never growth independence, activity, flux or phenotype.
  • nothing here has become more certain than the calls underneath it. A trait built on an ambiguous call inherits that ambiguity.

Where to go next

Vignette 3, A genome-resolved community, applies the same discipline across many genomes: how redundantly a capability is provided, which genome is its only provider, and which chains the community completes that no single member does.