Documentation style guide#

This is the single document that any contributor reads before writing or editing a docstring, an RST page, or a tutorial. Phase 0 of the commercial-release documentation overhaul (issue #583) makes it the authoritative bar; every Phase 1+ change is reviewed against the rules below.

Anything not specified here defaults to the PyVista documentation conventions, which this guide is closely modelled on.

Why a style guide#

femorph-solver targets a paying-customer audience that is comparing us against Ansys / Abaqus / COMSOL doc sets. Inconsistent voice, half-written docstrings, or “Examples” blocks that don’t actually run all read the same way to a reader: not ready. The rules below keep the surface uniform regardless of which contributor authored a given page.

Docstring format#

Every public callable uses NumPy-style docstrings (the format parsed by numpydoc), with the section ordering and headings shown in the canonical template:

def something(arg, *, opt=None):
    """One-line summary, ending with a period.

    Longer description with the *why*, not just the *what*.
    Cross-link related concepts with ``:class:``, ``:meth:``,
    ``:func:``, ``:term:``, and ``:ref:`` roles.

    Parameters
    ----------
    arg : type
        Plain-English description of role and units.
    opt : type, optional
        Default ``None``.  What it does when set.

    Returns
    -------
    type
        What the caller gets and what shape / units.

    See Also
    --------
    related_function : one-line pointer.

    Notes
    -----
    Hidden constraints, numerical-stability remarks, references
    to the theory page that derives the formula.

    References
    ----------
    .. [1] Author, *Title*, Publisher, Year, §section.

    Examples
    --------
    Plain-English lead-in, one sentence.

    >>> import femorph_solver as fs
    >>> model = fs.examples.cyclic_bladed_rotor_sector()
    >>> model.something(arg, opt=value)  # doctest: +ELLIPSIS
    ...
    """

The summary line is mandatory for every public callable. The remaining sections are required only when the signature warrants them — see Section taxonomy by callable type below.

Section taxonomy by callable type#

Callable type

Summary

Parameters

Returns

Raises

Examples

See Also

Public function

required

required

required

if any

required

encouraged

Public method

required

required

required

if any

required

encouraged

__init__

required

required

n/a

if any

on the class

n/a

Public property

required

n/a

required (as a Returns block, not a Yields)

if any

encouraged

encouraged

Public class

required

n/a

n/a

n/a

required (one per class)

encouraged

Enum / dataclass

required at class level

n/a

n/a

n/a

encouraged

n/a

Private (_name)

encouraged

encouraged

encouraged

n/a

n/a

n/a

The rule of thumb: every public surface a user can reach without underscores has a runnable Examples block. The block is what readers look at first; if it works, the page works.

Examples blocks#

  • Use the >>> doctest format. Examples blocks are collected by pytest --doctest-modules and must run on every CI build (see CI gates).

  • Lead with one sentence of plain English explaining what the block demonstrates. Then the code.

  • Prefer the bundled fixtures in femorph_solver.examples over re-building meshes inline, so the doctest stays cheap and self-contained:

    • cantilever_hex8 — 40-cell clamped HEX8 cantilever, pre-clamped, 0.3 s to a six-mode modal. The default fixture for Examples blocks across the public API.

    • quarter_arc_sector — annular HEX20 sector for sector / cylindrical-coord demos.

    • cyclic_bladed_rotor_sector — full bladed-rotor base sector for cyclic-symmetry demos.

  • For floating-point output, append the # doctest: +ELLIPSIS directive and elide the digits with ... rather than pinning exact decimals. Numerical drift across BLAS / OS / threading modes will otherwise break the doctest on different runners.

  • Never reach the network or write outside tmp_path from a doctest. If a method writes to disk, use the standard library tempfile pattern shown in the canonical template.

A worked example, for Model.solve_modal:

"""Solve the generalised eigenproblem :math:`K\\,\\varphi = \\omega^2 M\\,\\varphi`.

...

Examples
--------
Run a modal solve on the bundled cantilever and print the first
six natural frequencies.

>>> import femorph_solver as fs
>>> model = fs.examples.cyclic_bladed_rotor_sector()
>>> result = model.solve_modal(n_modes=6)
>>> result.frequency.shape
(6,)
>>> float(result.frequency[0]) > 0.0
True
"""

Voice and tone#

  • Present tense, technical, no marketing language. We are documenting what the code does today, not pitching what it could do tomorrow.

  • Explain the why, not just the what. The signature already shows the what; the prose carries the why. “Returns the lowest n_modes natural frequencies and the corresponding mass-normalised mode shapes — mass normalisation lets the user superpose modes without re-orthogonalising” is a useful description. “Returns the lowest n_modes natural frequencies” is a re-statement of the signature.

  • Plain past tense in change-log entries; present tense everywhere else. Voice is consistent inside a page even if a reader skims many in sequence.

  • No emoji, no exclamation points, no “we recommend” hedging. Engineering documentation states behaviour and constraints; if there’s a recommendation, give the reason and let the reader decide.

  • Comments in code blocks explain the why only when it isn’t obvious from the code itself — otherwise omit them. Same rule as the runtime source code (see CLAUDE.md).

Cross-reference conventions#

  • :class: for a class — :class:`~femorph_solver.Model`.

  • :meth: for a method — :meth:`~femorph_solver.Model.solve_modal`.

  • :func: for a free function — :func:`~femorph_solver.recover.compute_nodal_stress`.

  • :term: for a glossary entry — :term:`shift-invert`.

  • :ref: for a labelled section — :ref:`section-taxonomy`.

  • :doc: for a relative document path — :doc:`/user-guide/solving/modal`.

  • The leading ~ shortens the rendered link to just the final attribute name. Use it everywhere except when the fully-qualified path is informative.

External cross-references resolve via intersphinx for numpy, scipy, pyvista, matplotlib, and pymapdl. Examples:

:class:`numpy.ndarray`
:class:`pyvista.UnstructuredGrid`
:class:`scipy.sparse.csr_array`
:func:`scipy.sparse.linalg.eigsh`

Sphinx -W strict mode treats unresolved references as errors, so a typo here fails the build immediately — that is on purpose.

Math#

  • Inline math uses the role :math::math:`\omega^2 = K / M`.

  • Displayed math uses the directive .. math:: on its own line, with a blank line above and below.

  • Symbol conventions inherit from Theory. Use \(K, M, C\) for the global stiffness / mass / damping matrices, \(\\varphi\) for a mode shape, \(\\omega\) for a circular frequency, \(\\xi\) for a damping ratio. Bold lowercase for vectors (\(\\mathbf{u}\)); upright capitals for matrices.

  • No vendor symbol conventions. MAPDL’s {F} for load vectors is not the convention here — write \(\\mathbf{f}_{\\text{ext}}\) instead.

Figures#

  • SVG is preferred for diagrams (architecture, element topology, free-body diagrams). Vector, search-friendly, diff-friendly, and renders sharply at every zoom level.

  • PNG for screenshots (gallery output, GUI captures).

  • Both are checked in under doc/source/_static/figures/<topic>/ with the convention <page>_<short>.svg — e.g. _static/figures/elements/hex8_topology.svg.

  • Reference a figure with the figure directive, never the bare image directive — captions are required for everything that isn’t purely decorative:

    .. figure:: /_static/figures/elements/hex8_topology.svg
        :alt: HEX8 reference geometry showing local node order 1–8.
        :width: 60%
    
        HEX8 reference element with the local 1–8 node order
        used by femorph-solver and by MAPDL ``SOLID185``.
    

Vendor-citation pattern#

femorph-solver is independently developed from public FEM literature. When citing a vendor verification problem, follow the fair-use posture established in Verification:

  • One-line factual citations only — e.g. “The published target for tip deflection is \(\\delta = 0.0163\) m (NAFEMS LE5).”

  • Never reproduce vendor input-deck text. When a verification case is re-authored from a published problem statement, write the deck from scratch and add the footnote (re-authored from \\[VM-N\\] under fair use; no deck text lifted).

  • Vendor element names (SOLID185, BEAM188, …) appear only in the interop layer. The neutral user-guide and reference layers use the topology-first names (HEX8, BEAM2, …).

Acceptance for any new docstring#

A new public-API docstring is “done” when:

  1. The summary line is one sentence ending in a period.

  2. Parameters lists every positional and keyword argument.

  3. Returns describes every return value (including its shape / units / lazy-evaluation contract for Result types).

  4. Examples runs under pytest --doctest-modules with no warnings or errors.

  5. numpydoc.validate reports zero violations from the enforced class set (see Documentation CI gates).

  6. The cross-references resolve under sphinx-build -W -n.

A new RST page is “done” when:

  1. The page title is a single H1 underline of the same length as the title.

  2. Every internal cross-reference resolves.

  3. Every code block either uses .. code-block:: python (or bash, rst, …) or is a >>> doctest block.

  4. make linkcheck returns zero broken external links.

  5. The page renders with no Sphinx warnings under -W.

See also#