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, SUM over sets of tuples

  • Built-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 o is the fact p(s, o) and s rdf:type C is also C(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:

  1. Builtins (builtins) — registers known functions (exp, log, startswith) as callable symbols. Simple list in YAML:

    builtins: [exp, log, startswith]
    
  2. Base symbols (use_base_symbols) — when use_base_symbols: true is set (and requires_mni_mask is 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 calling init_base_engine() from within a Python init module:

    use_base_symbols: true
    
  3. Python init (python_init) — imports the module and calls init_engine(nl, mask, data_dir). Use this for downloading data, registering complex symbols, or loading neuroimaging atlases. If use_base_symbols already covers your needs, this phase can be omitted entirely — see the destrieux engine entry for an example.

  4. 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 to python_init). Supports archive extraction:

    downloads:
      - url: "https://example.com/data.tar.gz"
        dest: my_engine/
        extract: true
    
  5. Templates (templates) — downloads neuroimaging templates via nilearn or TemplateFlow (if the templateflow package is installed). Each template can optionally register a voxel(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; pass resolution: 1 or resolution: 2)

    • gm_template — grey-matter template

    • wm_template — white-matter template

    TemplateFlow templates require pip install templateflow and are identified by a template name from the TemplateFlow repository (e.g. MNI152NLin2009cAsym, MNIInfant). The suffix and resolution fields select the specific image variant.

    When predicate is set, the template is loaded as a NIfTI image and all non-zero voxels are registered as a predicate(i, j, k) tuple set — useful as a coordinate-space reference in Datalog queries.

  6. Atlases (atlases) — downloads brain atlases via nilearn and registers each region as a predicate with ExplicitVBR geometry:

    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 by prob_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).

  7. Ontologies (ontologies) — loads OWL/RDF ontologies from URLs or local paths, one file per url or several, loaded together, with urls. format is the rdflib format (guessed from the file extension by default); with individuals: true every triple is also loaded as a fact, and names: prefixed names classes and properties prefix: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
    
  8. 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)
    
  9. Relations (relations) — loads CSV/TSV files as extensional predicates. Each entry maps a relation name to a file path relative to the engines/ directory. The file value 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-predicates output, use a dict with an optional description field:

    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 with pandas.read_csv and registered via add_tuple_set(). Relations are loaded after the Datalog init phase. Rules in datalog_init can nevertheless refer to them, because rules are only evaluated when a query is run, by which time all phases have completed.

  10. 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).