Conventions and Style
This page is the canonical reference for what Blackchirp code, Python code, and documentation prose look like once you sit down to add or modify a file. Three sections cover the three trees, and a final cross-cutting section covers the API reference contract that ties C++ headers, Python source, and the Sphinx pages under API Reference and Python Module together.
What this page does not cover: build commands, test invocations,
dependency installation, and the rest of the how do I run X surface
live in Build System and Project Layout and in the per-tree
AGENTS.md files at the root of src/, doc/, and python/.
This page is for the open-the-file-and-write-code half of the work.
C++
The C++ section assumes fluency with Qt6 (QObject, the metaobject
system, QString / QAnyStringView) and the standard Qt
vocabulary. It is not a Qt tutorial.
Naming and indentation
Classes, structs, and enums use
UpperCamelCase:HardwareObject,FtmwConfig,CommunicationProtocol::CommType.Free functions and variables use
lowerCamelCase:hwReadSettings(),currentExperimentNum.Member variables carry a one- or two-letter prefix that records what they hold:
Prefix
Meaning
d_Value member (
d_key,d_currentShots).p_Raw pointer (
p_comm,p_settings).pu_std::unique_ptr(pu_worker).ps_std::shared_ptr(ps_storage).The prefix is part of the name, not a hint: code reviews catch
int currentShotson a member whered_currentShotswas intended. Static class members follow the same rule (s_instancefor a singleton handle).Indentation is four spaces, spaces only, no tabs. New files inherit the project’s
.editorconfig-equivalent settings; if your editor produces tab characters, fix it before committing.
String literals
Blackchirp uses Qt6’s user-defined string-literal suffixes everywhere strings appear in source. Pick the form by what the call site needs.
Form |
Type |
When to use |
|---|---|---|
|
|
ASCII content. Default choice — accepted by any
|
|
|
Non-ASCII content (e.g., |
|
|
Only when the call site genuinely requires a |
|
|
Do not use in new code. Replace existing occurrences when editing a file that already uses them; otherwise leave alone. |
Do not reach for "..."_s just because a parameter is
QAnyStringView. Constructing a temporary QString defeats the
view parameter; "..."_L1 is what the parameter is designed for.
The Qt::Literals::StringLiterals namespace is pulled in globally
through data/loghandler.h, so any translation unit that includes
the log header (directly or transitively) gets _s and _L1 for
free. Headers that are read from contexts that do not include the log
header should still spell out the constructor form for declarations
(see Key declaration patterns below) so the file does not depend on
the suffixes being visible.
Function signatures
Never pass ``QString`` by value unless the callee takes ownership and moves. Implicit sharing makes copying cheap, but a value parameter still costs an atomic refcount round-trip per call.
Use
QAnyStringViewfor pure lookup, comparison, or pass-through functions — anything that is going to forward the string into another view-aware API or compare it.QAnyStringViewacceptsQString,QStringView,QLatin1StringView, andconst char *without a conversion.Use
const QString &when the callee genuinely needs aQString: it calls.arg()on it, stores it asQString, or hands it to another API whose parameter isconst QString &.
In practice, the rule of thumb is: if the body would otherwise begin
QString s = view.toString();, the parameter should have been
const QString & to begin with.
Containers
For new std::map declarations keyed on QString, use the
transparent comparator:
std::map<QString, MyValue, std::less<>> d_table;
The transparent std::less<> enables heterogeneous lookup: a call
to find("key"_L1) or find(QStringView{...}) will not allocate a
temporary QString to perform the comparison. Retrofit existing
declarations opportunistically when editing the surrounding code.
QHash<QString, T> does not need any special declaration. Qt6 ships
qHash overloads that hash a QStringView to the same value as
the equivalent QString, so heterogeneous lookup works on
QHash directly.
Key declaration patterns
Blackchirp uses three patterns to declare named string keys at namespace
scope. All three are inline, so the symbol has external linkage with
exactly one definition in the program — there is no per-translation-unit
copy. Pick the pattern by the type the call site needs.
Pattern A — ``inline const QString``. Use when the key is consumed
as a QString (typically because .arg() is called on it):
inline const QString flow = "Flow%1"_s;
One heap allocation per process, not per translation unit; the cost is
the same as any other static QString.
Pattern B — ``inline constexpr QLatin1StringView``. Use for ASCII
keys consumed by QAnyStringView parameters or stored in
std::map<QString, T, std::less<>> with heterogeneous lookup. This
is the dominant pattern for hardware setting keys:
inline constexpr QLatin1StringView trigCh{"trigCh"};
True constexpr at namespace scope; zero runtime cost. In headers,
use the constructor form {"..."} rather than "..."_L1 so the
declaration does not depend on a using namespace
Qt::Literals::StringLiterals being visible in the including file.
Pattern C — ``inline constexpr QStringView``. Same trade-offs as Pattern B but UTF-16, so non-ASCII keys are safe:
inline constexpr QStringView us = u"μs";
The compile-error case is worth noting because the suffix syntax suggests it should work:
// Does NOT compile — QString is not a literal type.
inline constexpr auto k = "key"_s;
QString is not constexpr-eligible in any Qt version Blackchirp
targets. If a key needs to be QString at the call site, use Pattern
A; if it needs to be constexpr, use Pattern B or C.
Several namespaces collect related keys so the compiler catches typos at the call site rather than letting a stray string literal silently miss:
BC::Key::— application-wide and hardware settings keys, declared indata/bcglobals.hand the per-base-class blocks ofdata/settings/hardwarekeys.h. The hardware-key namespace is split into sub-namespaces by hardware type (BC::Key::HW,BC::Key::Comm,BC::Key::Clock,BC::Key::AWG, …) so that ausing namespace BC::Key::Clockpulls in only the clock keys.BC::Store::— persistent storage keys for serializable data classes (BC::Store::RFCforRfConfig,BC::Store::CCforChirpConfig,BC::Store::FtmwLOfor FTMW LO scans, and so on). Declared in the header next to the class that owns them.BC::CSV::— canonical filenames and column headers for the semicolon-delimited CSV files produced by an experiment. Declared indata/storage/blackchirpcsv.h.
Always reach for an existing namespace before introducing a string literal at a call site. Adding a new key is a one-line edit to the appropriate header.
Logging
All log output goes through the application-wide
LogHandler singleton. Normal code calls one of the five
free functions declared in data/loghandler.h:
bcLog(u"connected to %1"_s.arg(d_prettyName)); // Normal severity (default)
bcLog(u"detail"_s, LogHandler::Debug); // explicit severity
bcDebug(u"protocol byte 0x%1"_s.arg(b, 2, 16)); // Debug severity
bcWarn(u"timeout, retrying"_s); // Warning severity
bcError(u"hardware failure: %1"_s.arg(err)); // Error severity
bcHighlight(u"experiment %1 complete"_s.arg(num)); // Highlight severity
Inside HardwareObject subclasses, prefer the four
member helpers — hwLog, hwDebug, hwWarn, hwError —
which prepend the device key (d_key) automatically. The result is
that every line a driver writes is unambiguously attributed to that
driver in the in-app log and on disk; a hardware author should not have
to remember to do this manually.
Do not use qDebug() or emit logMessage() in new code. Do not
call LogHandler::instance() directly except in connection setup at
application startup; the free functions cover every other case.
Five severity levels are defined by LogHandler::MessageCode:
Level |
Use for |
|---|---|
|
Failures requiring user action or indicating data-loss risk. |
|
Automatically-corrected mismatches the user should know about. |
|
Connection outcomes, experiment milestones, user-initiated state changes. |
|
Major milestones such as experiment start and end. |
|
Hardware lifecycle, configuration loading, protocol details, parameter traces. Written to the debug log file only when debug logging is enabled. |
The full API surface — singleton lifetime, on-disk file layout, the
per-experiment log lifecycle driven by beginExperimentLog and
endExperimentLog, the sendLogMessage and iconUpdate
signals — is documented on LogHandler.
Persistent settings
Persistent state is owned by SettingsStorage. The class
is the single trust boundary between code that may read a value and
code that may change it.
The
getfamily is public. Anywhere in the program may construct a transientSettingsStorageover a group and read from it. UI code routinely does this to populate a widget from a hardware driver’s persisted settings.The
setfamily is protected. Only a class that owns a group — i.e. inherits fromSettingsStorageand initializes its base with the group keys — may write. The split is what keeps persisted state from drifting behind the owner’s back; a friend helper is the explicit escape hatch when one manager class needs to compose state across many groups.
Setting keys are declared statically. New code does not pass string
literals to get and set; it passes a constant from
BC::Key:: (for hardware settings and other application-wide keys)
or BC::Store:: (for serializable data classes). Adding a new key is
a one-line addition to the appropriate namespace block in the relevant
header. The compiler then catches typos at the call site, and a
grep for the constant finds every reader and writer.
For hardware drivers specifically, defaults are not set by ad-hoc
setDefault calls in the subclass constructor. They are declared
through the hardware-settings registry — REGISTER_HARDWARE_SETTINGS
for a driver, REGISTER_HARDWARE_BASE for shared base-class
defaults — and applied by HardwareObject::applyRegisteredSettings()
during construction. The registry is also what populates the hardware
settings dialog without the dialog ever instantiating a driver. The
full registration story and the rest of the runtime configuration model
are covered in Hardware Configuration. The
registration macros themselves and the descriptor structs they produce
are documented on HardwareRegistry; the
SettingsStorage API surface itself, including the array, group,
getter, and discard mechanisms, is documented on
SettingsStorage.
Documentation comments
Doxygen comments on C++ entities are governed by the
API reference style section of this page, which covers C++
headers and Python source files together. The same section sets out the
contract for what lives in headers versus on the rendered .rst
pages under API Reference.
Python
The Python section governs the standalone blackchirp PyPI module
under python/blackchirp/. The module reads experiment folders from
disk and reproduces Blackchirp’s data-processing pipeline (FID Fourier
transforms, sideband deconvolution, LIF gate integration) without
depending on any of the C++ runtime. The conventions below preserve
that property; deviations are not casual decisions.
Naming and formatting
Module names use
snake_case. The package isblackchirp; individual modules under it arebcfid,bcftmw,bclif,coaverage, and so on.Classes use
PascalCase:BCExperiment,BCFid,BCFTMW. TheBCprefix is the existing convention; new public classes follow it.Functions, methods, and variables use
snake_case:get_fid,coaverage_fids,shot_count.Module-level constants use
UPPER_SNAKE_CASE:_WINDOW_MAP,_FT_UNITS_MAP. The leading underscore marks an internal-to-the-module helper.
Formatting is enforced by black with default settings (88-character
line length). Run black . from python/blackchirp/ before
committing. Linting is pylint -E (errors only — warnings are not
load-bearing). Both should run clean before the PR is opened.
Imports follow PEP 8:
standard library first, third-party second (numpy, pandas,
scipy), then intra-package relative imports last. Wildcard imports
are reserved for blackchirp/__init__.py and the example notebooks;
ordinary modules import what they use by name.
Docstrings
Every public class and function must carry a docstring in
Google style,
which the Sphinx napoleon extension converts into proper RST at
documentation build time. The first line is a one-sentence summary;
subsequent paragraphs and sections describe behavior, parameters, and
return values.
Standard sections, in the order they typically appear:
Args:— one entry per parameter, writtenname: description.. Optional parameters note their default in the description.Returns:(orYields:for generators) — describes the return value. Omit when the function returnsNone.Raises:— exception types the caller may need to catch, with the condition that triggers each.Attributes:— for classes, public instance attributes the user is expected to read or write directly. Internal_attrand__attrare not documented here.Example:(orExamples:) — a short executable snippet that exercises the typical call path. Class-level docstrings include an example whenever the class has a non-trivial constructor signature.Note:/Warning:— for caveats that the brief and parameter list cannot accommodate (algorithmic limitations, performance cliffs, schema-version dependencies).
Mathematical content uses inline RST math (:math:`y = ax + b`)
inside the docstring; napoleon passes it through unchanged. See
the coaverage_spectra() docstring for an example.
Internal helpers prefixed with _ may carry a one-line docstring or
none at all. The line between “internal” and “public” is
blackchirp/__init__.py: anything re-exported there is public; the
rest is an implementation detail.
The header-vs-page contract that governs how docstrings render in the Python Module API reference is in the API reference style section below.
Dependency policy
The blackchirp module depends only on numpy, scipy, and pandas
(plus the standard library). No matplotlib, no Qt, no requests, no
aiohttp, no other plotting or networking libraries. The
minimal-dependency property is a deliberate design constraint: the
module must install cleanly into any scientific Python environment
without dragging heavy optional infrastructure with it.
Treat [project.dependencies] in
python/blackchirp/pyproject.toml as load-bearing. Adding a runtime
dependency requires user consent and a documented justification on the
PR. Importing matplotlib at module top-level — even guarded by
try/except ImportError — is not the right answer; if a downstream
caller wants plotting, it owns the matplotlib import.
The example notebooks under python/ may import matplotlib at the
cell level. Notebooks have their own dependency story (they run in an
analysis environment that already has plotting libraries) and do not
push imports back into the module.
The only declared dev dependency is pytest, listed under
[project.optional-dependencies] as dev. Add other dev tools
(coverage, ruff, etc.) only with user consent and only after
discussion.
Public API surface
The names re-exported from blackchirp/__init__.py are the public
contract:
Adding a name to that list is a deliberate decision with semantic
versioning consequences; removing one is a breaking change. New
internal helpers live in bcfid.py, bclif.py, etc. without being
re-exported. When a helper graduates to public status:
Add it to the
from .module import nameblock in__init__.py.Add a paragraph to the module docstring at the top of
__init__.pydescribing the new symbol.Add (or extend) a page under
doc/source/python/for the symbol, following API reference style below.Add an entry to the next-release changelog page under
doc/source/changelog/.
Internal modules (_enum_helpers.py, leading-underscore names) are
not part of the public surface and may change without a version bump.
Documentation
The Documentation section governs the Sphinx + Doxygen + Breathe + nbsphinx
documentation under doc/source/, deployed to
https://blackchirp.readthedocs.io/. The conventions cover prose
style; the build pipeline and CMake target wiring are in
Build System and Project Layout.
Voice and tense
User-facing prose is present tense and impersonal. “Blackchirp writes the FID to disk”, not “Blackchirp will write” and not “we write”. The user guide describes the program as it is, in the moment the user is operating it.
Developer-guide prose follows the same default with one allowance: second person is acceptable when giving a contributor explicit instructions (“Add the driver header to the aggregator”, “Read the sub-bundle file before drafting the page”). Use it sparingly; most developer prose can be written impersonally too.
Do not use source-evolution temporal markers in any prose: no “Phase 2”, “v1.1.0 introduced”, “recently added”, “now uses”, “previously did X but now does Y”. Permanent version-keyed information lives in the changelog or migration guide, not in the user guide or developer guide. Markers describing runtime behavior are fine (“after the experiment completes”, “before any FID is acquired”, “while connected”).
The same rule applies to commit messages and code comments. The test: would the sentence read correctly to a reader five years from now with no knowledge of the commit that introduced it?
American English
All prose uses American English: normalize / normalization,
behavior, color, visualization, analyze, co-averaging,
randomize, initialize. Match UI labels exactly when quoting
them, even if the UI label uses an American spelling — do not “correct”
a label such as “Randomize Delay Order” to British spelling in prose.
Cross-references and index entries
Use Sphinx cross-reference roles, not raw HTML anchors. Replace any
<page.html>-style links opportunistically when editing a page.
:doc:`/path/to/page`— links to another documentation page.:ref:`label-name`— links to a labeled section anchor across pages. Define labels with.. _label-name:immediately above the section header.:cpp:class:`ClassName`and:cpp:func:`namespace::function`— link to C++ entities documented through Doxygen / Breathe.:class:`~blackchirp.BCFid`and:func:`~blackchirp.coaverage_fids`— link to Python entities documented throughautoclass/autofunction. The leading~collapses the displayed name to the last component.:meth:`~blackchirp.BCFid.ft`— link to a method.
Every new page begins with a .. index:: block listing the
user-facing terms it introduces. The block sits between the optional
.. _label: line and the page title. Use single: entries for
one-word terms and single: parent; child for hierarchical entries
that group multiple subtopics under one parent. Pages without an index
block are reachable but invisible from the documentation’s index page.
Screenshots
UI screenshots live directly under doc/source/_static/user_guide/
in a single flat directory. Filenames follow the pattern
<page>-<topic>.<ext>, where <page> matches the basename of the
.rst page that uses the screenshot (or, for per-device pages under
user_guide/hw/, the hw-<device> form). The hyphen separates
the page prefix from the topic; the topic is the descriptive part of
the original filename. Reference screenshots with .. figure:: (not
.. image::) so they get a caption and fit into the page flow:
.. figure:: /_static/user_guide/hardware_config-addprofile.png
:width: 80%
:align: center
The hardware profile dialog after creating a new profile.
Width is expressed as a percentage; the percentage depends on the content density of the screenshot (forms and dialogs typically render at 60–80%, full-window shots at 90–100%). Screenshots are PNG. Hardware-specific screenshots that may need re-capture with new firmware revisions follow the same naming pattern as any other screenshot.
Notebooks
The example notebooks under python/single-*.ipynb are referenced
from the documentation via nbsphinx-link. Wrappers under
doc/source/python/notebooks/ carry the Sphinx-side metadata; the
notebook source itself is the artifact nbsphinx renders.
Three rules apply to any change that touches a notebook:
Execute the notebook end-to-end before commit.
nbsphinxrenders the existing cell outputs verbatim; an unexecuted or partially-executed notebook will render with empty or stale output cells. Execute in an analysis environment with the published dependency set (numpy,scipy,pandas,matplotlib, plusblackchirpitself).Exercise the public API only. Notebooks demonstrate the same contract a downstream user has — anything imported with
from blackchirp import *orimport blackchirp as bc. Reaching into_internalmodules to make a notebook work means the public API is missing something; fix that first.Notebook content is illustrative, not exhaustive. Use the notebook to walk through a realistic analysis session, not to document every parameter of every function. Reference detail lives on the corresponding API reference page.
API reference style
This section defines the contract between source code (C++ headers,
Python modules), the generators that read it (Doxygen for C++, Sphinx
autodoc + napoleon for Python), and the rendered API pages
under API Reference and Python Module. The contract is symmetric
across the two languages: member-level documentation lives next to the
code it describes, and the .rst page carries only orientation prose
plus the directive that pulls the generated content in.
Where prose lives
Member-level documentation — what a function does, what its parameters
mean, the invariants it preserves, threading notes — lives next to
the code: in Doxygen comments in the C++ header, or in Google-style
docstrings on the Python class or function. The corresponding .rst
page (under doc/source/classes/ for C++ classes, under
doc/source/python/ for Python entities) holds only:
a 1–3 paragraph orientation intro that situates the class in the larger system, names the most relevant collaborators, and links outward to the user-guide and developer-guide chapters that cover the feature it supports,
optional named subsections (H2,
-underline) that group prose by topic when the orientation runs longer than three paragraphs (e.g. Validation, System profiles, Registration macros), anda final
API Referencesection (also H2,-underline) that contains the directive that pulls in the generated member documentation.
The source code is the single source of truth for member-level
documentation. Per-method /// blocks (C++) or """..."""
docstrings (Python) describe what a function does, what its parameters
mean, what it returns, and which invariants it preserves; those blocks
are read by the generator, by IDE tooltips, by codebase-memory,
and by any contributor opening the file. The .rst page must not
paraphrase those per-member blocks — that just creates two places to
keep in sync.
The class-level docstring or header block is governed by a different
rule: orientation prose lives on the .rst, not next to the code.
The class-level
\brief(C++) or one-line summary (Python) stays tight: one or two sentences naming what the class is and its primary collaborators, optionally followed by internals notes a code reader genuinely needs — lifecycle invariants, ownership rules, threading contracts, configuration-flag fields, the cache or re-entrancy invariants a subclass author would otherwise miss.What does not belong in the class-level block: extended motivation prose, worked code examples (interface/implementation driver pairs, getter binding examples, friend-helper templates), enumerated lists of usage patterns, paragraph-form orientation for the class’s role in the larger system. All of that lives on the
.rstpage or, where the topic spans multiple classes, in the developer guide.
The test for a class-level source-side sentence: would removing it
leave a contributor reading the source in isolation unable to use the
class correctly? If yes, keep it. If the sentence is structural
orientation that already appears (or could appear) on the .rst
page, delete it from the source.
Doxygen comment style (C++)
Triple-slash (
///) on consecutive lines is preferred for new code.///<is used for trailing field/parameter comments. Multi-line/*! ... */blocks remain valid and are not converted for cosmetic reasons; match the surrounding file when editing.Use the standard tags:
\brief,\param,\return,\note,\warning,\sa. Begin every documented entity with a single-sentence\brief.Document every public and protected member that a subclass author or external caller would reach for. Default-implementation virtuals whose meaning is “do nothing” are still documented so subclasses know what they can override.
The
EXTRACT_ALLDoxygen setting means undocumented members still appear in the rendered output. Aim for zero undocumented public members in any class that has a dedicated API page.
Python docstring rendering
Sphinx renders Python docstrings through two extensions configured in
doc/source/conf.py: sphinx.ext.autodoc introspects the module
to discover classes, methods, and attributes, and
sphinx.ext.napoleon translates Google-style sections (Args:,
Returns:, Raises:, Attributes:, Example:) into the
corresponding RST field lists before Sphinx parses the result.
The practical consequences:
The docstring is the source of truth. Writing one Google-style docstring covers the in-source view (IDE tooltip,
help()output, GitHub source view) and the rendered API page simultaneously.Type hints in the function signature are picked up automatically by
autodocand rendered next to the parameter list. Repeating the type in theArgs:description is redundant; describe the meaning of the parameter, not its type.RST inline markup inside the docstring works:
:class:`~blackchirp.BCFid`,:math:`y = ax + b`,:meth:`~blackchirp.BCFTMW.get_fid`.napoleondoes not rewrite these; it passes them through to Sphinx unchanged.:param:/:type:/:returns:field-list syntax works but is discouraged. Use Google-style sections instead — they are easier to read in source.
The .rst page for a Python class follows the same shape as a C++
class page: an orientation intro, optional named subsections, and a
final API Reference section with the autodoc directive. See
BCFid for a representative example.
Sphinx directives
Pull generated content into the .rst page with the directive that
matches the entity:
C++ (Doxygen / Breathe):
.. doxygenclass:: ClassName
:members:
:protected-members:
:undoc-members:
.. doxygenstruct:: StructName
:members:
.. doxygenenum:: EnumName
Prefer
.. doxygenclass::over.. doxygenfile::— one focused page per class, members grouped by member rather than by source position.Match the directive to the entity kind: using
.. doxygenclass::on a struct or enum produces empty output because Breathe looks up by exact compound kind.Drop
:protected-members:for classes whose protected interface is not part of the contract (rare in this codebase — most base classes expose a protected hook layer that subclass authors are expected to override).
Python (autodoc / napoleon):
.. autoclass:: blackchirp.BCFid
:members:
.. autofunction:: blackchirp.coaverage_fids
.. automodule:: blackchirp._enum_helpers
:members:
Prefer
.. autoclass::and.. autofunction::over.. automodule::for the public API... automodule::is appropriate for internal-helper modules whose entire surface is documentation-relevant.Use the fully-qualified import path (
blackchirp.BCFid, not justBCFid) soautodocresolves the import unambiguously.
Page placement. Place every directive under the page’s final
API Reference section. Without this wrapper, sibling directives
appear visually nested under whatever prose subsection precedes them
in the page TOC.
Cross-reference roles in prose: :cpp:class: and :cpp:func:
for C++ symbols; :class:, :meth:, :func: for Python
symbols (with the leading ~ to collapse the display name); :doc:
for cross-page references; :ref: for labeled-section anchors.
Refresh checklist when editing a class
When changing a C++ header that has a corresponding API page:
Update the Doxygen comments alongside the code change.
Re-skim the
.rstintro: if a collaborator referenced there has been renamed or removed, fix the prose.Build the docs with
cmake --build build --target docsand checkdoxygen.logfor new warnings about undocumented public members of the touched class.
When changing a Python class or module that has a corresponding API page:
Update the Google-style docstring alongside the code change. If the parameter list changed, the
Args:section needs the same edits.Re-skim the
.rstintro: if a collaborator referenced there has been renamed or removed, fix the prose.Run
pytestfrompython/blackchirp/to confirm the docstring examples still execute (if any are doctest-style).Build the docs and check the Sphinx output for new warnings about the touched module.
For both languages: if the change adds a new public member, ensure the
member is documented — EXTRACT_ALL (Doxygen) and :members:
(autodoc) will both surface an undocumented member in the rendered
output.