Registering Lab Constants

Seven macros register lab constants: @location_kind, @attribute, @read, @species, @reagent, @organism, and @stock. Each defines a constant in the calling module and records it in a registry so that it can be found by name. The pattern follows Unitful.jl, which registers units and dimensions with @unit and @dimension and recalls them with u"...". This page describes the shared machinery using a small lab module:

julia> module MyLab
       using CHESS
       @location_kind Flask Symbol[] nothing nothing nothing nothing nothing
       @attribute Turbidity u"percent"
       @organism EC_K12 "Escherichia" "coli" "K-12"
       @reagent ethanol "ethanol" Liquid 46.07u"g/mol" 0.789u"g/mL" 702
       end;

julia> CHESSCore.register_lab(MyLab);

Per-module registries and the central merge

Each registration macro keeps a private dictionary scoped to the module that calls it. For example, @location_kind stores every kind it defines in a dictionary kept in hidden storage that user code cannot reference by accident. The central location_kinds registry of CHESSCore is a dictionary of the same kind, scoped to CHESSCore.

CHESSCore.register_lab(lab_module) connects the two. It adds lab_module to CHESSCore.labmodules and, unless lab_module is CHESSCore, merges the module's registries into the central ones: chemprops, orgprops, location_kinds, attribute_kinds, read_kinds, and stock_recipes. Reagents have no central registry (see registry_summary below). A lab module can therefore be developed and tested alone, because its own dictionary works before register_lab is called, and is merged into the shared namespace once, typically from the module's __init__.

Duplicate registration

@location_kind, @attribute, @read, and @stock throw ArgumentError when a name is registered twice, checked against the central registry. MyLab already registered Flask, so registering it again fails:

julia> @location_kind Flask Symbol[] nothing nothing nothing nothing nothing
ERROR: ArgumentError: LocationKind Flask already exists

@organism, @reagent, and @species have no such check. Registering a name again with these macros rebinds it to the new value without a warning.

Namespaces

A lab module can register hundreds of constants, and loading it should not add all of them to the namespace of the loading code. None of the seven macros exports the names it defines. The constant exists in the module, and names lists it only when called with all=true. Each macro has a paired string macro that looks a name up safely: @loc_str, @attr_str, @read_str, @spc_str, @rgt_str, @org_str, and @stock_str.

How the string macros resolve a name

All string macros use the same lookup:

  1. Search the lab modules visible to the caller. When the code runs, this list is built from CHESSCore.labmodules, keeping only modules that the calling module has itself loaded. Registering a lab module globally is not enough on its own.

  2. If nothing matches, try charge-symbol candidates, in which ASCII + and - are converted to Unicode superscripts (see Reagents & Species).

  3. If the name exists in a globally registered lab module that is not visible to the caller, an error names the module that must be loaded:

    julia> module OtherModule
           using CHESS
           flask_kind() = loc"Flask"
           end
    ERROR: LoadError: ArgumentError: Symbol `Flask` was found in the globally registered lab module MyLab
    but was not in the provided list of lab modules CHESSCore, CHESSLabConstants.
    
    (Consider `using MyLab` in your module?)
  4. If the name does not exist anywhere, a typo-tolerant "did you mean" search runs against the visible modules.

If a name resolves in more than one visible module, the last registered one wins, and a warning is logged if the values differ.

Browsing registered constants

registry_summary collects every registered constant into one NamedTuple, keyed by category. It is the fastest way to see what a lab module such as CHESSLabConstants defines:

julia> registry_summary([CHESSCore, MyLab]).reagents
1-element Vector{NamedTuple}:
 (module_ = MyLab, name = :ethanol, type = Liquid, molecular_weight = 46.07 g mol⁻¹, density = 0.789 g mL⁻¹, pubchemid = 702)

With no arguments, registry_summary() covers every lab module registered globally. Unlike the string macros, it is not limited to modules visible to the caller. The organisms, locations, attributes, and reads entries come from the central registries. The reagents, species, and stocks entries are found by scanning each module's names and filtering by type, because reagents and species have no central registry and stock_recipes holds only stocks registered with @stock.

Generating registration lines

CHESSLabConstants has four functions that help write the registrations of a lab module. They are used while the module is being written and are not needed to use registered constants. register_reagent!, register_species!, and register_organism! each return a line of code that registers the constant, ready to paste into the source of the module. They do not register anything and do not write to any source file.

For a reagent or a species, the function looks up the elemental formula, molecular weight, and density in a local cache. If the cache has no entry, it fetches them from PubChem with get_molecular_formula and get_mw_density and stores them in the cache. The lookup needs a PubChem ID and a network connection. The returned line gives the formula in place of a molecular weight, so the weight is derived from atomic masses (see Reagents & Species). A species has no density, so the fetched density is discarded. PubChem writes the charge of an ion as a suffix of its formula, and register_species! removes it after checking that it agrees with the charge given. When PubChem's formula cannot be used, because its charge disagrees or it cannot be read as an elemental formula, the function warns and the line keeps the stored molecular weight. For an organism, the function does not look anything up. It records the genus, species, strain, and optional ATCC ID and notes in the cache.

using CHESSLabConstants

register_reagent!(CHESSCore.Solid, "boric_acid", "Boric Acid", 7628)
# "@reagent boric_acid \"Boric Acid\" Solid \"BH3O3\" 1.435u\"g/mL\" 7628"

register_species!("Na⁺", "Na+", 1, 923)
# "@species Na⁺ \"Na+\" 1 \"Na\""

register_organism!("SMU_UA159", "Streptococcus", "mutans", "UA159"; atcc_id = "700610", notes = "wild type")
# "@organism SMU_UA159 \"Streptococcus\" \"mutans\" \"UA159\""

PubChem describes the compound of the ID it is given, so the ID must be that of the form actually weighed out. A hydrate needs the hydrate's PubChem ID. A salt that dissolves into ions needs a Dissolution in place of the single formula the function returns. A compound with no PubChem entry, such as a rich-media broth, is registered without a PubChem ID. The line then has missing chemistry, or opaque when register_reagent! is called with opaque=true for an undefined mixture. get_molecular_formula takes a PubChem ID and returns the formula as PubChem writes it, and get_mw_density returns the molecular weight in g/mol and the density in g/mL.

Database Architecture describes how CHESSDatabase stores the locations and stocks built from registered constants.