|
Uni20
Asynchronous tensor and runtime library.
|
Status: design and implementation note. The first C++ slice now provides uni20::display status helpers, sink routing, report emission, and schema-first streaming tables. Python sinks, async queue sinks, display contexts, and richer streaming-table controls remain planned work.
Uni20 already has a presentation layer that can render styled spans, semantic glyphs, reports, tables, and tensor previews. That layer is intentionally low level: it answers "how should this semantic presentation object render under this output policy?"
The display layer should answer a different question: "how can ordinary Uni20 code emit useful human-facing output without caring whether the caller is a C++ terminal executable, a redirected batch job, a Python script, pytest, or a Jupyter notebook?"
The intended namespace is:
The API should be easier than ad hoc std::cout and no harder than fmt::print, while giving callers semantic glyphs, colors, width-aware formatting, tables, and Python-friendly routing.
The design center is ephemeral user display:
This is not primarily an archival logging layer. Output may be redirected to a file, especially on clusters, but the content is still aimed at a human reading the run transcript. A durable structured logger can be a later sink or a separate layer.
trace.hpp correctness-boundary macros such as CHECK, PRECONDITION, PANIC, or debug traces with source-location-heavy output.stdout or stderr.repr(obj) in Python exhaustive or terminal-dependent.The design has three explicit layers.
Callers create or emit semantic content:
presentation::styled_text,presentation::semantic_glyph status markers,presentation::report_builder reports,This layer must not write to FILE*, std::ostream, or Python objects.
Rendering is owned by uni20::presentation.
The display layer should not duplicate glyph fallback, ANSI style generation, display-cell width calculations, text wrapping, table drawing, or tensor art. It should build presentation objects and ask presentation renderers to render them with an appropriate presentation::output_policy.
The active sink decides where rendered output goes:
stdout or stderr;sys.stdout.write or sys.stderr.write;Sinks are routing and policy adapters. They should not invent their own glyph or table formatting rules.
The common path should look like fmt::print with a semantic prefix:
The same concept should also support explicit semantic glyphs:
A minimal first API could be:
The exact names can change, but the ergonomics should remain: one line for ordinary messages, and direct emission for richer presentation objects.
Helpers used inside reports and tables should not emit. For example, display::status_cell(...) or a similar name can return a presentation::styled_text value that combines a semantic glyph with text. This keeps display::success(...) as an action and avoids surprising output while a row is being assembled.
The display layer should use presentation::semantic_glyph, not literal characters. The presentation layer currently has status and disposition glyphs for:
| Semantic meaning | Glyph name | Typical emoji rendering | ASCII rendering |
|---|---|---|---|
| completed / passed | success | â
| [OK] |
| failed / open problem | failure | â | [FAIL] |
| aborting / fatal | fatal | đ¨ | [FATAL] |
| warning / advisory | warning | â ī¸ | [WARN] |
| neutral information | info | âšī¸ | [INFO] |
| partly done / incomplete | partial | đĄ | [PARTIAL] |
| intentionally postponed | deferred | â¸ī¸ | [DEFER] |
| not applicable / out of scope / not run | skipped | đĢ | [SKIP] |
These are not trace severities. In particular, partial is not the same as warning, and skipped is not the same as failure. They are useful when a report or progress summary communicates disposition rather than error severity.
When all rows are known, display should reuse the existing batch report and table machinery:
Batch tables may inspect all rows, choose widths from the complete content, and fit the table to the available width. This is appropriate for setup summaries, final reports, and compact diagnostics.
For numerical results shared with CSV/TSV/JSON outputs, use the typed data table and its terminal adapter. It supplies display::formatted_cell values through the preformatted streaming_table::row overload, preserving finite-number decimal alignment and missing/nonfinite alignment after scalar formatting. Numeric cells set keep_together so their formatted values, including half-integer fractions, cannot be split across lines. The ordinary string-cell interface continues to treat strings as text.
The default C++ display router checks write and flush errors and throws std::ios_base::failure. Custom routers must expose their own output failures; this allows typed table subscriptions to apply required/optional sink policies.
Per-step output often does not know future rows. For example, DMRG sweeps, iterative eigensolvers, benchmark progress, and async task progress are streaming. A streaming table should be schema-first rather than data-first:
The table object owns a fixed schema:
The streaming table should choose initial widths from the schema, headers, and the sink's current display width. It should not wait for future rows.
Recommended behavior:
Long text values should wrap inside that row only. They should not permanently expand the column, and they should not force future rows to wrap merely to preserve alignment with an outlier row.
This means one long status message may produce:
The table schema remains stable after row 2. Row 3 does not inherit row 2's wrapping. Continuation lines should carry a visible marker, rendered from a semantic arrow glyph, inside the cell that wrapped so progress output is easy to scan without changing the chosen column widths or relying on a blank leading column.
Streaming tables should not rely on terminal auto-wrap. The formatter should emit physical lines that fit the selected terminal width; if the configured schema cannot fit, it switches to a vertical key/value fallback. Typed numeric cells are kept intact: if a value cannot fit its column after fit-width growth, the table switches to vertical output for that row and subsequent rows. This also applies when the first wide value arrives after compact rows have already been emitted. A numeric token wider than the vertical value area is emitted intact, even if the resulting line exceeds the selected width. Uni20 inserts no line breaks inside the token; a physical terminal may still visually wrap an overlong line.
Streaming-table events set event::preserve_layout. The default sink disables additional wrapping for these events, and custom sinks must likewise preserve their line breaks. Ordinary display messages retain the sink's wrapping policy.
Streaming columns can declare default value formatting as part of the schema. For example, a solver progress table can use fixed-point formatting for an energy column and scientific formatting for a residual column. Typed numeric values use the column formatter and participate in decimal alignment. If a numeric format such as display::format::fixed(...), display::format::scientific(...), or display::format::general(...) is supplied without an explicit alignment, the column defaults to decimal alignment. If the width is omitted for a numeric format, the column uses a non-greedy fit width: it starts from a format-specific minimum, does not absorb terminal slack, and can grow for later rows by using unused space or by shrinking shared columns toward their minimum widths. If the width is omitted for text or automatic columns, the column uses the ordinary shared-width terminal allocation. Use explicit fixed widths when a field should never change width; reserve shared-width columns for prose-like fields that should absorb terminal slack. Text sent to a numeric column, for example "non-converged", is treated as exceptional text: it is displayed in the column but does not update the decimal anchor.
Decimal-aligned numeric streaming columns keep a per-column decimal anchor based on numeric rows seen so far. This aligns later shorter values with earlier values in the common progress-output case. If a later row has more display cells before the decimal than any previous row, future rows can use the wider anchor, but already-emitted rows are not reflowed.
The current streaming-table API accepts values that can be converted to strings through fmt::format, plus presentation::styled_text and presentation::table_cell values. Styled cells keep semantic glyphs and style metadata until the active sink renders them, so ASCII, Unicode, emoji, color, and charset policy still apply at the output boundary.
Streaming display should not drop cell content by default. If a cell is too wide, it wraps. If a token is wider than the cell and has no whitespace, it is hard-split. Clipping and ellipsis should be opt-in for progress displays where the caller explicitly prefers compactness over full content.
For very narrow terminals, a vertical fallback is preferable:
This is less pretty, but it preserves information and avoids broken table rules.
The first implementation can choose widths when the streaming table is created. Later, the table may support an explicit resize() or an automatic policy:
stable: never recompute widths;on_terminal_change: recompute at row boundaries when the sink width changes;manual: recompute only when the caller asks.The default should be stable. A progress table that changes width while the user watches it can be distracting.
Display output may be redirected to a file, especially on clusters. The default policy should follow current presentation behavior:
NO_COLOR disables automatic color;UNI20_COLOR=always or an explicit policy preserves ANSI escapes;UNI20_GLYPHS controls emoji, Unicode, or ASCII glyph spelling;UNI20_CHARSET controls UTF-8 preservation, escaping, or replacement.Redirected display is still human-facing transcript output. It should not be treated as structured logging unless the caller installs a structured sink.
Python bindings must not rely on C stdout or stderr for ordinary display. When Uni20 is imported from Python, the binding layer should be able to install a Python-aware sink:
sys.stdout.write or sys.stderr.write;Python policy detection should not require a fake FILE*. It should be able to construct a presentation::output_policy directly from:
sys.stdout.isatty() / sys.stderr.isatty();UNI20_COLOR, UNI20_GLYPHS, UNI20_CHARSET, NO_COLOR, and COLUMNS.For long-running C++ calls entered from Python, worker threads should not call Python directly. They should emit display events into a queue or context. The owning Python boundary can drain the queue at safe points while holding the GIL.
Async tasks should not render directly in the general case. They should emit events:
This avoids:
Events should optionally carry task metadata:
krylov, dmrg, or tensor,The default screen renderer may ignore most metadata. A debug sink or test sink can preserve it.
trace.hpp remains the emergency and correctness-boundary layer:
CHECK, PRECONDITION, PANIC: aborting diagnostics;ERROR / ERROR_IF: exception-boundary diagnostics;display:: is for ordinary user-facing output:
Some output may be semantically warning-like in both systems. The distinction is the caller intent. If a condition indicates a violated invariant, use trace. If it is something the user should know while the computation continues, use display.
Logging can be implemented as a display sink, but it should not drive the first API.
Display output:
Durable logging:
The same event model can support both, but the default API should stay pleasant for screen output.
A first implementation can use a small value-type sink interface:
This keeps the implementation simple and testable. A later design can replace or supplement std::function with an abstract sink class if flushing, back-pressure, or queue ownership becomes important.
Public emitting helpers should capture std::source_location::current() at the call site and store it in the event. The first implementation can use a small display::format_string<Args...> wrapper around fmt::format_string<Args...> so calls such as display::info("sweep {}", sweep) keep fmt compile-time checking while also preserving the user's source location. The event type itself should not use a default member initializer for source location, because that would capture the line in the header rather than the user's call site.
The default sink should:
stdout or stderr from event::destination;event::preserve_layout is set, then render the event text with the presentation layer;Tests can install a vector sink:
Python can install a sink that renders to Python strings and calls Python stream objects. It should not need to change call sites.
Global convenience functions are important, but algorithms often need local control. A context object should let a caller redirect or silence one algorithm without changing global state:
Possible context controls:
The global functions can delegate to a default context.
Display should use terminal width only at layout boundaries:
Display should not format a table and then rely on generic whole-string wrapping. Wrapping belongs to cells, messages, and preview metadata before the final line is emitted.
For plain redirected output, width may come from COLUMNS, an explicit policy, or a conservative default. The caller should be able to override it.
src/uni20/common/display.hpp and .cpp.uni20::display convenience functions for status messages.display::emit(styled_text) and display::emit(report_builder).partial, deferred, and skipped or excluded to presentation::semantic_glyph.display::table.bindings/python.sys.stdout / sys.stderr redirection.uni20::display, or should the low-level sink API live under uni20::display::detail while the user API stays flat?skipped and excluded be separate semantic glyphs, or is one concept enough?info and success be stdout while warning and failure go to stderr, or should all display messages default to stderr unless explicitly directed?Start small but preserve the architecture:
display::info, success, warning, failure, status, and emit.presentation::styled_text and presentation::report_builder directly.partial, deferred, and skipped semantic glyphs in the presentation layer.