Skip to content

Train

hypertorch.train

LaTexTableConfig

Bases: TypedDict

Configuration for the LaTex table logger.

Attributes:

Name Type Description
table_caption NotRequired[str]

Caption for the LaTex table.

sort_by NotRequired[list[str]]

Per-column sorting criteria ("asc" or "des").

border NotRequired[bool]

Whether to include borders in the LaTex table.

Source code in hypertorch/train/latex_logger.py
class LaTexTableConfig(TypedDict):
    """
    Configuration for the LaTex table logger.

    Attributes:
        table_caption: Caption for the LaTex table.
        sort_by: Per-column sorting criteria ("asc" or "des").
        border: Whether to include borders in the LaTex table.
    """

    table_caption: NotRequired[str]
    sort_by: NotRequired[list[str]]
    border: NotRequired[bool]

LaTexTableLogger

Bases: Logger

A Lightning Logger that accumulates metrics and writes a LaTex comparison table.

Multiple instances (one per model) share a class-level store keyed by experiment_name. Every time finalize() is called (after fit() or test() for each model), the current state of all accumulated metrics is written to a LaTex file. The last model to finalize produces the most complete table.

This means the file is progressively updated as models finish training/testing, so you can open it mid-run to see partial results.

Source code in hypertorch/train/latex_logger.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
class LaTexTableLogger(Logger):
    """
    A Lightning Logger that accumulates metrics and writes a LaTex comparison table.

    Multiple instances (one per model) share a class-level store keyed by experiment_name.
    Every time finalize() is called (after fit() or test() for each model), the current
    state of all accumulated metrics is written to a LaTex file. The last model to
    finalize produces the most complete table.

    This means the file is progressively updated as models finish training/testing,
    so you can open it mid-run to see partial results.
    """

    # Class-level shared store: {experiment_name: {model_name: {metric_name: value}}}
    __shared_stores: ClassVar[dict[str, dict[str, dict[str, Any]]]] = {}

    def __init__(
        self,
        save_dir: str | Path,
        model_name: str,
        experiment_name: str,
        precision: int = 4,
        options: LaTexTableConfig | None = None,
    ) -> None:
        """
        Initialize the LaTex table logger.

        Args:
            save_dir: Base directory where comparison files are written.
            model_name: Full model name to use as the table row label.
            experiment_name: Shared key grouping models in the same experiment.
            precision: Decimal places for metric values.
            options: Optional table rendering configuration.
                Defaults to an empty dict configuration, if not provided.
        """
        super().__init__()
        validate_is_non_negative("precision", precision)

        self.__save_dir: str | Path = save_dir
        self.__model_name: str = model_name
        self.__experiment_name: str = experiment_name
        self.__precision: int = precision

        default_empty_options: LaTexTableConfig = {}
        self.__options: LaTexTableConfig = options if options is not None else default_empty_options

        if experiment_name not in self.__shared_stores:
            self.__shared_stores[experiment_name] = {}

    @property
    def name(self) -> str:
        """
        Return the logger name.

        Returns:
            name: Logger name.
        """
        return "LaTexTableLogger"

    @property
    def version(self) -> str | int:
        """
        Return the logger version.

        Returns:
            version: Model name used as the logger version.
        """
        return self.__model_name

    @property
    def store(self) -> dict[str, dict[str, Any]]:
        """Access the shared store for the current experiment."""
        return dict(self.__shared_stores.get(self.__experiment_name, {}))

    @property
    def save_dir(self) -> str | Path:
        """
        Return the logger save directory.

        Returns:
            save_dir: Base directory for comparison files.
        """
        return self.__save_dir

    @property
    def experiment_name(self) -> str | Path:
        """
        Return the experiment name.

        Returns:
            experiment_name: Shared experiment key.
        """
        return self.__experiment_name

    def clear(self, experiment_name: str) -> None:
        """
        Remove accumulated data for an experiment.

        Args:
            experiment_name: The experiment name whose data should be cleared.
        """
        self.__shared_stores.pop(experiment_name, None)

    def log_hyperparams(self, params: Any) -> None:
        """
        Accept hyperparameter logging calls from Lightning.

        Args:
            params: Hyperparameters provided by Lightning, unused.
        """
        pass

    def log_metrics(self, metrics: dict[str, Any], step: int | None = None) -> None:
        """Accumulate metrics for this model. Called by Lightning on every log step.

        Keeps only the latest value for each metric name. For example, if
        "val_auc" is logged at step 10 and step 20, only the step 20 value is kept.
        """
        store = self.__shared_stores[self.__experiment_name]
        if self.__model_name not in store:
            store[self.__model_name] = {}
        store[self.__model_name].update(metrics)

    def finalize(self, status: str) -> None:
        """Write the LaTex comparison table with all accumulated metrics so far.

        Called by Lightning after fit() and after test() for each model. Since models
        train/test sequentially, each finalize() overwrites the file with all data
        accumulated up to that point. The file grows more complete over time.
        """
        test_results, train_results, val_results = self.__split_results()
        if not test_results and not train_results and not val_results:
            return

        comparison_dir = Path(self.__save_dir) / "comparison"
        table_caption_opt = self.__options.get("table_caption")
        sort_by_opt = self.__options.get("sort_by")
        border_opt = self.__options.get("border")

        table_caption = table_caption_opt if isinstance(table_caption_opt, str) else None
        sort_by = sort_by_opt if isinstance(sort_by_opt, list) and sort_by_opt else ["asc"]
        border = border_opt if isinstance(border_opt, bool) else True

        self.__save_comparison_tables(
            test_results=test_results,
            save_dir=comparison_dir,
            train_results=train_results or None,
            val_results=val_results or None,
            precision=self.__precision,
            table_caption=table_caption,
            sort_by=sort_by,
            border=border,
        )
        # test
        self.__save_comparison_tables(
            test_results=test_results,
            save_dir=comparison_dir,
            train_results=None,
            val_results=None,
            precision=self.__precision,
            filename="test.tex",
            table_caption=table_caption,
            sort_by=sort_by,
            border=border,
        )
        # train
        self.__save_comparison_tables(
            test_results={},
            save_dir=comparison_dir,
            train_results=train_results or None,
            val_results=None,
            precision=self.__precision,
            filename="train.tex",
            table_caption=table_caption,
            sort_by=sort_by,
            border=border,
        )
        # val
        self.__save_comparison_tables(
            test_results={},
            save_dir=comparison_dir,
            train_results=None,
            val_results=val_results or None,
            precision=self.__precision,
            filename="val.tex",
            table_caption=table_caption,
            sort_by=sort_by,
            border=border,
        )

    def __build_comparison_table(
        self,
        sections_data: list[tuple[str, Mapping[str, Mapping[str, Any]]]],
        precision: int = 4,
        table_caption: str | None = None,
        sort_by: list[str] | None = None,
        border: bool = True,
    ) -> str:
        """
        Build a LaTex comparison table from grouped result sections.

        Args:
            sections_data: Section titles and per-model metric mappings.
            precision: Decimal places for metric values. Defaults to ``4``.
            table_caption: Optional table caption. Defaults to ``None``.
            sort_by: Optional per-metric sort directions. Defaults to ``None``.
            border: Whether to use bordered table rules. Defaults to ``True``.

        Returns:
            table: LaTex table content.
        """
        if not sections_data:
            return ""

        # One tabular must have fixed column count; use max needed across sections.
        max_metrics = max(len({m for mm in rs.values() for m in mm}) for _, rs in sections_data)
        total_cols = 1 + max_metrics
        if border:
            col_spec = "|".join(["l", *(["c"] * (total_cols - 1))])
        else:
            col_spec = "l" + "c" * (total_cols - 1)

        lines: list[str] = [
            rf"\begin{{tabular}}{{{col_spec}}}",
            r"\hline" if border else r"\toprule",
        ]

        for title, results in sections_data:
            lines.extend(
                self.__get_section_lines(title, results, total_cols, precision, sort_by, border)
            )

        # Replace the last section-ending rule with a final closing \hline.
        last_rule = r"\hline" if border else r"\midrule"
        final_rule = r"\hline"
        lines and lines[-1] == last_rule and lines.pop()
        (lines and lines[-1] == final_rule) or lines.append(final_rule)

        lines.append(r"\end{tabular}")
        table_lines: list[str] = [r"\begin{table}[htbp]", r"\centering"]

        if table_caption:
            escaped_caption = escape(table_caption, LATEX_CHARACTER_ESCAPE_TABLE)
            table_lines.append(rf"\caption{{{escaped_caption}}}")

        table_lines.extend(lines)
        table_lines.append(r"\end{table}")
        return "\n".join(table_lines) + "\n"

    def __get_section_lines(
        self,
        title: str,
        results: Mapping[str, Mapping[str, Any]],
        total_cols: int,
        precision: int,
        sort_by: list[str] | None,
        border: bool,
    ) -> list[str]:
        """
        Build LaTex table rows for a result section.

        Args:
            title: Section title.
            results: Mapping from model names to metric dictionaries.
            total_cols: Total number of columns in the shared table.
            precision: Decimal places for metric values.
            sort_by: Optional per-metric sort directions.
            border: Whether to use bordered table rules.

        Returns:
            lines: LaTex rows for the section.

        Raises:
            ValueError: If any sort direction is unsupported.
        """
        metrics = sorted({m for mm in results.values() for m in mm})
        sort_orders = sort_by or ["asc"]

        normalized_orders: list[str] = []
        for order in sort_orders:
            normalized = order.lower()
            if normalized not in ("asc", "des"):
                raise ValueError(f"Invalid 'sort_by' value: {order}. Use 'asc' or 'des'.")
            normalized_orders.append(normalized)

        metric_sort: dict[str, str] = {}
        for idx, metric in enumerate(metrics):
            metric_sort[metric] = (
                normalized_orders[idx] if idx < len(normalized_orders) else normalized_orders[-1]
            )

        metric_bounds = collect_metric_bounds(results, metrics)

        best_by_metric: dict[str, float] = {}

        for metric in metrics:
            vals = [
                metric_value
                for model_metrics in results.values()
                for metric_name, metric_value in model_metrics.items()
                if metric_name == metric and isinstance(metric_value, (int, float))
            ]
            if vals:
                best_by_metric[metric] = (
                    min(vals) if metric_sort.get(metric, "asc") == "asc" else max(vals)
                )

        header_cells = [
            "Model",
            *[escape(metric, LATEX_CHARACTER_ESCAPE_TABLE) for metric in metrics],
        ]
        while len(header_cells) < total_cols:
            header_cells.append("")

        escaped_title = escape(title, LATEX_CHARACTER_ESCAPE_TABLE)
        lines = [
            r"\addlinespace[3pt]",
            rf"\multicolumn{{{total_cols}}}{{c}}{{\textbf{{{escaped_title}}}}} \\",
            r"\midrule",
            " & ".join(header_cells) + r" \\",
        ]

        for model_name in sorted(results):
            model_metrics = results[model_name]
            row = [escape(model_name, LATEX_CHARACTER_ESCAPE_TABLE)]

            for metric in metrics:
                value = model_metrics.get(metric)
                if isinstance(value, (int, float)):
                    formatted = f"{value:.{precision}f}"
                    best = best_by_metric.get(metric)
                    if best is not None and value == best:
                        formatted = rf"\underline{{{formatted}}}"

                    row.append(
                        colorize_metric_value(
                            metric=metric,
                            value=float(value),
                            text=formatted,
                            metric_bounds=metric_bounds,
                            sort_order=metric_sort.get(metric, "asc"),
                        )
                    )
                else:
                    row.append("-")

            while len(row) < total_cols:
                row.append("")
            lines.append(" & ".join(row) + r" \\")

        lines.append(r"\hline" if border else r"\midrule")
        return lines

    def __save_comparison_tables(
        self,
        test_results: Mapping[str, Mapping[str, Any]],
        save_dir: str | Path,
        train_results: Mapping[str, Mapping[str, Any]] | None = None,
        val_results: Mapping[str, Mapping[str, Any]] | None = None,
        filename: str = "overall.tex",
        precision: int = 4,
        table_caption: str | None = None,
        sort_by: list[str] | None = None,
        border: bool = True,
    ) -> Path:
        """
        Build and save LaTex comparison tables to a file.

        Args:
            test_results: Mapping from model names to test metrics.
            save_dir: Directory where the LaTex file will be written.
            train_results: Optional mapping from model names to train metrics.
                Defaults to ``None``.
            val_results: Optional mapping from model names to validation metrics.
                Defaults to ``None``.
            filename: Name of the output file. Defaults to ``"overall.tex"``.
            precision: Decimal places for metric values. Defaults to ``4``.
            table_caption: Optional table caption. Defaults to ``None``.
            sort_by: Optional per-metric sort directions. Defaults to ``None``.
            border: Whether to use bordered table rules. Defaults to ``True``.

        Returns:
            path: Path to the written file.
        """
        sections_data: list[tuple[str, Mapping[str, Mapping[str, Any]]]] = []
        if test_results:
            sections_data.append(("Test Results", test_results))
        if train_results:
            sections_data.append(("Train Results", train_results))
        if val_results:
            sections_data.append(("Val Results", val_results))

        content = self.__build_comparison_table(
            sections_data=sections_data,
            precision=precision,
            border=border,
            table_caption=table_caption,
            sort_by=sort_by,
        )

        save_path = Path(save_dir)
        save_path.mkdir(parents=True, exist_ok=True)

        file_path = save_path / filename
        if content != "":
            content = (
                "% Requires: \\usepackage{booktabs}\n"
                "% Requires: \\usepackage[table]{xcolor}\n" + content
            )
        file_path.write_text(content)
        return file_path

    def __split_results(
        self,
    ) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
        """
        Split all accumulated metrics into test vs train/val groups.

        Metrics are classified by their name prefix:
        - "test/*"  -> test_results
        - "train/*" -> train_results
        - "val/*"   -> val_results
        - anything else (e.g., "epoch") -> ignored
        """
        store = self.__shared_stores.get(self.__experiment_name, {})
        test_results: dict[str, dict[str, Any]] = {}
        train_results: dict[str, dict[str, Any]] = {}
        val_results: dict[str, dict[str, Any]] = {}

        for model_name, metrics in store.items():
            test_metrics: dict[str, Any] = {}
            train_metrics: dict[str, Any] = {}
            val_metrics: dict[str, Any] = {}

            for metric_name, value in metrics.items():
                if metric_name.startswith("test/"):
                    test_metrics[metric_name] = value
                elif metric_name.startswith("train/"):
                    train_metrics[metric_name] = value
                elif metric_name.startswith("val/"):
                    val_metrics[metric_name] = value

            if test_metrics:
                test_results[model_name] = test_metrics
            if train_metrics:
                train_results[model_name] = train_metrics
            if val_metrics:
                val_results[model_name] = val_metrics

        return test_results, train_results, val_results

name property

Return the logger name.

Returns:

Name Type Description
name str

Logger name.

version property

Return the logger version.

Returns:

Name Type Description
version str | int

Model name used as the logger version.

store property

Access the shared store for the current experiment.

save_dir property

Return the logger save directory.

Returns:

Name Type Description
save_dir str | Path

Base directory for comparison files.

experiment_name property

Return the experiment name.

Returns:

Name Type Description
experiment_name str | Path

Shared experiment key.

__init__(save_dir, model_name, experiment_name, precision=4, options=None)

Initialize the LaTex table logger.

Parameters:

Name Type Description Default
save_dir str | Path

Base directory where comparison files are written.

required
model_name str

Full model name to use as the table row label.

required
experiment_name str

Shared key grouping models in the same experiment.

required
precision int

Decimal places for metric values.

4
options LaTexTableConfig | None

Optional table rendering configuration. Defaults to an empty dict configuration, if not provided.

None
Source code in hypertorch/train/latex_logger.py
def __init__(
    self,
    save_dir: str | Path,
    model_name: str,
    experiment_name: str,
    precision: int = 4,
    options: LaTexTableConfig | None = None,
) -> None:
    """
    Initialize the LaTex table logger.

    Args:
        save_dir: Base directory where comparison files are written.
        model_name: Full model name to use as the table row label.
        experiment_name: Shared key grouping models in the same experiment.
        precision: Decimal places for metric values.
        options: Optional table rendering configuration.
            Defaults to an empty dict configuration, if not provided.
    """
    super().__init__()
    validate_is_non_negative("precision", precision)

    self.__save_dir: str | Path = save_dir
    self.__model_name: str = model_name
    self.__experiment_name: str = experiment_name
    self.__precision: int = precision

    default_empty_options: LaTexTableConfig = {}
    self.__options: LaTexTableConfig = options if options is not None else default_empty_options

    if experiment_name not in self.__shared_stores:
        self.__shared_stores[experiment_name] = {}

clear(experiment_name)

Remove accumulated data for an experiment.

Parameters:

Name Type Description Default
experiment_name str

The experiment name whose data should be cleared.

required
Source code in hypertorch/train/latex_logger.py
def clear(self, experiment_name: str) -> None:
    """
    Remove accumulated data for an experiment.

    Args:
        experiment_name: The experiment name whose data should be cleared.
    """
    self.__shared_stores.pop(experiment_name, None)

log_hyperparams(params)

Accept hyperparameter logging calls from Lightning.

Parameters:

Name Type Description Default
params Any

Hyperparameters provided by Lightning, unused.

required
Source code in hypertorch/train/latex_logger.py
def log_hyperparams(self, params: Any) -> None:
    """
    Accept hyperparameter logging calls from Lightning.

    Args:
        params: Hyperparameters provided by Lightning, unused.
    """
    pass

log_metrics(metrics, step=None)

Accumulate metrics for this model. Called by Lightning on every log step.

Keeps only the latest value for each metric name. For example, if "val_auc" is logged at step 10 and step 20, only the step 20 value is kept.

Source code in hypertorch/train/latex_logger.py
def log_metrics(self, metrics: dict[str, Any], step: int | None = None) -> None:
    """Accumulate metrics for this model. Called by Lightning on every log step.

    Keeps only the latest value for each metric name. For example, if
    "val_auc" is logged at step 10 and step 20, only the step 20 value is kept.
    """
    store = self.__shared_stores[self.__experiment_name]
    if self.__model_name not in store:
        store[self.__model_name] = {}
    store[self.__model_name].update(metrics)

finalize(status)

Write the LaTex comparison table with all accumulated metrics so far.

Called by Lightning after fit() and after test() for each model. Since models train/test sequentially, each finalize() overwrites the file with all data accumulated up to that point. The file grows more complete over time.

Source code in hypertorch/train/latex_logger.py
def finalize(self, status: str) -> None:
    """Write the LaTex comparison table with all accumulated metrics so far.

    Called by Lightning after fit() and after test() for each model. Since models
    train/test sequentially, each finalize() overwrites the file with all data
    accumulated up to that point. The file grows more complete over time.
    """
    test_results, train_results, val_results = self.__split_results()
    if not test_results and not train_results and not val_results:
        return

    comparison_dir = Path(self.__save_dir) / "comparison"
    table_caption_opt = self.__options.get("table_caption")
    sort_by_opt = self.__options.get("sort_by")
    border_opt = self.__options.get("border")

    table_caption = table_caption_opt if isinstance(table_caption_opt, str) else None
    sort_by = sort_by_opt if isinstance(sort_by_opt, list) and sort_by_opt else ["asc"]
    border = border_opt if isinstance(border_opt, bool) else True

    self.__save_comparison_tables(
        test_results=test_results,
        save_dir=comparison_dir,
        train_results=train_results or None,
        val_results=val_results or None,
        precision=self.__precision,
        table_caption=table_caption,
        sort_by=sort_by,
        border=border,
    )
    # test
    self.__save_comparison_tables(
        test_results=test_results,
        save_dir=comparison_dir,
        train_results=None,
        val_results=None,
        precision=self.__precision,
        filename="test.tex",
        table_caption=table_caption,
        sort_by=sort_by,
        border=border,
    )
    # train
    self.__save_comparison_tables(
        test_results={},
        save_dir=comparison_dir,
        train_results=train_results or None,
        val_results=None,
        precision=self.__precision,
        filename="train.tex",
        table_caption=table_caption,
        sort_by=sort_by,
        border=border,
    )
    # val
    self.__save_comparison_tables(
        test_results={},
        save_dir=comparison_dir,
        train_results=None,
        val_results=val_results or None,
        precision=self.__precision,
        filename="val.tex",
        table_caption=table_caption,
        sort_by=sort_by,
        border=border,
    )

__build_comparison_table(sections_data, precision=4, table_caption=None, sort_by=None, border=True)

Build a LaTex comparison table from grouped result sections.

Parameters:

Name Type Description Default
sections_data list[tuple[str, Mapping[str, Mapping[str, Any]]]]

Section titles and per-model metric mappings.

required
precision int

Decimal places for metric values. Defaults to 4.

4
table_caption str | None

Optional table caption. Defaults to None.

None
sort_by list[str] | None

Optional per-metric sort directions. Defaults to None.

None
border bool

Whether to use bordered table rules. Defaults to True.

True

Returns:

Name Type Description
table str

LaTex table content.

Source code in hypertorch/train/latex_logger.py
def __build_comparison_table(
    self,
    sections_data: list[tuple[str, Mapping[str, Mapping[str, Any]]]],
    precision: int = 4,
    table_caption: str | None = None,
    sort_by: list[str] | None = None,
    border: bool = True,
) -> str:
    """
    Build a LaTex comparison table from grouped result sections.

    Args:
        sections_data: Section titles and per-model metric mappings.
        precision: Decimal places for metric values. Defaults to ``4``.
        table_caption: Optional table caption. Defaults to ``None``.
        sort_by: Optional per-metric sort directions. Defaults to ``None``.
        border: Whether to use bordered table rules. Defaults to ``True``.

    Returns:
        table: LaTex table content.
    """
    if not sections_data:
        return ""

    # One tabular must have fixed column count; use max needed across sections.
    max_metrics = max(len({m for mm in rs.values() for m in mm}) for _, rs in sections_data)
    total_cols = 1 + max_metrics
    if border:
        col_spec = "|".join(["l", *(["c"] * (total_cols - 1))])
    else:
        col_spec = "l" + "c" * (total_cols - 1)

    lines: list[str] = [
        rf"\begin{{tabular}}{{{col_spec}}}",
        r"\hline" if border else r"\toprule",
    ]

    for title, results in sections_data:
        lines.extend(
            self.__get_section_lines(title, results, total_cols, precision, sort_by, border)
        )

    # Replace the last section-ending rule with a final closing \hline.
    last_rule = r"\hline" if border else r"\midrule"
    final_rule = r"\hline"
    lines and lines[-1] == last_rule and lines.pop()
    (lines and lines[-1] == final_rule) or lines.append(final_rule)

    lines.append(r"\end{tabular}")
    table_lines: list[str] = [r"\begin{table}[htbp]", r"\centering"]

    if table_caption:
        escaped_caption = escape(table_caption, LATEX_CHARACTER_ESCAPE_TABLE)
        table_lines.append(rf"\caption{{{escaped_caption}}}")

    table_lines.extend(lines)
    table_lines.append(r"\end{table}")
    return "\n".join(table_lines) + "\n"

__get_section_lines(title, results, total_cols, precision, sort_by, border)

Build LaTex table rows for a result section.

Parameters:

Name Type Description Default
title str

Section title.

required
results Mapping[str, Mapping[str, Any]]

Mapping from model names to metric dictionaries.

required
total_cols int

Total number of columns in the shared table.

required
precision int

Decimal places for metric values.

required
sort_by list[str] | None

Optional per-metric sort directions.

required
border bool

Whether to use bordered table rules.

required

Returns:

Name Type Description
lines list[str]

LaTex rows for the section.

Raises:

Type Description
ValueError

If any sort direction is unsupported.

Source code in hypertorch/train/latex_logger.py
def __get_section_lines(
    self,
    title: str,
    results: Mapping[str, Mapping[str, Any]],
    total_cols: int,
    precision: int,
    sort_by: list[str] | None,
    border: bool,
) -> list[str]:
    """
    Build LaTex table rows for a result section.

    Args:
        title: Section title.
        results: Mapping from model names to metric dictionaries.
        total_cols: Total number of columns in the shared table.
        precision: Decimal places for metric values.
        sort_by: Optional per-metric sort directions.
        border: Whether to use bordered table rules.

    Returns:
        lines: LaTex rows for the section.

    Raises:
        ValueError: If any sort direction is unsupported.
    """
    metrics = sorted({m for mm in results.values() for m in mm})
    sort_orders = sort_by or ["asc"]

    normalized_orders: list[str] = []
    for order in sort_orders:
        normalized = order.lower()
        if normalized not in ("asc", "des"):
            raise ValueError(f"Invalid 'sort_by' value: {order}. Use 'asc' or 'des'.")
        normalized_orders.append(normalized)

    metric_sort: dict[str, str] = {}
    for idx, metric in enumerate(metrics):
        metric_sort[metric] = (
            normalized_orders[idx] if idx < len(normalized_orders) else normalized_orders[-1]
        )

    metric_bounds = collect_metric_bounds(results, metrics)

    best_by_metric: dict[str, float] = {}

    for metric in metrics:
        vals = [
            metric_value
            for model_metrics in results.values()
            for metric_name, metric_value in model_metrics.items()
            if metric_name == metric and isinstance(metric_value, (int, float))
        ]
        if vals:
            best_by_metric[metric] = (
                min(vals) if metric_sort.get(metric, "asc") == "asc" else max(vals)
            )

    header_cells = [
        "Model",
        *[escape(metric, LATEX_CHARACTER_ESCAPE_TABLE) for metric in metrics],
    ]
    while len(header_cells) < total_cols:
        header_cells.append("")

    escaped_title = escape(title, LATEX_CHARACTER_ESCAPE_TABLE)
    lines = [
        r"\addlinespace[3pt]",
        rf"\multicolumn{{{total_cols}}}{{c}}{{\textbf{{{escaped_title}}}}} \\",
        r"\midrule",
        " & ".join(header_cells) + r" \\",
    ]

    for model_name in sorted(results):
        model_metrics = results[model_name]
        row = [escape(model_name, LATEX_CHARACTER_ESCAPE_TABLE)]

        for metric in metrics:
            value = model_metrics.get(metric)
            if isinstance(value, (int, float)):
                formatted = f"{value:.{precision}f}"
                best = best_by_metric.get(metric)
                if best is not None and value == best:
                    formatted = rf"\underline{{{formatted}}}"

                row.append(
                    colorize_metric_value(
                        metric=metric,
                        value=float(value),
                        text=formatted,
                        metric_bounds=metric_bounds,
                        sort_order=metric_sort.get(metric, "asc"),
                    )
                )
            else:
                row.append("-")

        while len(row) < total_cols:
            row.append("")
        lines.append(" & ".join(row) + r" \\")

    lines.append(r"\hline" if border else r"\midrule")
    return lines

__save_comparison_tables(test_results, save_dir, train_results=None, val_results=None, filename='overall.tex', precision=4, table_caption=None, sort_by=None, border=True)

Build and save LaTex comparison tables to a file.

Parameters:

Name Type Description Default
test_results Mapping[str, Mapping[str, Any]]

Mapping from model names to test metrics.

required
save_dir str | Path

Directory where the LaTex file will be written.

required
train_results Mapping[str, Mapping[str, Any]] | None

Optional mapping from model names to train metrics. Defaults to None.

None
val_results Mapping[str, Mapping[str, Any]] | None

Optional mapping from model names to validation metrics. Defaults to None.

None
filename str

Name of the output file. Defaults to "overall.tex".

'overall.tex'
precision int

Decimal places for metric values. Defaults to 4.

4
table_caption str | None

Optional table caption. Defaults to None.

None
sort_by list[str] | None

Optional per-metric sort directions. Defaults to None.

None
border bool

Whether to use bordered table rules. Defaults to True.

True

Returns:

Name Type Description
path Path

Path to the written file.

Source code in hypertorch/train/latex_logger.py
def __save_comparison_tables(
    self,
    test_results: Mapping[str, Mapping[str, Any]],
    save_dir: str | Path,
    train_results: Mapping[str, Mapping[str, Any]] | None = None,
    val_results: Mapping[str, Mapping[str, Any]] | None = None,
    filename: str = "overall.tex",
    precision: int = 4,
    table_caption: str | None = None,
    sort_by: list[str] | None = None,
    border: bool = True,
) -> Path:
    """
    Build and save LaTex comparison tables to a file.

    Args:
        test_results: Mapping from model names to test metrics.
        save_dir: Directory where the LaTex file will be written.
        train_results: Optional mapping from model names to train metrics.
            Defaults to ``None``.
        val_results: Optional mapping from model names to validation metrics.
            Defaults to ``None``.
        filename: Name of the output file. Defaults to ``"overall.tex"``.
        precision: Decimal places for metric values. Defaults to ``4``.
        table_caption: Optional table caption. Defaults to ``None``.
        sort_by: Optional per-metric sort directions. Defaults to ``None``.
        border: Whether to use bordered table rules. Defaults to ``True``.

    Returns:
        path: Path to the written file.
    """
    sections_data: list[tuple[str, Mapping[str, Mapping[str, Any]]]] = []
    if test_results:
        sections_data.append(("Test Results", test_results))
    if train_results:
        sections_data.append(("Train Results", train_results))
    if val_results:
        sections_data.append(("Val Results", val_results))

    content = self.__build_comparison_table(
        sections_data=sections_data,
        precision=precision,
        border=border,
        table_caption=table_caption,
        sort_by=sort_by,
    )

    save_path = Path(save_dir)
    save_path.mkdir(parents=True, exist_ok=True)

    file_path = save_path / filename
    if content != "":
        content = (
            "% Requires: \\usepackage{booktabs}\n"
            "% Requires: \\usepackage[table]{xcolor}\n" + content
        )
    file_path.write_text(content)
    return file_path

__split_results()

Split all accumulated metrics into test vs train/val groups.

Metrics are classified by their name prefix: - "test/" -> test_results - "train/" -> train_results - "val/*" -> val_results - anything else (e.g., "epoch") -> ignored

Source code in hypertorch/train/latex_logger.py
def __split_results(
    self,
) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
    """
    Split all accumulated metrics into test vs train/val groups.

    Metrics are classified by their name prefix:
    - "test/*"  -> test_results
    - "train/*" -> train_results
    - "val/*"   -> val_results
    - anything else (e.g., "epoch") -> ignored
    """
    store = self.__shared_stores.get(self.__experiment_name, {})
    test_results: dict[str, dict[str, Any]] = {}
    train_results: dict[str, dict[str, Any]] = {}
    val_results: dict[str, dict[str, Any]] = {}

    for model_name, metrics in store.items():
        test_metrics: dict[str, Any] = {}
        train_metrics: dict[str, Any] = {}
        val_metrics: dict[str, Any] = {}

        for metric_name, value in metrics.items():
            if metric_name.startswith("test/"):
                test_metrics[metric_name] = value
            elif metric_name.startswith("train/"):
                train_metrics[metric_name] = value
            elif metric_name.startswith("val/"):
                val_metrics[metric_name] = value

        if test_metrics:
            test_results[model_name] = test_metrics
        if train_metrics:
            train_results[model_name] = train_metrics
        if val_metrics:
            val_results[model_name] = val_metrics

    return test_results, train_results, val_results

MarkdownTableLogger

Bases: Logger

A Lightning Logger that accumulates metrics and writes a markdown comparison table.

Multiple instances (one per model) share a class-level store keyed by experiment_name. Every time finalize() is called (after fit() or test() for each model), the current state of all accumulated metrics is written to a markdown file. The last model to finalize produces the most complete table.

This means the file is progressively updated as models finish training/testing, so partial results are available while running.

Source code in hypertorch/train/markdown_logger.py
class MarkdownTableLogger(Logger):
    """
    A Lightning Logger that accumulates metrics and writes a markdown comparison table.

    Multiple instances (one per model) share a class-level store keyed by experiment_name.
    Every time ``finalize()`` is called (after ``fit()`` or ``test()`` for each model), the current
    state of all accumulated metrics is written to a markdown file. The last model to
    finalize produces the most complete table.

    This means the file is progressively updated as models finish training/testing,
    so partial results are available while running.
    """

    # Class-level shared store: {experiment_name: {model_name: {metric_name: value}}}
    __shared_stores: ClassVar[dict[str, dict[str, dict[str, float]]]] = {}

    def __init__(
        self,
        save_dir: str | Path,
        model_name: str,
        experiment_name: str,
        precision: int = 4,
    ) -> None:
        """
        Initialize the markdown table logger.

        Args:
            save_dir: Base directory where comparison files are written.
            model_name: Full model name to use as the table row label.
            experiment_name: Shared key grouping models in the same experiment.
            precision: Decimal places for metric values. Default is ``4``.

        Raises:
            ValueError: If ``precision`` is negative.
        """
        super().__init__()
        validate_is_non_negative("precision", precision)

        self.__save_dir: str | Path = save_dir
        self.__model_name: str = model_name
        self.__experiment_name: str = experiment_name
        self.__precision: int = precision

        if experiment_name not in self.__shared_stores:
            self.__shared_stores[experiment_name] = {}

    @property
    def name(self) -> str:
        """
        Return the logger name.

        Returns:
            name: Logger name.
        """
        return "MarkdownTableLogger"

    @property
    def version(self) -> str | int:
        """
        Return the logger version.

        Returns:
            version: Model name used as the logger version.
        """
        return self.__model_name

    @property
    def store(self) -> dict[str, dict[str, float]]:
        """Access the shared store for the current experiment."""
        return copy.deepcopy(self.__shared_stores.get(self.__experiment_name, {}))

    @property
    def save_dir(self) -> str | Path:
        """
        Return the logger save directory.

        Returns:
            save_dir: Base directory for comparison files.
        """
        return self.__save_dir

    @property
    def experiment_name(self) -> str | Path:
        """
        Return the experiment name.

        Returns:
            experiment_name: Shared experiment key.
        """
        return self.__experiment_name

    def clear(self, experiment_name: str) -> None:
        """
        Remove accumulated data for an experiment.

        Args:
            experiment_name: The experiment name whose data should be cleared.
        """
        self.__shared_stores.pop(experiment_name, None)

    def log_hyperparams(self, params: Any) -> None:
        """
        Accept hyperparameter logging calls from Lightning.

        Args:
            params: Hyperparameters provided by Lightning, unused.
        """
        pass

    def log_metrics(self, metrics: dict[str, float], step: int | None = None) -> None:
        """
        Accumulate metrics for this model. Called by Lightning on every log step.

        Keeps only the latest value for each metric name. For example, if
        "val_auc" is logged at step 10 and step 20, only the step 20 value is kept.

        Args:
            metrics: Dictionary of metric names to values.
            step: Optional step number, unused. Defaults to ``None``.
        """
        store = self.__shared_stores[self.__experiment_name]
        if self.__model_name not in store:
            store[self.__model_name] = {}
        store[self.__model_name].update(metrics)

    def finalize(self, status: str) -> None:
        """
        Write the markdown comparison table with all accumulated metrics so far.

        Called by Lightning after fit() and after test() for each model. Since models
        train/test sequentially, each finalize() overwrites the file with all data
        accumulated up to that point. The file grows more complete over time.

        Args:
            status: The stage that just completed, unused. For example, "fit" or "test".
        """
        test_results, train_results, val_results = self.__split_results()

        if not test_results and not train_results and not val_results:
            return

        comparison_dir = Path(self.__save_dir) / "comparison"
        self.__save_comparison_tables(
            test_results=test_results,
            save_dir=comparison_dir,
            train_results=train_results or None,
            val_results=val_results or None,
            precision=self.__precision,
            filename="overall.md",
        )
        # test
        self.__save_comparison_tables(
            test_results=test_results,
            save_dir=comparison_dir,
            train_results=None,
            val_results=None,
            precision=self.__precision,
            filename="test.md",
        )
        # train
        self.__save_comparison_tables(
            test_results={},
            save_dir=comparison_dir,
            train_results=train_results or None,
            val_results=None,
            precision=self.__precision,
            filename="train.md",
        )
        # val
        self.__save_comparison_tables(
            test_results={},
            save_dir=comparison_dir,
            train_results=None,
            val_results=val_results or None,
            precision=self.__precision,
            filename="val.md",
        )

    def __build_comparison_table(
        self,
        results: Mapping[str, Mapping[str, float]],
        precision: int = 4,
    ) -> str:
        """
        Build a markdown comparison table from model results.

        Examples:
            Input:

            ```python
            {
            "mlp:mean": {"test_auc": 0.85, "test_loss": 0.32},
            "gat:default": {"test_auc": 0.82},
            }
            ```

            Output:

            ```md
            | Model | test_auc | test_loss |
            | --- | --- | --- |
            | gat:default | 0.8200 | - |
            | mlp:mean | 0.8500 | 0.3200 |
            ```

        Args:
            results: Mapping of model names to metric dictionaries.
            precision: Number of decimal places for numeric metric values.
                Defaults to ``4``.

        Returns:
            table: Markdown table string. Returns an empty string if ``results`` is empty.
        """
        if not results:
            return ""

        # Collect all unique metric names across all models, sorted for determinism
        all_metrics = sorted(
            {metric for model_metrics in results.values() for metric in model_metrics}
        )

        # Build header row
        escaped_metrics = [
            escape(metric, MARKDOWN_CHARACTER_ESCAPE_TABLE) for metric in all_metrics
        ]
        header = "| Model | " + " | ".join(escaped_metrics) + " |"
        separator = "| --- | " + " | ".join("---" for _ in all_metrics) + " |"

        # Build one row per model, sorted by model name for determinism
        rows = []
        for model_name in sorted(results):
            model_metrics = results[model_name]
            cells = []
            for metric in all_metrics:
                value = model_metrics.get(metric)
                if isinstance(value, (int, float)):
                    cells.append(f"{value:.{precision}f}")
                else:
                    cells.append("-")
            rows.append(
                f"| {escape(model_name, MARKDOWN_CHARACTER_ESCAPE_TABLE)} | "
                + " | ".join(cells)
                + " |"
            )

        return "\n".join([header, separator, *rows])

    def __save_comparison_tables(
        self,
        test_results: Mapping[str, Mapping[str, float]],
        save_dir: str | Path,
        train_results: Mapping[str, Mapping[str, float]] | None = None,
        val_results: Mapping[str, Mapping[str, float]] | None = None,
        filename: str = "results.md",
        precision: int = 4,
    ) -> Path:
        """
        Build and save markdown comparison tables to a file.

        Writes two sections:
        - "## Test Results" with the test metrics table
        - "## Train/Val Results" with the train/val metrics table (if provided)

        Args:
            test_results: Dict from test_all(), mapping model names to test metric dicts.
            save_dir: Directory where the markdown file will be written.
            train_results: Optional dict mapping model names to train metric dicts.
                Defaults to ``None``.
            val_results: Optional dict mapping model names to val metric dicts.
                Defaults to ``None``.
            filename: Name of the output file. Defaults to ``"results.md"``.
            precision: Decimal places for metric values. Defaults to ``4``.

        Returns:
            path: Path to the written file.
        """
        sections = []

        # Test results table
        test_table = self.__build_comparison_table(test_results, precision)
        if test_table:
            sections.append(f"## Test Results\n\n{test_table}")

        # Train/val results table
        if train_results or val_results:
            if train_results:
                train_table = self.__build_comparison_table(train_results, precision)
                sections.append(f"## Train Results\n\n{train_table}")
            if val_results:
                val_table = self.__build_comparison_table(val_results, precision)
                sections.append(f"## Val Results\n\n{val_table}")

        content = "\n\n".join(sections) + "\n" if sections else ""

        save_path = Path(save_dir)
        save_path.mkdir(parents=True, exist_ok=True)

        file_path = save_path / filename
        file_path.write_text(content)

        return file_path

    def __split_results(
        self,
    ) -> tuple[
        dict[str, dict[str, float]], dict[str, dict[str, float]], dict[str, dict[str, float]]
    ]:
        """
        Split all accumulated metrics into test vs train/val groups.

        Metrics are classified by their name prefix:
        - "test/*"  -> test_results
        - "train/*" -> train_results
        - "val/*"   -> val_results
        - anything else (e.g., "epoch") -> ignored

        Returns:
            results: Tuple of (test_results, train_results, val_results), where each is a dict
            mapping model names to their respective metric dicts. Models with no metrics
            in a category are excluded from that category's dict.
        """
        store = self.__shared_stores.get(self.__experiment_name, {})
        test_results: dict[str, dict[str, float]] = {}
        train_results: dict[str, dict[str, float]] = {}
        val_results: dict[str, dict[str, float]] = {}

        for model_name, metrics in store.items():
            test_metrics: dict[str, float] = {}
            train_metrics: dict[str, float] = {}
            val_metrics: dict[str, float] = {}

            for metric_name, value in metrics.items():
                if metric_name.startswith("test/"):
                    test_metrics[metric_name] = value
                elif metric_name.startswith("train/"):
                    train_metrics[metric_name] = value
                elif metric_name.startswith("val/"):
                    val_metrics[metric_name] = value

            if test_metrics:
                test_results[model_name] = test_metrics
            if train_metrics:
                train_results[model_name] = train_metrics
            if val_metrics:
                val_results[model_name] = val_metrics

        return test_results, train_results, val_results

name property

Return the logger name.

Returns:

Name Type Description
name str

Logger name.

version property

Return the logger version.

Returns:

Name Type Description
version str | int

Model name used as the logger version.

store property

Access the shared store for the current experiment.

save_dir property

Return the logger save directory.

Returns:

Name Type Description
save_dir str | Path

Base directory for comparison files.

experiment_name property

Return the experiment name.

Returns:

Name Type Description
experiment_name str | Path

Shared experiment key.

__init__(save_dir, model_name, experiment_name, precision=4)

Initialize the markdown table logger.

Parameters:

Name Type Description Default
save_dir str | Path

Base directory where comparison files are written.

required
model_name str

Full model name to use as the table row label.

required
experiment_name str

Shared key grouping models in the same experiment.

required
precision int

Decimal places for metric values. Default is 4.

4

Raises:

Type Description
ValueError

If precision is negative.

Source code in hypertorch/train/markdown_logger.py
def __init__(
    self,
    save_dir: str | Path,
    model_name: str,
    experiment_name: str,
    precision: int = 4,
) -> None:
    """
    Initialize the markdown table logger.

    Args:
        save_dir: Base directory where comparison files are written.
        model_name: Full model name to use as the table row label.
        experiment_name: Shared key grouping models in the same experiment.
        precision: Decimal places for metric values. Default is ``4``.

    Raises:
        ValueError: If ``precision`` is negative.
    """
    super().__init__()
    validate_is_non_negative("precision", precision)

    self.__save_dir: str | Path = save_dir
    self.__model_name: str = model_name
    self.__experiment_name: str = experiment_name
    self.__precision: int = precision

    if experiment_name not in self.__shared_stores:
        self.__shared_stores[experiment_name] = {}

clear(experiment_name)

Remove accumulated data for an experiment.

Parameters:

Name Type Description Default
experiment_name str

The experiment name whose data should be cleared.

required
Source code in hypertorch/train/markdown_logger.py
def clear(self, experiment_name: str) -> None:
    """
    Remove accumulated data for an experiment.

    Args:
        experiment_name: The experiment name whose data should be cleared.
    """
    self.__shared_stores.pop(experiment_name, None)

log_hyperparams(params)

Accept hyperparameter logging calls from Lightning.

Parameters:

Name Type Description Default
params Any

Hyperparameters provided by Lightning, unused.

required
Source code in hypertorch/train/markdown_logger.py
def log_hyperparams(self, params: Any) -> None:
    """
    Accept hyperparameter logging calls from Lightning.

    Args:
        params: Hyperparameters provided by Lightning, unused.
    """
    pass

log_metrics(metrics, step=None)

Accumulate metrics for this model. Called by Lightning on every log step.

Keeps only the latest value for each metric name. For example, if "val_auc" is logged at step 10 and step 20, only the step 20 value is kept.

Parameters:

Name Type Description Default
metrics dict[str, float]

Dictionary of metric names to values.

required
step int | None

Optional step number, unused. Defaults to None.

None
Source code in hypertorch/train/markdown_logger.py
def log_metrics(self, metrics: dict[str, float], step: int | None = None) -> None:
    """
    Accumulate metrics for this model. Called by Lightning on every log step.

    Keeps only the latest value for each metric name. For example, if
    "val_auc" is logged at step 10 and step 20, only the step 20 value is kept.

    Args:
        metrics: Dictionary of metric names to values.
        step: Optional step number, unused. Defaults to ``None``.
    """
    store = self.__shared_stores[self.__experiment_name]
    if self.__model_name not in store:
        store[self.__model_name] = {}
    store[self.__model_name].update(metrics)

finalize(status)

Write the markdown comparison table with all accumulated metrics so far.

Called by Lightning after fit() and after test() for each model. Since models train/test sequentially, each finalize() overwrites the file with all data accumulated up to that point. The file grows more complete over time.

Parameters:

Name Type Description Default
status str

The stage that just completed, unused. For example, "fit" or "test".

required
Source code in hypertorch/train/markdown_logger.py
def finalize(self, status: str) -> None:
    """
    Write the markdown comparison table with all accumulated metrics so far.

    Called by Lightning after fit() and after test() for each model. Since models
    train/test sequentially, each finalize() overwrites the file with all data
    accumulated up to that point. The file grows more complete over time.

    Args:
        status: The stage that just completed, unused. For example, "fit" or "test".
    """
    test_results, train_results, val_results = self.__split_results()

    if not test_results and not train_results and not val_results:
        return

    comparison_dir = Path(self.__save_dir) / "comparison"
    self.__save_comparison_tables(
        test_results=test_results,
        save_dir=comparison_dir,
        train_results=train_results or None,
        val_results=val_results or None,
        precision=self.__precision,
        filename="overall.md",
    )
    # test
    self.__save_comparison_tables(
        test_results=test_results,
        save_dir=comparison_dir,
        train_results=None,
        val_results=None,
        precision=self.__precision,
        filename="test.md",
    )
    # train
    self.__save_comparison_tables(
        test_results={},
        save_dir=comparison_dir,
        train_results=train_results or None,
        val_results=None,
        precision=self.__precision,
        filename="train.md",
    )
    # val
    self.__save_comparison_tables(
        test_results={},
        save_dir=comparison_dir,
        train_results=None,
        val_results=val_results or None,
        precision=self.__precision,
        filename="val.md",
    )

__build_comparison_table(results, precision=4)

Build a markdown comparison table from model results.

Examples:

Input:

{
"mlp:mean": {"test_auc": 0.85, "test_loss": 0.32},
"gat:default": {"test_auc": 0.82},
}

Output:

| Model | test_auc | test_loss |
| --- | --- | --- |
| gat:default | 0.8200 | - |
| mlp:mean | 0.8500 | 0.3200 |

Parameters:

Name Type Description Default
results Mapping[str, Mapping[str, float]]

Mapping of model names to metric dictionaries.

required
precision int

Number of decimal places for numeric metric values. Defaults to 4.

4

Returns:

Name Type Description
table str

Markdown table string. Returns an empty string if results is empty.

Source code in hypertorch/train/markdown_logger.py
def __build_comparison_table(
    self,
    results: Mapping[str, Mapping[str, float]],
    precision: int = 4,
) -> str:
    """
    Build a markdown comparison table from model results.

    Examples:
        Input:

        ```python
        {
        "mlp:mean": {"test_auc": 0.85, "test_loss": 0.32},
        "gat:default": {"test_auc": 0.82},
        }
        ```

        Output:

        ```md
        | Model | test_auc | test_loss |
        | --- | --- | --- |
        | gat:default | 0.8200 | - |
        | mlp:mean | 0.8500 | 0.3200 |
        ```

    Args:
        results: Mapping of model names to metric dictionaries.
        precision: Number of decimal places for numeric metric values.
            Defaults to ``4``.

    Returns:
        table: Markdown table string. Returns an empty string if ``results`` is empty.
    """
    if not results:
        return ""

    # Collect all unique metric names across all models, sorted for determinism
    all_metrics = sorted(
        {metric for model_metrics in results.values() for metric in model_metrics}
    )

    # Build header row
    escaped_metrics = [
        escape(metric, MARKDOWN_CHARACTER_ESCAPE_TABLE) for metric in all_metrics
    ]
    header = "| Model | " + " | ".join(escaped_metrics) + " |"
    separator = "| --- | " + " | ".join("---" for _ in all_metrics) + " |"

    # Build one row per model, sorted by model name for determinism
    rows = []
    for model_name in sorted(results):
        model_metrics = results[model_name]
        cells = []
        for metric in all_metrics:
            value = model_metrics.get(metric)
            if isinstance(value, (int, float)):
                cells.append(f"{value:.{precision}f}")
            else:
                cells.append("-")
        rows.append(
            f"| {escape(model_name, MARKDOWN_CHARACTER_ESCAPE_TABLE)} | "
            + " | ".join(cells)
            + " |"
        )

    return "\n".join([header, separator, *rows])

__save_comparison_tables(test_results, save_dir, train_results=None, val_results=None, filename='results.md', precision=4)

Build and save markdown comparison tables to a file.

Writes two sections: - "## Test Results" with the test metrics table - "## Train/Val Results" with the train/val metrics table (if provided)

Parameters:

Name Type Description Default
test_results Mapping[str, Mapping[str, float]]

Dict from test_all(), mapping model names to test metric dicts.

required
save_dir str | Path

Directory where the markdown file will be written.

required
train_results Mapping[str, Mapping[str, float]] | None

Optional dict mapping model names to train metric dicts. Defaults to None.

None
val_results Mapping[str, Mapping[str, float]] | None

Optional dict mapping model names to val metric dicts. Defaults to None.

None
filename str

Name of the output file. Defaults to "results.md".

'results.md'
precision int

Decimal places for metric values. Defaults to 4.

4

Returns:

Name Type Description
path Path

Path to the written file.

Source code in hypertorch/train/markdown_logger.py
def __save_comparison_tables(
    self,
    test_results: Mapping[str, Mapping[str, float]],
    save_dir: str | Path,
    train_results: Mapping[str, Mapping[str, float]] | None = None,
    val_results: Mapping[str, Mapping[str, float]] | None = None,
    filename: str = "results.md",
    precision: int = 4,
) -> Path:
    """
    Build and save markdown comparison tables to a file.

    Writes two sections:
    - "## Test Results" with the test metrics table
    - "## Train/Val Results" with the train/val metrics table (if provided)

    Args:
        test_results: Dict from test_all(), mapping model names to test metric dicts.
        save_dir: Directory where the markdown file will be written.
        train_results: Optional dict mapping model names to train metric dicts.
            Defaults to ``None``.
        val_results: Optional dict mapping model names to val metric dicts.
            Defaults to ``None``.
        filename: Name of the output file. Defaults to ``"results.md"``.
        precision: Decimal places for metric values. Defaults to ``4``.

    Returns:
        path: Path to the written file.
    """
    sections = []

    # Test results table
    test_table = self.__build_comparison_table(test_results, precision)
    if test_table:
        sections.append(f"## Test Results\n\n{test_table}")

    # Train/val results table
    if train_results or val_results:
        if train_results:
            train_table = self.__build_comparison_table(train_results, precision)
            sections.append(f"## Train Results\n\n{train_table}")
        if val_results:
            val_table = self.__build_comparison_table(val_results, precision)
            sections.append(f"## Val Results\n\n{val_table}")

    content = "\n\n".join(sections) + "\n" if sections else ""

    save_path = Path(save_dir)
    save_path.mkdir(parents=True, exist_ok=True)

    file_path = save_path / filename
    file_path.write_text(content)

    return file_path

__split_results()

Split all accumulated metrics into test vs train/val groups.

Metrics are classified by their name prefix: - "test/" -> test_results - "train/" -> train_results - "val/*" -> val_results - anything else (e.g., "epoch") -> ignored

Returns:

Name Type Description
results dict[str, dict[str, float]]

Tuple of (test_results, train_results, val_results), where each is a dict

dict[str, dict[str, float]]

mapping model names to their respective metric dicts. Models with no metrics

dict[str, dict[str, float]]

in a category are excluded from that category's dict.

Source code in hypertorch/train/markdown_logger.py
def __split_results(
    self,
) -> tuple[
    dict[str, dict[str, float]], dict[str, dict[str, float]], dict[str, dict[str, float]]
]:
    """
    Split all accumulated metrics into test vs train/val groups.

    Metrics are classified by their name prefix:
    - "test/*"  -> test_results
    - "train/*" -> train_results
    - "val/*"   -> val_results
    - anything else (e.g., "epoch") -> ignored

    Returns:
        results: Tuple of (test_results, train_results, val_results), where each is a dict
        mapping model names to their respective metric dicts. Models with no metrics
        in a category are excluded from that category's dict.
    """
    store = self.__shared_stores.get(self.__experiment_name, {})
    test_results: dict[str, dict[str, float]] = {}
    train_results: dict[str, dict[str, float]] = {}
    val_results: dict[str, dict[str, float]] = {}

    for model_name, metrics in store.items():
        test_metrics: dict[str, float] = {}
        train_metrics: dict[str, float] = {}
        val_metrics: dict[str, float] = {}

        for metric_name, value in metrics.items():
            if metric_name.startswith("test/"):
                test_metrics[metric_name] = value
            elif metric_name.startswith("train/"):
                train_metrics[metric_name] = value
            elif metric_name.startswith("val/"):
                val_metrics[metric_name] = value

        if test_metrics:
            test_results[model_name] = test_metrics
        if train_metrics:
            train_results[model_name] = train_metrics
        if val_metrics:
            val_results[model_name] = val_metrics

    return test_results, train_results, val_results

MultiModelTrainer

A trainer class to handle training multiple models with individual trainers.

Attributes:

Name Type Description
model_configs list[ModelConfig]

Model configurations managed by the trainer.

log_dir Path

Directory used for logs and default checkpoints.

auto_start_tensorboard bool

Whether TensorBoard should be started automatically.

tensorboard_port int

Port used by the auto-started TensorBoard server.

auto_wait bool

Whether finalize should wait before stopping TensorBoard.

Source code in hypertorch/train/trainer.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
class MultiModelTrainer:
    """
    A trainer class to handle training multiple models with individual trainers.

    Attributes:
        model_configs: Model configurations managed by the trainer.
        log_dir: Directory used for logs and default checkpoints.
        auto_start_tensorboard: Whether TensorBoard should be started automatically.
        tensorboard_port: Port used by the auto-started TensorBoard server.
        auto_wait: Whether ``finalize`` should wait before stopping TensorBoard.
    """

    DEFAULT_BASE_LOG_DIR = "hypertorch_logs"
    EXPERIMENT_NAME_PREFIX = "experiment"
    EXPERIMENT_SEPARATOR = "_"
    FIRST_EXPERIMENT_NUMBER = 0
    VERSION_NAME_PREFIX = "version"

    DEFAULT_BASE_CHECKPOINT_DIR = "checkpoints"

    __UNKNOWN_DEVICE = "unknown"

    def __init__(
        self,
        model_configs: list[ModelConfig],
        experiment_name: str | None = None,
        # args to pass to each Trainer
        accelerator: str | Accelerator = "auto",
        devices: list[int] | str | int = "auto",
        test_devices: list[int] | str | int | None = None,
        strategy: str | Strategy = "auto",
        num_nodes: int = 1,
        precision: Any
        | None = None,  # Any as Lightning accepts multiple types (int, str, Literal, etc.)
        max_epochs: int | None = None,
        min_epochs: int | None = None,
        max_steps: int = -1,
        min_steps: int | None = None,
        check_val_every_n_epoch: int | None = 1,
        logger: Logger | Iterable[Logger] | bool | None = None,
        default_root_dir: str | Path | None = None,
        enable_autolog_hparams: bool = True,
        log_every_n_steps: int | None = None,
        profiler: Profiler | str | None = None,
        fast_dev_run: int | bool = False,
        enable_checkpointing: bool = True,
        enable_progress_bar: bool = True,
        enable_model_summary: bool | None = None,
        callbacks: list[Callback] | Callback | None = None,
        checkpoint_callback_kwargs: dict[str, Any] | None = None,
        auto_start_tensorboard: bool = False,
        tensorboard_port: int = 6006,
        auto_wait: bool = False,
        **kwargs: Any,
    ) -> None:
        """
        Initialize trainers for one or more model configurations.

        Args:
            model_configs: A list of ModelConfig objects, each containing a model and its
                associated trainer (if any).
            experiment_name: Name for this experiment run's log directory. When ``None`` (default),
                auto-increments as ``experiment_0``, ``experiment_1``, etc. under
                the log root directory. Only used when ``logger`` is not provided.
            accelerator: Supports passing different accelerator types
                ("cpu", "gpu", "tpu", "hpu", "mps", "auto") as well as custom accelerator instances.
            devices: The devices to use. Can be set to a positive number (int or str), a
                sequence of device indices (list or str), the value ``-1`` to indicate all available
                devices should be used, or ``"auto"`` for automatic selection based on the chosen
                accelerator. Defaults to ``"auto"``.
            test_devices: Optional device configuration for automatically-created test trainers.
                When set, ``test_all`` uses a separate Trainer with the same Trainer parameters as
                training except for ``devices``. This is useful for running distributed training
                but single-device testing, e.g. ``test_devices=1``. Defaults to ``None``,
                which makes testing use the fit trainer unless a
                ``ModelConfig.test_trainer`` is provided.
            strategy: Supports different training strategies with aliases as well custom strategies.
                Defaults to ``"auto"``.
            num_nodes: Number of GPU nodes for distributed training. Defaults to ``1``.
            precision: Double precision (64, '64' or '64-true'),
                full precision (32, '32' or '32-true'), 16bit mixed precision (16, '16', '16-mixed')
                or bfloat16 mixed precision ('bf16', 'bf16-mixed').
                Can be used on CPU, GPU, TPUs, or HPUs. Defaults to ``'32-true'``.
            max_epochs: Stop training once this number of epochs is reached.
                Disabled by default (None). If both max_epochs and max_steps are not specified,
                defaults to ``max_epochs = 1000``. To enable infinite training,
                set ``max_epochs = -1``.
            min_epochs: Force training for at least these many epochs. Disabled by default (None).
            max_steps: Stop training after this number of steps. Disabled by default (-1).
                If ``max_steps = -1`` and ``max_epochs = None``, will default to
                ``max_epochs = 1000``. To enable infinite training, set ``max_epochs`` to ``-1``.
            min_steps: Force training for at least these number of steps.
                Disabled by default (``None``).
            check_val_every_n_epoch: Perform a validation loop after every `N` training epochs.
                If ``None``, validation will be done solely based on the number of training batches,
                requiring ``val_check_interval`` to be an integer value. When used together with a
                time-based ``val_check_interval`` and ``check_val_every_n_epoch`` > 1, validation is
                aligned to epoch multiples: if the interval elapses before the next multiple-N
                epoch, validation runs at the start of that epoch (after the first batch) and the
                timer resets; if it elapses during a multiple-N epoch, validation runs after the
                current batch. For ``None`` or ``1`` cases, the time-based behavior of
                ``val_check_interval`` applies without additional alignment. Defaults to ``1``.
            logger: Logger (or iterable collection of loggers) for experiment tracking. A ``True``
                value uses the default ``TensorBoardLogger`` if it is installed,
                otherwise ``CSVLogger``. ``False`` will disable logging. If multiple loggers are
                provided, local files (checkpoints, profiler traces, etc.) are saved in the
                ``log_dir`` of the first logger. Defaults to ``True``.
            default_root_dir: Default path for logs and weights when no logger/ckpt_callback passed.
                Defaults to ``os.getcwd()``.
                Can be remote file paths such as `s3://mybucket/path` or 'hdfs://path/'
            enable_autolog_hparams: Whether to log hyperparameters at the start of a run.
                Defaults to ``True``.
            log_every_n_steps: How often to log within steps.
                Defaults to ``50``.
            profiler: To profile individual steps during training and assist in
                identifying bottlenecks. Defaults to ``None``.
            fast_dev_run: Runs n if set to ``n`` (int) else 1 if set to ``True`` batch(es)
                of train, val and test to find any bugs (ie: a sort of unit test).
                Defaults to ``False``.
            enable_checkpointing: If ``True``, enable checkpointing.
                It will configure a default ModelCheckpoint callback if there is no user-defined
                    ModelCheckpoint in :paramref:`~hypertorch.train.MultiModelTrainer.callbacks`.
                Defaults to ``True``.
            enable_progress_bar: Whether to enable the progress bar by default.
                Defaults to ``True``.
            enable_model_summary: Whether to enable model summarization by default.
                Defaults to ``True``.
            callbacks: Add a callback or list of callbacks.
                Defaults to ``None``.
            checkpoint_callback_kwargs: Keyword arguments passed to the default
                ``ModelCheckpoint`` callback when checkpointing is enabled and no
                user-defined ``ModelCheckpoint`` is provided. Pass ``dirpath`` to
                override the default checkpoint directory.
                Defaults to ``None``.
            auto_start_tensorboard: When ``True`` and tensorboard is installed, automatically
                starts a TensorBoard server pointing at the experiment log directory.
                Using this option requires that TensorBoard is installed in the environment and
                moves control of the TensorBoard server lifecycle to the trainer, which will
                automatically terminate the server when the trainer is finalized
                (e.g., at the end of a ``with`` block or when the object is garbage collected).
                Enable `auto_wait` to keep the server alive after training completes so you can
                inspect results before the trainer is finalized. Defaults to ``False``.
            tensorboard_port: Port for the auto-launched TensorBoard server.
                Defaults to ``6006``.
            auto_wait: When ``True`` and a TensorBoard server is running, automatically calls
                ``wait`` inside `finalize` before terminating the server, so the user
                can inspect results before the process is stopped.
                Defaults to ``False``.

            kwargs:
                max_time: Maximum wall-clock training time. Passed to Lightning's ``Trainer``.
                limit_train_batches: Fraction or number of training batches to use.
                limit_val_batches: Fraction or number of validation batches to use.
                limit_test_batches: Fraction or number of test batches to use.
                limit_predict_batches: Fraction or number of prediction batches to use.
                overfit_batches: Fraction or number of batches to use for overfitting checks.
                val_check_interval: How often to run validation within a training epoch.
                num_sanity_val_steps: Number of validation batches to run before training starts.
                accumulate_grad_batches: Number of batches over which to accumulate gradients.
                gradient_clip_val: Value used for gradient clipping.
                gradient_clip_algorithm: Gradient clipping algorithm to use.
                deterministic: Whether to enable deterministic operations, or ``"warn"``
                    to warn when deterministic execution is not available.
                benchmark: Whether to enable cuDNN benchmarking.
                inference_mode: Whether validation, test, and prediction loops should use
                    ``torch.inference_mode``.
                use_distributed_sampler: Whether Lightning should inject distributed samplers for
                    distributed training.
                detect_anomaly: Whether to enable PyTorch autograd anomaly detection.
                barebones: Whether to disable features that may affect raw training-loop speed.
                plugins: Lightning plugins or list of plugins to use.
                sync_batchnorm: Whether to synchronize batch normalization across devices.
                reload_dataloaders_every_n_epochs: How often to recreate dataloaders.
                model_registry: Name of the Lightning model registry to use.

        Raises:
            ValueError: If no model configurations are provided or numeric settings are invalid.
        """
        self.auto_wait: bool = auto_wait
        self.__tensorboard_process: subprocess.Popen | None = None
        validate_is_non_negative("tensorboard_port", tensorboard_port)

        self.model_configs: list[ModelConfig] = model_configs
        validate_is_non_empty("model_configs", self.model_configs)

        self.log_dir: Path = self.__logdir(default_root_dir, experiment_name)

        self.auto_start_tensorboard: bool = auto_start_tensorboard
        self.tensorboard_port: int = tensorboard_port
        self.__checkpoint_callback_kwargs: dict[str, Any] = (
            checkpoint_callback_kwargs if checkpoint_callback_kwargs is not None else {}
        )

        full_model_name_counts = self.__full_model_name_counts(model_configs)
        for model_index, model_config in enumerate(model_configs):
            should_create_trainer = model_config.trainer is None
            should_create_test_trainer = (
                model_config.test_trainer is None and test_devices is not None
            )
            should_create_at_least_one_trainer = should_create_trainer or should_create_test_trainer

            if should_create_at_least_one_trainer:
                has_duplicate_full_model_name = (
                    full_model_name_counts[model_config.full_model_name()] > 1
                )
                model_logger = self.__setup_logger(model_config, logger)
                model_callbacks = self.__setup_callbacks(
                    model_config=model_config,
                    model_index=model_index,
                    has_duplicate_full_model_name=has_duplicate_full_model_name,
                    callbacks=callbacks,
                    enable_checkpointing=enable_checkpointing,
                )

                def __trainer_for(
                    trainer_devices: list[int] | str | int,
                    model_logger: Logger | Iterable[Logger] | bool | None,
                    model_callbacks: list[Callback] | Callback | None,
                ) -> L.Trainer:
                    return L.Trainer(
                        accelerator=accelerator,
                        devices=trainer_devices,
                        strategy=strategy,
                        num_nodes=num_nodes,
                        precision=precision,
                        max_epochs=max_epochs,
                        min_epochs=min_epochs,
                        max_steps=max_steps,
                        min_steps=min_steps,
                        check_val_every_n_epoch=check_val_every_n_epoch,
                        logger=model_logger,
                        default_root_dir=default_root_dir,
                        enable_autolog_hparams=enable_autolog_hparams,
                        log_every_n_steps=log_every_n_steps,
                        profiler=profiler,
                        fast_dev_run=fast_dev_run,
                        enable_checkpointing=enable_checkpointing,
                        enable_progress_bar=enable_progress_bar,
                        enable_model_summary=enable_model_summary,
                        callbacks=copy.deepcopy(model_callbacks),
                        **kwargs,
                    )

                if should_create_trainer:
                    model_config.trainer = __trainer_for(
                        trainer_devices=devices,
                        model_logger=model_logger,
                        model_callbacks=model_callbacks,
                    )

                if should_create_test_trainer:
                    model_config.test_trainer = __trainer_for(
                        trainer_devices=test_devices if test_devices is not None else devices,
                        model_logger=model_logger,
                        model_callbacks=model_callbacks,
                    )

        print(f"Initialized trainer(models: {len(model_configs)}, log_dir: {self.log_dir})")
        self.__auto_start_tensorboard_if_enabled()

    def __enter__(self) -> MultiModelTrainer:
        """
        Enter the trainer context manager.

        Returns:
            trainer: This trainer instance.
        """
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: Any,
    ) -> None:
        """
        Exit the trainer context manager and finalize resources.

        Args:
            exc_type: Exception type from the context, if any, unused.
            exc_val: Exception value from the context, if any, unused.
            exc_tb: Exception traceback from the context, if any, unused.
        """
        self.finalize()

    def __del__(self) -> None:
        """
        Finalize external resources during object cleanup.
        """
        try:
            self.finalize()
        except Exception as e:
            warnings.warn(
                f"Exception occurred during {self.__class__.__name__} cleanup. Error: {e}",
                category=UserWarning,
                stacklevel=2,
            )

    @property
    def distributed_global_rank(self) -> int:
        """
        Return the global rank of the current process.

        Returns:
            global_rank: The global rank of the current process.
        """
        if torch.distributed.is_available() and torch.distributed.is_initialized():
            return torch.distributed.get_rank()
        return 0  # if distributed not available or not initialized, we are in single-process setup

    @property
    def is_global_zero(self) -> bool:
        """
        Return whether the current process is the global zero process.

        Check first if any of the trainers is the global zero, which covers the case where
        a custom strategy that handles distributed differently was provided.
        If no trainer is available, fall back to checking the global rank.

        Returns:
            is_global_zero: ``True`` if the current process is the global zero process,
                ``False`` otherwise.
        """
        model_config = self.model_configs[0]  # It's a process-level property, so we can check any
        trainer = (
            model_config.trainer if model_config.trainer is not None else model_config.test_trainer
        )
        if trainer is not None:
            return trainer.is_global_zero
        return self.distributed_global_rank == 0

    @property
    def models(self) -> list[L.LightningModule]:
        """
        Return all configured Lightning modules.

        Returns:
            models: List of configured models.
        """
        return [config.model for config in self.model_configs]

    def model(self, name: str, version: str = "default") -> L.LightningModule | None:
        """
        Return a configured model by name and version.

        Args:
            name: Model name.
            version: Model version. Defaults to ``"default"``.

        Returns:
            model: Matching model, or ``None`` when no model matches.
        """
        for config in self.model_configs:
            if config.name == name and config.version == version:
                return config.model
        return None

    def fit_all(
        self,
        train_dataloader: DataLoader | None = None,
        val_dataloader: DataLoader | None = None,
        datamodule: L.LightningDataModule | None = None,
        ckpt_path: CkptStrategy | None = None,
        verbose: bool = True,
    ) -> None:
        """
        Fit every trainable configured model.

        Args:
            train_dataloader: Shared training dataloader used when a model has no override.
                Defaults to ``None``, which requires each model to have its own
                training dataloader provided.
            val_dataloader: Shared validation dataloader used when a model has no override.
                Defaults to ``None``, which requires each model to have its own
                validation dataloader provided.
            datamodule: Optional Lightning datamodule.
                Defaults to ``None``, which requires each model to have its own
                test dataloader provided.
            ckpt_path: Optional checkpoint strategy or path.
                Defaults to ``None``, which uses the best checkpoint if available,
                otherwise the last checkpoint.
            verbose: Whether to print progress messages. Defaults to ``True``.

        Raises:
            ValueError: If a trainable model has no trainer.
        """
        for i, config in enumerate(self.model_configs):
            if not config.is_trainable:
                if verbose:
                    print(
                        f"Skipping training for model {config.full_model_name()} "
                        f"[{i + 1}/{len(self.model_configs)} models] (is_trainable=False)"
                    )
                continue

            if config.trainer is None:
                raise ValueError(f"Trainer not defined for model {config.full_model_name()}.")

            if verbose:
                print(
                    f"Fit model {config.full_model_name()} "
                    f"[{i + 1}/{len(self.model_configs)} models] "
                    f"(device: {self.__device(config.trainer)}, "
                    f"log_dir: {config.trainer.log_dir}, "
                    f"ckpt_path: {ckpt_path if ckpt_path is not None else 'None'})"
                )

            train_dataloaders = (
                config.train_dataloader if config.train_dataloader is not None else train_dataloader
            )
            val_dataloaders = (
                config.val_dataloader if config.val_dataloader is not None else val_dataloader
            )
            config.trainer.fit(
                model=config.model,
                train_dataloaders=train_dataloaders,
                val_dataloaders=val_dataloaders,
                datamodule=datamodule,
                ckpt_path=ckpt_path,
            )

    def test_all(
        self,
        dataloader: DataLoader | None = None,
        datamodule: L.LightningDataModule | None = None,
        ckpt_path: CkptStrategy | None = None,
        verbose: bool = True,
        verbose_loop: bool = True,
    ) -> Mapping[str, TestResult]:
        """
        Test every configured model.

        Args:
            dataloader: Shared test dataloader used when a model has no override.
                Defaults to ``None``, which requires each model to have
                its own test dataloader provided.
            datamodule: Optional Lightning datamodule. Defaults to ``None``,
                which requires each model to have its own test dataloader provided.
            ckpt_path: Optional checkpoint strategy or path. Defaults to ``None``,
                which uses the best checkpoint if available, otherwise the last checkpoint.
            verbose: Whether to print model-level progress messages. Defaults to ``True``.
            verbose_loop: Whether Lightning should print test-loop output. Defaults to ``True``.

        Returns:
            test_results: Mapping from full model name to Lightning test metrics.

        Raises:
            ValueError: If a model has no trainer available for testing.
        """
        test_results: dict[str, TestResult] = {}
        for i, config in enumerate(self.model_configs):
            test_trainer = (
                config.test_trainer if config.test_trainer is not None else config.trainer
            )
            if test_trainer is None:
                raise ValueError(f"Trainer not defined for model {config.full_model_name()}.")

            if self.__is_separate_single_process_test_trainer(config, test_trainer):
                # Ensure all processes have finished training before testing
                # to avoid warning about sync_dist=True when no distributed is happening in test
                self.__wait_for_distributed()

            if self.__should_skip_test_on_current_rank(config, test_trainer):
                test_results[config.full_model_name()] = {}
                continue

            if verbose:
                print(
                    f"Test model {config.full_model_name()} "
                    f"[{i + 1}/{len(self.model_configs)} models] "
                    f"(device: {self.__device(test_trainer)}, "
                    f"log_dir: {test_trainer.log_dir}, "
                    f"ckpt_path: {ckpt_path if ckpt_path is not None else 'None'})"
                )

            test_dataloaders = (
                config.test_dataloader if config.test_dataloader is not None else dataloader
            )
            trainer_test_results: list[TestResult] = test_trainer.test(
                model=config.model,
                dataloaders=test_dataloaders,
                datamodule=datamodule,
                ckpt_path=ckpt_path,
                verbose=verbose_loop,
            )

            # In Lightning, test() returns a list of dicts, one per dataloader,
            # but we use a single dataloader
            test_results[config.full_model_name()] = (
                trainer_test_results[0] if len(trainer_test_results) > 0 else {}
            )

        return test_results

    def finalize(self) -> None:
        """
        Finalize trainer-managed external resources.
        """
        if self.auto_wait:
            self.wait()
        if self.__tensorboard_process is not None:
            self.__tensorboard_process.terminate()
            self.__tensorboard_process = None

    def wait(self) -> None:
        """
        Wait until the user presses Enter, keeping process alive.

        If no process is running, this method does nothing.
        """
        # For now, we only use this for waiting on TensorBoard, but this can be extended
        # to support waiting for other processes or conditions as needed
        if self.__tensorboard_process is None:
            return

        print(f"TensorBoard is running at http://localhost:{self.tensorboard_port}")

        try:
            input("Press Enter to stop...")
        except (KeyboardInterrupt, EOFError):
            print("Stopping TensorBoard...")

    def __auto_start_tensorboard_if_enabled(self) -> None:
        """
        Start TensorBoard when auto-start is enabled and available.
        """
        if self.auto_start_tensorboard:
            if self.__is_tensorboard_available():
                self.__tensorboard_process = self.__start_tensorboard_process()
            else:
                warnings.warn(
                    "TensorBoard is not available. "
                    "Install it with `pip install hypertorch[tensorboard]` or "
                    "`pip install tensorboard`"
                    "to enable auto-start.",
                    category=UserWarning,
                    stacklevel=2,
                )

    def __is_separate_single_process_test_trainer(
        self,
        model_config: ModelConfig,
        test_trainer: L.Trainer,
    ) -> bool:
        """
        Check whether testing uses a separate single-process trainer.

        Args:
            model_config: Model configuration being tested.
            test_trainer: Trainer selected for testing.

        Returns:
            result: ``True`` when a separate single-process test trainer is being used.
        """
        return (
            model_config.test_trainer is not None
            and test_trainer is not model_config.trainer
            and test_trainer.world_size <= 1
        )

    def __is_tensorboard_available(self) -> bool:
        """
        Check whether TensorBoard is importable.

        Returns:
            available: ``True`` when TensorBoard is installed.
        """
        return importlib.util.find_spec("tensorboard") is not None

    def __start_tensorboard_process(self) -> subprocess.Popen | None:
        """
        Start a TensorBoard subprocess for the trainer log directory.

        Returns:
            process: Started TensorBoard process, or ``None`` when startup is unavailable.
        """
        try:
            tensorboard_executable = shutil.which("tensorboard")
            if tensorboard_executable is None:
                return None

            log_dir = str(self.log_dir)
            tensorboard_port = str(self.tensorboard_port)
            process = subprocess.Popen(
                [tensorboard_executable, "--logdir", log_dir, "--port", tensorboard_port],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
            print(f"TensorBoard started at http://localhost:{tensorboard_port} (logdir={log_dir})")
            return process
        except Exception as e:
            warnings.warn(
                f"Proceeding without starting TensorBoard as it failed: {e}",
                category=UserWarning,
                stacklevel=2,
            )
            return None

    def __checkpoint_dir(
        self,
        model_config: ModelConfig,
        model_index: int,
        has_duplicate_full_model_name: bool,
    ) -> Path:
        """
        Resolve the checkpoint directory for a model configuration.

        Args:
            model_config: Model configuration that owns the checkpoints.
            model_index: Index of the model in ``model_configs``.
            has_duplicate_full_model_name: Whether the full model name is duplicated.

        Returns:
            checkpoint_dir: Directory for model checkpoints.
        """
        provided_dirpath: str | Path | None = self.__checkpoint_callback_kwargs.get("dirpath")
        if provided_dirpath is not None:
            return Path(provided_dirpath)

        checkpoint_dir = (
            self.log_dir / model_config.name / f"{self.VERSION_NAME_PREFIX}_{model_config.version}"
        )

        if has_duplicate_full_model_name:
            checkpoint_dir /= f"model_{model_index}"

        return checkpoint_dir / self.DEFAULT_BASE_CHECKPOINT_DIR

    def __device(self, trainer: L.Trainer) -> str:
        """
        Return the root device string for a trainer.

        Args:
            trainer: Lightning trainer to inspect.

        Returns:
            device: Root device string, or ``"unknown"`` when unavailable.
        """
        if trainer.strategy is None:
            return self.__UNKNOWN_DEVICE
        strategy = trainer.strategy
        if strategy.root_device is None:
            return self.__UNKNOWN_DEVICE
        return str(strategy.root_device)

    def __full_model_name_counts(self, model_configs: list[ModelConfig]) -> dict[str, int]:
        """
        Count occurrences of full model names.

        Args:
            model_configs: Model configurations to inspect.

        Returns:
            counts: Mapping from full model name to occurrence count.
        """
        full_model_name_counts: dict[str, int] = {}
        for model_config in model_configs:
            full_model_name = model_config.full_model_name()
            full_model_name_counts[full_model_name] = (
                full_model_name_counts.get(full_model_name, 0) + 1
            )
        return full_model_name_counts

    def __next_experiment_name(self, save_dir: Path) -> Path:
        """
        Return the next available experiment directory name.

        Args:
            save_dir: Base directory containing experiment directories.

        Returns:
            experiment_name: Next experiment directory name.
        """
        if not save_dir.exists():
            # Example: EXPERIMENT_NAME_PREFIX = "experiment",
            #          EXPERIMENT_SEPARATOR = "_",
            #          FIRST_EXPERIMENT_NUMBER = 0
            #          -> next_experiment_name = "experiment_0"
            return Path(
                f"{self.EXPERIMENT_NAME_PREFIX}{self.EXPERIMENT_SEPARATOR}{self.FIRST_EXPERIMENT_NUMBER}"
            )

        existing_experiment_names: list[str] = [
            dir.name
            for dir in save_dir.iterdir()
            if dir.is_dir() and dir.name.startswith(self.EXPERIMENT_NAME_PREFIX)
        ]
        if len(existing_experiment_names) < 1:
            return Path(
                f"{self.EXPERIMENT_NAME_PREFIX}{self.EXPERIMENT_SEPARATOR}{self.FIRST_EXPERIMENT_NUMBER}"
            )

        last_experiment_number = max(
            int(experiment_name.split(self.EXPERIMENT_SEPARATOR)[1])
            for experiment_name in existing_experiment_names
            if experiment_name.split(self.EXPERIMENT_SEPARATOR)[1].isdigit()
        )
        return Path(
            f"{self.EXPERIMENT_NAME_PREFIX}{self.EXPERIMENT_SEPARATOR}{last_experiment_number + 1}"
        )

    def __logdir(
        self,
        default_root_dir: str | Path | None,
        experiment_name: str | None,
    ) -> Path:
        """
        Resolve the log directory for this trainer instance.

        Args:
            default_root_dir: Optional root directory for logs.
            experiment_name: Optional explicit experiment directory name.

        Returns:
            log_dir: Resolved log directory.
        """
        base_dir = (
            Path(self.DEFAULT_BASE_LOG_DIR) if default_root_dir is None else Path(default_root_dir)
        )
        next_experiment_name = (
            self.__next_experiment_name(base_dir)
            if experiment_name is None
            else Path(experiment_name)
        )
        return base_dir / next_experiment_name

    def __setup_logger(
        self,
        model_config: ModelConfig,
        logger: Logger | Iterable[Logger] | bool | None,
    ) -> Logger | Iterable[Logger] | bool | None:
        """
        Resolve loggers for a model configuration.

        Args:
            model_config: Model configuration being prepared.
            logger: User-provided logger configuration.

        Returns:
            logger: User-provided logger configuration or default HyperTorch loggers.
        """
        if logger is not None:
            return logger

        experiment_name = str(self.__next_experiment_name(self.log_dir))

        loggers: list[Logger] = [
            CSVLogger(
                save_dir=self.log_dir,
                name=model_config.name,
                version=f"{self.VERSION_NAME_PREFIX}_{model_config.version}",
            ),
            MarkdownTableLogger(
                save_dir=self.log_dir,
                model_name=model_config.full_model_name(),
                experiment_name=experiment_name,
            ),
            LaTexTableLogger(
                save_dir=self.log_dir,
                model_name=model_config.full_model_name(),
                experiment_name=experiment_name,
                options={
                    "table_caption": "Results for Experiments",
                    "sort_by": ["des", "asc"],
                    "border": False,
                },
            ),
        ]

        if self.__is_tensorboard_available():
            from lightning.pytorch.loggers import TensorBoardLogger

            loggers.append(
                TensorBoardLogger(
                    save_dir=self.log_dir,
                    name=model_config.name,
                    version=f"{self.VERSION_NAME_PREFIX}_{model_config.version}",
                ),
            )

        return loggers

    def __setup_callbacks(
        self,
        model_config: ModelConfig,
        model_index: int,
        has_duplicate_full_model_name: bool,
        callbacks: list[Callback] | Callback | None,
        enable_checkpointing: bool,
    ) -> list[Callback] | Callback | None:
        """
        Resolve callbacks for a model configuration.

        Args:
            model_config: Model configuration being prepared.
            model_index: Index of the model in ``model_configs``.
            has_duplicate_full_model_name: Whether the full model name is duplicated.
            callbacks: User-provided callbacks.
            enable_checkpointing: Whether checkpointing is enabled.

        Returns:
            callbacks: Resolved callbacks for the trainer.
        """
        model_callbacks = copy.deepcopy(callbacks)

        if not enable_checkpointing:
            return model_callbacks

        checkpoint_dir = self.__checkpoint_dir(
            model_config=model_config,
            model_index=model_index,
            has_duplicate_full_model_name=has_duplicate_full_model_name,
        )
        callback_list = self.__to_callback_list(model_callbacks)
        checkpoint_callbacks = [
            callback for callback in callback_list if isinstance(callback, ModelCheckpoint)
        ]

        if len(checkpoint_callbacks) < 1:
            checkpoint_callback_kwargs = copy.deepcopy(self.__checkpoint_callback_kwargs)
            checkpoint_callback_kwargs["dirpath"] = checkpoint_dir
            callback_list.append(ModelCheckpoint(**checkpoint_callback_kwargs))
            return callback_list

        for callback in checkpoint_callbacks:
            if callback.dirpath is None:
                callback.dirpath = str(checkpoint_dir)
        return callback_list

    def __should_skip_test_on_current_rank(
        self,
        model_config: ModelConfig,
        test_trainer: L.Trainer,
    ) -> bool:
        """
        Check whether the current rank should skip testing.

        Args:
            model_config: Model configuration being tested.
            test_trainer: Trainer selected for testing.

        Returns:
            result: ``True`` when this process should skip the test call.
        """
        if not self.__is_separate_single_process_test_trainer(model_config, test_trainer):
            return False

        if model_config.trainer is not None:
            return not model_config.trainer.is_global_zero

        return self.distributed_global_rank != 0

    def __to_callback_list(self, callbacks: list[Callback] | Callback | None) -> list[Callback]:
        """
        Normalize callback configuration to a list.

        Args:
            callbacks: Optional callback, callback list, or ``None``.

        Returns:
            callbacks: Callback list.
        """
        if callbacks is None:
            return []
        if isinstance(callbacks, Callback):
            return [callbacks]
        return callbacks

    def __wait_for_distributed(self) -> None:
        """
        Synchronize and tear down an initialized distributed process group.
        """
        if torch.distributed.is_available() and torch.distributed.is_initialized():
            torch.distributed.barrier()
            torch.distributed.destroy_process_group()

distributed_global_rank property

Return the global rank of the current process.

Returns:

Name Type Description
global_rank int

The global rank of the current process.

is_global_zero property

Return whether the current process is the global zero process.

Check first if any of the trainers is the global zero, which covers the case where a custom strategy that handles distributed differently was provided. If no trainer is available, fall back to checking the global rank.

Returns:

Name Type Description
is_global_zero bool

True if the current process is the global zero process, False otherwise.

models property

Return all configured Lightning modules.

Returns:

Name Type Description
models list[LightningModule]

List of configured models.

__init__(model_configs, experiment_name=None, accelerator='auto', devices='auto', test_devices=None, strategy='auto', num_nodes=1, precision=None, max_epochs=None, min_epochs=None, max_steps=-1, min_steps=None, check_val_every_n_epoch=1, logger=None, default_root_dir=None, enable_autolog_hparams=True, log_every_n_steps=None, profiler=None, fast_dev_run=False, enable_checkpointing=True, enable_progress_bar=True, enable_model_summary=None, callbacks=None, checkpoint_callback_kwargs=None, auto_start_tensorboard=False, tensorboard_port=6006, auto_wait=False, **kwargs)

Initialize trainers for one or more model configurations.

Parameters:

Name Type Description Default
model_configs list[ModelConfig]

A list of ModelConfig objects, each containing a model and its associated trainer (if any).

required
experiment_name str | None

Name for this experiment run's log directory. When None (default), auto-increments as experiment_0, experiment_1, etc. under the log root directory. Only used when logger is not provided.

None
accelerator str | Accelerator

Supports passing different accelerator types ("cpu", "gpu", "tpu", "hpu", "mps", "auto") as well as custom accelerator instances.

'auto'
devices list[int] | str | int

The devices to use. Can be set to a positive number (int or str), a sequence of device indices (list or str), the value -1 to indicate all available devices should be used, or "auto" for automatic selection based on the chosen accelerator. Defaults to "auto".

'auto'
test_devices list[int] | str | int | None

Optional device configuration for automatically-created test trainers. When set, test_all uses a separate Trainer with the same Trainer parameters as training except for devices. This is useful for running distributed training but single-device testing, e.g. test_devices=1. Defaults to None, which makes testing use the fit trainer unless a ModelConfig.test_trainer is provided.

None
strategy str | Strategy

Supports different training strategies with aliases as well custom strategies. Defaults to "auto".

'auto'
num_nodes int

Number of GPU nodes for distributed training. Defaults to 1.

1
precision Any | None

Double precision (64, '64' or '64-true'), full precision (32, '32' or '32-true'), 16bit mixed precision (16, '16', '16-mixed') or bfloat16 mixed precision ('bf16', 'bf16-mixed'). Can be used on CPU, GPU, TPUs, or HPUs. Defaults to '32-true'.

None
max_epochs int | None

Stop training once this number of epochs is reached. Disabled by default (None). If both max_epochs and max_steps are not specified, defaults to max_epochs = 1000. To enable infinite training, set max_epochs = -1.

None
min_epochs int | None

Force training for at least these many epochs. Disabled by default (None).

None
max_steps int

Stop training after this number of steps. Disabled by default (-1). If max_steps = -1 and max_epochs = None, will default to max_epochs = 1000. To enable infinite training, set max_epochs to -1.

-1
min_steps int | None

Force training for at least these number of steps. Disabled by default (None).

None
check_val_every_n_epoch int | None

Perform a validation loop after every N training epochs. If None, validation will be done solely based on the number of training batches, requiring val_check_interval to be an integer value. When used together with a time-based val_check_interval and check_val_every_n_epoch > 1, validation is aligned to epoch multiples: if the interval elapses before the next multiple-N epoch, validation runs at the start of that epoch (after the first batch) and the timer resets; if it elapses during a multiple-N epoch, validation runs after the current batch. For None or 1 cases, the time-based behavior of val_check_interval applies without additional alignment. Defaults to 1.

1
logger Logger | Iterable[Logger] | bool | None

Logger (or iterable collection of loggers) for experiment tracking. A True value uses the default TensorBoardLogger if it is installed, otherwise CSVLogger. False will disable logging. If multiple loggers are provided, local files (checkpoints, profiler traces, etc.) are saved in the log_dir of the first logger. Defaults to True.

None
default_root_dir str | Path | None

Default path for logs and weights when no logger/ckpt_callback passed. Defaults to os.getcwd(). Can be remote file paths such as s3://mybucket/path or 'hdfs://path/'

None
enable_autolog_hparams bool

Whether to log hyperparameters at the start of a run. Defaults to True.

True
log_every_n_steps int | None

How often to log within steps. Defaults to 50.

None
profiler Profiler | str | None

To profile individual steps during training and assist in identifying bottlenecks. Defaults to None.

None
fast_dev_run int | bool

Runs n if set to n (int) else 1 if set to True batch(es) of train, val and test to find any bugs (ie: a sort of unit test). Defaults to False.

False
enable_checkpointing bool

If True, enable checkpointing. It will configure a default ModelCheckpoint callback if there is no user-defined ModelCheckpoint in :paramref:~hypertorch.train.MultiModelTrainer.callbacks. Defaults to True.

True
enable_progress_bar bool

Whether to enable the progress bar by default. Defaults to True.

True
enable_model_summary bool | None

Whether to enable model summarization by default. Defaults to True.

None
callbacks list[Callback] | Callback | None

Add a callback or list of callbacks. Defaults to None.

None
checkpoint_callback_kwargs dict[str, Any] | None

Keyword arguments passed to the default ModelCheckpoint callback when checkpointing is enabled and no user-defined ModelCheckpoint is provided. Pass dirpath to override the default checkpoint directory. Defaults to None.

None
auto_start_tensorboard bool

When True and tensorboard is installed, automatically starts a TensorBoard server pointing at the experiment log directory. Using this option requires that TensorBoard is installed in the environment and moves control of the TensorBoard server lifecycle to the trainer, which will automatically terminate the server when the trainer is finalized (e.g., at the end of a with block or when the object is garbage collected). Enable auto_wait to keep the server alive after training completes so you can inspect results before the trainer is finalized. Defaults to False.

False
tensorboard_port int

Port for the auto-launched TensorBoard server. Defaults to 6006.

6006
auto_wait bool

When True and a TensorBoard server is running, automatically calls wait inside finalize before terminating the server, so the user can inspect results before the process is stopped. Defaults to False.

False
kwargs Any

max_time: Maximum wall-clock training time. Passed to Lightning's Trainer. limit_train_batches: Fraction or number of training batches to use. limit_val_batches: Fraction or number of validation batches to use. limit_test_batches: Fraction or number of test batches to use. limit_predict_batches: Fraction or number of prediction batches to use. overfit_batches: Fraction or number of batches to use for overfitting checks. val_check_interval: How often to run validation within a training epoch. num_sanity_val_steps: Number of validation batches to run before training starts. accumulate_grad_batches: Number of batches over which to accumulate gradients. gradient_clip_val: Value used for gradient clipping. gradient_clip_algorithm: Gradient clipping algorithm to use. deterministic: Whether to enable deterministic operations, or "warn" to warn when deterministic execution is not available. benchmark: Whether to enable cuDNN benchmarking. inference_mode: Whether validation, test, and prediction loops should use torch.inference_mode. use_distributed_sampler: Whether Lightning should inject distributed samplers for distributed training. detect_anomaly: Whether to enable PyTorch autograd anomaly detection. barebones: Whether to disable features that may affect raw training-loop speed. plugins: Lightning plugins or list of plugins to use. sync_batchnorm: Whether to synchronize batch normalization across devices. reload_dataloaders_every_n_epochs: How often to recreate dataloaders. model_registry: Name of the Lightning model registry to use.

{}

Raises:

Type Description
ValueError

If no model configurations are provided or numeric settings are invalid.

Source code in hypertorch/train/trainer.py
def __init__(
    self,
    model_configs: list[ModelConfig],
    experiment_name: str | None = None,
    # args to pass to each Trainer
    accelerator: str | Accelerator = "auto",
    devices: list[int] | str | int = "auto",
    test_devices: list[int] | str | int | None = None,
    strategy: str | Strategy = "auto",
    num_nodes: int = 1,
    precision: Any
    | None = None,  # Any as Lightning accepts multiple types (int, str, Literal, etc.)
    max_epochs: int | None = None,
    min_epochs: int | None = None,
    max_steps: int = -1,
    min_steps: int | None = None,
    check_val_every_n_epoch: int | None = 1,
    logger: Logger | Iterable[Logger] | bool | None = None,
    default_root_dir: str | Path | None = None,
    enable_autolog_hparams: bool = True,
    log_every_n_steps: int | None = None,
    profiler: Profiler | str | None = None,
    fast_dev_run: int | bool = False,
    enable_checkpointing: bool = True,
    enable_progress_bar: bool = True,
    enable_model_summary: bool | None = None,
    callbacks: list[Callback] | Callback | None = None,
    checkpoint_callback_kwargs: dict[str, Any] | None = None,
    auto_start_tensorboard: bool = False,
    tensorboard_port: int = 6006,
    auto_wait: bool = False,
    **kwargs: Any,
) -> None:
    """
    Initialize trainers for one or more model configurations.

    Args:
        model_configs: A list of ModelConfig objects, each containing a model and its
            associated trainer (if any).
        experiment_name: Name for this experiment run's log directory. When ``None`` (default),
            auto-increments as ``experiment_0``, ``experiment_1``, etc. under
            the log root directory. Only used when ``logger`` is not provided.
        accelerator: Supports passing different accelerator types
            ("cpu", "gpu", "tpu", "hpu", "mps", "auto") as well as custom accelerator instances.
        devices: The devices to use. Can be set to a positive number (int or str), a
            sequence of device indices (list or str), the value ``-1`` to indicate all available
            devices should be used, or ``"auto"`` for automatic selection based on the chosen
            accelerator. Defaults to ``"auto"``.
        test_devices: Optional device configuration for automatically-created test trainers.
            When set, ``test_all`` uses a separate Trainer with the same Trainer parameters as
            training except for ``devices``. This is useful for running distributed training
            but single-device testing, e.g. ``test_devices=1``. Defaults to ``None``,
            which makes testing use the fit trainer unless a
            ``ModelConfig.test_trainer`` is provided.
        strategy: Supports different training strategies with aliases as well custom strategies.
            Defaults to ``"auto"``.
        num_nodes: Number of GPU nodes for distributed training. Defaults to ``1``.
        precision: Double precision (64, '64' or '64-true'),
            full precision (32, '32' or '32-true'), 16bit mixed precision (16, '16', '16-mixed')
            or bfloat16 mixed precision ('bf16', 'bf16-mixed').
            Can be used on CPU, GPU, TPUs, or HPUs. Defaults to ``'32-true'``.
        max_epochs: Stop training once this number of epochs is reached.
            Disabled by default (None). If both max_epochs and max_steps are not specified,
            defaults to ``max_epochs = 1000``. To enable infinite training,
            set ``max_epochs = -1``.
        min_epochs: Force training for at least these many epochs. Disabled by default (None).
        max_steps: Stop training after this number of steps. Disabled by default (-1).
            If ``max_steps = -1`` and ``max_epochs = None``, will default to
            ``max_epochs = 1000``. To enable infinite training, set ``max_epochs`` to ``-1``.
        min_steps: Force training for at least these number of steps.
            Disabled by default (``None``).
        check_val_every_n_epoch: Perform a validation loop after every `N` training epochs.
            If ``None``, validation will be done solely based on the number of training batches,
            requiring ``val_check_interval`` to be an integer value. When used together with a
            time-based ``val_check_interval`` and ``check_val_every_n_epoch`` > 1, validation is
            aligned to epoch multiples: if the interval elapses before the next multiple-N
            epoch, validation runs at the start of that epoch (after the first batch) and the
            timer resets; if it elapses during a multiple-N epoch, validation runs after the
            current batch. For ``None`` or ``1`` cases, the time-based behavior of
            ``val_check_interval`` applies without additional alignment. Defaults to ``1``.
        logger: Logger (or iterable collection of loggers) for experiment tracking. A ``True``
            value uses the default ``TensorBoardLogger`` if it is installed,
            otherwise ``CSVLogger``. ``False`` will disable logging. If multiple loggers are
            provided, local files (checkpoints, profiler traces, etc.) are saved in the
            ``log_dir`` of the first logger. Defaults to ``True``.
        default_root_dir: Default path for logs and weights when no logger/ckpt_callback passed.
            Defaults to ``os.getcwd()``.
            Can be remote file paths such as `s3://mybucket/path` or 'hdfs://path/'
        enable_autolog_hparams: Whether to log hyperparameters at the start of a run.
            Defaults to ``True``.
        log_every_n_steps: How often to log within steps.
            Defaults to ``50``.
        profiler: To profile individual steps during training and assist in
            identifying bottlenecks. Defaults to ``None``.
        fast_dev_run: Runs n if set to ``n`` (int) else 1 if set to ``True`` batch(es)
            of train, val and test to find any bugs (ie: a sort of unit test).
            Defaults to ``False``.
        enable_checkpointing: If ``True``, enable checkpointing.
            It will configure a default ModelCheckpoint callback if there is no user-defined
                ModelCheckpoint in :paramref:`~hypertorch.train.MultiModelTrainer.callbacks`.
            Defaults to ``True``.
        enable_progress_bar: Whether to enable the progress bar by default.
            Defaults to ``True``.
        enable_model_summary: Whether to enable model summarization by default.
            Defaults to ``True``.
        callbacks: Add a callback or list of callbacks.
            Defaults to ``None``.
        checkpoint_callback_kwargs: Keyword arguments passed to the default
            ``ModelCheckpoint`` callback when checkpointing is enabled and no
            user-defined ``ModelCheckpoint`` is provided. Pass ``dirpath`` to
            override the default checkpoint directory.
            Defaults to ``None``.
        auto_start_tensorboard: When ``True`` and tensorboard is installed, automatically
            starts a TensorBoard server pointing at the experiment log directory.
            Using this option requires that TensorBoard is installed in the environment and
            moves control of the TensorBoard server lifecycle to the trainer, which will
            automatically terminate the server when the trainer is finalized
            (e.g., at the end of a ``with`` block or when the object is garbage collected).
            Enable `auto_wait` to keep the server alive after training completes so you can
            inspect results before the trainer is finalized. Defaults to ``False``.
        tensorboard_port: Port for the auto-launched TensorBoard server.
            Defaults to ``6006``.
        auto_wait: When ``True`` and a TensorBoard server is running, automatically calls
            ``wait`` inside `finalize` before terminating the server, so the user
            can inspect results before the process is stopped.
            Defaults to ``False``.

        kwargs:
            max_time: Maximum wall-clock training time. Passed to Lightning's ``Trainer``.
            limit_train_batches: Fraction or number of training batches to use.
            limit_val_batches: Fraction or number of validation batches to use.
            limit_test_batches: Fraction or number of test batches to use.
            limit_predict_batches: Fraction or number of prediction batches to use.
            overfit_batches: Fraction or number of batches to use for overfitting checks.
            val_check_interval: How often to run validation within a training epoch.
            num_sanity_val_steps: Number of validation batches to run before training starts.
            accumulate_grad_batches: Number of batches over which to accumulate gradients.
            gradient_clip_val: Value used for gradient clipping.
            gradient_clip_algorithm: Gradient clipping algorithm to use.
            deterministic: Whether to enable deterministic operations, or ``"warn"``
                to warn when deterministic execution is not available.
            benchmark: Whether to enable cuDNN benchmarking.
            inference_mode: Whether validation, test, and prediction loops should use
                ``torch.inference_mode``.
            use_distributed_sampler: Whether Lightning should inject distributed samplers for
                distributed training.
            detect_anomaly: Whether to enable PyTorch autograd anomaly detection.
            barebones: Whether to disable features that may affect raw training-loop speed.
            plugins: Lightning plugins or list of plugins to use.
            sync_batchnorm: Whether to synchronize batch normalization across devices.
            reload_dataloaders_every_n_epochs: How often to recreate dataloaders.
            model_registry: Name of the Lightning model registry to use.

    Raises:
        ValueError: If no model configurations are provided or numeric settings are invalid.
    """
    self.auto_wait: bool = auto_wait
    self.__tensorboard_process: subprocess.Popen | None = None
    validate_is_non_negative("tensorboard_port", tensorboard_port)

    self.model_configs: list[ModelConfig] = model_configs
    validate_is_non_empty("model_configs", self.model_configs)

    self.log_dir: Path = self.__logdir(default_root_dir, experiment_name)

    self.auto_start_tensorboard: bool = auto_start_tensorboard
    self.tensorboard_port: int = tensorboard_port
    self.__checkpoint_callback_kwargs: dict[str, Any] = (
        checkpoint_callback_kwargs if checkpoint_callback_kwargs is not None else {}
    )

    full_model_name_counts = self.__full_model_name_counts(model_configs)
    for model_index, model_config in enumerate(model_configs):
        should_create_trainer = model_config.trainer is None
        should_create_test_trainer = (
            model_config.test_trainer is None and test_devices is not None
        )
        should_create_at_least_one_trainer = should_create_trainer or should_create_test_trainer

        if should_create_at_least_one_trainer:
            has_duplicate_full_model_name = (
                full_model_name_counts[model_config.full_model_name()] > 1
            )
            model_logger = self.__setup_logger(model_config, logger)
            model_callbacks = self.__setup_callbacks(
                model_config=model_config,
                model_index=model_index,
                has_duplicate_full_model_name=has_duplicate_full_model_name,
                callbacks=callbacks,
                enable_checkpointing=enable_checkpointing,
            )

            def __trainer_for(
                trainer_devices: list[int] | str | int,
                model_logger: Logger | Iterable[Logger] | bool | None,
                model_callbacks: list[Callback] | Callback | None,
            ) -> L.Trainer:
                return L.Trainer(
                    accelerator=accelerator,
                    devices=trainer_devices,
                    strategy=strategy,
                    num_nodes=num_nodes,
                    precision=precision,
                    max_epochs=max_epochs,
                    min_epochs=min_epochs,
                    max_steps=max_steps,
                    min_steps=min_steps,
                    check_val_every_n_epoch=check_val_every_n_epoch,
                    logger=model_logger,
                    default_root_dir=default_root_dir,
                    enable_autolog_hparams=enable_autolog_hparams,
                    log_every_n_steps=log_every_n_steps,
                    profiler=profiler,
                    fast_dev_run=fast_dev_run,
                    enable_checkpointing=enable_checkpointing,
                    enable_progress_bar=enable_progress_bar,
                    enable_model_summary=enable_model_summary,
                    callbacks=copy.deepcopy(model_callbacks),
                    **kwargs,
                )

            if should_create_trainer:
                model_config.trainer = __trainer_for(
                    trainer_devices=devices,
                    model_logger=model_logger,
                    model_callbacks=model_callbacks,
                )

            if should_create_test_trainer:
                model_config.test_trainer = __trainer_for(
                    trainer_devices=test_devices if test_devices is not None else devices,
                    model_logger=model_logger,
                    model_callbacks=model_callbacks,
                )

    print(f"Initialized trainer(models: {len(model_configs)}, log_dir: {self.log_dir})")
    self.__auto_start_tensorboard_if_enabled()

__enter__()

Enter the trainer context manager.

Returns:

Name Type Description
trainer MultiModelTrainer

This trainer instance.

Source code in hypertorch/train/trainer.py
def __enter__(self) -> MultiModelTrainer:
    """
    Enter the trainer context manager.

    Returns:
        trainer: This trainer instance.
    """
    return self

__exit__(exc_type, exc_val, exc_tb)

Exit the trainer context manager and finalize resources.

Parameters:

Name Type Description Default
exc_type type[BaseException] | None

Exception type from the context, if any, unused.

required
exc_val BaseException | None

Exception value from the context, if any, unused.

required
exc_tb Any

Exception traceback from the context, if any, unused.

required
Source code in hypertorch/train/trainer.py
def __exit__(
    self,
    exc_type: type[BaseException] | None,
    exc_val: BaseException | None,
    exc_tb: Any,
) -> None:
    """
    Exit the trainer context manager and finalize resources.

    Args:
        exc_type: Exception type from the context, if any, unused.
        exc_val: Exception value from the context, if any, unused.
        exc_tb: Exception traceback from the context, if any, unused.
    """
    self.finalize()

__del__()

Finalize external resources during object cleanup.

Source code in hypertorch/train/trainer.py
def __del__(self) -> None:
    """
    Finalize external resources during object cleanup.
    """
    try:
        self.finalize()
    except Exception as e:
        warnings.warn(
            f"Exception occurred during {self.__class__.__name__} cleanup. Error: {e}",
            category=UserWarning,
            stacklevel=2,
        )

model(name, version='default')

Return a configured model by name and version.

Parameters:

Name Type Description Default
name str

Model name.

required
version str

Model version. Defaults to "default".

'default'

Returns:

Name Type Description
model LightningModule | None

Matching model, or None when no model matches.

Source code in hypertorch/train/trainer.py
def model(self, name: str, version: str = "default") -> L.LightningModule | None:
    """
    Return a configured model by name and version.

    Args:
        name: Model name.
        version: Model version. Defaults to ``"default"``.

    Returns:
        model: Matching model, or ``None`` when no model matches.
    """
    for config in self.model_configs:
        if config.name == name and config.version == version:
            return config.model
    return None

fit_all(train_dataloader=None, val_dataloader=None, datamodule=None, ckpt_path=None, verbose=True)

Fit every trainable configured model.

Parameters:

Name Type Description Default
train_dataloader DataLoader | None

Shared training dataloader used when a model has no override. Defaults to None, which requires each model to have its own training dataloader provided.

None
val_dataloader DataLoader | None

Shared validation dataloader used when a model has no override. Defaults to None, which requires each model to have its own validation dataloader provided.

None
datamodule LightningDataModule | None

Optional Lightning datamodule. Defaults to None, which requires each model to have its own test dataloader provided.

None
ckpt_path CkptStrategy | None

Optional checkpoint strategy or path. Defaults to None, which uses the best checkpoint if available, otherwise the last checkpoint.

None
verbose bool

Whether to print progress messages. Defaults to True.

True

Raises:

Type Description
ValueError

If a trainable model has no trainer.

Source code in hypertorch/train/trainer.py
def fit_all(
    self,
    train_dataloader: DataLoader | None = None,
    val_dataloader: DataLoader | None = None,
    datamodule: L.LightningDataModule | None = None,
    ckpt_path: CkptStrategy | None = None,
    verbose: bool = True,
) -> None:
    """
    Fit every trainable configured model.

    Args:
        train_dataloader: Shared training dataloader used when a model has no override.
            Defaults to ``None``, which requires each model to have its own
            training dataloader provided.
        val_dataloader: Shared validation dataloader used when a model has no override.
            Defaults to ``None``, which requires each model to have its own
            validation dataloader provided.
        datamodule: Optional Lightning datamodule.
            Defaults to ``None``, which requires each model to have its own
            test dataloader provided.
        ckpt_path: Optional checkpoint strategy or path.
            Defaults to ``None``, which uses the best checkpoint if available,
            otherwise the last checkpoint.
        verbose: Whether to print progress messages. Defaults to ``True``.

    Raises:
        ValueError: If a trainable model has no trainer.
    """
    for i, config in enumerate(self.model_configs):
        if not config.is_trainable:
            if verbose:
                print(
                    f"Skipping training for model {config.full_model_name()} "
                    f"[{i + 1}/{len(self.model_configs)} models] (is_trainable=False)"
                )
            continue

        if config.trainer is None:
            raise ValueError(f"Trainer not defined for model {config.full_model_name()}.")

        if verbose:
            print(
                f"Fit model {config.full_model_name()} "
                f"[{i + 1}/{len(self.model_configs)} models] "
                f"(device: {self.__device(config.trainer)}, "
                f"log_dir: {config.trainer.log_dir}, "
                f"ckpt_path: {ckpt_path if ckpt_path is not None else 'None'})"
            )

        train_dataloaders = (
            config.train_dataloader if config.train_dataloader is not None else train_dataloader
        )
        val_dataloaders = (
            config.val_dataloader if config.val_dataloader is not None else val_dataloader
        )
        config.trainer.fit(
            model=config.model,
            train_dataloaders=train_dataloaders,
            val_dataloaders=val_dataloaders,
            datamodule=datamodule,
            ckpt_path=ckpt_path,
        )

test_all(dataloader=None, datamodule=None, ckpt_path=None, verbose=True, verbose_loop=True)

Test every configured model.

Parameters:

Name Type Description Default
dataloader DataLoader | None

Shared test dataloader used when a model has no override. Defaults to None, which requires each model to have its own test dataloader provided.

None
datamodule LightningDataModule | None

Optional Lightning datamodule. Defaults to None, which requires each model to have its own test dataloader provided.

None
ckpt_path CkptStrategy | None

Optional checkpoint strategy or path. Defaults to None, which uses the best checkpoint if available, otherwise the last checkpoint.

None
verbose bool

Whether to print model-level progress messages. Defaults to True.

True
verbose_loop bool

Whether Lightning should print test-loop output. Defaults to True.

True

Returns:

Name Type Description
test_results Mapping[str, TestResult]

Mapping from full model name to Lightning test metrics.

Raises:

Type Description
ValueError

If a model has no trainer available for testing.

Source code in hypertorch/train/trainer.py
def test_all(
    self,
    dataloader: DataLoader | None = None,
    datamodule: L.LightningDataModule | None = None,
    ckpt_path: CkptStrategy | None = None,
    verbose: bool = True,
    verbose_loop: bool = True,
) -> Mapping[str, TestResult]:
    """
    Test every configured model.

    Args:
        dataloader: Shared test dataloader used when a model has no override.
            Defaults to ``None``, which requires each model to have
            its own test dataloader provided.
        datamodule: Optional Lightning datamodule. Defaults to ``None``,
            which requires each model to have its own test dataloader provided.
        ckpt_path: Optional checkpoint strategy or path. Defaults to ``None``,
            which uses the best checkpoint if available, otherwise the last checkpoint.
        verbose: Whether to print model-level progress messages. Defaults to ``True``.
        verbose_loop: Whether Lightning should print test-loop output. Defaults to ``True``.

    Returns:
        test_results: Mapping from full model name to Lightning test metrics.

    Raises:
        ValueError: If a model has no trainer available for testing.
    """
    test_results: dict[str, TestResult] = {}
    for i, config in enumerate(self.model_configs):
        test_trainer = (
            config.test_trainer if config.test_trainer is not None else config.trainer
        )
        if test_trainer is None:
            raise ValueError(f"Trainer not defined for model {config.full_model_name()}.")

        if self.__is_separate_single_process_test_trainer(config, test_trainer):
            # Ensure all processes have finished training before testing
            # to avoid warning about sync_dist=True when no distributed is happening in test
            self.__wait_for_distributed()

        if self.__should_skip_test_on_current_rank(config, test_trainer):
            test_results[config.full_model_name()] = {}
            continue

        if verbose:
            print(
                f"Test model {config.full_model_name()} "
                f"[{i + 1}/{len(self.model_configs)} models] "
                f"(device: {self.__device(test_trainer)}, "
                f"log_dir: {test_trainer.log_dir}, "
                f"ckpt_path: {ckpt_path if ckpt_path is not None else 'None'})"
            )

        test_dataloaders = (
            config.test_dataloader if config.test_dataloader is not None else dataloader
        )
        trainer_test_results: list[TestResult] = test_trainer.test(
            model=config.model,
            dataloaders=test_dataloaders,
            datamodule=datamodule,
            ckpt_path=ckpt_path,
            verbose=verbose_loop,
        )

        # In Lightning, test() returns a list of dicts, one per dataloader,
        # but we use a single dataloader
        test_results[config.full_model_name()] = (
            trainer_test_results[0] if len(trainer_test_results) > 0 else {}
        )

    return test_results

finalize()

Finalize trainer-managed external resources.

Source code in hypertorch/train/trainer.py
def finalize(self) -> None:
    """
    Finalize trainer-managed external resources.
    """
    if self.auto_wait:
        self.wait()
    if self.__tensorboard_process is not None:
        self.__tensorboard_process.terminate()
        self.__tensorboard_process = None

wait()

Wait until the user presses Enter, keeping process alive.

If no process is running, this method does nothing.

Source code in hypertorch/train/trainer.py
def wait(self) -> None:
    """
    Wait until the user presses Enter, keeping process alive.

    If no process is running, this method does nothing.
    """
    # For now, we only use this for waiting on TensorBoard, but this can be extended
    # to support waiting for other processes or conditions as needed
    if self.__tensorboard_process is None:
        return

    print(f"TensorBoard is running at http://localhost:{self.tensorboard_port}")

    try:
        input("Press Enter to stop...")
    except (KeyboardInterrupt, EOFError):
        print("Stopping TensorBoard...")

__auto_start_tensorboard_if_enabled()

Start TensorBoard when auto-start is enabled and available.

Source code in hypertorch/train/trainer.py
def __auto_start_tensorboard_if_enabled(self) -> None:
    """
    Start TensorBoard when auto-start is enabled and available.
    """
    if self.auto_start_tensorboard:
        if self.__is_tensorboard_available():
            self.__tensorboard_process = self.__start_tensorboard_process()
        else:
            warnings.warn(
                "TensorBoard is not available. "
                "Install it with `pip install hypertorch[tensorboard]` or "
                "`pip install tensorboard`"
                "to enable auto-start.",
                category=UserWarning,
                stacklevel=2,
            )

__is_separate_single_process_test_trainer(model_config, test_trainer)

Check whether testing uses a separate single-process trainer.

Parameters:

Name Type Description Default
model_config ModelConfig

Model configuration being tested.

required
test_trainer Trainer

Trainer selected for testing.

required

Returns:

Name Type Description
result bool

True when a separate single-process test trainer is being used.

Source code in hypertorch/train/trainer.py
def __is_separate_single_process_test_trainer(
    self,
    model_config: ModelConfig,
    test_trainer: L.Trainer,
) -> bool:
    """
    Check whether testing uses a separate single-process trainer.

    Args:
        model_config: Model configuration being tested.
        test_trainer: Trainer selected for testing.

    Returns:
        result: ``True`` when a separate single-process test trainer is being used.
    """
    return (
        model_config.test_trainer is not None
        and test_trainer is not model_config.trainer
        and test_trainer.world_size <= 1
    )

__is_tensorboard_available()

Check whether TensorBoard is importable.

Returns:

Name Type Description
available bool

True when TensorBoard is installed.

Source code in hypertorch/train/trainer.py
def __is_tensorboard_available(self) -> bool:
    """
    Check whether TensorBoard is importable.

    Returns:
        available: ``True`` when TensorBoard is installed.
    """
    return importlib.util.find_spec("tensorboard") is not None

__start_tensorboard_process()

Start a TensorBoard subprocess for the trainer log directory.

Returns:

Name Type Description
process Popen | None

Started TensorBoard process, or None when startup is unavailable.

Source code in hypertorch/train/trainer.py
def __start_tensorboard_process(self) -> subprocess.Popen | None:
    """
    Start a TensorBoard subprocess for the trainer log directory.

    Returns:
        process: Started TensorBoard process, or ``None`` when startup is unavailable.
    """
    try:
        tensorboard_executable = shutil.which("tensorboard")
        if tensorboard_executable is None:
            return None

        log_dir = str(self.log_dir)
        tensorboard_port = str(self.tensorboard_port)
        process = subprocess.Popen(
            [tensorboard_executable, "--logdir", log_dir, "--port", tensorboard_port],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        print(f"TensorBoard started at http://localhost:{tensorboard_port} (logdir={log_dir})")
        return process
    except Exception as e:
        warnings.warn(
            f"Proceeding without starting TensorBoard as it failed: {e}",
            category=UserWarning,
            stacklevel=2,
        )
        return None

__checkpoint_dir(model_config, model_index, has_duplicate_full_model_name)

Resolve the checkpoint directory for a model configuration.

Parameters:

Name Type Description Default
model_config ModelConfig

Model configuration that owns the checkpoints.

required
model_index int

Index of the model in model_configs.

required
has_duplicate_full_model_name bool

Whether the full model name is duplicated.

required

Returns:

Name Type Description
checkpoint_dir Path

Directory for model checkpoints.

Source code in hypertorch/train/trainer.py
def __checkpoint_dir(
    self,
    model_config: ModelConfig,
    model_index: int,
    has_duplicate_full_model_name: bool,
) -> Path:
    """
    Resolve the checkpoint directory for a model configuration.

    Args:
        model_config: Model configuration that owns the checkpoints.
        model_index: Index of the model in ``model_configs``.
        has_duplicate_full_model_name: Whether the full model name is duplicated.

    Returns:
        checkpoint_dir: Directory for model checkpoints.
    """
    provided_dirpath: str | Path | None = self.__checkpoint_callback_kwargs.get("dirpath")
    if provided_dirpath is not None:
        return Path(provided_dirpath)

    checkpoint_dir = (
        self.log_dir / model_config.name / f"{self.VERSION_NAME_PREFIX}_{model_config.version}"
    )

    if has_duplicate_full_model_name:
        checkpoint_dir /= f"model_{model_index}"

    return checkpoint_dir / self.DEFAULT_BASE_CHECKPOINT_DIR

__device(trainer)

Return the root device string for a trainer.

Parameters:

Name Type Description Default
trainer Trainer

Lightning trainer to inspect.

required

Returns:

Name Type Description
device str

Root device string, or "unknown" when unavailable.

Source code in hypertorch/train/trainer.py
def __device(self, trainer: L.Trainer) -> str:
    """
    Return the root device string for a trainer.

    Args:
        trainer: Lightning trainer to inspect.

    Returns:
        device: Root device string, or ``"unknown"`` when unavailable.
    """
    if trainer.strategy is None:
        return self.__UNKNOWN_DEVICE
    strategy = trainer.strategy
    if strategy.root_device is None:
        return self.__UNKNOWN_DEVICE
    return str(strategy.root_device)

__full_model_name_counts(model_configs)

Count occurrences of full model names.

Parameters:

Name Type Description Default
model_configs list[ModelConfig]

Model configurations to inspect.

required

Returns:

Name Type Description
counts dict[str, int]

Mapping from full model name to occurrence count.

Source code in hypertorch/train/trainer.py
def __full_model_name_counts(self, model_configs: list[ModelConfig]) -> dict[str, int]:
    """
    Count occurrences of full model names.

    Args:
        model_configs: Model configurations to inspect.

    Returns:
        counts: Mapping from full model name to occurrence count.
    """
    full_model_name_counts: dict[str, int] = {}
    for model_config in model_configs:
        full_model_name = model_config.full_model_name()
        full_model_name_counts[full_model_name] = (
            full_model_name_counts.get(full_model_name, 0) + 1
        )
    return full_model_name_counts

__next_experiment_name(save_dir)

Return the next available experiment directory name.

Parameters:

Name Type Description Default
save_dir Path

Base directory containing experiment directories.

required

Returns:

Name Type Description
experiment_name Path

Next experiment directory name.

Source code in hypertorch/train/trainer.py
def __next_experiment_name(self, save_dir: Path) -> Path:
    """
    Return the next available experiment directory name.

    Args:
        save_dir: Base directory containing experiment directories.

    Returns:
        experiment_name: Next experiment directory name.
    """
    if not save_dir.exists():
        # Example: EXPERIMENT_NAME_PREFIX = "experiment",
        #          EXPERIMENT_SEPARATOR = "_",
        #          FIRST_EXPERIMENT_NUMBER = 0
        #          -> next_experiment_name = "experiment_0"
        return Path(
            f"{self.EXPERIMENT_NAME_PREFIX}{self.EXPERIMENT_SEPARATOR}{self.FIRST_EXPERIMENT_NUMBER}"
        )

    existing_experiment_names: list[str] = [
        dir.name
        for dir in save_dir.iterdir()
        if dir.is_dir() and dir.name.startswith(self.EXPERIMENT_NAME_PREFIX)
    ]
    if len(existing_experiment_names) < 1:
        return Path(
            f"{self.EXPERIMENT_NAME_PREFIX}{self.EXPERIMENT_SEPARATOR}{self.FIRST_EXPERIMENT_NUMBER}"
        )

    last_experiment_number = max(
        int(experiment_name.split(self.EXPERIMENT_SEPARATOR)[1])
        for experiment_name in existing_experiment_names
        if experiment_name.split(self.EXPERIMENT_SEPARATOR)[1].isdigit()
    )
    return Path(
        f"{self.EXPERIMENT_NAME_PREFIX}{self.EXPERIMENT_SEPARATOR}{last_experiment_number + 1}"
    )

__logdir(default_root_dir, experiment_name)

Resolve the log directory for this trainer instance.

Parameters:

Name Type Description Default
default_root_dir str | Path | None

Optional root directory for logs.

required
experiment_name str | None

Optional explicit experiment directory name.

required

Returns:

Name Type Description
log_dir Path

Resolved log directory.

Source code in hypertorch/train/trainer.py
def __logdir(
    self,
    default_root_dir: str | Path | None,
    experiment_name: str | None,
) -> Path:
    """
    Resolve the log directory for this trainer instance.

    Args:
        default_root_dir: Optional root directory for logs.
        experiment_name: Optional explicit experiment directory name.

    Returns:
        log_dir: Resolved log directory.
    """
    base_dir = (
        Path(self.DEFAULT_BASE_LOG_DIR) if default_root_dir is None else Path(default_root_dir)
    )
    next_experiment_name = (
        self.__next_experiment_name(base_dir)
        if experiment_name is None
        else Path(experiment_name)
    )
    return base_dir / next_experiment_name

__setup_logger(model_config, logger)

Resolve loggers for a model configuration.

Parameters:

Name Type Description Default
model_config ModelConfig

Model configuration being prepared.

required
logger Logger | Iterable[Logger] | bool | None

User-provided logger configuration.

required

Returns:

Name Type Description
logger Logger | Iterable[Logger] | bool | None

User-provided logger configuration or default HyperTorch loggers.

Source code in hypertorch/train/trainer.py
def __setup_logger(
    self,
    model_config: ModelConfig,
    logger: Logger | Iterable[Logger] | bool | None,
) -> Logger | Iterable[Logger] | bool | None:
    """
    Resolve loggers for a model configuration.

    Args:
        model_config: Model configuration being prepared.
        logger: User-provided logger configuration.

    Returns:
        logger: User-provided logger configuration or default HyperTorch loggers.
    """
    if logger is not None:
        return logger

    experiment_name = str(self.__next_experiment_name(self.log_dir))

    loggers: list[Logger] = [
        CSVLogger(
            save_dir=self.log_dir,
            name=model_config.name,
            version=f"{self.VERSION_NAME_PREFIX}_{model_config.version}",
        ),
        MarkdownTableLogger(
            save_dir=self.log_dir,
            model_name=model_config.full_model_name(),
            experiment_name=experiment_name,
        ),
        LaTexTableLogger(
            save_dir=self.log_dir,
            model_name=model_config.full_model_name(),
            experiment_name=experiment_name,
            options={
                "table_caption": "Results for Experiments",
                "sort_by": ["des", "asc"],
                "border": False,
            },
        ),
    ]

    if self.__is_tensorboard_available():
        from lightning.pytorch.loggers import TensorBoardLogger

        loggers.append(
            TensorBoardLogger(
                save_dir=self.log_dir,
                name=model_config.name,
                version=f"{self.VERSION_NAME_PREFIX}_{model_config.version}",
            ),
        )

    return loggers

__setup_callbacks(model_config, model_index, has_duplicate_full_model_name, callbacks, enable_checkpointing)

Resolve callbacks for a model configuration.

Parameters:

Name Type Description Default
model_config ModelConfig

Model configuration being prepared.

required
model_index int

Index of the model in model_configs.

required
has_duplicate_full_model_name bool

Whether the full model name is duplicated.

required
callbacks list[Callback] | Callback | None

User-provided callbacks.

required
enable_checkpointing bool

Whether checkpointing is enabled.

required

Returns:

Name Type Description
callbacks list[Callback] | Callback | None

Resolved callbacks for the trainer.

Source code in hypertorch/train/trainer.py
def __setup_callbacks(
    self,
    model_config: ModelConfig,
    model_index: int,
    has_duplicate_full_model_name: bool,
    callbacks: list[Callback] | Callback | None,
    enable_checkpointing: bool,
) -> list[Callback] | Callback | None:
    """
    Resolve callbacks for a model configuration.

    Args:
        model_config: Model configuration being prepared.
        model_index: Index of the model in ``model_configs``.
        has_duplicate_full_model_name: Whether the full model name is duplicated.
        callbacks: User-provided callbacks.
        enable_checkpointing: Whether checkpointing is enabled.

    Returns:
        callbacks: Resolved callbacks for the trainer.
    """
    model_callbacks = copy.deepcopy(callbacks)

    if not enable_checkpointing:
        return model_callbacks

    checkpoint_dir = self.__checkpoint_dir(
        model_config=model_config,
        model_index=model_index,
        has_duplicate_full_model_name=has_duplicate_full_model_name,
    )
    callback_list = self.__to_callback_list(model_callbacks)
    checkpoint_callbacks = [
        callback for callback in callback_list if isinstance(callback, ModelCheckpoint)
    ]

    if len(checkpoint_callbacks) < 1:
        checkpoint_callback_kwargs = copy.deepcopy(self.__checkpoint_callback_kwargs)
        checkpoint_callback_kwargs["dirpath"] = checkpoint_dir
        callback_list.append(ModelCheckpoint(**checkpoint_callback_kwargs))
        return callback_list

    for callback in checkpoint_callbacks:
        if callback.dirpath is None:
            callback.dirpath = str(checkpoint_dir)
    return callback_list

__should_skip_test_on_current_rank(model_config, test_trainer)

Check whether the current rank should skip testing.

Parameters:

Name Type Description Default
model_config ModelConfig

Model configuration being tested.

required
test_trainer Trainer

Trainer selected for testing.

required

Returns:

Name Type Description
result bool

True when this process should skip the test call.

Source code in hypertorch/train/trainer.py
def __should_skip_test_on_current_rank(
    self,
    model_config: ModelConfig,
    test_trainer: L.Trainer,
) -> bool:
    """
    Check whether the current rank should skip testing.

    Args:
        model_config: Model configuration being tested.
        test_trainer: Trainer selected for testing.

    Returns:
        result: ``True`` when this process should skip the test call.
    """
    if not self.__is_separate_single_process_test_trainer(model_config, test_trainer):
        return False

    if model_config.trainer is not None:
        return not model_config.trainer.is_global_zero

    return self.distributed_global_rank != 0

__to_callback_list(callbacks)

Normalize callback configuration to a list.

Parameters:

Name Type Description Default
callbacks list[Callback] | Callback | None

Optional callback, callback list, or None.

required

Returns:

Name Type Description
callbacks list[Callback]

Callback list.

Source code in hypertorch/train/trainer.py
def __to_callback_list(self, callbacks: list[Callback] | Callback | None) -> list[Callback]:
    """
    Normalize callback configuration to a list.

    Args:
        callbacks: Optional callback, callback list, or ``None``.

    Returns:
        callbacks: Callback list.
    """
    if callbacks is None:
        return []
    if isinstance(callbacks, Callback):
        return [callbacks]
    return callbacks

__wait_for_distributed()

Synchronize and tear down an initialized distributed process group.

Source code in hypertorch/train/trainer.py
def __wait_for_distributed(self) -> None:
    """
    Synchronize and tear down an initialized distributed process group.
    """
    if torch.distributed.is_available() and torch.distributed.is_initialized():
        torch.distributed.barrier()
        torch.distributed.destroy_process_group()

colorize_metric_value(metric, value, text, metric_bounds, sort_order)

Wrap a formatted metric value in a LaTex cell color command.

Parameters:

Name Type Description Default
metric str

Metric name.

required
value float

Numeric metric value.

required
text str

Already formatted metric text.

required
metric_bounds Mapping[str, tuple[float, float]] | None

Optional metric bounds used for color scaling.

required
sort_order str

"asc" when lower is better, or "des" when higher is better.

required

Returns:

Name Type Description
text str

Original or colorized LaTex cell text.

Raises:

Type Description
ValueError

If sort_order is unsupported.

Source code in hypertorch/train/latex_logger.py
def colorize_metric_value(
    metric: str,
    value: float,
    text: str,
    metric_bounds: Mapping[str, tuple[float, float]] | None,
    sort_order: str,
) -> str:
    """
    Wrap a formatted metric value in a LaTex cell color command.

    Args:
        metric: Metric name.
        value: Numeric metric value.
        text: Already formatted metric text.
        metric_bounds: Optional metric bounds used for color scaling.
        sort_order: ``"asc"`` when lower is better, or ``"des"`` when higher is better.

    Returns:
        text: Original or colorized LaTex cell text.

    Raises:
        ValueError: If ``sort_order`` is unsupported.
    """
    bounds = None if metric_bounds is None else metric_bounds.get(metric)
    if bounds is None:
        return text

    normalized_sort_order = sort_order.lower()
    if normalized_sort_order not in ("asc", "des"):
        raise ValueError(f"'sort_order' must be 'asc' or 'des', got {sort_order!r}.")

    min_metric_value, max_metric_value = bounds
    if max_metric_value == min_metric_value:
        quality = 1.0
    else:
        normalized_metric_value = (value - min_metric_value) / (
            max_metric_value - min_metric_value
        )  # 0..1, low->high
        quality = (
            (1.0 - normalized_metric_value)
            if normalized_sort_order == "asc"
            else normalized_metric_value
        )

    red = round((1.0 - quality) * 100)
    green = round(quality * 100)

    # Blend toward white to keep the gradient readable but less bright.
    soften = 0.35
    red_chan = round(((red / 100) * (1.0 - soften) + soften) * 255)
    green_chan = round(((green / 100) * (1.0 - soften) + soften) * 255)
    blue_chan = round(soften * 255)

    return rf"\cellcolor[HTML]{{{red_chan:02X}{green_chan:02X}{blue_chan:02X}}}{text}"