Concepts#
This page explains the core ideas behind NeuroLang. You do not need to read this before the Get Started with NeuroLang — it is here as a reference once you want to understand why things work the way they do.
Logic Programming & Datalog#
Logic programming is a style of programming where you declare what is true, not how to compute it. A Datalog program consists of two kinds of statements:
Facts — ground truths about the world:
with nl.environment as e:
e.region["V1"] = True
e.region["V2"] = True
e.adjacent["V1", "V2"] = True
Rules — derived truths (if A and B are true, then C is true):
with nl.environment as e:
# x is reachable from y if x is adjacent to y
e.reachable[e.x, e.y] = e.adjacent[e.x, e.y]
# x is reachable from y if x is adjacent to z and z is reachable from y
e.reachable[e.x, e.y] = e.adjacent[e.x, e.z] & e.reachable[e.z, e.y]
The Datalog engine (the solver) computes all facts that can be derived from the rules. NeuroLang extends standard Datalog with:
Aggregations —
COUNT,MAX,SUMover sets of tuplesBuilt-in functions — register any Python callable as a Datalog symbol
Tuple-generating dependencies (TGDs) — open-world reasoning rules
Probabilistic Reasoning#
Standard Datalog is deterministic: a fact is either derived or it is not. In many neuroimaging applications we need to reason about uncertain data — for example, whether a brain region is functionally connected to another region given noisy fMRI data.
NeuroLang adds independent probabilistic facts: each tuple in a probabilistic relation is an independent Bernoulli random variable with an associated probability. For example, a term-to-region mapping derived from meta-analysis might state that the term “memory” is associated with region “hippocampus” with probability 0.87.
Queries over probabilistic data use possible worlds semantics: the answer to a query is the probability that the query holds in a randomly sampled world.
Besides independent facts, NeuroLang supports probabilistic choices: a
set of mutually exclusive tuples, exactly one of which holds in each possible
world. The typical example is a uniform choice over studies
(add_uniform_probabilistic_choice_over_set in Python, or define as
Selected_study as an equiprobable choice over every Study in SQUALL), which
turns “the fraction of studies satisfying a property” into a probability.
Probabilistic queries are written with PROB[…] = p (or MARG[…] = p)
in text Datalog and with inferred probability in SQUALL; adding a
condition with // (given / conditioned to in SQUALL) yields a
conditional probability. See Datalog: A Rule-Based Query Language for NeuroLang (Part 5) and
SQUALL: Controlled English for NeuroLang (Part 5) for worked examples.
To compute these probabilities, NeurolangPDL
tries its solvers in order: first exact lifted inference
(neurolang.probabilistic.small_dichotomy_theorem_based_solver and the
Dalvi–Suciu algorithm in neurolang.probabilistic.dalvi_suciu_lift),
which works directly on the query structure and scales to large data sets
when the query is liftable; if the query is not supported by these, it falls
back to knowledge compilation, i.e. weighted model counting over a
sentential decision diagram (SDD) built with pysdd
(neurolang.probabilistic.weighted_model_counting), which is general but
can be much more expensive. See [Zanitti2023] for details, and
Lifted probabilistic inference for the lifting strategies and their
references.
Note
The probabilistic extensions are implemented in
neurolang.probabilistic. See the example gallery for concrete
neuroimaging use cases.
Neuroimaging Integration#
NeuroLang treats neuroimaging data as relational data:
Volumetric images (NIfTI, loaded via nibabel) are represented as sets of
(voxel_id, intensity)or(x, y, z, intensity)tuples.Atlas labels (e.g., Destrieux, AAL) become
(label, region_id)relations.Ontologies (OWL/RDF via rdflib) give constraints, such as subclasses, that rewrite the queries; their triples can also be loaded as facts (see RDF Data and Ontologies).
Coordinate activations (NeuroSynth) become
(pmid, x, y, z)probabilistic relations.
The NeurolangDL and
NeurolangPDL frontends expose helper methods
such as add_tuple_set() and add_atlas_set() to load these
data sources.
RDF Data and Ontologies#
load_ontology() reads OWL/RDF files
(RDF/XML, Turtle, N-Triples…, guessed from the file extension or given
with format). The axioms of the ontology become constraints:
Manager rdfs:subClassOf Employee makes Employee(x) hold for every
Manager, by rewriting the queries.
With individuals=True every triple is also a fact, so that a knowledge
graph is queried as a SPARQL endpoint would be:
nl.load_ontology(
["prod-vocab.ttl", "prod-inst.ttl"],
individuals=True, names="prefixed",
)
a triple
s p ois the factp(s, o)ands rdf:type Cis alsoC(s);with
names="prefixed", classes and properties are named after the prefixes the files declare (pv:name,rdfs:label), or their full IRI when no prefix is declared;IRIs are the strings of their full IRI, written
<http://...>in Datalog; literals are their value (numbers, strings without their language tag, dates as ISO strings).
The SPARQL query:
SELECT ?name WHERE {
?c rdfs:subClassOf* pv:Employee .
?e a ?c ; pv:areaOfExpertise prodi:prod-cat-Sensor ; pv:name ?name .
}
reads in Datalog:
ans(name) :- pv:Employee(e),
pv:areaOfExpertise(e, <http://ld.company.org/prod-instances/prod-cat-Sensor>),
pv:name(e, name)
Declarative Engine Registry & CLI#
NeuroLang ships a command-line interface, neurolang-query, that provides
a quick way to run Datalog queries against pre-configured datasets without
writing Python:
# List available dataset engines
neurolang-query --list-engines
# List predicates for the NeuroSynth engine
neurolang-query --engine neurosynth --list-predicates
# Run a Datalog query (--engine defaults to neurosynth)
neurolang-query "ans(t) :- term_in_study_tfidf(s, t, w)"
# Use the Destrieux atlas engine
neurolang-query --engine destrieux "ans(name) :- destrieux(name, region)"
Engines are declared in packages/neurolang_apps/neurolang_apps/utils/engines/engines.yaml
(abridged; see Engine Configuration (engines.yaml) for the full reference):
engines:
neurosynth:
description: "Neurosynth database — forward and reverse inference"
requires_mni_mask: true
python_init: "neurolang.utils.engines.neurosynth.init"
datalog_init: |
study_with_peaks(S) :- peak_reported(I, J, K, S)
mentions(s, t) :- term_in_study_tfidf(s, t, v), (v > 0.03)
weighted_mention(s, t, w) :- term_in_study_tfidf(s, t, w)
relations:
extra_data: "shared/my_data.csv"
predicates:
peak_reported:
arity: 4
columns: [i, j, k, study_id]
description: "Reported activation peaks in voxel coordinates"
...
An engine may reference, through python_init, a Python module (given by
its dotted module path) that defines init_engine(nl, mask, data_dir). The neurolang.utils.engine_registry
module handles YAML loading, MNI mask retrieval, and delegating to the
engine’s init script.
Init phases. When build_engine()
builds an engine, it runs up to ten phases, in this order:
Builtins (
builtins) — registers known functions (exp,log,startswith) as callable symbols. Simple list in YAML:builtins: [exp, log, startswith]
Base symbols (
use_base_symbols) — whenuse_base_symbols: trueis set (andrequires_mni_maskis also true), the engine registry automatically registers common neuroimaging symbols (agg_count,agg_create_region,agg_create_region_overlay,principal_direction,region_union) without needing a custom Python init script. This replaces callinginit_base_engine()from within a Python init module:use_base_symbols: true
Python init (
python_init) — imports the module and callsinit_engine(nl, mask, data_dir). Use this for downloading data, registering complex symbols, or loading neuroimaging atlases. Ifuse_base_symbolsalready covers your needs, this phase can be omitted entirely — see thedestrieuxengine entry for an example.Downloads (
downloads) — fetches files from URLs into the data directory. This runs after the Python init but before templates, atlases, ontologies, Datalog init and relations, so files fetched here are available to those later phases (but not topython_init). Supports archive extraction:downloads: - url: "https://example.com/data.tar.gz" dest: my_engine/ extract: true
Templates (
templates) — downloads neuroimaging templates via nilearn or TemplateFlow (if thetemplateflowpackage is installed). Each template can optionally register avoxel(i, j, k)predicate from its non-zero mask:templates: # Nilearn brain mask → voxel(i, j, k) predicate mni_brain: source: nilearn variant: brain_mask predicate: voxel # Nilearn T1 template (2 mm) — just download, no predicate mni_t1: source: nilearn variant: template resolution: 2 # TemplateFlow T1 template at 1 mm t1_highres: source: templateflow template: MNI152NLin2009cAsym resolution: 1 suffix: T1w
Supported nilearn variants:
brain_mask— MNI152 brain mask (load_mni152_brain_mask)gm_mask— grey-matter mask (load_mni152_gm_mask)wm_mask— white-matter mask (load_mni152_wm_mask)template— MNI152 T1 template (load_mni152_template; passresolution: 1orresolution: 2)gm_template— grey-matter templatewm_template— white-matter template
TemplateFlow templates require
pip install templateflowand are identified by atemplatename from the TemplateFlow repository (e.g.MNI152NLin2009cAsym,MNIInfant). Thesuffixandresolutionfields select the specific image variant.When
predicateis set, the template is loaded as a NIfTI image and all non-zero voxels are registered as apredicate(i, j, k)tuple set — useful as a coordinate-space reference in Datalog queries.Atlases (
atlases) — downloads brain atlases via nilearn and registers each region as a predicate withExplicitVBRgeometry:atlases: destrieux: predicate_name: destrieux schaefer: n_rois: 400 yeo_networks: 7 resolution_mm: 2 predicate_name: schaefer_400 difumo: dimension: 64 threshold: 0.5 predicate_name: difumo_64
For probabilistic atlases (
difumo) a second predicate is registered automatically —{name}_prob(component, i, j, k, prob)— with the raw probability value at each voxel for each component (filtered byprob_threshold, default 0.01):atlases: difumo: dimension: 64 threshold: 0.5 prob_threshold: 0.01 predicate_name: difumo_64 prob_predicate_name: difumo_64_prob
Supported atlases:
destrieux(deterministic),schaefer(deterministic),difumo(probabilistic).Ontologies (
ontologies) — loads OWL/RDF ontologies from URLs or local paths, one file perurlor several, loaded together, withurls.formatis the rdflib format (guessed from the file extension by default); withindividuals: trueevery triple is also loaded as a fact, andnames: prefixednames classes and propertiesprefix:local(see RDF Data and Ontologies):ontologies: - url: "https://example.com/ontology.owl" - urls: - "https://example.com/vocab.ttl" - "data/instances.ttl" individuals: true names: prefixed
Datalog init (
datalog_init) — an optional YAML multiline string of Datalog rules. These are evaluated after templates and atlases so they can reference template-derived voxel predicates and atlas predicates:datalog_init: | left_region(N, R) :- destrieux(N, R), startswith('lh', N) right_region(N, R) :- destrieux(N, R), startswith('rh', N)
Relations (
relations) — loads CSV/TSV files as extensional predicates. Each entry maps a relation name to a file path relative to theengines/directory. Thefilevalue may be a URL for automatic download:relations: my_table: "my_engine/data.csv" extra_refs: "shared/references.tsv"
For richer metadata that appears in
--list-predicatesoutput, use a dict with an optionaldescriptionfield:relations: my_table: file: "https://example.com/data.csv" description: "Experiment metadata loaded from CSV"
Supported formats:
.csv,.tsv,.csv.gz,.tsv.gz. The file is read withpandas.read_csvand registered viaadd_tuple_set(). Relations are loaded after the Datalog init phase. Rules indatalog_initcan nevertheless refer to them, because rules are only evaluated when a query is run, by which time all phases have completed.Probabilistic choices (
probabilistic_choice) — declares uniform probabilistic choices over existing predicates:probabilistic_choice: selected_study: source: study description: "Uniform choice over studies"
All phases are optional — include only what your engine needs.
If use_base_symbols and the declarative YAML fields (atlases,
templates, relations, datalog_init, probabilistic_choice)
cover your engine, you can omit python_init entirely — the
destrieux engine is a complete example of this pattern.
To add a new engine, create a YAML entry. If you need a custom Python
init script, add a corresponding init module under
packages/neurolang_apps/neurolang_apps/utils/engines/. See
neurolang.utils.engines.neurosynth.init for a complete Python init
example, and the destrieux engine entry for a purely YAML-driven
example.
Architecture#
NeuroLang is structured in three layers:
┌─────────────────────────────────────────────────────────┐
│ User / Python │
│ NeurolangDL / NeurolangPDL frontend │
│ (packages/neurolang: neurolang.frontend) │
└───────────────────────┬─────────────────────────────────┘
│ Datalog/probabilistic program
▼
┌─────────────────────────────────────────────────────────┐
│ Intermediate Representation (IR) │
│ Expressions, symbol table, type system │
│ (packages/nl_expr: nl_expr.expressions, .logic) │
└───────────────────────┬─────────────────────────────────┘
│ Relational algebra plan
▼
┌─────────────────────────────────────────────────────────┐
│ Solver │
│ Chase algorithm, relational algebra, SDD/WMC │
│ (packages/nl_core: nl_core.datalog, .probabilistic) │
└─────────────────────────────────────────────────────────┘
The frontend layer translates Python expressions into an internal IR. The IR layer performs type inference and expression normalisation. The solver executes the query using a chase-based fixpoint algorithm (for deterministic queries) or a weighted model counter (for probabilistic queries).