Code Style And Syntax

Guidance for writing readable, maintainable TARDIS code.

Code Quality

Quick Reference

TARDIS follows PEP 8 using Ruff. Run Ruff directly from the active TARDIS development environment.

Rule locations:

  • Permanent Ruff rules: pyproject.toml

  • Non-permanent Ruff rules: .ruff.toml

Naming conventions: https://peps.python.org/pep-0008/#naming-conventions

Naming Conventions

We prefer names to be descriptive. For example, tardis/io/model/parse_density_configuration.py uses descriptive function names such as parse_density_section_config, parse_density_from_csvy, and calculate_power_law_density. These names are preferable to short generic names because they state the operation being performed and the model component being operated on.

While functions and methods should use a verb-noun style, class properties and variables should use a noun style. For example, calculate_power_law_density is a good function name, while power_law_density is a good variable or property name.

Explanation: Variable and Function Names

Use names that describe the scientific or codebase concept, not only the local implementation detail. Prefer names such as density_configuration, time_explosion, velocity_field_index, and density_field_index when those objects refer to specific model quantities or schema locations. Ensure that if in other parts of the codebase similar concepts exist, use those names.

If you use an object property e.g. packet.velocity, it is reasonable to assign it to velocity in a local scope if it is used more than once or twice.

Avoid names that are too generic for review, such as data, arr, x, or idx, unless the scope is very small and the meaning is obvious from the surrounding line.

Index vs. IDX in TARDIS

In the TARDIS codebase, we attempt to use index and idx as separate designations with different meanings. We use {var_name}_idx to specify that a variable refers to the integer position in some array-like object, with pythonic zero-indexed location (i.e., any pandas object accessed via .iloc would be done so with an idx variable). Alternatively, we use {var_name}_index as a variable that is explicitly not an integer location, but is still used for lookup via a key to a dictionary-like object (e.g., a pandas object accessed with .loc).

Consequently, an object named index in the tardis codebase will often store idx values. The dictionary-like is the index, and the values are idxs.

Good examples:

block_start_idx = opacity_state.macro_block_edge_index[
    absorbing_activation_level_idx
]
block_end_idx = opacity_state.macro_block_edge_index[
    absorbing_activation_level_idx + 1
]
emission_transition_probability = 0.0
probability_emission_event = np.random.random()

for deactivation_channel_idx in range(block_start_idx, block_end_idx):
    deactivation_probability = opacity_state.transition_probabilities[
        deactivation_channel_idx, current_shell_id
    ]
    emission_transition_probability += deactivation_probability

Type Hinting

Use type hints for all function definitions in new or touched code. Type hints improve readability and maintainability, and they enable static type checking.

from tardis.opacities.opacity_state_numba import (
    OpacityStateNumba,
)

def macro_atom_interaction(
    activation_level_id: int,
    current_shell_id: int,
    opacity_state: OpacityStateNumba,
):
    pass

Reference: NumPy Array Typing

Use numpy.typing.NDArray for typed NumPy array annotations in new or touched code when the function accepts or returns concrete NumPy arrays. NumPy provides numpy.typing.NDArray as a runtime-available alias for annotating arrays with a dtype and unspecified shape.

from typing import Any

import numpy as np
import numpy.typing as npt


def normalize(values: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
    return values / np.sum(values)


def as_array(values: npt.ArrayLike) -> npt.NDArray[Any]:
    return np.asarray(values)

Prefer typed arrays over bare np.ndarray.

def calculate_density_after_time(
    density_0: npt.NDArray[np.float64],
    time_0: float,
    time_explosion: float,
) -> npt.NDArray[np.float64]:
    ...

The typed NumPy API is stricter than runtime NumPy. For example, type checkers will discourage patterns that create object arrays accidentally or mutate array dtypes directly. If a function accepts broad array-like input, use npt.ArrayLike for the input and return a typed npt.NDArray[...] after conversion.

See the official NumPy typing reference: https://numpy.org/doc/stable/reference/typing.html