diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 909f6947c9c..5f2add85ce9 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -33,6 +33,17 @@ reviews: Examine code for logical error or inconsistencies, and suggest what may be changed to addressed these. Suggest any enhancements for code improving efficiency, maintainability, comprehensibility, and correctness. Ensure new or modified definitions will be covered by existing or new unit tests. + - path: '**/AGENTS.md' + instructions: >- + Check that the guidance matches the current repository conventions (pull requests target the dev branch, + commits need a DCO sign-off, formatting and tests run through runtests.sh) and that every file, command, + and path it mentions still exists and behaves as described. + - path: 'monai/**' + instructions: >- + This repository keeps AGENTS.md files at the root and in the subpackages. If this change alters behavior, + conventions, commands, or public APIs described in the AGENTS.md file closest to the changed code, or in + the root AGENTS.md, verify that the relevant AGENTS.md was updated in the same change to stay in sync, + and flag it if it was not. auto_review: # Automatic Review | Automatic code review diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..77cbf17770c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,101 @@ +# AGENTS.md + +Guidance for AI coding assistants (and the humans reviewing their work) in this +repository. The file closest to the code being edited takes precedence: start +here, then read the `AGENTS.md` in the subpackage you are changing. + +## Project overview + +MONAI (Medical Open Network for AI) is a PyTorch-based, open-source framework +for deep learning in healthcare imaging, part of the PyTorch Ecosystem. It +provides domain-optimized data transforms, networks, losses, metrics, training +engines, and a bundle configuration system, with research and clinical users. +Supported Python and PyTorch versions live in `pyproject.toml`; do not copy +version numbers into other files, link there instead. + +## Repository map + +| Path | Purpose | +|------|---------| +| `monai/transforms` | array and dictionary transforms, lazy resampling, invertibility | +| `monai/networks` | networks, building blocks, layers, schedulers | +| `monai/data` | datasets, data loaders, image readers and writers, `MetaTensor` | +| `monai/losses` | loss functions | +| `monai/metrics` | evaluation metrics, mostly cumulative across batches | +| `monai/engines` | training and evaluation workflows, built on PyTorch Ignite | +| `monai/handlers` | Ignite handlers: checkpointing, logging, metrics output | +| `monai/inferers` | sliding window and other inference strategies | +| `monai/bundle` | JSON/YAML configuration system and bundle scripts | +| `monai/apps` | application packages: detection, DeepEdit, pathology, and more | +| `monai/auto3dseg` | Auto3DSeg algorithms; the runner lives in `monai/apps/auto3dseg` | +| `monai/fl` | federated learning client and utilities | +| `monai/visualize` | visualization and interpretability tools | +| `monai/optimizers` | optimizers and learning rate utilities | +| `monai/csrc` | optional C++/CUDA extensions | +| `monai/utils` | shared utilities; check here before writing a new helper | +| `tests` | unit and integration tests, mirroring the `monai` tree | +| `docs` | Sphinx documentation sources | + +## Where the detail lives + +| File | Covers | +|------|--------| +| [`monai/transforms/AGENTS.md`](monai/transforms/AGENTS.md) | array/dict transform pairs, randomness, lazy and inverse transforms | +| [`monai/networks/AGENTS.md`](monai/networks/AGENTS.md) | network, block, and layer conventions | +| [`monai/data/AGENTS.md`](monai/data/AGENTS.md) | datasets, caching, readers, test data | +| [`monai/losses/AGENTS.md`](monai/losses/AGENTS.md) | loss conventions, shared with metrics | +| [`monai/metrics/AGENTS.md`](monai/metrics/AGENTS.md) | cumulative metrics lifecycle | +| [`monai/bundle/AGENTS.md`](monai/bundle/AGENTS.md) | config syntax, references, bundle scripts | +| [`monai/apps/AGENTS.md`](monai/apps/AGENTS.md) | application packages and downloaded data | +| [`monai/auto3dseg/AGENTS.md`](monai/auto3dseg/AGENTS.md) | Auto3DSeg pipeline and generated bundles | +| [`monai/engines/AGENTS.md`](monai/engines/AGENTS.md) | engines and handlers | +| [`monai/csrc/AGENTS.md`](monai/csrc/AGENTS.md) | C++/CUDA extension build | +| [`tests/AGENTS.md`](tests/AGENTS.md) | test layout, minimal tests, skip helpers | +| [`docs/AGENTS.md`](docs/AGENTS.md) | documentation sources and API lists | + +## Setup and commands + +Verified in a fresh checkout of `dev` (Python 3.12, CPU-only PyTorch): + +```bash +./runtests.sh --help # list every option of the test runner +./runtests.sh --setup # install git pre-commit hooks (black, isort, ruff, DCO check) +./runtests.sh --autofix # auto-format code with isort, black, and Ruff +./runtests.sh --ruff # ruff checks only, a fast pre-push check +python -m pytest tests/utils/test_look_up_option.py -q # run a single test module +``` + +For the full pre-PR test suite, see the "Unit testing" section of +`CONTRIBUTING.md`. Do not add a command to the list above unless you have +actually run it in this repository. + +## Repo-wide conventions + +- Pull requests target the `dev` branch. Name branches `[ticket_id]-[task_name]`, + for example `9199-introduce-agents-md`, based on the latest `dev`. +- Every commit needs a DCO sign-off: `git commit -s`. The sign-off identity is + the human author who takes responsibility for the change. +- Line length is 120. Docstrings are Google style. Public APIs carry type hints. +- American English spelling (`normalize`, `visualize`), as in PyTorch. +- New source files start with the Apache 2.0 license header used across the + repo (copy it from a neighboring file). +- Optional dependencies are only reached through `monai.utils.optional_import`; + never hard-import an optional package at module top level. +- A new public API is not finished until it is exported (the module's `__all__` + and the package `__init__.py`) and added to the matching page in + `docs/source/*.rst`. +- Deprecations go through `monai.utils.deprecated` / `deprecated_arg`, with + `since` and `removed` versions, following the process in `CONTRIBUTING.md`. +- Tests mirror the source tree under `tests/`; a change without a test is + treated as broken. +- Before writing a utility, search `monai/utils` and the subpackage's own + `utils.py`; MONAI already has many shared helpers. + +## AI-assisted contributions + +MONAI accepts tool-assisted contributions under the Tool-Assisted Contribution +Policy in `CONTRIBUTING.md`: the submitting author must review and understand +everything submitted, sign the DCO for it, and label AI-generated content in +issues and pull requests. These `AGENTS.md` files exist so that assisted work +arrives closer to mergeable. When a change alters a convention written down in +one of these files, update that file in the same pull request. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28a449e6c0f..ddbc093e168 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,8 @@ Attribution of AI-generated content should be included, for example with trailer Code review and analysis can be assisted through the use of tools, for example linting or formatting tools. Diagnostic output from such tools does not form part of the submission but should be clearly labelled. The key emphasis is that these tools assist and do not replace human review, so contributors and reviewers must consider what might be missed by these tools, or what might be hallucinated. It might be helpful to review code with a different tool from one used for generation. +For assisted coding work, read the root `AGENTS.md` and the nearest package-specific `AGENTS.md`. When a change alters a documented convention, update the applicable `AGENTS.md` file in the same pull request. + ## The contribution process *Pull request early* diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 00000000000..f8845f53647 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,27 @@ +# Documentation + +Sources live in `docs/source`: one page per area (`transforms.rst`, +`networks.rst`, `data.rst`, `losses.rst`, `metrics.rst`, `engines.rst`, +`bundle.rst`, and the rest), long-form guides in Markdown, and the Sphinx +configuration in `conf.py`. + +## Things that are easy to get wrong + +- The per-area `.rst` pages are the public API lists. A new public class or + function that is not added to its area page does not appear in the + published documentation, even if it is exported in `__init__.py`. +- Build before submitting documentation changes: + + ```bash + pip install -r docs/requirements.txt + cd docs && make html + ``` + + and check the page you changed in `docs/build/html`. Do not commit the + build output. +- Python docstrings are Google style and written in reStructuredText; they + are pulled into these pages, so a docstring error is a documentation + build error. +- Release notes and feature summaries go in the `whatsnew` page for the + upcoming release, not into older `whatsnew_*.md` files. +- Spelling is American English, matching the code (see `CONTRIBUTING.md`). diff --git a/monai/apps/AGENTS.md b/monai/apps/AGENTS.md new file mode 100644 index 00000000000..3c4562d1baa --- /dev/null +++ b/monai/apps/AGENTS.md @@ -0,0 +1,24 @@ +# Apps + +Application-level packages built on the core library: `detection` (3D +detection with RetinaNet), `deepedit` and `deepgrow` (interactive +segmentation), `pathology`, `nuclick`, `nnunet`, `generation`, +`reconstruction`, `tcia`, `mmars` (the MONAI model zoo metadata), and +`auto3dseg` (the Auto3DSeg runner; its algorithms live in +`monai/auto3dseg`, see that package's `AGENTS.md`). + +## Things that are easy to get wrong + +- Sample datasets in `datasets.py` are downloaded from the web at runtime. + Tests that use them need network access and are excluded from offline + runs; do not make a unit test depend on a download when a small synthetic + array covers the same path. +- Apps pin down end-to-end workflows, so they compose core APIs (transforms, + networks, engines) rather than reimplementing them. If an app needs a new + option, add it to the core component and use it here. +- Several apps wrap external toolkits or model hubs. Those dependencies stay + optional and are reached through `monai.utils.optional_import`, so that + `import monai.apps` works without them. +- Version-sensitive metadata (model zoo entries in `mmars`, TCIA collection + info) is data, not code: update the JSON or metadata file and its tests, + not the loader. diff --git a/monai/auto3dseg/AGENTS.md b/monai/auto3dseg/AGENTS.md new file mode 100644 index 00000000000..c39fb941f1d --- /dev/null +++ b/monai/auto3dseg/AGENTS.md @@ -0,0 +1,23 @@ +# Auto3DSeg + +This package holds the Auto3DSeg algorithms: data analysis (`analyzer.py`), +algorithm generation (`algo_gen.py`), operations on the generated bundles +(`operations.py`), and result summaries (`seg_summarizer.py`). The user-facing +runner, bundle generator, and ensemble builder live one level up in +`monai/apps/auto3dseg` (`AutoRunner`, `BundleGen`, `EnsembleBuilder`); most +changes need to consider both places. + +## Things that are easy to get wrong + +- Auto3DSeg works by generating bundle folders on disk and then running + them. Paths inside the generated configs are relative to the generated + work directory; do not bake in absolute paths from the machine that ran + the generation. +- The data analyzer's statistics drive algorithm selection and the generated + configs. Changing an analyzer output key silently changes every generated + bundle, so update the summarizer and the tests together. +- End-to-end Auto3DSeg runs are slow and usually GPU-based. Keep unit tests + on tiny synthetic datasets and leave full runs to the integration tests. +- Templates for the generated algorithms live with the runner in + `monai/apps/auto3dseg`; editing generated output by hand is never the fix, + edit the template or the generator. diff --git a/monai/bundle/AGENTS.md b/monai/bundle/AGENTS.md new file mode 100644 index 00000000000..3a11b2d4d8b --- /dev/null +++ b/monai/bundle/AGENTS.md @@ -0,0 +1,31 @@ +# Bundle and the configuration system + +## How configs work + +`ConfigParser` (`config_parser.py`) reads JSON and YAML configs. Inside a +config: + +- `@name` references another config item (nested keys are joined with `::`, + as in `@preprocessing::transforms::keys`). +- `$...` is a Python expression, evaluated when the config is parsed. +- A dict with `_target_` is instantiated as that class or function. + `_disabled_` skips instantiation, `_requires_` lists references that must + be resolved first, `_mode_` changes how the callable is invoked. These + keys are defined in `config_item.py`; do not add new underscore keys + without updating `ConfigComponent` and `docs/source/config_syntax.md`. + +## Things that are easy to get wrong + +- Reference resolution order matters. `ReferenceResolver` resolves items on + demand and caches them; changing resolution code can quietly alter every + bundle in the model zoo, so run the bundle tests even for small changes. +- Configs are user-facing files, often edited by hand. Error messages should + name the config ID that failed; a bare `KeyError` from deep in the parser + is a bug report waiting to happen. +- Command line entry points live in `scripts.py` and run through + `python -m monai.bundle` (`run`, `download`, `ckpt_export`, and others). + Their arguments map onto config overrides; keep the CLI and the config + syntax in sync. +- Bundle metadata follows the specification in + `docs/source/mb_specification.rst` and `mb_properties.rst`; `properties.py` + holds the key definitions. diff --git a/monai/csrc/AGENTS.md b/monai/csrc/AGENTS.md new file mode 100644 index 00000000000..d3e2e2158bf --- /dev/null +++ b/monai/csrc/AGENTS.md @@ -0,0 +1,21 @@ +# C++/CUDA extensions (csrc) + +Optional native extensions: resampling (`resample/`), filtering +(`filtering/`), the LLTM cell (`lltm/`), shared helpers (`utils/`), and the +pybind entry point (`ext.cpp`). + +## Things that are easy to get wrong + +- The extensions are not built by default. `setup.py` builds them only when + `BUILD_MONAI=1` is set, and CUDA sources only when a CUDA toolkit is + available or `FORCE_CUDA=1` is set. Most contributors and CI jobs run + without them. +- Every extension has, or shares, a pure PyTorch path in the Python + packages, and MONAI must import and pass its quick tests with the + extensions absent. If you add an accelerated path, the fallback stays. +- C++ style is governed by the repo-root `.clang-format`; format native + code with `./runtests.sh --clangformat` instead of formatting by hand. + Note that it applies clang-format in place and modifies the files, it is + not a read-only check. +- Python and native implementations of the same operation must agree on + dtype and device behavior; test both paths when you touch one. diff --git a/monai/data/AGENTS.md b/monai/data/AGENTS.md new file mode 100644 index 00000000000..302fe7e38db --- /dev/null +++ b/monai/data/AGENTS.md @@ -0,0 +1,27 @@ +# Data + +## Key types + +- `MetaTensor` (`meta_tensor.py`) is a `torch.Tensor` subclass carrying image + metadata, affine information, and the record of applied transforms used by + inverse transforms and lazy resampling. Code that silently converts it to a + plain tensor (for example through `np.asarray` round trips) loses that + information; preserve the type unless dropping it is the point. +- Datasets live in `dataset.py`: `Dataset`, `CacheDataset`, + `SmartCacheDataset`, `PersistentDataset`, and friends. Readers and writers + are registered in `image_reader.py` and `image_writer.py`. + +## Things that are easy to get wrong + +- `CacheDataset` caches the deterministic part of the pipeline. Its + `copy_cache=True` default deep-copies cached items before the random + transforms run; turning it off for speed reuses the cached tensors and can + corrupt the cache for the next epoch. +- Optional formats (nibabel, pydicom, PIL, and others) are loaded through + `monai.utils.optional_import` inside the readers. A reader must still + import cleanly when its backend package is missing. +- `monai.data.DataLoader` extends the PyTorch loader with MONAI defaults; + check it before reaching for `torch.utils.data.DataLoader` in MONAI code. +- Test images and other binary data do not belong in this repository. Tests + fetch data through `tests.utils` helpers from the MONAI-extra-test-data + releases; see `tests/AGENTS.md`. diff --git a/monai/engines/AGENTS.md b/monai/engines/AGENTS.md new file mode 100644 index 00000000000..2362ef2df86 --- /dev/null +++ b/monai/engines/AGENTS.md @@ -0,0 +1,23 @@ +# Engines and handlers + +`Trainer` and `Evaluator` (in `trainer.py` and `evaluator.py`) build on the +`Workflow` base class and on PyTorch Ignite. Handlers live in +`monai/handlers` and are attached to an engine to add checkpointing, +logging, early stopping, and metrics output. + +## Things that are easy to get wrong + +- Ignite is an optional dependency here: it is imported through + `monai.utils.optional_import` with the minimum version pinned in + `IgniteInfo.OPT_IMPORT_VERSION` (`monai/utils/enums.py`). Never import + `ignite` directly at module top level in this package. +- Handlers react to Ignite events (`Events.EPOCH_COMPLETED`, and so on) and + read from `state.output`. Changing what a trainer puts into `state.output` + breaks handlers in another package; keep the documented keys stable. +- Metrics handlers expect the metric lifecycle from + `monai/metrics/AGENTS.md`: accumulate per iteration, `aggregate()` and + `reset()` at event time. Do not call `aggregate()` per batch in a handler. +- `monai/engines` decides when things happen; `monai/handlers` decides what + happens. A new capability usually means a small handler plus, at most, a + new event or state entry in the engine, not logic inside the training + loop. diff --git a/monai/losses/AGENTS.md b/monai/losses/AGENTS.md new file mode 100644 index 00000000000..e8a746b50ea --- /dev/null +++ b/monai/losses/AGENTS.md @@ -0,0 +1,24 @@ +# Losses + +Shared conventions with `monai/metrics` (see `monai/metrics/AGENTS.md`): +most losses and metrics take an `include_background` flag (default `True`) +and a `reduction` from the enums in `monai.utils` (`LossReduction` here, +`MetricReduction` in metrics). Keep the flag names, defaults, and argument +order consistent with the neighboring classes when adding a new one. + +## Things that are easy to get wrong + +- Several losses (Dice and its variants in `dice.py`, focal loss) do not apply + sigmoid or softmax internally unless `sigmoid=True` or `softmax=True` is + passed. Check those flags before assuming the loss expects probabilities + rather than logits, and do not change a default: it silently changes + training results for existing users. +- `to_onehot_y` and `include_background` interact: with one-hot targets the + background channel is channel 0, and excluding it slices after the one-hot + conversion. Test both settings. +- Losses should support the batch and channel shapes their docstrings + promise and return a scalar under the default reduction. Reductions other + than the mean must not silently change the output shape contract. +- Numerical stability matters here (log of probabilities, division by sums + over empty classes). Follow the epsilon handling of the closest existing + loss instead of inventing a new one. diff --git a/monai/metrics/AGENTS.md b/monai/metrics/AGENTS.md new file mode 100644 index 00000000000..83ba6690781 --- /dev/null +++ b/monai/metrics/AGENTS.md @@ -0,0 +1,28 @@ +# Metrics + +Shared conventions with `monai/losses` (see `monai/losses/AGENTS.md`): +`include_background` defaults to `True`, and reductions come from +`MetricReduction` in `monai.utils`. + +## How metrics accumulate + +Most metrics derive from `Cumulative` or `CumulativeIterationMetric` in +`metric.py`. Calls during an epoch append to internal buffers through +`__call__`; `aggregate()` computes the result from the buffers; `reset()` +clears them. + +## Things that are easy to get wrong + +- Forgetting `reset()` between epochs or evaluations is the classic metrics + bug: the second result silently includes the first run's data. Any new + metric must reset every buffer it owns, and handlers in `monai/handlers` + call `reset()` and `aggregate()` at event time, not per batch. +- `aggregate()` must not destroy the buffers unless documented; some + workflows aggregate intermediate results and keep accumulating. +- Handle the not-applicable case (for example, a class missing from both + prediction and target) the way the neighboring metric does, usually by + producing `nan` and letting `get_not_nans` or the handler filter it, + instead of returning 0 and flattering the score. +- Distance-based metrics (Hausdorff, surface distance) are computed per + class and can be the slow part of a test suite; keep test inputs small + and mark slow tests as described in `tests/AGENTS.md`. diff --git a/monai/networks/AGENTS.md b/monai/networks/AGENTS.md new file mode 100644 index 00000000000..fe844f68084 --- /dev/null +++ b/monai/networks/AGENTS.md @@ -0,0 +1,26 @@ +# Networks + +## Layout + +- `nets/` holds full architectures (`basic_unet.py`, `attentionunet.py`, and + many more), one file per architecture family. +- `blocks/` holds reusable building blocks, `layers/` holds single layers and + the layer factories, `schedulers/` holds diffusion noise schedulers, and + `utils.py` holds helpers shared by this package only. + +## Things that are easy to get wrong + +- Most networks are parameterized by `spatial_dims` (1, 2, or 3). Do not write + 2D-only logic into a shared block; use the dimension-agnostic helpers in + `layers/` and `monai.networks.utils`. +- Activations, normalizations, and dropouts are commonly passed as names or + tuples and resolved through the factories in `monai/networks/layers/`. Keep + that pattern instead of hard-coding a module class, so configs and bundles + can still build the network. +- Channel arguments have an established order in MONAI signatures: + `spatial_dims`, `in_channels`, `out_channels` first. Follow the neighboring + networks when adding a new one. +- A new network is public API: export it in `nets/__init__.py` and in + `monai/networks/__init__.py`, add it to `docs/source/networks.rst`, and add + a test under `tests/networks/` that builds it in each supported + `spatial_dims` and runs one forward pass on CPU-sized inputs. diff --git a/monai/transforms/AGENTS.md b/monai/transforms/AGENTS.md new file mode 100644 index 00000000000..06ed8e5b8cc --- /dev/null +++ b/monai/transforms/AGENTS.md @@ -0,0 +1,35 @@ +# Transforms + +## Layout + +Each topic folder (`croppad`, `intensity`, `io`, `post`, `signal`, and the +others) splits into `array.py` and `dictionary.py`. The array class works on a +single array or `MetaTensor` (for example `LoadImage` in `io/array.py`); the +dictionary class applies the same operation per key of a dict and usually ends +in `d` (`LoadImaged` in `io/dictionary.py`). A fix for a transform almost +always needs both variants, plus their tests. + +## Things that are easy to get wrong + +- Random transforms derive from `RandomizableTransform` and draw from + `self.R`, set through `set_random_state`. Never use the global `random` or + `numpy.random` inside a transform: `Compose` and the data loaders seed each + transform per sample, and global state breaks reproducibility and worker + safety. +- Transforms run in multiple data loader worker processes. + `RandomizableTransform` may store sampled parameters on `self` for the + current call or for explicit `randomize=False` reuse. Do not share one + transform instance across concurrent calls or rely on prior-call state + unless the API explicitly requests reuse. +- Many transforms are invertible and record what they did through + `MetaTensor` applied operations and `InvertibleTransform` tracing. If you + change a transform's parameters or geometry handling, check the inverse + path (`monai/transforms/inverse.py`) and `Invertd` tests still make sense. +- Lazy transforms live in `lazy/`; changes to resampling behavior can affect + both the eager and the lazy execution of the same transform. Read + `docs/source/lazy_resampling.rst` before touching resample logic. +- Dictionary transforms take `keys` and share one code path through + `MapTransform`; missing-key behavior is controlled by `allow_missing_keys`, + do not invent a new flag for it. +- New transforms must be exported from the topic folder and from + `monai/transforms/__init__.py`, and listed in `docs/source/transforms.rst`. diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 00000000000..3a2c0b6e452 --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,30 @@ +# Tests + +## Layout + +Tests mirror the source tree: `tests/transforms`, `tests/data`, +`tests/losses`, and so on, plus cross-cutting modules at this level. +Unit tests are named `test_[module_name].py`; files ending in `_dist.py` +need a distributed, GPU-based environment; integration tests are named +`test_integration_[workflow_name].py`. Shared helpers live in +`tests/test_utils.py` and the `tests/utils` package. + +## Things that are easy to get wrong + +- Tests must run in the minimal environment too. Build new test modules from + the standard library, `torch`, `numpy`, `coverage`, and `parameterized` + only. If a test module needs another package, add its file name to + `exclude_cases` in `tests/min_tests.py`, or the minimal CI runner will + try to execute it and fail on import. +- Slow tests are skipped in quick runs with the `skip_if_quick` decorator + from `tests/test_utils.py`. Mark genuinely slow tests instead of shrinking + them until they stop testing anything. +- Do not commit test images or other binary data. Small inputs are generated + in code; real data is fetched through the `tests.utils` helpers from the + MONAI-extra-test-data releases (see `CONTRIBUTING.md`, "Testing data"). +- Prefer `parameterized.expand` case tables over copy-pasted test methods, + and assert on values, not only on output shapes: shape-only tests keep + passing when the numbers go wrong. +- Deterministic tests only: seed random transforms (`set_random_state`) and + torch generators explicitly, or the test will flake under a different + worker count.