Adding a Custom Shielding Function
A photo-reaction can attenuate its rate by a dimensionless shielding factor
S. JAFF resolves that factor from a registry of shielding models: each
model is a ShieldingFunction subclass that
registers itself with the @_register decorator and exposes a get_shielding
method returning a SymPy expression, which is
multiplied into the photo-rate. Adding a new model means writing one such class
and dropping it under physics/photo_reactions/shielding/.
There are two flavours, distinguished by the class's reaction attribute:
| Flavour | Scope | reaction attr |
Location |
|---|---|---|---|
| Local | A single reaction | the serialized reaction | shielding/<sanitized_reaction>/<type>.py |
| Global | Any reaction that selects it | None |
shielding/global_/<type>.py |
In both cases the class's name attribute is the keyword a reaction selects via
the TOML shielding.type option (matched case-insensitively).
How Shielding Is Resolved
When a reaction carries a [network.reactions."<serialized>".shielding] block, the
network parser copies it onto reaction._metadata["shielding"], and
Photochemistry.shielding (src/jaff/physics/photo_reactions/_photochemistry.py)
asks the registry for the model named by type. Lookup is keyed by
(name, reaction.serialized) and prefers a reaction-specific (local) model,
falling back to a global one registered with reaction = None. The resolved
instance's get_shielding method is called; the returned expression is cached
on reaction._metadata["shielding"]["value"] and folded into the rate.
flowchart TD
A[shielding block in TOML\ntype selects a model] --> B[Network parser\ncopies block onto\nreaction shielding metadata]
B --> C[Photochemistry.shielding]
C --> D[_get_shielding_function\ntype, reaction.serialized]
D --> E{local model registered\nfor this reaction?}
E -- yes --> F[use name, reaction model]
E -- no --> G{global model\nregistered for type?}
G -- yes --> H[use name, None model]
G -- no --> I[raise ParserError\nInvalid shielding type]
F --> J[call instance.get_shielding\nreaction, network]
H --> J
J --> K[Returned sympy.Expr\ncached on the\nshielding value metadata]
K --> L[Multiplied into\nthe photo-rate]
The registry is populated by importing every (non-underscore) module under
shielding/, which runs the @_register decorators — so a new model is
discovered automatically once its file is in place; no central list to edit.
Case-insensitive matching
The shielding.type string is lower-cased when copied onto the reaction
metadata, and the registry lower-cases the name it matches against. So
type = "HG2015", type = "hg2015", and a class with name = "hg2015" all
refer to the same model. Pick a lower-case name to avoid surprises.
The ShieldingFunction Contract
Every shielding model — local or global — subclasses ShieldingFunction
(shielding/_base.py), sets two class attributes, and implements one method:
from sympy import Expr
from jaff.physics.photo_reactions.shielding import _register
from jaff.physics.photo_reactions.shielding._base import ShieldingFunction
@_register
class MyModel(ShieldingFunction):
name = "my_model" # the shielding.type keyword (lower-case)
reaction = None # None = global; a serialized reaction = local
def get_shielding(self, reaction, network) -> Expr:
...
return shielding_expr
| Member | Purpose |
|---|---|
name |
Shielding-type identifier, matched case-insensitively against shielding.type. |
reaction |
Serialized reaction this model is bound to (local), or None for a global model. |
get_shielding |
Returns the dimensionless sympy.Expr. Read model params off reaction._metadata["shielding"]. |
The returned Expr may reference free symbols the code generator resolves at
runtime, by convention:
| Symbol | Meaning |
|---|---|
ncol_<species> |
Column density of <species> (cm⁻²), e.g. ncol_H2 |
vdisp |
Velocity dispersion (cm s⁻¹) |
The shielding block from the TOML is available verbatim (lower-cased strings)
on reaction._metadata["shielding"], so any extra option you add — floors,
tolerances, a radiation-field selector — is read straight from there. Validate
your inputs and raise jaff.errors.ParserError with a reaction-tagged message
on bad values; the existing models all do this.
Writing a Local Shielding Function
A local model is bound to one reaction via its reaction attribute, set to that
reaction's serialized form. Serialisation joins the species on each side
with . and separates the two sides with __, and photo-reactions carry the
_PHOTON agent. For example, H2 + _PHOTON -> H + H serialises to
H2._PHOTON__H.H.
Because a serialized key contains characters that are illegal in a Python
package name (., +, -), local models live in a folder whose name is the
sanitised serialized key — . → _, + → j, - → k. So
H2._PHOTON__H.H lives under H2__PHOTON__H_H/. The folder name is only a
filesystem container; the binding comes from the reaction class attribute,
which holds the real (unsanitised) serialized string.
physics/photo_reactions/shielding/
└── H2__PHOTON__H_H/ # sanitized folder for H2._PHOTON__H.H
├── __init__.py
├── hg2015.py # name = "hg2015"
├── db1996.py # name = "db1996"
└── _utils/ # shared helpers (leading "_" → not imported as a model)
├── __init__.py
└── db_shielding_function.py
A reaction selects one of them (note the quoted dotted key — TOML would otherwise read the dots as nested tables):
[network.reactions."H2._PHOTON__H.H".shielding]
type = "hg2015"
min_vdisp = 1.0e-20
min_ncol = 1.0e-35
The model reads its options off the metadata, validates them, and returns the
expression. Following H2__PHOTON__H_H/hg2015.py:
"""
H2 shielding by Hartwig et al. 2015
DOI: https://doi.org/10.1093/mnras/stv1368
"""
from typing import Any
from sympy import Expr
from jaff.errors import ParserError
from jaff.physics.photo_reactions.shielding import _register
from jaff.physics.photo_reactions.shielding._base import ShieldingFunction
from ._utils import shielding
@_register
class HG2015(ShieldingFunction):
"""Hartwig et al. (2015) H2 self-shielding (``shielding.type = "hg2015"``)."""
name = "hg2015"
reaction = "H2._PHOTON__H.H"
def get_shielding(self, reaction, network) -> Expr:
sprops: dict[str, Any] = reaction._metadata["shielding"]
if "min_ncol" in sprops and not isinstance(sprops["min_ncol"], (float, int)):
raise ParserError(
f"Minimum column density must be a float or int for: {reaction}"
)
if "min_vdisp" in sprops and not isinstance(sprops["min_vdisp"], (float, int)):
raise ParserError(
f"Minimum velocity dispersion must be a float or int for: {reaction}"
)
return shielding(
alpha=1.1,
min_ncol=sprops.get("min_ncol", 1e-50),
min_vdisp=sprops.get("min_vdisp", 1e-50),
)
Share maths between models
When several models in a folder differ only by a parameter (here db1996.py
and hg2015.py differ only in alpha), put the actual expression builder
in an underscore-prefixed helper package (_utils/). Files and folders
whose name starts with _ are not imported as selectable models, so
they make natural homes for shared code.
Writing a Global Shielding Function
A global model sets reaction = None and lives in shielding/global_/. It is
available to any reaction whose type matches its name; it derives
everything it needs from the reaction passed to get_shielding rather than being
bound to one reaction.
physics/photo_reactions/shielding/
├── global_/
│ ├── __init__.py
│ └── leiden.py # name = "leiden", reaction = None
└── H2__PHOTON__H_H/
└── ...
[network.reactions."CO._PHOTON__C.O".shielding]
type = "leiden"
radiation = "ISRF"
shielded_by = ["self", "H2"]
@_register
class Leiden(ShieldingFunction):
name = "leiden"
reaction = None # global — usable by any photo-reaction
def get_shielding(self, reaction, network) -> Expr:
...
See shielding/global_/leiden.py for a full example that builds one
interpolation call per shielding species.
Checklist
- Class subclasses
ShieldingFunctionand is decorated with@_register -
name(lower-case) equals the TOMLshielding.typekeyword -
reactionset to the serialized reaction (local) or leftNone(global) - Local model placed under the sanitised folder
(
shielding/<sanitized_reaction>/<type>.py); global undershielding/global_/ - Implements
get_shielding(self, reaction, network) -> sympy.Expr - Reads model options from
reaction._metadata["shielding"] - Validates inputs and raises
ParserError(reaction-tagged) on bad values - Returns a dimensionless expression using the
ncol_<species>/vdispsymbol conventions - Shared maths factored into an underscore-prefixed helper (if reused)