Pictograph SDK — vendored YOLOX architecture
============================================

This directory contains source code derived from YOLOX, redistributed under the
Apache License, Version 2.0. The full licence text is in ./LICENSE.

Origin
------
    Project:  YOLOX
    Commit:   6ddff4824372906469a7fae2dc3206c7aa4bbaee
    Upstream: https://github.com/Megvii-BaseDetection/YOLOX
    Copyright (c) Megvii Inc. All rights reserved.
    Licensed under the Apache License, Version 2.0.

That commit is the one pinned by
`pictograph-backend/modal/pictograph_training_service.py` (`YOLOX_SHA`), i.e. the
architecture the weights we publish were trained as. A verbatim source tarball of
it is mirrored at
`gs://pictograph-app/dependencies/YOLOX-6ddff4824372906469a7fae2dc3206c7aa4bbaee.tar.gz`
per the repo's Modal image dependency policy, so this tree can be re-diffed
against its source even if the upstream repository disappears.

WHAT IS HERE
------------
The model-construction subset only — `yolox/models/`, minus two files:

    network_blocks.py   BaseConv / DWConv / CSPLayer / Focus / SPPBottleneck / …
    darknet.py          CSPDarknet backbone
    yolo_pafpn.py       YOLOPAFPN neck
    yolo_head.py        YOLOXHead (incl. decode_outputs)
    yolox.py            the YOLOX module itself
    losses.py           IOUloss — constructed in YOLOXHead.__init__, so it is
                        required to build the module even though the loss is
                        never evaluated at inference
    _upstream_utils.py  the four `yolox.utils` helpers yolo_head.py imports

    NOT vendored: build.py (torch.hub model-zoo downloads) and yolo_fpn.py (the
    Darknet-53 FPN neck — no Pictograph pipeline trains it).

1,251 of the upstream package's 7,691 Python lines (16.3%), plus 127 lines of
gathered utils. Data loading, the `Exp` system, the trainer, evaluators, COCO
metrics, the distributed/EMA/MLflow utilities and the ONNX/TensorRT export tools
are not vendored.

NOTICE OF MODIFICATION (Apache-2.0 section 4b)
----------------------------------------------
Every file in this directory carries a header stating whether it was modified.
The changes are:

1.  A provenance header was prepended to each file, replacing the upstream
    three-line shebang/coding/copyright preamble (whose copyright line is
    reproduced in the new header).

2.  `yolo_head.py` — its ONE `from yolox.utils import bboxes_iou, cxcywh2xyxy,
    meshgrid, visualize_assign` line is redirected onto `._upstream_utils`, and
    its `from loguru import logger` is dropped in favour of the `logger` that
    same module exports. No other line of the file changed. Importing
    `yolox.utils` as a package would drag in the distributed-training, MLflow,
    EMA, LR-scheduler and COCO-metric modules, none of which a rebuilt
    checkpoint touches.

3.  `_upstream_utils.py` is a gathering file, not a rewrite: `meshgrid` (from
    `yolox/utils/compat.py`), `bboxes_iou` and `cxcywh2xyxy` (from
    `yolox/utils/boxes.py`), and `random_color` and `visualize_assign` (from
    `yolox/utils/demo_utils.py`) are VERBATIM apart from the one line covered by
    item 5. The single substitution is `logger`, which upstream binds to
    `loguru.logger`: it is bound to the standard library's
    `logging.getLogger("pictograph.inference")` instead, so that `loguru` is not
    a runtime dependency of `pip install pictograph` for two log calls on a
    label-assignment path a `.eval()`-mode checkpoint never reaches.

4.  `__init__.py` is original Pictograph work, not derived from YOLOX. It
    exposes `build_yolox(depth, width, num_classes)`, which assembles the same
    `YOLOX(YOLOPAFPN, YOLOXHead)` the training pipeline's `DynamicExp.get_model`
    does.

5.  Six casts spelled with a LEGACY TENSOR TYPE STRING are spelled with a dtype
    instead — four in `yolo_head.py`, one in `losses.py`, one in
    `_upstream_utils.py::bboxes_iou`:

        .type(xin[0].type())      ->  .to(device=…, dtype=xin[0].dtype)
        (tl < br).type(tl.type()) ->  (tl < br).to(tl.dtype)

    `Tensor.type()` MANUFACTURES `'torch.mps.FloatTensor'` on Apple silicon, and
    `Tensor.type(…)` then refuses to parse it — legacy type strings exist only
    for CPU and CUDA — so `YOLOXHead.decode_outputs` raised
    `ValueError: invalid type: 'torch.mps.FloatTensor'` on the LAST statement of
    an otherwise-complete forward pass. Upstream's own sibling call in the same
    method already passes a dtype (`get_losses(…, dtype=xin[0].dtype)`), which is
    the form adopted here; `.type(<string>)` additionally moves to that backend's
    current device, so the device is carried explicitly by `.to(device=…)` from
    the tensor being operated on.

No file in this directory has been altered in a way that changes the numerical
behaviour of the model it builds. Item 5 is a device-neutral spelling of the same
cast: on CPU and CUDA it is bit-identical (asserted by the upstream-parity test in
`tests/unit/test_yolox_vendored.py`, which strict-loads upstream weights into this
tree and requires `torch.equal` on the forward pass), and it newly PERMITS a
device on which the model previously could not run at all.

WHY VENDORED RATHER THAN DEPENDED ON
------------------------------------
See the module docstring in ./__init__.py. In short: PyPI's `yolox` is 0.3.0
(2022), sdist-only, and its `setup.py` asserts torch is importable in order to
compile a C++ extension — so pip cannot build it as a declared dependency at all
(`AssertionError: torch is required for pre-compiling ops`, reproduced in a clean
venv). It is also a different architecture from the pinned SHA, and its
`install_requires` would put a second `cv2` provider into the environment. The
only other option, a `git+https://…@<sha>` direct reference, is rejected by PyPI
at upload time (PEP 440), so a published wheel cannot declare one.
