# dialect { #tvbo.datamodel.dialect }

`datamodel.dialect`

The TVBO YAML dialect — one implementation, both construction paths.

TVBO authors write a dialect the schema does not describe on its own: a slot may be
written under a declared alias (``dt`` for ``step_size``), an object with one obvious
field may be written as a bare scalar (``omega: 0.0628`` for ``omega: {value: 0.0628}``),
and an entity may be named by ``iri`` instead of spelled out. None of the three is
something LinkML's loaders apply — ``aliases:`` is documentation to them, and
``simple_dict_value`` is specified but unimplemented.

Both are therefore applied here, from tables read off the schema at build time
(:mod:`tvbo.datamodel.dialect_tables`). The generated dataclasses install
:func:`install_on_dataclasses`, which folds the dialect in ``__init__``; the Pydantic
models call :func:`normalize` from a ``mode="before"`` validator. Sharing this module is
what keeps them from drifting: the dialect used to live twice, and the copy the Pydantic
path carried had the aliases but not the scalar shortcuts, so a model written the way the
README shows validated on one path and was rejected on the other.

Every function here takes and returns plain data for ONE level of nesting. Recursion
belongs to the caller — Pydantic already descends into members, and the dataclass
``__init__`` wrapper is reached once per constructed object.

## Attributes

| Name | Description |
| --- | --- |
| [SEMANTIC_FOLDS](#tvbo.datamodel.dialect.SEMANTIC_FOLDS) | Per-class dialect a table of renames cannot express, keyed like the other tables. |

## Functions

| Name | Description |
| --- | --- |
| [curated_entry](#tvbo.datamodel.dialect.curated_entry) | The curated *cls_name* record called *name*, alias-folded and ready to merge. |
| [expand_iri](#tvbo.datamodel.dialect.expand_iri) | Fill *data* from the curated record its ``iri`` names, letting the recipe win. |
| [fold_aliases](#tvbo.datamodel.dialect.fold_aliases) | Rename *cls_name*'s declared aliases to their canonical slots, in place. |
| [install_on_dataclasses](#tvbo.datamodel.dialect.install_on_dataclasses) | Wrap each generated dataclass's ``__init__`` so it accepts the dialect. |
| [is_literal](#tvbo.datamodel.dialect.is_literal) | A bare value the shortcut may lift: a scalar, or a (nested) list of scalars. |
| [key_members](#tvbo.datamodel.dialect.key_members) | Give a keyed collection the mapping spelling, each member named by its key. |
| [lift_scalar](#tvbo.datamodel.dialect.lift_scalar) | ``0.0628`` -> ``{'value': 0.0628}``, leaving an already-written mapping alone. |
| [normalize](#tvbo.datamodel.dialect.normalize) | Fold *cls_name*'s dialect into *data*, in place: aliases, ``iri``, shortcuts, keys. |
| [peer_module](#tvbo.datamodel.dialect.peer_module) | The generated module *instance*'s class comes from. |

### curated_entry { #tvbo.datamodel.dialect.curated_entry }

```python
datamodel.dialect.curated_entry(cls_name, name)
```

The curated *cls_name* record called *name*, alias-folded and ready to merge.

``None`` when the database holds no such record — including when *cls_name* is not a
category it keeps at all.

Cached because a recipe naming the same entity twice — every node of a homogeneous
network — would otherwise re-read and re-parse the same file per object. Callers must
not mutate the result: :func:`expand_iri` only feeds it to
:func:`tvbo.utils.deep_merge`, which mutates neither side, and
:meth:`IriEnrichable._from_database` only reads it into a constructor.

The entry's own ``iri`` is dropped: keeping it would make the expanded record ask to be
expanded again on every later construction, and a self-referential entry would not
terminate. So is its envelope: :func:`normalize` strips the recipe's own ``tvbo_class``
before expanding, and a curated record that carries one would put it back. Every network
sidecar in the database opens with ``tvbo_class: tvbo:Network``, so a recipe naming a
curated network by ``iri`` reached ``Network.__init__`` with a keyword it has no slot for.

### expand_iri { #tvbo.datamodel.dialect.expand_iri }

```python
datamodel.dialect.expand_iri(cls_name, data)
```

Fill *data* from the curated record its ``iri`` names, letting the recipe win.

Naming an entity by ``iri`` instead of spelling it out is the same kind of dialect as
an alias or a bare scalar: a spelling the schema does not describe. It is resolved here,
before validation, because this is the only point that still knows which keys the recipe
actually wrote — after construction every slot carrying a schema default reads as though
it had been authored, and "the recipe did not say" becomes unanswerable. That is why a
curated ``delayed:`` was never applied, and why an explicit value equal to a default
could be overwritten by the entry.

Only the curated database is consulted. It is a local file read and its content is
ordered, whereas the ontology answers with an unordered set — a different parameter
order per process, which no frozen record can be written against. Reaching it is
:meth:`tvbo.behaviour._enrich.IriEnrichable.enrich`, which the caller asks for.

Only a *reference* expands. A record that also states its own ``name`` is a definition,
and its ``iri`` is grounding — "this model is a ReducedWongWang in the ontology" — not
an instruction to inherit. Fifty curated files are written that way, and expanding them
would re-derive a definition from a name lookup: ``ReducedWongWangFunc.yaml`` states its
own name and grounds on ``tvbo:ReducedWongWang``, so expanding it would replace a
distinct record with the canonical ``ReducedWongWang.yaml`` it merely relates to.

An ``iri`` naming nothing is left alone: it may point at an entity that exists only in
the ontology, and this pass cannot tell that from a typo. ``tvbo validate`` is where a
name that resolves nowhere is reported.

Once a reference has expanded, the ``iri`` survives only for a class that keeps one as a
slot, where it is grounding worth recording. ``Network`` does not: its curated record is
reached through ``iri`` but its own connectivity is the ``data_file`` the expansion just
merged in, so keeping the key would hand ``Network.__init__`` a keyword it has no slot
for. That is what stopped a study from naming a curated connectome by ``iri`` at all.

### fold_aliases { #tvbo.datamodel.dialect.fold_aliases }

```python
datamodel.dialect.fold_aliases(cls_name, data)
```

Rename *cls_name*'s declared aliases to their canonical slots, in place.

Class-scoped on purpose: an alias is only an alias where its class declares it.
``target_variable`` is an ``Edge`` alias for ``target_var`` but the canonical slot on
a stimulus ``Event``, so a table keyed by slot name alone would rename it in the one
place it must not be.

### install_on_dataclasses { #tvbo.datamodel.dialect.install_on_dataclasses }

```python
datamodel.dialect.install_on_dataclasses(namespace)
```

Wrap each generated dataclass's ``__init__`` so it accepts the dialect.

``__init__`` is the one place where a keyword is known to name a slot of this class,
so an alias resolves without guessing whether a mapping is an instance or a keyed
collection, and a free-form key (a parameter literally named ``dt``) is never mistaken
for one. Every construction path — the LinkML loaders, ``cls(**data)``, nested and
inlined members, subclasses — goes through it.

### is_literal { #tvbo.datamodel.dialect.is_literal }

```python
datamodel.dialect.is_literal(value)
```

A bare value the shortcut may lift: a scalar, or a (nested) list of scalars.

An array literal counts — a coordinate list to select, a coefficient matrix — because
the slot it lifts into holds arrays as well as scalars. A list of MAPPINGS does not:
that is the list spelling of a keyed collection, whose members lift individually.

### key_members { #tvbo.datamodel.dialect.key_members }

```python
datamodel.dialect.key_members(cls_name, data)
```

Give a keyed collection the mapping spelling, each member named by its key.

``parameters: {TR: {value: 720.0}}`` means a Parameter *called* ``TR``, so writing the
name a second time inside the member is the redundancy this project's records are
written without. The generated dataclasses fill it from the key in ``__post_init__``;
the generated Pydantic models leave it missing and reject the member as incomplete, so
a record that loaded on one form failed on the other.

The same collection may equally be written as a LIST — of bare identifiers
(``arguments: [v]``) or of whole members — which the dataclasses key and the Pydantic
models reject outright as not a mapping. Both spellings arrive as the mapping here, so
which one a record uses stops being a question of which form is loading it. A list
whose members state no identifier is left alone: there is nothing to key it by, and
that is a record to reject rather than to guess at.

Members are rebuilt rather than mutated: the mapping may be a cached curated entry,
shared by every object that names it.

### lift_scalar { #tvbo.datamodel.dialect.lift_scalar }

```python
datamodel.dialect.lift_scalar(value, target, multivalued, keyed=False)
```

``0.0628`` -> ``{'value': 0.0628}``, leaving an already-written mapping alone.

On a multivalued slot the members are lifted, not the collection: ``{omega: 0.0628}``
is a keyed collection of one Parameter, not a Parameter. A ``keyed`` collection's LIST
spelling (``arguments: [v]``) is a list of member identifiers, not values, so its bare
scalars are left for the loader to key on; only a non-keyed list
(``additional_equations: ["x = -x"]`` -> ``[{rhs: "x = -x"}]``) lifts its elements.

### normalize { #tvbo.datamodel.dialect.normalize }

```python
datamodel.dialect.normalize(cls_name, data)
```

Fold *cls_name*'s dialect into *data*, in place: aliases, ``iri``, shortcuts, keys.

Aliases fold first, so the recipe and the curated record are keyed alike before they
are merged and the shortcut pass can see every value under the name it looks for.
Lifting first left ``BoundaryCondition(value="0")`` — the older spelling of
``equation`` — a bare string where the generated ``__post_init__`` wanted a mapping,
and it raised. Keying comes last, once every member is a mapping that can carry a name.

The semantic folds come last, after keying, so a fold reads each collection under the
names the class will — a record spelling one as a list is not a different case to it.
The terse ``distribution`` lift follows the domain fold, since a clamp folded out of
``boundaries`` can leave one behind for it to complete.

The document envelope goes first. ``tvbo_class`` states which class a *file* holds,
which is a fact about the file and never a slot, so every constructor route drops it.

### peer_module { #tvbo.datamodel.dialect.peer_module }

```python
datamodel.dialect.peer_module(instance)
```

The generated module *instance*'s class comes from.

A record is filled with members — an ``Equation``, a ``Parameter`` — and those have to
be of the same generated form as the record itself: the strict Pydantic models validate
on assignment and reject a LinkML dataclass where they want their own peer. Behaviour
that builds members reads the peer off the instance rather than importing one form,
which is what lets one implementation serve both.