drytorch 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. drytorch/__init__.py +165 -0
  2. drytorch/contrib/__init__.py +1 -0
  3. drytorch/contrib/optuna.py +265 -0
  4. drytorch/contrib/torcheval.py +88 -0
  5. drytorch/contrib/torchmetrics.py +118 -0
  6. drytorch/core/__init__.py +1 -0
  7. drytorch/core/exceptions.py +1286 -0
  8. drytorch/core/experimenting.py +762 -0
  9. drytorch/core/log_events.py +387 -0
  10. drytorch/core/protocols.py +508 -0
  11. drytorch/core/registering.py +227 -0
  12. drytorch/core/tracking.py +447 -0
  13. drytorch/lib/__init__.py +1 -0
  14. drytorch/lib/accumulators.py +163 -0
  15. drytorch/lib/checkpoints.py +416 -0
  16. drytorch/lib/evaluations.py +96 -0
  17. drytorch/lib/gradient_ops.py +646 -0
  18. drytorch/lib/hooks.py +476 -0
  19. drytorch/lib/learning.py +163 -0
  20. drytorch/lib/loading.py +473 -0
  21. drytorch/lib/models.py +530 -0
  22. drytorch/lib/monitoring.py +809 -0
  23. drytorch/lib/objectives.py +1027 -0
  24. drytorch/lib/runners.py +417 -0
  25. drytorch/lib/schedulers.py +502 -0
  26. drytorch/lib/training.py +759 -0
  27. drytorch/py.typed +0 -0
  28. drytorch/trackers/__init__.py +1 -0
  29. drytorch/trackers/base_classes.py +839 -0
  30. drytorch/trackers/csv.py +309 -0
  31. drytorch/trackers/hydra.py +120 -0
  32. drytorch/trackers/logging.py +433 -0
  33. drytorch/trackers/matplotlib.py +275 -0
  34. drytorch/trackers/plotly.py +98 -0
  35. drytorch/trackers/sqlalchemy.py +715 -0
  36. drytorch/trackers/tensorboard.py +180 -0
  37. drytorch/trackers/tqdm.py +300 -0
  38. drytorch/trackers/wandb.py +231 -0
  39. drytorch/trackers/yaml.py +235 -0
  40. drytorch/utils/__init__.py +1 -0
  41. drytorch/utils/apply_ops.py +153 -0
  42. drytorch/utils/averages.py +86 -0
  43. drytorch/utils/local_ops.py +151 -0
  44. drytorch/utils/repr_utils.py +575 -0
  45. drytorch-0.1.0.dist-info/METADATA +170 -0
  46. drytorch-0.1.0.dist-info/RECORD +48 -0
  47. drytorch-0.1.0.dist-info/WHEEL +4 -0
  48. drytorch-0.1.0.dist-info/licenses/LICENSE +21 -0
drytorch/__init__.py ADDED
@@ -0,0 +1,165 @@
1
+ """Package initialization and default tracker setup.
2
+
3
+ It automatically initializes some trackers with sets of settings (modes) that
4
+ work well together. The mode can be set as an environmental variable
5
+ DRYTORCH_INIT_MODE before loading the package or explicitly reset after.
6
+ The DRYTORCH_STRICT environment variable turns reproducibility warnings into
7
+ errors when set to 1, requiring a readable commit.
8
+
9
+ Available modes:
10
+ 1) standard: log to stderr, preferring tqdm over the built-in logger.
11
+ 2) hydra: log to stdout and accommodate default Hydra settings.
12
+ 3) minimal: reduce output and avoid dumping metadata.
13
+ 4) none: no tracker is added to the default ones.
14
+
15
+ Attributes:
16
+ INIT_MODE: the mode the trackers will be initialized with at the start.
17
+ If DRYTORCH_INIT_MODE is not present, it defaults to "standard".
18
+ """
19
+
20
+ import importlib.metadata
21
+ import logging
22
+ import os
23
+ import sys
24
+ import warnings
25
+
26
+ from typing import Literal, TypeGuard
27
+
28
+ from drytorch.core import exceptions
29
+ from drytorch.core.exceptions import FailedOptionalImportWarning
30
+ from drytorch.core.experimenting import Experiment
31
+ from drytorch.core.tracking import (
32
+ Tracker,
33
+ extend_default_trackers,
34
+ remove_all_default_trackers,
35
+ )
36
+ from drytorch.lib.evaluations import Evaluation, Test, Validation
37
+ from drytorch.lib.learning import LearningSchema
38
+ from drytorch.lib.loading import DataLoader
39
+ from drytorch.lib.models import Model
40
+ from drytorch.lib.objectives import Loss, Metric
41
+ from drytorch.lib.training import Trainer
42
+ from drytorch.trackers import logging as builtin_logging
43
+ from drytorch.trackers.logging import INFO_LEVELS
44
+
45
+
46
+ __version__ = importlib.metadata.version('drytorch')
47
+
48
+ __all__ = [
49
+ 'DataLoader',
50
+ 'Evaluation',
51
+ 'Experiment',
52
+ 'LearningSchema',
53
+ 'Loss',
54
+ 'Metric',
55
+ 'Model',
56
+ 'Test',
57
+ 'Trainer',
58
+ 'Validation',
59
+ 'extend_default_trackers',
60
+ 'init_trackers',
61
+ 'remove_all_default_trackers',
62
+ ]
63
+
64
+ _InitMode = Literal['standard', 'hydra', 'minimal']
65
+
66
+ logger = logging.getLogger('drytorch')
67
+
68
+
69
+ def init_trackers(mode: _InitMode = 'standard') -> None:
70
+ """Initialize trackers used by default during the experiment.
71
+
72
+ Three initializations are available:
73
+ 1) standard: log to stderr, preferring tqdm over the built-in logger.
74
+ 2) hydra: log to stdout and accommodate default Hydra settings.
75
+ 3) minimal: reduce output and avoid dumping metadata.
76
+
77
+ Args:
78
+ mode: one of the suggested initialization modes.
79
+
80
+ Raises:
81
+ ValueError if mode is not available.
82
+ """
83
+ remove_all_default_trackers()
84
+ tracker_list: list[Tracker] = [builtin_logging.BuiltinLogger()]
85
+ is_tqdm_installed = _add_tqdm(tracker_list, mode=mode)
86
+ if mode == 'hydra':
87
+ # hydra logs to stdout by default
88
+ builtin_logging.enable_default_handler(sys.stdout)
89
+ builtin_logging.enable_propagation()
90
+ _add_yaml(tracker_list)
91
+ verbosity = builtin_logging.INFO_LEVELS.metrics
92
+
93
+ elif mode == 'minimal':
94
+ if is_tqdm_installed:
95
+ verbosity = builtin_logging.INFO_LEVELS.training
96
+ else:
97
+ verbosity = builtin_logging.INFO_LEVELS.epoch
98
+ builtin_logging.set_formatter('progress')
99
+
100
+ elif mode == 'standard':
101
+ _add_yaml(tracker_list)
102
+ if is_tqdm_installed:
103
+ verbosity = builtin_logging.INFO_LEVELS.epoch
104
+ else:
105
+ verbosity = builtin_logging.INFO_LEVELS.metrics
106
+
107
+ else:
108
+ raise ValueError('Mode {mode} not available.')
109
+
110
+ extend_default_trackers(tracker_list)
111
+ builtin_logging.set_verbosity(verbosity)
112
+ return
113
+
114
+
115
+ def _add_tqdm(tracker_list: list[Tracker], mode: _InitMode) -> bool:
116
+ try:
117
+ from drytorch.trackers import tqdm
118
+ except (ImportError, ModuleNotFoundError):
119
+ warnings.warn(FailedOptionalImportWarning('tqdm'), stacklevel=2)
120
+ return False
121
+
122
+ if mode == 'hydra':
123
+ # progress bar disappears leaving only log metrics.
124
+ tqdm_logger = tqdm.TqdmLogger(leave=False)
125
+ elif mode == 'minimal':
126
+ # double bar replaces most logs.
127
+ tqdm_logger = tqdm.TqdmLogger(enable_training_bar=True)
128
+ else:
129
+ # console metrics from the progress bar
130
+ tqdm_logger = tqdm.TqdmLogger()
131
+
132
+ tracker_list.append(tqdm_logger)
133
+ return True
134
+
135
+
136
+ def _add_yaml(tracker_list: list[Tracker]) -> bool:
137
+ try:
138
+ from drytorch.trackers import yaml
139
+ except (ImportError, ModuleNotFoundError):
140
+ warnings.warn(FailedOptionalImportWarning('yaml'), stacklevel=2)
141
+ return False
142
+
143
+ tracker_list.append(yaml.MetadataDumper())
144
+ return True
145
+
146
+
147
+ def _check_mode_is_valid(
148
+ mode: str,
149
+ ) -> TypeGuard[Literal['standard', 'hydra', 'minimal']]:
150
+ return mode in ('standard', 'hydra', 'minimal')
151
+
152
+
153
+ INIT_MODE = os.getenv('DRYTORCH_INIT_MODE', 'standard')
154
+ if _check_mode_is_valid(INIT_MODE):
155
+ logger.log(INFO_LEVELS.internal, 'Initializing %s mode.', INIT_MODE)
156
+ init_trackers(INIT_MODE)
157
+ elif INIT_MODE != 'none':
158
+ raise ValueError(f'DRYTORCH_INIT_MODE: {INIT_MODE} not a valid setting.')
159
+
160
+ warnings.filterwarnings('ignore', category=exceptions.GitUnavailableWarning)
161
+ _strict = os.getenv('DRYTORCH_STRICT', '0')
162
+ if _strict == '1':
163
+ warnings.filterwarnings('error', category=exceptions.ReproducibilityWarning)
164
+ elif _strict != '0':
165
+ raise ValueError(f'DRYTORCH_STRICT: {_strict} not a valid setting.')
@@ -0,0 +1 @@
1
+ """Extensions and contributions."""
@@ -0,0 +1,265 @@
1
+ """Support for optuna."""
2
+
3
+ import collections
4
+ import string
5
+
6
+ from collections.abc import Callable, Sequence
7
+ from typing import Any, Final, TypeVar
8
+
9
+ import omegaconf
10
+ import optuna
11
+
12
+ from typing_extensions import override
13
+
14
+ from drytorch.core import exceptions
15
+ from drytorch.core import protocols as p
16
+ from drytorch.lib import hooks, monitoring
17
+
18
+
19
+ __all__ = [
20
+ 'OptunaError',
21
+ 'TrialCallback',
22
+ 'get_final_value',
23
+ 'suggest_overrides',
24
+ ]
25
+
26
+ _Target = TypeVar('_Target', bound=p.TargetType)
27
+ _Output = TypeVar('_Output', bound=p.OutputType)
28
+
29
+
30
+ class OptunaError(exceptions.DryTorchError):
31
+ """Base class for Optuna errors."""
32
+
33
+ _template = 'Optuna: {}'
34
+
35
+
36
+ class TrialCallback(hooks.TrainerHook):
37
+ """Report a metric to an optuna trial and prune the training.
38
+
39
+ Attributes:
40
+ reported: the values reported at each epoch.
41
+ trial: ``optuna`` trial.
42
+ watch: what the callback reports.
43
+ when: decides whether the hook runs when the trainer calls it.
44
+ """
45
+
46
+ reported: Final[dict[int, float]]
47
+ trial: Final[optuna.Trial]
48
+ watch: Final[monitoring.Watch]
49
+ _monitor: monitoring.Monitor | None
50
+
51
+ def __init__(
52
+ self,
53
+ trial: optuna.Trial,
54
+ *,
55
+ watch: monitoring.Watch | None = None,
56
+ ) -> None:
57
+ """Initialize.
58
+
59
+ Args:
60
+ trial: ``optuna`` trial.
61
+ watch: what to report. Default reports the loss of the trainer.
62
+ """
63
+ super().__init__()
64
+ self.reported = {}
65
+ self.trial = trial
66
+ self.watch = monitoring.Watch() if watch is None else watch
67
+ self._monitor = None
68
+ return
69
+
70
+ @property
71
+ def monitor(self) -> monitoring.Monitor:
72
+ """The record of the reported metric.
73
+
74
+ Raises:
75
+ ResultNotAvailableError: if no trainer bound the callback yet.
76
+ """
77
+ if self._monitor is None:
78
+ raise exceptions.ResultNotAvailableError()
79
+
80
+ return self._monitor
81
+
82
+ @override
83
+ def bind_trainer(self, trainer: p.TrainerProtocol[Any, Any, Any]) -> None:
84
+ """Create the record of the metric for the trainer."""
85
+ super().bind_trainer(trainer)
86
+ self._monitor = self.watch.resolve(trainer)
87
+ return
88
+
89
+ @override
90
+ def run(self) -> None:
91
+ """Report the value to the trial and prune the training.
92
+
93
+ Raises:
94
+ optuna.TrialPruned: if the trial should be pruned.
95
+ """
96
+ epoch = self.trainer.model.epoch
97
+ self.monitor.record(epoch)
98
+ if epoch not in self.monitor.values:
99
+ return
100
+
101
+ value = self.monitor.value
102
+ self.trial.report(value, epoch)
103
+ self.reported[epoch] = value
104
+ if self.trial.should_prune():
105
+ msg = f'Optuna pruning while monitoring {self.monitor.metric_name}'
106
+ self.trainer.terminate_training(msg)
107
+ raise optuna.TrialPruned()
108
+
109
+ return
110
+
111
+
112
+ def suggest_overrides(
113
+ tune_cfg: omegaconf.DictConfig,
114
+ trial: optuna.Trial,
115
+ *,
116
+ use_full_name: bool = False,
117
+ ) -> list[str]:
118
+ """Suggest values for a trial from structured configurations.
119
+
120
+ This function helps integrate optuna into hydra by specifying trial
121
+ parameters present in the hydra run configuration.
122
+
123
+ The configuration file (loadable with hydra) should follow this structure:
124
+
125
+ .. code-block:: yaml
126
+
127
+ tune:
128
+ params:
129
+ param_name:
130
+ suggest: "suggest_float" # or other optuna suggest method
131
+ settings:
132
+ low: 0.0
133
+ high: 1.0
134
+ list_param:
135
+ suggest: "suggest_list"
136
+ settings:
137
+ min_length: 1
138
+ max_length: 5
139
+ suggest: "suggest_float" # method for sampling list elements
140
+ settings:
141
+ low: 0.0
142
+ high: 1.0
143
+ overrides: [] # additional static overrides
144
+
145
+ For 'suggest_list' configurations, the settings must specify:
146
+ - min_length and max_length: bounds for the size of the list.
147
+ - nested suggest and settings: used to sample each list element.
148
+
149
+ The resulting values can be used with hydra.initialize and hydra.compose.
150
+ Example usage:
151
+
152
+ .. code-block:: python
153
+
154
+ import hydra
155
+
156
+ with hydra.initialize(version_base=None, config_path='path/to/config'):
157
+ overrides = suggest_overrides(tune_cfg, trial)
158
+ dict_cfg = hydra.compose(config_name='config', overrides=overrides)
159
+
160
+ Here, "your_hydra_config" is the name of the configuration file that
161
+ includes the configuration parameters to override.
162
+
163
+ Args:
164
+ tune_cfg: a structure specifying how to sample new parameter values.
165
+ trial: the optuna trial related to the sampled parameters.
166
+ use_full_name: use the fully qualified setting name. Default to a
167
+ human-readable name.
168
+
169
+ Returns:
170
+ A list of strings for hydra configuration overrides.
171
+
172
+ Raises:
173
+ OptunaError: if the suggested configuration is invalid.
174
+ """
175
+
176
+ def _get_short_name(name: str) -> str:
177
+ *prefix_parts, suffix = name.rsplit('.', maxsplit=2)
178
+ if suffix.isdigit() and prefix_parts:
179
+ suffix = f'{prefix_parts[-1]} {suffix}'
180
+
181
+ return string.capwords(suffix.replace('_', ' '))
182
+
183
+ short_names = {k: _get_short_name(k) for k in tune_cfg.tune.params}
184
+ name_counts = collections.Counter(short_names.values())
185
+
186
+ all_overrides: list[str] = [*tune_cfg.overrides]
187
+ for setting_name, param_value in tune_cfg.tune.params.items():
188
+ param_name = short_names[setting_name]
189
+ if use_full_name or name_counts[param_name] > 1:
190
+ param_name = setting_name
191
+
192
+ if param_value.suggest == 'suggest_list':
193
+ new_value: list[Any] = []
194
+ for i in range(
195
+ trial.suggest_int(
196
+ name=f'{param_name} #',
197
+ low=param_value.settings.min_length,
198
+ high=param_value.settings.max_length,
199
+ )
200
+ ):
201
+ try:
202
+ bound_suggest = getattr(trial, param_value.settings.suggest)
203
+ except AttributeError as ae:
204
+ msg = f'Invalid suggest configuration: {ae}.'
205
+ raise OptunaError(msg) from ae
206
+ new_value.append(
207
+ bound_suggest(
208
+ f'{param_name} {i}', **param_value.settings.settings
209
+ )
210
+ )
211
+ else:
212
+ try:
213
+ bound_suggest = getattr(trial, param_value.suggest)
214
+ except AttributeError as ae:
215
+ msg = f'Invalid suggest configuration: {ae}.'
216
+ raise OptunaError(msg) from ae
217
+
218
+ new_value = bound_suggest(param_name, **param_value.settings)
219
+ all_overrides.append(f'{setting_name}={new_value}')
220
+
221
+ return all_overrides
222
+
223
+
224
+ def get_final_value(
225
+ trial: optuna.Trial,
226
+ *,
227
+ filter_fn: Callable[[Sequence[float]], float] | None = None,
228
+ ) -> float:
229
+ """Calculates a trial's final value from its intermediate reported values.
230
+
231
+ This function aggregates the intermediate values reported during trial
232
+ optimization using trial.report().
233
+
234
+ Important: This function will not work with trials created using study.ask()
235
+ as these don't populate the intermediate values in the corresponding
236
+ FrozenTrial.
237
+
238
+ Args:
239
+ trial: the completed ``optuna`` trial to evaluate.
240
+ filter_fn: function to aggregate the trial's intermediate values.
241
+ Defaults to min or max depending on the study direction.
242
+
243
+ Returns:
244
+ The aggregated final value for the trial.
245
+
246
+ Raises:
247
+ OptunaError: if the trial has no reported values, or if there's
248
+ a trial number mismatch.
249
+ """
250
+ current_study = trial.study
251
+ if filter_fn is None:
252
+ filter_fn = min if current_study.direction.name == 'MINIMIZE' else max
253
+
254
+ for frozen_trial in reversed(current_study.trials):
255
+ if frozen_trial.number == trial.number:
256
+ break
257
+ else:
258
+ raise OptunaError('trial number mismatch.')
259
+
260
+ reported_values = list(frozen_trial.intermediate_values.values())
261
+ if not reported_values:
262
+ msg = 'trial has no reported values. Did you use study.optimize?'
263
+ raise OptunaError(msg)
264
+
265
+ return filter_fn(reported_values)
@@ -0,0 +1,88 @@
1
+ """Support for torcheval syncing recommended for distributed training."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final, TypeVar
6
+
7
+ import torch
8
+
9
+ from torcheval import metrics
10
+ from torcheval.metrics import toolkit
11
+
12
+ from drytorch.core import exceptions
13
+ from drytorch.core import protocols as p
14
+
15
+
16
+ __all__ = [
17
+ 'from_torcheval',
18
+ ]
19
+
20
+
21
+ _Tensor = torch.Tensor
22
+
23
+ _ComputeReturn = TypeVar('_ComputeReturn', bound=_Tensor | dict[str, _Tensor])
24
+
25
+
26
+ def from_torcheval(
27
+ torch_eval: metrics.Metric[_ComputeReturn],
28
+ /,
29
+ *,
30
+ higher_is_better: bool | None = None,
31
+ name: str | None = None,
32
+ ) -> p.SyncedObjectiveProtocol[_Tensor, _Tensor]:
33
+ """Returns a wrapper of a Metric from torcheval with a sync method.
34
+
35
+ Args:
36
+ torch_eval: the torcheval metric to wrap.
37
+ higher_is_better: whether higher values are better.
38
+ name: optional name for the metric. Defaults to the class name.
39
+
40
+ Raises:
41
+ exceptions.ComputedMetricsTypeError: If the metric computes to an
42
+ invalid type.
43
+ """
44
+
45
+ class _TorchEvalWithSync:
46
+ higher_is_better: Final[bool | None]
47
+ metric: Final[metrics.Metric[_ComputeReturn]]
48
+ name: Final[str]
49
+ _synced_value: _ComputeReturn | None
50
+
51
+ def __init__(self, _metric: metrics.Metric[_ComputeReturn]) -> None:
52
+ self.higher_is_better = higher_is_better
53
+ self.metric = _metric
54
+ name_str = name if name is not None else _metric.__class__.__name__
55
+ self.name = name_str
56
+ self._synced_value = None
57
+ return
58
+
59
+ def compute(self) -> dict[str, _Tensor]:
60
+ if self._synced_value is None:
61
+ val = self.metric.compute()
62
+ else:
63
+ val = self._synced_value
64
+
65
+ if isinstance(val, dict):
66
+ return val
67
+
68
+ if not isinstance(val, _Tensor):
69
+ raise exceptions.ComputedMetricsTypeError(type(val))
70
+
71
+ return {self.name: val}
72
+
73
+ def reset(self) -> None:
74
+ self.metric.reset()
75
+ self._synced_value = None
76
+ return
77
+
78
+ def sync(self) -> None:
79
+ """Use torcheval toolkit to synchronize and compute metrics."""
80
+ self._synced_value = toolkit.sync_and_compute(self.metric)
81
+ return
82
+
83
+ def update(self, outputs: _Tensor, targets: _Tensor) -> None:
84
+ self.metric.update(outputs, targets)
85
+ self._synced_value = None
86
+ return
87
+
88
+ return _TorchEvalWithSync(torch_eval)
@@ -0,0 +1,118 @@
1
+ """Utilities to ensure compatibility with torchmetrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Final
6
+
7
+ import torch
8
+
9
+ from drytorch.core import exceptions
10
+ from drytorch.core import protocols as p
11
+
12
+
13
+ __all__ = [
14
+ 'from_torchmetrics',
15
+ ]
16
+
17
+ if TYPE_CHECKING:
18
+ from torchmetrics import metric
19
+
20
+ _Tensor = torch.Tensor
21
+
22
+
23
+ def from_torchmetrics(
24
+ torch_metric: metric.CompositionalMetric,
25
+ /,
26
+ ) -> p.SyncedLossProtocol[_Tensor, _Tensor]:
27
+ """Returns a wrapper of a CompositionalMetric for integration.
28
+
29
+ Args:
30
+ torch_metric: the composed torchmetrics metric to wrap.
31
+
32
+ Returns:
33
+ The wrapped metric, usable as a loss.
34
+
35
+ Raises:
36
+ RepeatedMetricsError: if two components of the same class are
37
+ present in the composition. Subclass the metric so each
38
+ configuration has its own class name.
39
+ """
40
+
41
+ class _TorchMetricCompositionalMetric:
42
+ """Wrapper of Compositional Metric reporting internal calculations.
43
+
44
+ Note that this CompositionalMetric can be used as loss.
45
+ """
46
+
47
+ higher_is_better: Final[bool | None]
48
+ metric: Final[metric.CompositionalMetric]
49
+ name = 'Loss'
50
+ _leaf_metrics: list[metric.Metric]
51
+
52
+ def __init__(self, _metric: metric.CompositionalMetric) -> None:
53
+ self.higher_is_better = _metric.higher_is_better
54
+ self.metric = _metric
55
+ self.metric.sync_on_compute = False
56
+ self.metric.dist_sync_on_step = False
57
+ self._leaf_metrics = _get_leaf_metrics(self.metric)
58
+ _disable_leaf_syncing(self._leaf_metrics)
59
+ return
60
+
61
+ def compute(self) -> dict[str, _Tensor]:
62
+ """Output a dictionary of metric values for each component."""
63
+ dict_output = dict[str, _Tensor]({self.name: self.metric.compute()})
64
+ for m in self._leaf_metrics:
65
+ if isinstance(value := m.compute(), _Tensor):
66
+ dict_output[m.__class__.__name__] = value
67
+
68
+ return dict_output
69
+
70
+ def forward(self, outputs: _Tensor, targets: _Tensor) -> _Tensor:
71
+ return self.metric(outputs, targets)
72
+
73
+ def reset(self) -> None:
74
+ self.metric.reset()
75
+ return
76
+
77
+ def sync(self) -> None:
78
+ for m in self._leaf_metrics:
79
+ m.sync()
80
+
81
+ return
82
+
83
+ def update(self, outputs: _Tensor, targets: _Tensor) -> None:
84
+ self.metric.update(outputs, targets)
85
+ return
86
+
87
+ return _TorchMetricCompositionalMetric(torch_metric)
88
+
89
+
90
+ def _get_leaf_metrics(
91
+ comp_metric: metric.CompositionalMetric,
92
+ ) -> list[metric.Metric]:
93
+ leaves: list[metric.Metric] = []
94
+ stack: list[metric.Metric | float | int | _Tensor | None] = [comp_metric]
95
+ names_seen: set[str] = set()
96
+ while stack:
97
+ node = stack.pop()
98
+ if isinstance(node, comp_metric.__class__):
99
+ stack.extend([node.metric_b, node.metric_a])
100
+ elif isinstance(node, float | int | _Tensor) or node is None:
101
+ continue
102
+ else:
103
+ leaves.append(node)
104
+ name = node.__class__.__name__
105
+ if name in names_seen:
106
+ raise exceptions.RepeatedMetricsError([name])
107
+
108
+ names_seen.add(name)
109
+
110
+ return leaves
111
+
112
+
113
+ def _disable_leaf_syncing(leaf_metrics: list[metric.Metric]) -> None:
114
+ for m in leaf_metrics:
115
+ m.sync_on_compute = False
116
+ m.dist_sync_on_step = False
117
+
118
+ return
@@ -0,0 +1 @@
1
+ """Core modules setting up and defining the framework."""