Source code for general_python.lattices.visualization.formatting
"""
Human-friendly formatting helpers for lattice objects.
The functions in this module intentionally avoid any side-effects on the passed
`Lattice` instances. They simply transform existing lattice data into plain
Python strings so the caller can direct the output to terminals, loggers, or
documentation tooling.
-----------------------------------------------------------------------------
Author : Maksymilian Kliczkowski
Created : 2026-01-15
File : general_python/lattices/visualization/formatting.py
-----------------------------------------------------------------------------
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Iterable, Optional, Sequence
import numpy as np
try:
from ..lattice import Lattice, LatticeDirection
except ImportError:
raise ImportError("Failed to import Lattice classes for formatting module. Ensure the lattice module is available and correctly structured.")
# -----------------------------------------------------------------------------
# Internal helper functions and data structures
# -----------------------------------------------------------------------------
@dataclass(frozen=True)
class _VectorTableConfig:
"""Configuration holder for vector table formatting."""
max_rows : int = 10
precision : int = 3
column_labels : Optional[Sequence[str]] = None
index_label : str = "#"
indentation : str = ""
# -----------------------------------------------------------------------------
def _as_float_array(vectors: Iterable[Sequence[float]]) -> np.ndarray:
"""
Convert arbitrary vector-like input into a 2D float64 array.
"""
arr = np.asarray(vectors, dtype=float)
if arr.ndim == 1:
arr = arr.reshape(-1, arr.shape[0])
if arr.ndim != 2:
raise ValueError(f"Expected a 2D array of vectors, got shape {arr.shape!r}.")
return arr
def _default_labels(dimension: int) -> Sequence[str]:
axes = ("x", "y", "z")
return axes[:dimension]
# -----------------------------------------------------------------------------
# Public formatting functions
# -----------------------------------------------------------------------------
[docs]
def format_vector_table(
vectors : Iterable[Sequence[float]],
*,
max_rows : int = 10,
precision : int = 3,
column_labels : Optional[Sequence[str]] = None,
index_label : str = "#",
indentation : str = "",
) -> str:
"""
Return a tabular string representation of an array of vectors.
Parameters
----------
vectors:
Any iterable producing coordinate sequences.
max_rows:
Maximum number of rows to include in the formatted output.
precision:
Number of decimal places to use when printing floating point values.
column_labels:
Optional axis labels. Defaults to Cartesian axes based on vector dimension.
index_label:
Label for the index column.
indentation:
Optional indentation prefix applied to each line of the table.
"""
cfg = _VectorTableConfig(
max_rows = max_rows,
precision = precision,
column_labels = column_labels,
index_label = index_label,
indentation = indentation,
)
array = _as_float_array(vectors)
dim = array.shape[1]
labels = cfg.column_labels or _default_labels(dim)
if len(labels) != dim:
raise ValueError(f"Expected {dim} column labels, received {len(labels)}.")
header = cfg.indentation + "\t".join([cfg.index_label, *labels])
lines = [header]
row_limit = min(cfg.max_rows, array.shape[0])
fmt = "{:." + str(cfg.precision) + "f}"
for idx in range(row_limit):
values = "\t".join(fmt.format(val) for val in array[idx])
lines.append(f"{cfg.indentation}{idx:>4d}\t{values}")
if array.shape[0] > row_limit:
remaining = array.shape[0] - row_limit
lines.append(f"{cfg.indentation}... ({remaining} more rows)")
return "\n".join(lines)
# ----
# Flux
# ----
def _format_flux_lines(lattice: Lattice, precision: int) -> Sequence[str]:
flux = getattr(lattice, "flux", None)
if flux is None or not getattr(flux, "values", None):
return ("Boundary flux: none",)
lines = ["Boundary flux:"]
for direction in LatticeDirection:
if direction in flux.values:
phi = flux.values[direction]
phase = flux.phase(direction)
amp = abs(phase)
argument = math.atan2(phase.imag, phase.real)
lines.append(
f" {direction.name}: {phi:.{precision}f} rad "
f"(phase = exp(i*{phi:.{precision}f}) "
f"|phase|={amp:.2f}, arg={argument:.{precision}f})"
)
return tuple(lines)
def _format_basis(prefix: str, vector: Optional[Sequence[float]], precision: int) -> str:
if vector is None:
return f"{prefix}: unavailable"
arr = np.asarray(vector, dtype=float).flatten()
if arr.size == 0:
return f"{prefix}: unavailable"
comps = ", ".join(f"{val:.{precision}f}" for val in arr[:3])
return f"{prefix}: ({comps})"
# -----------------------------------------------------------------------------
# Main lattice summary and overview functions
# -----------------------------------------------------------------------------
[docs]
def format_lattice_summary(lattice: Lattice, *, precision: int = 3) -> str:
"""
Produce a multi-line summary describing key lattice metadata.
"""
lines: list[str] = []
type_name = getattr(lattice, "typek", None)
lattice_kind = type_name.name if type_name is not None else lattice.__class__.__name__
# Append size
size_str = f"sites={lattice.Ns} (Lx={lattice.Lx}"
if lattice.dim >= 2:
size_str += f", Ly={lattice.Ly}"
if lattice.dim >= 3:
size_str += f", Lz={lattice.Lz}"
size_str += ")"
lines.append(size_str)
lines.append(f"Lattice type: {lattice_kind}")
lines.append(f"Dimensions: d={lattice.dim}")
lines.append(f"Boundary: {lattice.bc.name} periodic flags={lattice.periodic_flags()}")
# Primitive real-space vectors when available
lines.append(_format_basis("a1", getattr(lattice, "a1", None), precision))
if lattice.dim >= 2:
lines.append(_format_basis("a2", getattr(lattice, "a2", None), precision))
if lattice.dim >= 3:
lines.append(_format_basis("a3", getattr(lattice, "a3", None), precision))
# Reciprocal primitive vectors when available
if hasattr(lattice, "k1"):
lines.append(_format_basis("b1", getattr(lattice, "k1", None), precision))
if lattice.dim >= 2 and hasattr(lattice, "k2"):
lines.append(_format_basis("b2", getattr(lattice, "k2", None), precision))
if lattice.dim >= 3 and hasattr(lattice, "k3"):
lines.append(_format_basis("b3", getattr(lattice, "k3", None), precision))
# Boundary flux information when available
lines.extend(_format_flux_lines(lattice, precision))
# Stored coordinates information when available
coords = getattr(lattice, "coordinates", None)
if coords is not None and isinstance(coords, (list, np.ndarray)):
lines.append(f"Stored coordinates: {len(coords)} entries")
return "\n".join(lines)
# -----------------------------------------------------------------------------
[docs]
def format_real_space_vectors(
lattice: Lattice,
*,
max_rows: int = 10,
precision: int = 3,
indentation: str = "",
) -> str:
"""
Format a table of lattice real-space vectors.
"""
arr = _as_float_array(lattice.rvectors)
target_dim = lattice.dim if lattice.dim and lattice.dim > 0 else arr.shape[1]
dim = max(1, min(arr.shape[1], target_dim))
trimmed = arr[:, :dim]
column_labels = _default_labels(dim)
return format_vector_table(
trimmed,
max_rows=max_rows,
precision=precision,
column_labels=column_labels,
indentation=indentation,
)
[docs]
def format_reciprocal_space_vectors(
lattice: Lattice,
*,
max_rows: int = 10,
precision: int = 3,
indentation: str = "",
) -> str:
"""
Format a table of reciprocal (k-space) vectors.
"""
arr = _as_float_array(lattice.kvectors)
target_dim = lattice.dim if lattice.dim and lattice.dim > 0 else arr.shape[1]
dim = max(1, min(arr.shape[1], target_dim))
trimmed = arr[:, :dim]
column_labels = _default_labels(dim)
return format_vector_table(
trimmed,
max_rows=max_rows,
precision=precision,
column_labels=column_labels,
indentation=indentation,
)
# -----------------------------------------------------------------------------
[docs]
def format_brillouin_zone_overview(
lattice: Lattice,
*,
precision: int = 3,
) -> str:
"""
Provide a textual overview of the sampled Brillouin zone.
The function reports bounding box limits and attempts to compute the
convex hull measure (length/area/volume) when SciPy is available.
"""
kvectors = getattr(lattice, "kvectors", None)
if kvectors is None or len(kvectors) == 0:
return "No reciprocal-space vectors available."
arr = _as_float_array(kvectors)
target_dim = lattice.dim if lattice.dim and lattice.dim > 0 else arr.shape[1]
dim = max(1, min(arr.shape[1], target_dim))
arr = arr[:, :dim]
labels = _default_labels(min(3, dim))
bounds = []
for axis, label in enumerate(labels):
comp = arr[:, axis]
bounds.append(f"{label}: [{comp.min():.{precision}f}, {comp.max():.{precision}f}]")
lines = [f"Reciprocal-space bounds: {', '.join(bounds)}"]
# Attempt to compute convex hull measure when applicable.
try:
from scipy.spatial import ConvexHull # type: ignore
if dim >= 2 and arr.shape[0] >= dim + 1:
hull = ConvexHull(arr[:, :dim])
measure_name = {2: "area", 3: "volume"}.get(dim, "measure")
lines.append(f"Convex hull {measure_name}: {hull.volume:.{precision}f}")
except ImportError:
lines.append("Convex hull metrics unavailable (scipy not installed).")
except Exception as exc: # pragma: no cover - defensive
lines.append(f"Convex hull computation failed: {exc}")
return "\n".join(lines)
# -----------------------------------------------------------------------------
#! EOF
# -----------------------------------------------------------------------------