# `qlat.fields_io` — Shuffled Binary Field I/O
Source: `qlat/qlat/fields_io.pyx`
> **Note:** Update this document when updating the source file.
## Outline
1. [Overview](#overview)
2. [`ShuffledFieldsWriter` Class](#shuffledfieldswriter-class)
- [Constructor](#constructor)
- [Methods](#writer-methods)
- [Properties](#writer-properties)
3. [`ShuffledFieldsReader` Class](#shuffledfieldsreader-class)
- [Constructor](#reader-constructor)
- [Methods](#reader-methods)
- [Properties](#reader-properties)
4. [`ShuffledBitSet` Class](#shufflebitset-class)
5. [Module-Level Functions](#module-level-functions)
6. [Examples](#examples)
---
## Overview
`fields_io` provides efficient parallel I/O for lattice fields using a
shuffled binary format. Data is distributed across MPI nodes and stored in a
directory structure where each node writes its portion to a separate file.
This avoids the bottleneck of single-node I/O while maintaining a simple
on-disk layout.
The two main classes are:
- **`ShuffledFieldsWriter`** — writes fields to a directory of shuffled
binary files. Supports append mode.
- **`ShuffledFieldsReader`** — reads fields from a shuffled binary directory.
Fields are written/read by name (`fn`). Both `Field` and `SelectedField`
(sparse) objects are supported. A `FieldSelection` can be provided to
write/read only selected sites.
```python
import qlat as q
q.begin_with_mpi([[1, 1, 1, 1]])
total_site = q.Coordinate([4, 4, 4, 8])
geo = q.Geometry(total_site)
new_size_node = q.Coordinate([1, 1, 1, 1])
# Write a field
sfw = q.open_fields("/tmp/test_fields", "w", new_size_node)
f = q.Field(q.ElemTypeComplexD, geo, 1)
sfw.write("my_field", f)
sfw.close()
# Read it back
sfr = q.open_fields("/tmp/test_fields", "r")
f2 = q.Field(q.ElemTypeComplexD, geo, 1)
sfr.read("my_field", f2)
sfr.close()
q.end_with_mpi()
```
---
## `ShuffledFieldsWriter` Class
Writes lattice fields to a shuffled binary directory.
### Constructor
### `ShuffledFieldsWriter(path: str, new_size_node: Coordinate, is_append=False)`
Open a writer at `path` with the given I/O node layout.
| Parameter | Type | Description |
|---|---|---|
| `path` | `str` | Directory path for the output files |
| `new_size_node` | `Coordinate` | I/O node grid dimensions for the shuffled layout |
| `is_append` | `bool` | If `True`, append to existing data; otherwise create new |
---
### Methods
### `close()`
Close the writer and release resources.
### `path() -> str`
Return the directory path.
### `new_size_node() -> Coordinate`
Return the I/O node grid dimensions.
### `list() -> list`
Return a list of field names currently stored.
### `has(fn: str) -> bool`
Check if a field with name `fn` exists.
### `flush()`
Flush buffered data to disk.
### `write(fn: str, obj)`
Write a field object (`FieldBase` or `SelectedFieldBase`) under the name `fn`.
Internally dispatches to `obj.write_sfw_direct(self, fn)`.
### `get_cache_sbs(fsel: FieldSelection) -> ShuffledBitSet`
Get or create a cached `ShuffledBitSet` for the given `FieldSelection`. Used
internally for sparse field I/O.
---
### Properties
### `__contains__(fn: str) -> bool`
Check membership via the `in` operator.
---
## `ShuffledFieldsReader` Class
Reads lattice fields from a shuffled binary directory.
### Constructor
### `ShuffledFieldsReader(path: str, new_size_node=None)`
Open a reader at `path`.
| Parameter | Type | Description |
|---|---|---|
| `path` | `str` | Directory path to read from |
| `new_size_node` | `Coordinate` or `None` | I/O node grid; if `None`, auto-detect from stored metadata |
---
### Methods
### `close()`
Close the reader and release resources.
### `path() -> str`
Return the directory path.
### `new_size_node() -> Coordinate`
Return the I/O node grid dimensions.
### `list() -> list`
Return a list of field names stored in the directory.
### `has(fn: str) -> bool`
Check if a field with name `fn` exists.
### `has_duplicates() -> bool`
Check if the stored data contains duplicate entries.
### `is_sparse_field(fn: str) -> bool`
Return `True` if the named field is stored as a sparse (selected) field.
### `read_as_char(fn: str)`
Read a field as raw bytes. Returns `SelectedFieldChar` for sparse fields,
`FieldChar` for dense fields, or `None` if the field does not exist.
### `read(fn: str, obj)`
Read a field into `obj` (`FieldBase` or `SelectedFieldBase`). For
`SelectedField` objects, `obj.fsel` may be `None` before reading and will
be properly populated afterwards.
### `get_cache_sbs(fsel: FieldSelection) -> ShuffledBitSet`
Get or create a cached `ShuffledBitSet` for the given `FieldSelection`.
---
### Properties
### `__contains__(fn: str) -> bool`
Check membership via the `in` operator.
---
## `ShuffledBitSet` Class
Maps a `FieldSelection` to the shuffled I/O node layout. Used internally by
both `ShuffledFieldsWriter` and `ShuffledFieldsReader` to determine which
sites each I/O node is responsible for.
### `ShuffledBitSet(fsel: FieldSelection, new_size_node: Coordinate)`
| Parameter | Type | Description |
|---|---|---|
| `fsel` | `FieldSelection` | The field selection to map |
| `new_size_node` | `Coordinate` | The I/O node grid dimensions |
---
## Module-Level Functions
### `open_fields(path: str, mode: str, new_size_node=None)`
Open a shuffled fields directory. Convenience function that creates a
`ShuffledFieldsReader` or `ShuffledFieldsWriter` depending on `mode`.
| Parameter | Type | Description |
|---|---|---|
| `path` | `str` | Directory path (or path to `geon-info.txt` inside it) |
| `mode` | `str` | `"r"` for read, `"w"` for write, `"a"` for append |
| `new_size_node` | `Coordinate` or `None` | Required for `"w"` mode; optional otherwise |
### `list_fields(path: str, new_size_node=None) -> list`
Return a list of field names stored in the directory. Opens and closes a
reader internally.
### `fields_build_index(path: str, new_size_node=None)`
Build the internal index for faster field lookup. Opens and closes a reader
internally.
### `fields_has_duplicates(path: str, new_size_node=None) -> bool`
Check whether the stored data contains duplicate entries.
### `properly_truncate_fields(path, is_check_all=False, is_only_check=False, new_size_node=None)`
Check and optionally truncate corrupted trailing data from the shuffled
files. Returns a list of successfully stored field names.
| Parameter | Type | Description |
|---|---|---|
| `path` | `str` | Directory path |
| `is_check_all` | `bool` | If `True`, check all fields (not just the last one) |
| `is_only_check` | `bool` | If `True`, only check without modifying |
| `new_size_node` | `Coordinate` | I/O node grid |
### `truncate_fields(path: str, fns_keep: list, new_size_node=None)`
Truncate the stored data to keep only the fields listed in `fns_keep`.
The names must be in the same order as they appear on disk. All names in
`fns_keep` must already exist in the directory.
### `check_fields(path: str, is_check_all=True, new_size_node=None) -> list`
Return a list of field names that can be read successfully from the
directory.
### `check_compressed_eigen_vectors(path: str) -> bool`
Check whether compressed eigenvector data can be read. Returns `True` if
there is a problem, `False` if the data is OK.
### `eigen_system_repartition(new_size_node, path, path_new="")`
Repartition a stored eigensystem to a new I/O node layout.
### `show_all_shuffled_fields_writer() -> str`
Return debug information about all currently open `ShuffledFieldsWriter`
instances.
---
## Examples
### Writing and Reading Fields
```python
import qlat as q
q.begin_with_mpi([[1, 1, 1, 1]])
total_site = q.Coordinate([4, 4, 4, 8])
geo = q.Geometry(total_site)
new_size_node = q.Coordinate([1, 1, 1, 1])
# Create fields
f1 = q.Field(q.ElemTypeComplexD, geo, 1)
f2 = q.Field(q.ElemTypeComplexD, geo, 1)
# Write fields
sfw = q.open_fields("/tmp/fields_test", "w", new_size_node)
sfw.write("field_a", f1)
sfw.write("field_b", f2)
sfw.close()
# List stored fields
names = q.list_fields("/tmp/fields_test", new_size_node)
print(f"Stored fields: {names}") # ["field_a", "field_b"]
# Read fields back
sfr = q.open_fields("/tmp/fields_test", "r")
f1_read = q.Field(q.ElemTypeComplexD, geo, 1)
sfr.read("field_a", f1_read)
print("field_a" in sfr) # True
sfr.close()
q.end_with_mpi()
```
### Append Mode
```python
import qlat as q
q.begin_with_mpi([[1, 1, 1, 1]])
total_site = q.Coordinate([4, 4, 4, 8])
geo = q.Geometry(total_site)
new_size_node = q.Coordinate([1, 1, 1, 1])
# Initial write
sfw = q.open_fields("/tmp/fields_append", "w", new_size_node)
sfw.write("field_1", q.Field(q.ElemTypeComplexD, geo, 1))
sfw.close()
# Append more fields
sfw = q.open_fields("/tmp/fields_append", "a", new_size_node)
sfw.write("field_2", q.Field(q.ElemTypeComplexD, geo, 1))
sfw.close()
names = q.list_fields("/tmp/fields_append", new_size_node)
print(f"Stored fields: {names}") # ["field_1", "field_2"]
q.end_with_mpi()
```
### Checking and Truncating Fields
```python
import qlat as q
q.begin_with_mpi([[1, 1, 1, 1]])
total_site = q.Coordinate([4, 4, 4, 8])
geo = q.Geometry(total_site)
new_size_node = q.Coordinate([1, 1, 1, 1])
# Write some fields
sfw = q.open_fields("/tmp/fields_check", "w", new_size_node)
sfw.write("f1", q.Field(q.ElemTypeComplexD, geo, 1))
sfw.write("f2", q.Field(q.ElemTypeComplexD, geo, 1))
sfw.write("f3", q.Field(q.ElemTypeComplexD, geo, 1))
sfw.close()
# Check which fields are readable
ok = q.check_fields("/tmp/fields_check", new_size_node=new_size_node)
print(f"Readable fields: {ok}")
# Truncate to keep only f1 and f2
q.truncate_fields("/tmp/fields_check", ["f1", "f2"], new_size_node)
names = q.list_fields("/tmp/fields_check", new_size_node)
print(f"After truncate: {names}") # ["f1", "f2"]
q.end_with_mpi()
```
### Using open_fields with geon-info.txt Path
```python
import qlat as q
q.begin_with_mpi([[1, 1, 1, 1]])
total_site = q.Coordinate([4, 4, 4, 8])
geo = q.Geometry(total_site)
new_size_node = q.Coordinate([1, 1, 1, 1])
path = "/tmp/fields_geon"
# Write
sfw = q.open_fields(path, "w", new_size_node)
sfw.write("test", q.Field(q.ElemTypeComplexD, geo, 1))
sfw.close()
# Read using geon-info.txt path (the /geon-info.txt suffix is stripped)
sfr = q.open_fields(path + "/geon-info.txt", "r")
print(sfr.list())
sfr.close()
q.end_with_mpi()
```