From HSD Text to Tree: A Visual Guide

This document walks through a concrete HSD configuration and shows exactly how the parser transforms it into the internal tree of hsd_node_t nodes. It bridges the HSD Format Reference (syntax) and Internal HSD Tree Storage (data structures) documentation.

Core Concepts at a Glance

Before diving in, here are the two building blocks of every HSD tree:

Table node (NODE_TYPE_TABLE)

A container (branch) that holds an ordered list of child nodes. Every { } block in the HSD text becomes a table node. The invisible root of every parsed file is also a table.

Value node (NODE_TYPE_VALUE)

A leaf that stores a single datum as a string (parsed into the requested type on access). Scalars, arrays, and multi-line matrix data are all value nodes.

Both node kinds share the same Fortran type (hsd_node_t) and are distinguished by the node_type field. See Internal HSD Tree Storage for the full field reference.

A Sample Configuration

Consider this HSD input file:

Driver = ConjugateGradient {
  MaxSteps = 200
  MaxForce [eV/Angstrom] = 0.001
}

Hamiltonian = DFTB {
  SCC = Yes
  Filling = Fermi {
    Temperature [Kelvin] = 300.0
  }
  KPointsAndWeights {
    4 0 0
    0 4 0
    0 0 4
  }
}

Analysis {
  PrintForces = Yes
}

The Resulting Tree

After hsd_load_file (or hsd_load_string), the parser produces this tree:

root (TABLE, name="")
│
├── driver (TABLE, name="driver")
│   └── conjugategradient (TABLE, name="conjugategradient")
│       ├── maxsteps (VALUE, name="maxsteps", string_value="200")
│       └── maxforce (VALUE, name="maxforce", attrib="eV/Angstrom",
│                     string_value="0.001")
│
├── hamiltonian (TABLE, name="hamiltonian")
│   └── dftb (TABLE, name="dftb")
│       ├── scc (VALUE, name="scc", string_value="Yes")
│       ├── filling (TABLE, name="filling")
│       │   └── fermi (TABLE, name="fermi")
│       │       └── temperature (VALUE, name="temperature",
│       │                        attrib="Kelvin", string_value="300.0")
│       └── kpointsandweights (TABLE, name="kpointsandweights")
│           └── (VALUE, name="#text",
│                string_value="4 0 0\n0 4 0\n0 0 4")
│
└── analysis (TABLE, name="analysis")
    └── printforces (VALUE, name="printforces", string_value="Yes")

Key observations:

  1. All tag names are lowercased. Driver"driver", SCC"scc". Lookups are therefore case-insensitive.

  2. The root is an unnamed table that the parser creates automatically.

  3. Typed blocks produce two levels. Driver = ConjugateGradient { ... } creates a driver table whose sole child is a conjugategradient table. See Typed blocks (Tag = Type { ... }) below.

  4. Multi-line data becomes a ``#text`` value. The KPointsAndWeights block has no named children — its content is stored as a single anonymous value node named "#text".

How Each Syntax Feature Maps to the Tree

Simple assignment (Tag = value)

MaxSteps = 200

Creates a single value node:

maxsteps (VALUE, string_value="200")

The value is always stored as a string. When you call access%get("Driver/.../MaxSteps", n), the library parses "200" into an integer on the fly.

Typed blocks (Tag = Type { ... })

Hamiltonian = DFTB {
  SCC = Yes
}

This is syntactic sugar. The parser creates two nested tables:

hamiltonian (TABLE)
└── dftb (TABLE)
    └── scc (VALUE, string_value="Yes")

The outer table (hamiltonian) has exactly one child: the inner table (dftb), which holds the block’s content. This lets access%get("Hamiltonian/DFTB/SCC", val) navigate both levels naturally.

When serialized back to HSD, single-child tables are automatically compressed to the Tag = Type { ... } short form.

Plain blocks (Tag { ... })

Analysis {
  PrintForces = Yes
}

Creates a single table node with one level:

analysis (TABLE)
└── printforces (VALUE, string_value="Yes")

Attributes (Tag [attr] = value)

MaxForce [eV/Angstrom] = 0.001

The text inside [...] is stored in the node’s attrib field:

maxforce (VALUE, attrib="eV/Angstrom", string_value="0.001")

Attributes can appear on both value nodes and table nodes:

Lattice [Angstrom] {
  10.0 0.0 0.0
  0.0 10.0 0.0
  0.0 0.0 10.0
}
lattice (TABLE, attrib="Angstrom")
└── (VALUE, name="#text", string_value="10.0 0.0 0.0\n0.0 10.0 0.0\n0.0 0.0 10.0")

Multi-line / inline data (#text nodes)

When a block contains raw data instead of named children:

KPointsAndWeights {
  4 0 0
  0 4 0
  0 0 4
}

The content is collected into a single anonymous value node named "#text":

kpointsandweights (TABLE)
└── (VALUE, name="#text", string_value="4 0 0\n0 4 0\n0 0 4")

The "#text" name follows the legacy xmlf90 convention from DFTB+. Use hsd_get_inline_text(table, text, stat) to retrieve it, or access it directly via access%get("KPointsAndWeights", array) or access%get_matrix("KPointsAndWeights", matrix, nrows, ncols) which resolve #text children transparently.

Arrays (space or comma separated)

Values = 1 2 3 4 5
Coords = 1.0, 2.5, 3.0
Elements = C H O N

Each creates a single value node whose string_value holds the full text:

values  (VALUE, string_value="1 2 3 4 5")
coords  (VALUE, string_value="1.0, 2.5, 3.0")
elements (VALUE, string_value="C H O N")

The typed array is parsed on demand when you call access%get with an array argument.

Case-insensitive tag names

Temperature = 300
temperature = 500   # Overwrites the previous one

Both tags are stored as "temperature". Since the second occurrence comes later, access%get returns "500" (last-wins semantics). Both nodes are preserved in the tree and can be seen when iterating children.

Duplicate keys

Option = alpha
Option = beta

Both values are kept in the tree as separate children:

parent (TABLE)
├── option (VALUE, string_value="alpha")
└── option (VALUE, string_value="beta")

access%get returns the last occurrence ("beta"). To see all occurrences, iterate the parent’s children with hsd_iterator_t.

Semicolons as statement separators

Semicolons act like newlines, allowing multiple statements on one line:

A = 1; B = 2; C = 3

Is identical to:

A = 1
B = 2
C = 3

All three become sibling value nodes.

File includes

Parsed include (<<+) — the included file is parsed as HSD and its top-level nodes are merged into the current block:

<<+ "defaults.hsd"
Driver { MaxSteps = 100 }

If defaults.hsd contains Driver { MaxSteps = 50 }, the tree will have two driver table nodes; the explicitly written one (MaxSteps = 100) takes precedence via last-wins lookup.

Text include (<<<) — the file is read as raw text and injected as a #text value node:

Geometry {
  <<< "coords.xyz"
}
geometry (TABLE)
└── (VALUE, name="#text", string_value=<contents of coords.xyz>)

Include paths are resolved relative to the including file’s directory. Circular includes are detected and produce an error.

Amendments (+Tag { ... })

Amendments merge additional children into an existing table:

Driver {
  MaxSteps = 100
}

+Driver {
  Tolerance = 1.0e-6
}

After parsing, the tree has a single driver table with both children:

driver (TABLE)
├── maxsteps  (VALUE, string_value="100")
└── tolerance (VALUE, string_value="1.0e-6")

The + prefix tells the parser to find the existing driver node in the parent table and parse the new block’s content into it, rather than creating a separate table. If no existing node is found, the parser reports an error.

Comments

# Full-line comment
MaxSteps = 100  # Inline comment

Comments are stripped during lexing and leave no trace in the tree.

Putting It All Together

Here is a more complex example that exercises most features:

# Typed block with attribute
Hamiltonian = DFTB {
  SCC = Yes
  SCCTolerance = 1.0e-5

  MaxAngularMomentum {
    C = "p"
    H = "s"
  }

  Filling = Fermi {
    Temperature [Kelvin] = 300.0
  }

  KPointsAndWeights {
    4 0 0
    0 4 0
    0 0 4
  }
}

+Hamiltonian {
  +DFTB {
    ReadInitialCharges = Yes
  }
}

The final in-memory tree:

root (TABLE)
└── hamiltonian (TABLE)
    └── dftb (TABLE)
        ├── scc              (VALUE, string_value="Yes")
        ├── scctolerance     (VALUE, string_value="1.0e-5")
        ├── maxangularmomentum (TABLE)
        │   ├── c            (VALUE, string_value="p")
        │   └── h            (VALUE, string_value="s")
        ├── filling (TABLE)
        │   └── fermi (TABLE)
        │       └── temperature (VALUE, attrib="Kelvin",
        │                        string_value="300.0")
        ├── kpointsandweights (TABLE)
        │   └── (VALUE, name="#text",
        │        string_value="4 0 0\n0 4 0\n0 0 4")
        └── readinitialcharges (VALUE, string_value="Yes")

Notice that the +Hamiltonian { +DFTB { ... } } amendment added readinitialcharges directly into the existing dftb table — no duplicate tables were created.

Accessing the Tree from Fortran

With the tree above loaded into root:

use hsd
implicit none

type(hsd_node_t), target :: root
type(hsd_access_t) :: access
type(hsd_error_t), allocatable :: error
real(dp) :: temperature
logical :: scc
real(dp), allocatable :: kpoints(:,:)
integer :: nrows, ncols

call hsd_load_file("input.hsd", root, error)
call access%init(root)

! Scalar value — string "300.0" is parsed to real(dp)
call access%get("Hamiltonian/DFTB/Filling/Fermi/Temperature", &
  & temperature)

! Logical — "Yes" is parsed to .true.
call access%get("Hamiltonian/DFTB/SCC", scc)

! Matrix — the #text node is parsed into a 2D array
call access%get_matrix("Hamiltonian/DFTB/KPointsAndWeights", &
  & kpoints, nrows, ncols)
! kpoints is now shape (3, 3)

if (access%has_errors()) call access%print_errors()
call root%destroy()

Path navigation with / separators walks table children by name. Typed blocks add one extra path segment (Hamiltonian/DFTB/... rather than just Hamiltonian/...).

Summary Table

HSD Syntax

Tree Result

Access Pattern

Tag = value

Value node

access%get("Tag", v)

Tag { children }

Table node

hsd_get_child(root, "Tag", child, stat)

Tag = Type { children }

Nested tables (Tag → Type)

access%get("Tag/Type/...", ...)

Tag [unit] = value

Value node with attrib

hsd_get_attrib(root, "Tag", unit, stat)

Tag { raw data }

Table with #text child

hsd_get_inline_text(table, text, stat)

<<+ "file.hsd"

Parsed HSD merged in

(transparent)

<<< "file.txt"

#text value node

hsd_get_inline_text(...)

+Tag { ... }

Merge into existing table

(transparent)

A = 1; B = 2

Two sibling nodes

(same as newline-separated)

# comment

Nothing (stripped)