Skip to content

Model Card for CanvasVAE

arXiv venue license base dataset vendor-parity hub

This package ports CanvasVAE, a variational autoencoder for vector graphic documents, into a 🤗transformers-style package for training and layout generation. RICO full-run parity is confirmed: an independent, from-scratch held-out run in normal mode passed all five staged checks under calibrated limits, from static initialization through training outputs and optimizer updates to production-loader replay (S0–S4). The three-seed RICO evaluation comparison also passes all three primary metrics. Crello remains blocked and unclaimed.

The original and package each completed three 500-epoch RICO runs and pass the predeclared comparison rule on 5,623 test layouts for the reconstruction score (Sreconst, mean BLEU-1 across fields and test layouts), layout mIoU (mean class intersection-over-union on rasterized component maps), and generated-layout score (Sgen, mean histogram intersection for generated-layout field and element-count distributions against RICO); each Sgen value averages three evaluation seeds. TRAINING.md gives per-run results, the comparison rule, evaluation scope, calibration, and reproduction commands.

Model Details

Model Description

CanvasVAE encodes a layout, meaning a sequence of up to 50 UI elements, into a 256-dimensional latent code with a transformer encoder, and decodes a latent code back into the element count and every element field in one pass. Each RICO element has a position and size discretized into 64 bins over the canvas, a component label, an icon class, a text_button class, and a clickable flag. Sampling the latent code from a standard normal prior generates new layouts. The pipeline returns normalized center xywh boxes in [0, 1], component ids as labels, a valid-element mask, and id2label.

  • Developed by: Kota Yamaguchi.
  • Shared by: creative-graphic-design.
  • Model type: content-agnostic; task: single-task; conditioning: unconditional.
  • Language(s) (NLP): not applicable.
  • License: Apache-2.0.

Model Sources

Supported Checkpoints

Checkpoint Hub ID Status
RICO creative-graphic-design/canvas-vae-rico not-published

RICO full-run parity is complete: an independent held-out normal-mode run passed the calibrated S0–S4 checks, and the three-seed S5 comparison passed all primary metrics. The corrected package checkpoints passed the final-epoch gate at epoch 499 and global step 22,500; all package outputs from before that correction are excluded. Model weights are not published. Crello remains blocked and unclaimed because it requires a separately trained image encoder. See TRAINING.md for the full evidence.

Uses

Direct Use

Use this package to train CanvasVAE on RICO, to generate UI layouts from the prior, and to reconstruct layouts through CanvasVAEModel.

condition_type Required inputs Effect
unconditional none samples latent codes and decodes layouts
label, label_size, completion, refinement, text, content_image, relation, hierarchical, retrieval not applicable raises NotImplementedError

Downstream Use

Generated layouts may feed UI prototyping, layout evaluation, or retrieval by latent code after task-specific validation.

Out-of-Scope Use

Do not treat generated layouts as accessibility annotations, semantic screen understanding, or license-cleared design assets without separate review.

Bias, Risks, and Limitations

The model learns from RICO Android screens collected in 2017; layouts of other platforms, languages, or design eras are outside its training distribution. Element geometry is quantized to 64 bins, so generated boxes snap to a coarse grid.

Recommendations

Rerun the agreement checks in REPRODUCING.md after changing the model, losses, optimizer, or data pipeline.

How to Get Started with the Model

Install the package directly from this repository. The command includes the shared laygen package because it is not published on PyPI.

pip install \
  "laygen @ git+https://github.com/creative-graphic-design/design-generators.git#subdirectory=lib/laygen" \
  "canvas-vae @ git+https://github.com/creative-graphic-design/design-generators.git#subdirectory=models/canvas-vae"

Clone this repository, install the workspace member, and prepare RICO and a checkpoint with REPRODUCING.md or TRAINING.md. The conversion step there creates .cache/canvas-vae/converted/initial. The snippet below loads those untrained starting weights as a wiring check, so its layouts are not meaningful; a trained checkpoint needs the full training run in TRAINING.md.

git clone https://github.com/creative-graphic-design/design-generators.git
cd design-generators
uv sync --package canvas-vae
uv run --package canvas-vae python
from canvas_vae import CanvasVAEPipeline

path = ".cache/canvas-vae/converted/initial"
# After Hub publication: from_pretrained("creative-graphic-design/canvas-vae-rico")
pipe = CanvasVAEPipeline.from_pretrained(path)
out = pipe(batch_size=4, seed=0)

print(out.bbox.shape)
print(out.labels.shape)
print(out.id2label[int(out.labels[out.mask][0])])
torch.Size([4, 49, 4])
torch.Size([4, 49])
Modal

Training Details

Training Data

Dataset Dataset ID Notes
RICO creative-graphic-design/Rico Training reads the original semantic-annotation archive because its content-hash split and textButtonClass field are not in the hosted configuration.

Training Procedure

TRAINING.md documents the LightningCLI configs and the staged agreement evidence against the original trainer.

Preprocessing

scripts/prepare_rico.py deduplicates identical annotation files, flattens each view hierarchy in depth-first pre-order, drops screens with more than 50 elements, and assigns 10% validation and 10% test splits by an MD5 content hash. Lookup tables keep component values seen at least once and icon and text_button values seen at least 500 times.

Training Hyperparameters

  • Training regime: fp32, batch size 1024, 500 epochs of 45 batches, Adam with learning rate 1e-3 and per-tensor gradient-norm clipping at 1.0, KL weight 16, and L2 weight 1e-6.

Speeds, Sizes, Times

The model has 1,558,435 trainable parameters. Full-run training time has not been measured yet.

Evaluation

Testing Data, Factors & Metrics

Testing Data

Agreement checks use data exported from the original trainer's RICO stream, with a different population per stage. S0's forward check, S1, and S2 use the first training batch of 1,024 layouts. S3 uses the first 50 training batches: the natural trajectory trains on all 50, and the synchronized diagnostic checks 49 steps. S4 compares all 45,222 / 5,584 / 5,623 train/validation/test records and replays 102 batches through the production loaders: 90 training, 6 validation, and 6 test batches. Generated references stay under .cache/canvas-vae/ and are not committed.

Factors

Results are reported per training-reproduction stage on RICO.

Metrics

Training agreement compares forward activations, losses, gradients, optimizer state, and parameters with float tolerances, and data records and stream order exactly. Layout quality uses the original reconstruction score, layout mIoU, and random-generation histogram score.

Parity Results

The table keeps reference and calibration-set maxima as historical measurements and adds the independent held-out normal-mode results. The held-out run used a fresh clone, initialized its submodule, downloaded and prepared RICO, and regenerated references on an Intel Xeon CPU at 2.20 GHz without AVX-512. All five S0–S4 commands passed; the documented full suite reported 10 passed and 3 warnings in 3,591.26 seconds (59 minutes 51 seconds). The read-back evidence bundle contained all 208 manifest entries. See the held-out evidence comment. A later documentation/comment-only update did not change executable checks or limits. The full-run reproduction claim covers RICO only; Crello remains blocked and unclaimed.

Check Cases Criterion Historical calibration maximum Held-out normal-mode result
S0 static configuration and checkpoint key map 1,558,435 trainable parameters; 71 original trainable-parameter name keys Exact key-map equality Reference and calibration reports retain equal topology. Exact topology and key-map assertions passed.
S0 evaluation forward 10 outputs from one batch of 1,024 layouts Maximum relative-to-max difference (rtol=1e-5) 1.266045e-6; reference run. 1.139062e-6; limit 1e-5.
S1 activations and logits One exact encoder-context tensor and 14 relative-to-max comparisons from one batch Maximum relative-to-max difference (rtol=1e-5) 2.916977e-6; independent rerun 1. 2.087399e-6; limit 1e-5.
S1 per-field losses, KL, L2, and total loss 12 scalars from one batch Maximum relative-to-max difference (rtol=1e-6) 1.684800e-7; independent rerun 3. 1.617341e-7; limit 1e-6.
S2 gradients, clipped gradients, and Adam moments 69 trainable tensors per family; two key-projection bias tensors checked separately Direct relative L2 <= 3.5e-4 per family; float64 arbitration <= 4.1e-3 if direct bound exceeded Gradient 2.278771e-4, clipped gradient 2.277752e-4, first moment 2.277754e-4 (calibration run 1); second moment 2.301702e-4 (independent rerun 2); max arbitration error 2.696621e-3 (calibration run 1). Raw/clipped/first-moment/second-moment maxima 6.333509e-5 / 6.333466e-5 / 6.333450e-5 / 6.393008e-5; 276 direct, 0 arbitrated.
S2 optimizer update, excluded-bias gradient, and batch normalization 69 updates per system; two excluded bias tensors × two systems; 256 moving-mean and moving-variance values Adam-rule relative L2 <= 3.5e-4; well-conditioned update <= 1.6e-3; excluded-bias maximum absolute gradient <= 1e-6; moving-statistic relative-to-max difference <= 1e-5 Update-rule 2.829523e-5, well-conditioned update 2.623599e-5, excluded-bias gradient 6.600749e-8, moving statistics 2.608958e-7; historical maxima in Calibration Results. Update-rule 2.7879995e-5; well-conditioned update 6.479192e-6; excluded-bias gradient 7.178460e-8; moving-mean difference 1.553235e-7. The moving-variance gate also passed; its separate maximum was not included in the supplied summary.
S3 synchronized loss, gradients, and updates 49 synchronized steps × 69 tensors = 3,381 gradient comparisons and per-tensor update checks Loss <= 1.2e-6; direct routing 3.5e-4; shared float64 arbitration 4.1e-3; Adam rule 5.1e-4; well-conditioned update 6.1e-3; running variance 1e-5 Loss 7.353933e-7 (calibration run 3); arbitration 2.696621e-3 (calibration run 1); Adam rule 3.350313e-4 (independent rerun 3); well-conditioned update 4.037209e-3 (calibration run 1); running variance 8.221208e-8 (independent rerun 3). Loss 3.044964e-7; 3,268 direct and 113 arbitrated gradients, maximum arbitrated error 7.491022e-4; Adam rule 2.973134e-4; well-conditioned update 1.698453e-3; running variance 8.067037e-8.
S4 original records and production-loader stream replay 45,222 / 5,584 / 5,623 train/validation/test records; record and stream gate counts recorded separately Exact content-hash sets, vocabularies, lookups, sample IDs, encoded fields, masks, and padding widths Calibration reports recorded exact equality. Exact equality; 24 record gates and 427 stream gates passed.

Reproducibility

See REPRODUCING.md for commands to prepare RICO, generate original-code references, run agreement checks, convert checkpoints, and smoke-test loading. TRAINING.md records the completed RICO S0–S4 held-out pass, historical calibration measurements, S5 results, and package-local LightningCLI training. Full-run reproduction is claimed for RICO only; Crello remains unclaimed.

Environmental Impact

The staged agreement checks run on CPU in minutes. Full-run GPU telemetry is report-only and does not affect the S5 decision.

Technical Specifications

Model Architecture and Objective

The encoder sums per-field embeddings with learned position embeddings, applies one pre-norm transformer block conditioned on the element-count embedding, pools valid elements, applies batch normalization, and predicts a diagonal Gaussian posterior. The decoder applies one conditioned transformer block to position embeddings and predicts every field with linear heads. Training minimizes per-field cross-entropy summed over elements, plus 16 times the KL divergence, plus an L2 penalty on dense and embedding weights.

Compute Infrastructure

Agreement checks run on CPU. Full RICO training used one A100 per training process, with training processes run sequentially per GPU.

Hardware

CPU is sufficient for inference, unit tests, and the staged agreement checks.

Software

Use uv run --package canvas-vae ... from the repository root. The training extra installs Lightning, and the vendor extra installs CUDA-enabled TensorFlow 2.15.1 and Apache Beam for original-code references. The separate convert extra installs CPU-only TensorFlow for original initial.ckpt and final.ckpt conversion. Package training keeps validation-best weights in best.ckpt; a separate unmonitored callback with save_top_k: 1 retains one rolling checkpoint under Lightning’s epoch/step name and updates last.ckpt at configured checkpoint events. The final-checkpoint gate requires epoch 499 and global step 22,500 before evaluation.

License

Repository wrapper code is Apache-2.0. The original implementation is released under the Apache-2.0 license.

Citation

@inproceedings{yamaguchi2021canvasvae,
  title = {{CanvasVAE: Learning To Generate Vector Graphic Documents}},
  author = {Kota Yamaguchi},
  booktitle = {Proceedings of the IEEE/CVF International Conference on Computer Vision (ICCV)},
  year = {2021},
  pages = {5481-5489}
}

API Reference

Transformers-style CanvasVAE layout generation package.

CanvasVAEConfig

Bases: PretrainedConfig

Configuration for a CanvasVAE model trained on RICO.

A layout has a document-level element count and eight categorical per-element fields. Box coordinates and sizes are discretized into num_bins bins over [0, 1]; component, icon, and text_button use the lookup tables in vocabularies, whose index 0 is the out-of-vocabulary token.

Parameters:

Name Type Description Default
vocabularies Mapping[str, Sequence[str]] | None

Lookup tables for component, icon, and text_button, each starting with the out-of-vocabulary token. Required for a usable model; the default only exists so that Transformers can instantiate an empty config while serializing.

None
max_length int

Maximum number of elements per layout.

50
num_bins int

Number of discretization bins for box coordinates and sizes.

64
latent_dim int

Hidden and latent dimension.

256
num_blocks int

Number of transformer blocks in the encoder and decoder.

1
num_heads int

Number of attention heads.

8
dropout float

Dropout rate of position embeddings and blocks.

0.1
kl_weight float

Weight of the KL divergence term.

16.0
l2_weight float

Weight of the squared-norm penalty on dense and embedding weights.

1e-06
layer_norm_epsilon float

Layer-normalization epsilon.

0.001
batch_norm_epsilon float

Batch-normalization epsilon.

0.001
batch_norm_momentum float

Batch-normalization moving-average momentum.

0.99
**kwargs str | float | bool | None

Extra PretrainedConfig fields.

{}

Examples:

>>> config = CanvasVAEConfig(
...     vocabularies={
...         "component": ["[UNK]", "", "Text"],
...         "icon": ["[UNK]", ""],
...         "text_button": ["[UNK]", ""],
...     },
...     latent_dim=16,
...     num_heads=2,
... )
>>> config.field_sizes["component"], config.primary_label_id
(3, 1)
Source code in models/canvas-vae/src/canvas_vae/configuration_canvas_vae.py
 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
class CanvasVAEConfig(PretrainedConfig):
    """Configuration for a CanvasVAE model trained on RICO.

    A layout has a document-level element count and eight categorical
    per-element fields. Box coordinates and sizes are discretized into
    ``num_bins`` bins over ``[0, 1]``; ``component``, ``icon``, and
    ``text_button`` use the lookup tables in ``vocabularies``, whose index 0 is
    the out-of-vocabulary token.

    Args:
        vocabularies: Lookup tables for ``component``, ``icon``, and
            ``text_button``, each starting with the out-of-vocabulary token.
            Required for a usable model; the default only exists so that
            Transformers can instantiate an empty config while serializing.
        max_length: Maximum number of elements per layout.
        num_bins: Number of discretization bins for box coordinates and sizes.
        latent_dim: Hidden and latent dimension.
        num_blocks: Number of transformer blocks in the encoder and decoder.
        num_heads: Number of attention heads.
        dropout: Dropout rate of position embeddings and blocks.
        kl_weight: Weight of the KL divergence term.
        l2_weight: Weight of the squared-norm penalty on dense and embedding
            weights.
        layer_norm_epsilon: Layer-normalization epsilon.
        batch_norm_epsilon: Batch-normalization epsilon.
        batch_norm_momentum: Batch-normalization moving-average momentum.
        **kwargs: Extra ``PretrainedConfig`` fields.

    Examples:
        >>> config = CanvasVAEConfig(
        ...     vocabularies={
        ...         "component": ["[UNK]", "", "Text"],
        ...         "icon": ["[UNK]", ""],
        ...         "text_button": ["[UNK]", ""],
        ...     },
        ...     latent_dim=16,
        ...     num_heads=2,
        ... )
        >>> config.field_sizes["component"], config.primary_label_id
        (3, 1)
    """

    model_type = "canvas-vae"

    def __init__(
        self,
        vocabularies: Mapping[str, Sequence[str]] | None = None,
        max_length: int = 50,
        num_bins: int = 64,
        latent_dim: int = 256,
        num_blocks: int = 1,
        num_heads: int = 8,
        dropout: float = 0.1,
        kl_weight: float = 16.0,
        l2_weight: float = 1e-6,
        layer_norm_epsilon: float = 1e-3,
        batch_norm_epsilon: float = 1e-3,
        batch_norm_momentum: float = 0.99,
        **kwargs: str | float | bool | None,
    ) -> None:
        """Initialize the config; see the class docstring for arguments."""
        tables = {
            str(key): [str(token) for token in tokens]
            for key, tokens in (vocabularies or {}).items()
        }
        components = tables.get(CanvasVAEField.component, [])
        kwargs.pop("id2label", None)
        kwargs.pop("label2id", None)
        super().__init__(
            id2label=dict(enumerate(components)),
            label2id={token: index for index, token in enumerate(components)},
            **kwargs,  # ty: ignore[invalid-argument-type]
        )
        self.vocabularies = tables
        self.max_length = max_length
        self.num_bins = num_bins
        self.latent_dim = latent_dim
        self.num_blocks = num_blocks
        self.num_heads = num_heads
        self.dropout = dropout
        self.kl_weight = kl_weight
        self.l2_weight = l2_weight
        self.layer_norm_epsilon = layer_norm_epsilon
        self.batch_norm_epsilon = batch_norm_epsilon
        self.batch_norm_momentum = batch_norm_momentum

    @property
    def field_sizes(self) -> dict[str, int]:
        """Return the number of classes of every per-element field.

        Raises:
            ValueError: If a required vocabulary is missing.
        """
        missing = [key for key in VOCABULARY_FIELDS if key not in self.vocabularies]
        if missing:
            raise ValueError(f"CanvasVAEConfig is missing vocabularies: {missing}")

        sizes = {str(key): self.num_bins for key in GEOMETRY_FIELDS}
        sizes[CanvasVAEField.clickable] = NUM_CLICKABLE_CLASSES
        for key in VOCABULARY_FIELDS:
            sizes[key] = len(self.vocabularies[key])

        return sizes

    @property
    def primary_label_id(self) -> int:
        """Return the ``component`` id of the empty label used as background."""
        return self.vocabularies[CanvasVAEField.component].index(PRIMARY_LABEL_TOKEN)

field_sizes property

field_sizes: dict[str, int]

Return the number of classes of every per-element field.

Raises:

Type Description
ValueError

If a required vocabulary is missing.

primary_label_id property

primary_label_id: int

Return the component id of the empty label used as background.

__init__

__init__(
    vocabularies: Mapping[str, Sequence[str]] | None = None,
    max_length: int = 50,
    num_bins: int = 64,
    latent_dim: int = 256,
    num_blocks: int = 1,
    num_heads: int = 8,
    dropout: float = 0.1,
    kl_weight: float = 16.0,
    l2_weight: float = 1e-06,
    layer_norm_epsilon: float = 0.001,
    batch_norm_epsilon: float = 0.001,
    batch_norm_momentum: float = 0.99,
    **kwargs: str | float | bool | None,
) -> None

Initialize the config; see the class docstring for arguments.

Source code in models/canvas-vae/src/canvas_vae/configuration_canvas_vae.py
 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
def __init__(
    self,
    vocabularies: Mapping[str, Sequence[str]] | None = None,
    max_length: int = 50,
    num_bins: int = 64,
    latent_dim: int = 256,
    num_blocks: int = 1,
    num_heads: int = 8,
    dropout: float = 0.1,
    kl_weight: float = 16.0,
    l2_weight: float = 1e-6,
    layer_norm_epsilon: float = 1e-3,
    batch_norm_epsilon: float = 1e-3,
    batch_norm_momentum: float = 0.99,
    **kwargs: str | float | bool | None,
) -> None:
    """Initialize the config; see the class docstring for arguments."""
    tables = {
        str(key): [str(token) for token in tokens]
        for key, tokens in (vocabularies or {}).items()
    }
    components = tables.get(CanvasVAEField.component, [])
    kwargs.pop("id2label", None)
    kwargs.pop("label2id", None)
    super().__init__(
        id2label=dict(enumerate(components)),
        label2id={token: index for index, token in enumerate(components)},
        **kwargs,  # ty: ignore[invalid-argument-type]
    )
    self.vocabularies = tables
    self.max_length = max_length
    self.num_bins = num_bins
    self.latent_dim = latent_dim
    self.num_blocks = num_blocks
    self.num_heads = num_heads
    self.dropout = dropout
    self.kl_weight = kl_weight
    self.l2_weight = l2_weight
    self.layer_norm_epsilon = layer_norm_epsilon
    self.batch_norm_epsilon = batch_norm_epsilon
    self.batch_norm_momentum = batch_norm_momentum

CanvasVAEField

Bases: StrEnum

Per-element fields of a CanvasVAE layout, in model column order.

Source code in models/canvas-vae/src/canvas_vae/configuration_canvas_vae.py
12
13
14
15
16
17
18
19
20
21
22
class CanvasVAEField(StrEnum):
    """Per-element fields of a CanvasVAE layout, in model column order."""

    left = auto()
    top = auto()
    width = auto()
    height = auto()
    clickable = auto()
    component = auto()
    icon = auto()
    text_button = auto()

CanvasVAEModel

Bases: CanvasVAEPreTrainedModel

CanvasVAE variational autoencoder over layout element sequences.

Given layout ids, the model encodes them, takes a posterior sample in training mode (or when posterior_noise is given) and the posterior mean otherwise, and decodes with the ground-truth element mask in training mode or the predicted element count in evaluation mode. Given only latents, it decodes them with the predicted element count.

Parameters:

Name Type Description Default
config CanvasVAEConfig

Model config with vocabularies.

required

Examples:

>>> config = CanvasVAEConfig(
...     vocabularies={
...         "component": ["[UNK]", "", "Text"],
...         "icon": ["[UNK]", ""],
...         "text_button": ["[UNK]", ""],
...     },
...     max_length=4,
...     num_bins=8,
...     latent_dim=16,
...     num_heads=2,
... )
>>> model = CanvasVAEModel(config).eval()
>>> output = model(latents=torch.zeros(2, 16))
>>> output.length_logits.shape
torch.Size([2, 4])
Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
class CanvasVAEModel(CanvasVAEPreTrainedModel):
    """CanvasVAE variational autoencoder over layout element sequences.

    Given layout ids, the model encodes them, takes a posterior sample in
    training mode (or when ``posterior_noise`` is given) and the posterior mean
    otherwise, and decodes with the ground-truth element mask in training mode
    or the predicted element count in evaluation mode. Given only
    ``latents``, it decodes them with the predicted element count.

    Args:
        config: Model config with vocabularies.

    Examples:
        >>> config = CanvasVAEConfig(
        ...     vocabularies={
        ...         "component": ["[UNK]", "", "Text"],
        ...         "icon": ["[UNK]", ""],
        ...         "text_button": ["[UNK]", ""],
        ...     },
        ...     max_length=4,
        ...     num_bins=8,
        ...     latent_dim=16,
        ...     num_heads=2,
        ... )
        >>> model = CanvasVAEModel(config).eval()
        >>> output = model(latents=torch.zeros(2, 16))
        >>> output.length_logits.shape
        torch.Size([2, 4])
    """

    def __init__(self, config: CanvasVAEConfig) -> None:
        """Create the encoder and decoder."""
        super().__init__(config)
        self.encoder = CanvasVAEEncoder(config)
        self.decoder = CanvasVAEDecoder(config)
        self.post_init()

    def forward(
        self,
        num_elements: Int[torch.Tensor, "batch"] | None = None,
        element_ids: Int[torch.Tensor, "batch elements fields"] | None = None,
        *,
        latents: Float[torch.Tensor, "batch dim"] | None = None,
        posterior_noise: Float[torch.Tensor, "batch dim"] | None = None,
    ) -> CanvasVAEModelOutput:
        """Encode and decode a layout batch, or decode latent codes.

        Args:
            num_elements: Element count of every layout.
            element_ids: Field ids in :class:`CanvasVAEField` order, padded to
                the longest layout in the batch.
            latents: Latent codes to decode instead of encoding a layout.
            posterior_noise: Standard-normal noise for the posterior sample.
                Drawn from the global generator in training mode when omitted.

        Returns:
            Model outputs; ``loss`` is set only in training mode.

        Raises:
            ValueError: If neither a layout nor latent codes are given.
        """
        if latents is not None:
            length_logits, logits, mask = self.decoder(latents)
            return CanvasVAEModelOutput(
                length_logits=length_logits,
                element_logits=logits,
                mask=mask,
                latents=latents,
            )

        if num_elements is None or element_ids is None:
            raise ValueError("pass num_elements and element_ids, or latents")

        length_ids = num_elements - 1
        z_mean, z_log_var = self.encoder(length_ids, element_ids)
        kl_divergence = -0.5 * torch.mean(
            1 + z_log_var - z_mean.square() - torch.exp(z_log_var)
        )
        if posterior_noise is None and self.training:
            posterior_noise = randn(
                z_mean.shape, device=z_mean.device, dtype=z_mean.dtype
            )

        latents = z_mean
        if posterior_noise is not None:
            latents = z_mean + torch.exp(0.5 * z_log_var) * posterior_noise

        mask = length_mask(length_ids, element_ids.shape[1]) if self.training else None
        length_logits, logits, mask = self.decoder(latents, mask)
        output = CanvasVAEModelOutput(
            length_logits=length_logits,
            element_logits=logits,
            mask=mask,
            latents=latents,
            z_mean=z_mean,
            z_log_var=z_log_var,
            kl_divergence=kl_divergence,
        )
        if not self.training:
            return output

        losses = {
            "length": nn.functional.cross_entropy(length_logits, length_ids),
            **{
                key: _sequence_cross_entropy(logits[key], element_ids[..., index], mask)
                for index, key in enumerate(CanvasVAEField)
            },
        }
        reconstruction = sum(losses.values(), torch.zeros((), device=z_mean.device))
        output.reconstruction_losses = losses
        output.loss = reconstruction + self.config.kl_weight * kl_divergence
        return output

__init__

__init__(config: CanvasVAEConfig) -> None

Create the encoder and decoder.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
380
381
382
383
384
385
def __init__(self, config: CanvasVAEConfig) -> None:
    """Create the encoder and decoder."""
    super().__init__(config)
    self.encoder = CanvasVAEEncoder(config)
    self.decoder = CanvasVAEDecoder(config)
    self.post_init()

forward

forward(
    num_elements: Int[Tensor, "batch"] | None = None,
    element_ids: Int[Tensor, "batch elements fields"]
    | None = None,
    *,
    latents: Float[Tensor, "batch dim"] | None = None,
    posterior_noise: Float[Tensor, "batch dim"]
    | None = None,
) -> CanvasVAEModelOutput

Encode and decode a layout batch, or decode latent codes.

Parameters:

Name Type Description Default
num_elements Int[Tensor, 'batch'] | None

Element count of every layout.

None
element_ids Int[Tensor, 'batch elements fields'] | None

Field ids in :class:CanvasVAEField order, padded to the longest layout in the batch.

None
latents Float[Tensor, 'batch dim'] | None

Latent codes to decode instead of encoding a layout.

None
posterior_noise Float[Tensor, 'batch dim'] | None

Standard-normal noise for the posterior sample. Drawn from the global generator in training mode when omitted.

None

Returns:

Type Description
CanvasVAEModelOutput

Model outputs; loss is set only in training mode.

Raises:

Type Description
ValueError

If neither a layout nor latent codes are given.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
def forward(
    self,
    num_elements: Int[torch.Tensor, "batch"] | None = None,
    element_ids: Int[torch.Tensor, "batch elements fields"] | None = None,
    *,
    latents: Float[torch.Tensor, "batch dim"] | None = None,
    posterior_noise: Float[torch.Tensor, "batch dim"] | None = None,
) -> CanvasVAEModelOutput:
    """Encode and decode a layout batch, or decode latent codes.

    Args:
        num_elements: Element count of every layout.
        element_ids: Field ids in :class:`CanvasVAEField` order, padded to
            the longest layout in the batch.
        latents: Latent codes to decode instead of encoding a layout.
        posterior_noise: Standard-normal noise for the posterior sample.
            Drawn from the global generator in training mode when omitted.

    Returns:
        Model outputs; ``loss`` is set only in training mode.

    Raises:
        ValueError: If neither a layout nor latent codes are given.
    """
    if latents is not None:
        length_logits, logits, mask = self.decoder(latents)
        return CanvasVAEModelOutput(
            length_logits=length_logits,
            element_logits=logits,
            mask=mask,
            latents=latents,
        )

    if num_elements is None or element_ids is None:
        raise ValueError("pass num_elements and element_ids, or latents")

    length_ids = num_elements - 1
    z_mean, z_log_var = self.encoder(length_ids, element_ids)
    kl_divergence = -0.5 * torch.mean(
        1 + z_log_var - z_mean.square() - torch.exp(z_log_var)
    )
    if posterior_noise is None and self.training:
        posterior_noise = randn(
            z_mean.shape, device=z_mean.device, dtype=z_mean.dtype
        )

    latents = z_mean
    if posterior_noise is not None:
        latents = z_mean + torch.exp(0.5 * z_log_var) * posterior_noise

    mask = length_mask(length_ids, element_ids.shape[1]) if self.training else None
    length_logits, logits, mask = self.decoder(latents, mask)
    output = CanvasVAEModelOutput(
        length_logits=length_logits,
        element_logits=logits,
        mask=mask,
        latents=latents,
        z_mean=z_mean,
        z_log_var=z_log_var,
        kl_divergence=kl_divergence,
    )
    if not self.training:
        return output

    losses = {
        "length": nn.functional.cross_entropy(length_logits, length_ids),
        **{
            key: _sequence_cross_entropy(logits[key], element_ids[..., index], mask)
            for index, key in enumerate(CanvasVAEField)
        },
    }
    reconstruction = sum(losses.values(), torch.zeros((), device=z_mean.device))
    output.reconstruction_losses = losses
    output.loss = reconstruction + self.config.kl_weight * kl_divergence
    return output

CanvasVAEModelOutput dataclass

Bases: ModelOutput

Outputs of :class:CanvasVAEModel.

Attributes:

Name Type Description
loss Float[Tensor, ''] | None

Reconstruction plus weighted KL loss; present when layout inputs are given and the model is in training mode.

length_logits Float[Tensor, 'batch lengths'] | None

Logits over the zero-based element count.

element_logits dict[str, Float[Tensor, 'batch elements classes']] | None

Logits of every per-element field, keyed by field name.

mask Bool[Tensor, 'batch elements'] | None

Valid-element mask used by the decoder.

latents Float[Tensor, 'batch latent'] | None

Latent codes fed to the decoder.

z_mean Float[Tensor, 'batch latent'] | None

Posterior mean; present when layout inputs are given.

z_log_var Float[Tensor, 'batch latent'] | None

Posterior log-variance; present when layout inputs are given.

kl_divergence Float[Tensor, ''] | None

Unweighted KL divergence to the standard normal prior.

reconstruction_losses dict[str, Float[Tensor, '']] | None

Per-field reconstruction losses, including length; present together with loss.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@dataclass
class CanvasVAEModelOutput(ModelOutput):
    """Outputs of :class:`CanvasVAEModel`.

    Attributes:
        loss: Reconstruction plus weighted KL loss; present when layout inputs
            are given and the model is in training mode.
        length_logits: Logits over the zero-based element count.
        element_logits: Logits of every per-element field, keyed by field name.
        mask: Valid-element mask used by the decoder.
        latents: Latent codes fed to the decoder.
        z_mean: Posterior mean; present when layout inputs are given.
        z_log_var: Posterior log-variance; present when layout inputs are given.
        kl_divergence: Unweighted KL divergence to the standard normal prior.
        reconstruction_losses: Per-field reconstruction losses, including
            ``length``; present together with ``loss``.
    """

    loss: Float[torch.Tensor, ""] | None = None
    length_logits: Float[torch.Tensor, "batch lengths"] | None = None
    element_logits: dict[str, Float[torch.Tensor, "batch elements classes"]] | None = (
        None
    )
    mask: Bool[torch.Tensor, "batch elements"] | None = None
    latents: Float[torch.Tensor, "batch latent"] | None = None
    z_mean: Float[torch.Tensor, "batch latent"] | None = None
    z_log_var: Float[torch.Tensor, "batch latent"] | None = None
    kl_divergence: Float[torch.Tensor, ""] | None = None
    reconstruction_losses: dict[str, Float[torch.Tensor, ""]] | None = None

CanvasVAEPipeline

Bases: LayoutGenerationPipeline

Sample RICO layouts from the CanvasVAE prior.

Each call draws latent codes from the standard normal prior, decodes the element count and every element field with argmax, and returns normalized center xywh boxes clamped to [0, 1] with component labels. intermediates holds the clickable, icon, and text_button ids and the latent codes.

Parameters:

Name Type Description Default
model CanvasVAEModel

CanvasVAE model.

required

Examples:

>>> from canvas_vae import CanvasVAEConfig
>>> config = CanvasVAEConfig(
...     vocabularies={
...         "component": ["[UNK]", "", "Text"],
...         "icon": ["[UNK]", ""],
...         "text_button": ["[UNK]", ""],
...     },
...     max_length=4,
...     num_bins=8,
...     latent_dim=16,
...     num_heads=2,
... )
>>> pipe = CanvasVAEPipeline(model=CanvasVAEModel(config))
>>> out = pipe(batch_size=2, seed=0)
>>> out.bbox.shape[0], out.bbox.shape[-1]
(2, 4)
Source code in models/canvas-vae/src/canvas_vae/pipeline_canvas_vae.py
 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
class CanvasVAEPipeline(LayoutGenerationPipeline):
    """Sample RICO layouts from the CanvasVAE prior.

    Each call draws latent codes from the standard normal prior, decodes the
    element count and every element field with ``argmax``, and returns
    normalized center ``xywh`` boxes clamped to ``[0, 1]`` with ``component``
    labels. ``intermediates`` holds the ``clickable``, ``icon``, and
    ``text_button`` ids and the latent codes.

    Args:
        model: CanvasVAE model.

    Examples:
        >>> from canvas_vae import CanvasVAEConfig
        >>> config = CanvasVAEConfig(
        ...     vocabularies={
        ...         "component": ["[UNK]", "", "Text"],
        ...         "icon": ["[UNK]", ""],
        ...         "text_button": ["[UNK]", ""],
        ...     },
        ...     max_length=4,
        ...     num_bins=8,
        ...     latent_dim=16,
        ...     num_heads=2,
        ... )
        >>> pipe = CanvasVAEPipeline(model=CanvasVAEModel(config))
        >>> out = pipe(batch_size=2, seed=0)
        >>> out.bbox.shape[0], out.bbox.shape[-1]
        (2, 4)
    """

    config_class: ClassVar[type[CanvasVAEConfig]] = CanvasVAEConfig
    component_specs: ClassVar[dict[str, PipelineComponentSpec]] = {
        "model": PipelineComponentSpec(attribute_name="model", loader=_load_model)
    }

    def __init__(self, model: CanvasVAEModel) -> None:
        """Wrap a model; the pipeline config is the model config."""
        super().__init__(model.config)
        self.model = model

    @classmethod
    def _from_pretrained_components(
        cls,
        *,
        config: PretrainedConfig,
        components: Mapping[str, PipelineComponent | None],
    ) -> CanvasVAEPipeline:
        """Build the pipeline from a loaded model."""
        del config
        return cls(model=cast(CanvasVAEModel, components["model"]))

    @torch.no_grad()
    def __call__(  # ty: ignore[invalid-method-override]
        self,
        *,
        batch_size: int = 1,
        seed: int | None = None,
        generator: torch.Generator | None = None,
        condition_type: ConditionType | str = ConditionType.unconditional,
        labels: Shaped[torch.Tensor, "..."]
        | Shaped[np.ndarray, "..."]
        | list[list[int]]
        | None = None,
        bbox: Shaped[torch.Tensor, "..."]
        | Shaped[np.ndarray, "..."]
        | list[list[list[float]]]
        | None = None,
        mask: Shaped[torch.Tensor, "..."]
        | Shaped[np.ndarray, "..."]
        | list[list[bool]]
        | None = None,
        num_elements: int | list[int] | Int[torch.Tensor, "batch"] | None = None,
        box_format: Literal["xywh", "ltwh", "ltrb"] = "xywh",
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
        num_inference_steps: int | None = None,
        output_type: OutputType | str = OutputType.dataclass,
        return_intermediates: bool = False,
    ) -> LayoutGenerationOutput | dict[str, Shaped[torch.Tensor, "..."]]:
        """Generate layouts from prior samples.

        Args:
            batch_size: Number of layouts.
            seed: Seed for a local generator when ``generator`` is absent.
            generator: Generator for the prior samples; takes precedence over
                ``seed``.
            condition_type: Only ``unconditional`` is supported.
            labels: Unsupported; must be ``None``.
            bbox: Unsupported; must be ``None``.
            mask: Unsupported; must be ``None``.
            num_elements: Unsupported; the model predicts the element count.
            box_format: Only ``xywh`` output is supported.
            normalized: Only normalized output is supported.
            canvas_size: Unsupported; must be ``None``.
            num_inference_steps: Unsupported; decoding is a single pass.
            output_type: ``dataclass`` or ``dict``.
            return_intermediates: Whether to fill ``intermediates``.

        Returns:
            Generated layouts in the shared layout schema.

        Raises:
            NotImplementedError: If a condition or layout input other than
                unconditional generation is requested.
        """
        condition = normalize_condition_type(condition_type)
        if condition is not ConditionType.unconditional:
            raise NotImplementedError(
                f"CanvasVAE supports only unconditional generation, got {condition}"
            )

        given = {
            "labels": labels,
            "bbox": bbox,
            "mask": mask,
            "num_elements": num_elements,
            "canvas_size": canvas_size,
            "num_inference_steps": num_inference_steps,
        }
        unsupported = [name for name, value in given.items() if value is not None]
        if unsupported or BoxFormat(box_format) is not BoxFormat.xywh or not normalized:
            raise NotImplementedError(
                "CanvasVAE unconditional generation returns normalized xywh boxes "
                f"and takes no layout inputs; got {unsupported or box_format}"
            )

        generator = self.prepare_generator(generator=generator, seed=seed)
        device = self.model.device
        latents = randn(
            (batch_size, self.model.config.latent_dim),
            generator=generator,
            device=device,
            dtype=self.model.dtype,
        )
        was_training = self.model.training
        self.model.eval()
        try:
            output = self.model(latents=latents)
        finally:
            self.model.train(was_training)

        ids = {
            key: logits.argmax(dim=-1) for key, logits in output.element_logits.items()
        }
        geometry = torch.stack(
            [
                ids[CanvasVAEField.left],
                ids[CanvasVAEField.top],
                ids[CanvasVAEField.width],
                ids[CanvasVAEField.height],
            ],
            dim=-1,
        )
        boxes = clamp_boxes(
            ltwh_to_xywh(bins_to_ltwh(geometry, self.model.config.num_bins))
        )
        valid = cast(Bool[torch.Tensor, "batch elements"], output.mask)
        result = LayoutGenerationOutput(
            bbox=boxes * valid.unsqueeze(-1),
            labels=ids[CanvasVAEField.component] * valid,
            mask=valid,
            id2label=dict(self.model.config.id2label),
            intermediates=(
                {
                    "clickable": ids[CanvasVAEField.clickable],
                    "icon": ids[CanvasVAEField.icon],
                    "text_button": ids[CanvasVAEField.text_button],
                    "latents": latents,
                }
                if return_intermediates
                else None
            ),
        )
        if OutputType(output_type) is OutputType.dict:
            return dict(result)

        return result

__init__

__init__(model: CanvasVAEModel) -> None

Wrap a model; the pipeline config is the model config.

Source code in models/canvas-vae/src/canvas_vae/pipeline_canvas_vae.py
100
101
102
103
def __init__(self, model: CanvasVAEModel) -> None:
    """Wrap a model; the pipeline config is the model config."""
    super().__init__(model.config)
    self.model = model

__call__

__call__(
    *,
    batch_size: int = 1,
    seed: int | None = None,
    generator: Generator | None = None,
    condition_type: ConditionType
    | str = ConditionType.unconditional,
    labels: Shaped[Tensor, "..."]
    | Shaped[ndarray, "..."]
    | list[list[int]]
    | None = None,
    bbox: Shaped[Tensor, "..."]
    | Shaped[ndarray, "..."]
    | list[list[list[float]]]
    | None = None,
    mask: Shaped[Tensor, "..."]
    | Shaped[ndarray, "..."]
    | list[list[bool]]
    | None = None,
    num_elements: int
    | list[int]
    | Int[Tensor, "batch"]
    | None = None,
    box_format: Literal["xywh", "ltwh", "ltrb"] = "xywh",
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    num_inference_steps: int | None = None,
    output_type: OutputType | str = OutputType.dataclass,
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[str, Shaped[torch.Tensor, "..."]]
)

Generate layouts from prior samples.

Parameters:

Name Type Description Default
batch_size int

Number of layouts.

1
seed int | None

Seed for a local generator when generator is absent.

None
generator Generator | None

Generator for the prior samples; takes precedence over seed.

None
condition_type ConditionType | str

Only unconditional is supported.

unconditional
labels Shaped[Tensor, '...'] | Shaped[ndarray, '...'] | list[list[int]] | None

Unsupported; must be None.

None
bbox Shaped[Tensor, '...'] | Shaped[ndarray, '...'] | list[list[list[float]]] | None

Unsupported; must be None.

None
mask Shaped[Tensor, '...'] | Shaped[ndarray, '...'] | list[list[bool]] | None

Unsupported; must be None.

None
num_elements int | list[int] | Int[Tensor, 'batch'] | None

Unsupported; the model predicts the element count.

None
box_format Literal['xywh', 'ltwh', 'ltrb']

Only xywh output is supported.

'xywh'
normalized bool

Only normalized output is supported.

True
canvas_size tuple[int, int] | None

Unsupported; must be None.

None
num_inference_steps int | None

Unsupported; decoding is a single pass.

None
output_type OutputType | str

dataclass or dict.

dataclass
return_intermediates bool

Whether to fill intermediates.

False

Returns:

Type Description
LayoutGenerationOutput | dict[str, Shaped[Tensor, '...']]

Generated layouts in the shared layout schema.

Raises:

Type Description
NotImplementedError

If a condition or layout input other than unconditional generation is requested.

Source code in models/canvas-vae/src/canvas_vae/pipeline_canvas_vae.py
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
@torch.no_grad()
def __call__(  # ty: ignore[invalid-method-override]
    self,
    *,
    batch_size: int = 1,
    seed: int | None = None,
    generator: torch.Generator | None = None,
    condition_type: ConditionType | str = ConditionType.unconditional,
    labels: Shaped[torch.Tensor, "..."]
    | Shaped[np.ndarray, "..."]
    | list[list[int]]
    | None = None,
    bbox: Shaped[torch.Tensor, "..."]
    | Shaped[np.ndarray, "..."]
    | list[list[list[float]]]
    | None = None,
    mask: Shaped[torch.Tensor, "..."]
    | Shaped[np.ndarray, "..."]
    | list[list[bool]]
    | None = None,
    num_elements: int | list[int] | Int[torch.Tensor, "batch"] | None = None,
    box_format: Literal["xywh", "ltwh", "ltrb"] = "xywh",
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    num_inference_steps: int | None = None,
    output_type: OutputType | str = OutputType.dataclass,
    return_intermediates: bool = False,
) -> LayoutGenerationOutput | dict[str, Shaped[torch.Tensor, "..."]]:
    """Generate layouts from prior samples.

    Args:
        batch_size: Number of layouts.
        seed: Seed for a local generator when ``generator`` is absent.
        generator: Generator for the prior samples; takes precedence over
            ``seed``.
        condition_type: Only ``unconditional`` is supported.
        labels: Unsupported; must be ``None``.
        bbox: Unsupported; must be ``None``.
        mask: Unsupported; must be ``None``.
        num_elements: Unsupported; the model predicts the element count.
        box_format: Only ``xywh`` output is supported.
        normalized: Only normalized output is supported.
        canvas_size: Unsupported; must be ``None``.
        num_inference_steps: Unsupported; decoding is a single pass.
        output_type: ``dataclass`` or ``dict``.
        return_intermediates: Whether to fill ``intermediates``.

    Returns:
        Generated layouts in the shared layout schema.

    Raises:
        NotImplementedError: If a condition or layout input other than
            unconditional generation is requested.
    """
    condition = normalize_condition_type(condition_type)
    if condition is not ConditionType.unconditional:
        raise NotImplementedError(
            f"CanvasVAE supports only unconditional generation, got {condition}"
        )

    given = {
        "labels": labels,
        "bbox": bbox,
        "mask": mask,
        "num_elements": num_elements,
        "canvas_size": canvas_size,
        "num_inference_steps": num_inference_steps,
    }
    unsupported = [name for name, value in given.items() if value is not None]
    if unsupported or BoxFormat(box_format) is not BoxFormat.xywh or not normalized:
        raise NotImplementedError(
            "CanvasVAE unconditional generation returns normalized xywh boxes "
            f"and takes no layout inputs; got {unsupported or box_format}"
        )

    generator = self.prepare_generator(generator=generator, seed=seed)
    device = self.model.device
    latents = randn(
        (batch_size, self.model.config.latent_dim),
        generator=generator,
        device=device,
        dtype=self.model.dtype,
    )
    was_training = self.model.training
    self.model.eval()
    try:
        output = self.model(latents=latents)
    finally:
        self.model.train(was_training)

    ids = {
        key: logits.argmax(dim=-1) for key, logits in output.element_logits.items()
    }
    geometry = torch.stack(
        [
            ids[CanvasVAEField.left],
            ids[CanvasVAEField.top],
            ids[CanvasVAEField.width],
            ids[CanvasVAEField.height],
        ],
        dim=-1,
    )
    boxes = clamp_boxes(
        ltwh_to_xywh(bins_to_ltwh(geometry, self.model.config.num_bins))
    )
    valid = cast(Bool[torch.Tensor, "batch elements"], output.mask)
    result = LayoutGenerationOutput(
        bbox=boxes * valid.unsqueeze(-1),
        labels=ids[CanvasVAEField.component] * valid,
        mask=valid,
        id2label=dict(self.model.config.id2label),
        intermediates=(
            {
                "clickable": ids[CanvasVAEField.clickable],
                "icon": ids[CanvasVAEField.icon],
                "text_button": ids[CanvasVAEField.text_button],
                "latents": latents,
            }
            if return_intermediates
            else None
        ),
    )
    if OutputType(output_type) is OutputType.dict:
        return dict(result)

    return result

CanvasVAEProcessor

Bases: ProcessorMixin

Encode flattened layouts into CanvasVAE model ids.

Coordinates and sizes in [0, 1] map to num_bins equal-width bins whose float32 boundaries are linspace(0, 1, num_bins)[1:]; values at or above a boundary fall in the next bin. Strings map through the lookup tables with unknown values at id 0. Layouts are padded to the longest one in the batch with zero geometry, clickable=0, and the id of the empty string.

Parameters:

Name Type Description Default
vocabularies Mapping[str, Sequence[str]]

Lookup tables for component, icon, and text_button.

required
num_bins int

Number of geometry bins.

64

Examples:

>>> processor = CanvasVAEProcessor(
...     vocabularies={
...         "component": ["[UNK]", "", "Text"],
...         "icon": ["[UNK]", ""],
...         "text_button": ["[UNK]", ""],
...     },
...     num_bins=4,
... )
>>> element = {"left": 0.5, "top": 0.0, "width": 1.0, "height": 0.2,
...            "clickable": 1, "component": "Text", "icon": "", "text_button": ""}
>>> processor([[element]])["element_ids"].tolist()
[[[1, 0, 3, 0, 1, 2, 1, 1]]]
Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
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
class CanvasVAEProcessor(ProcessorMixin):
    """Encode flattened layouts into CanvasVAE model ids.

    Coordinates and sizes in ``[0, 1]`` map to ``num_bins`` equal-width bins
    whose float32 boundaries are ``linspace(0, 1, num_bins)[1:]``; values at or
    above a boundary fall in the next bin. Strings map through the lookup
    tables with unknown values at id 0. Layouts are padded to the longest one
    in the batch with zero geometry, ``clickable=0``, and the id of the empty
    string.

    Args:
        vocabularies: Lookup tables for ``component``, ``icon``, and
            ``text_button``.
        num_bins: Number of geometry bins.

    Examples:
        >>> processor = CanvasVAEProcessor(
        ...     vocabularies={
        ...         "component": ["[UNK]", "", "Text"],
        ...         "icon": ["[UNK]", ""],
        ...         "text_button": ["[UNK]", ""],
        ...     },
        ...     num_bins=4,
        ... )
        >>> element = {"left": 0.5, "top": 0.0, "width": 1.0, "height": 0.2,
        ...            "clickable": 1, "component": "Text", "icon": "", "text_button": ""}
        >>> processor([[element]])["element_ids"].tolist()
        [[[1, 0, 3, 0, 1, 2, 1, 1]]]
    """

    def __init__(
        self, vocabularies: Mapping[str, Sequence[str]], num_bins: int = 64
    ) -> None:
        """Store lookup tables and the bin count."""
        self.chat_template = None
        self.vocabularies = {key: list(tokens) for key, tokens in vocabularies.items()}
        self.token_ids = {
            key: {token: index for index, token in reversed(list(enumerate(tokens)))}
            for key, tokens in self.vocabularies.items()
        }
        self.num_bins = num_bins

    @property
    def bin_boundaries(self) -> Float[np.ndarray, "boundaries"]:
        """Return the float32 geometry bin boundaries."""
        return np.linspace(0.0, 1.0, self.num_bins)[1:].astype(np.float32)

    def discretize(
        self, values: Float[np.ndarray, "*shape"]
    ) -> Int[np.ndarray, "*shape"]:
        """Map normalized geometry values to bin ids.

        Args:
            values: Normalized coordinates or sizes.

        Returns:
            Bin ids in ``[0, num_bins)``.
        """
        return np.searchsorted(
            self.bin_boundaries, values.astype(np.float32), side="right"
        )

    def lookup(self, field: str, value: str) -> int:
        """Return the id of ``value`` in the ``field`` table, or 0 when unknown.

        Args:
            field: Vocabulary field name.
            value: String value.

        Returns:
            Lookup id.
        """
        return self.token_ids[field].get(value, 0)

    def __call__(self, layouts: Sequence[Sequence[RicoElement]]) -> BatchFeature:
        """Encode a batch of flattened layouts.

        Args:
            layouts: Element lists, each non-empty.

        Returns:
            ``num_elements`` with shape ``(batch,)`` and ``element_ids`` with
            shape ``(batch, elements, 8)`` in :class:`CanvasVAEField` order.

        Raises:
            ValueError: If a layout has no elements.
        """
        if any(not layout for layout in layouts):
            raise ValueError("every layout needs at least one element")

        width = max(len(layout) for layout in layouts)
        geometry = np.zeros((len(layouts), width, len(GEOMETRY_FIELDS)), np.float32)
        categorical = np.zeros((len(layouts), width, 4), np.int64)
        categorical[..., 1:] = [
            self.lookup(field, PRIMARY_LABEL_TOKEN) for field in VOCABULARY_FIELDS
        ]
        for row, layout in enumerate(layouts):
            for column, element in enumerate(layout):
                geometry[row, column] = [element[key] for key in GEOMETRY_FIELDS]  # ty: ignore[invalid-key]
                categorical[row, column] = [
                    element["clickable"],
                    *(self.lookup(key, element[key]) for key in VOCABULARY_FIELDS),  # ty: ignore[invalid-key]
                ]

        element_ids = np.concatenate([self.discretize(geometry), categorical], axis=-1)
        return BatchFeature(
            {
                "num_elements": torch.tensor([len(layout) for layout in layouts]),
                "element_ids": torch.from_numpy(element_ids),
            }
        )

bin_boundaries property

bin_boundaries: Float[ndarray, 'boundaries']

Return the float32 geometry bin boundaries.

__init__

__init__(
    vocabularies: Mapping[str, Sequence[str]],
    num_bins: int = 64,
) -> None

Store lookup tables and the bin count.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
337
338
339
340
341
342
343
344
345
346
347
def __init__(
    self, vocabularies: Mapping[str, Sequence[str]], num_bins: int = 64
) -> None:
    """Store lookup tables and the bin count."""
    self.chat_template = None
    self.vocabularies = {key: list(tokens) for key, tokens in vocabularies.items()}
    self.token_ids = {
        key: {token: index for index, token in reversed(list(enumerate(tokens)))}
        for key, tokens in self.vocabularies.items()
    }
    self.num_bins = num_bins

discretize

discretize(
    values: Float[ndarray, "*shape"],
) -> Int[np.ndarray, "*shape"]

Map normalized geometry values to bin ids.

Parameters:

Name Type Description Default
values Float[ndarray, '*shape']

Normalized coordinates or sizes.

required

Returns:

Type Description
Int[ndarray, '*shape']

Bin ids in [0, num_bins).

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def discretize(
    self, values: Float[np.ndarray, "*shape"]
) -> Int[np.ndarray, "*shape"]:
    """Map normalized geometry values to bin ids.

    Args:
        values: Normalized coordinates or sizes.

    Returns:
        Bin ids in ``[0, num_bins)``.
    """
    return np.searchsorted(
        self.bin_boundaries, values.astype(np.float32), side="right"
    )

lookup

lookup(field: str, value: str) -> int

Return the id of value in the field table, or 0 when unknown.

Parameters:

Name Type Description Default
field str

Vocabulary field name.

required
value str

String value.

required

Returns:

Type Description
int

Lookup id.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
369
370
371
372
373
374
375
376
377
378
379
def lookup(self, field: str, value: str) -> int:
    """Return the id of ``value`` in the ``field`` table, or 0 when unknown.

    Args:
        field: Vocabulary field name.
        value: String value.

    Returns:
        Lookup id.
    """
    return self.token_ids[field].get(value, 0)

__call__

__call__(
    layouts: Sequence[Sequence[RicoElement]],
) -> BatchFeature

Encode a batch of flattened layouts.

Parameters:

Name Type Description Default
layouts Sequence[Sequence[RicoElement]]

Element lists, each non-empty.

required

Returns:

Type Description
BatchFeature

num_elements with shape (batch,) and element_ids with

BatchFeature

shape (batch, elements, 8) in :class:CanvasVAEField order.

Raises:

Type Description
ValueError

If a layout has no elements.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
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
def __call__(self, layouts: Sequence[Sequence[RicoElement]]) -> BatchFeature:
    """Encode a batch of flattened layouts.

    Args:
        layouts: Element lists, each non-empty.

    Returns:
        ``num_elements`` with shape ``(batch,)`` and ``element_ids`` with
        shape ``(batch, elements, 8)`` in :class:`CanvasVAEField` order.

    Raises:
        ValueError: If a layout has no elements.
    """
    if any(not layout for layout in layouts):
        raise ValueError("every layout needs at least one element")

    width = max(len(layout) for layout in layouts)
    geometry = np.zeros((len(layouts), width, len(GEOMETRY_FIELDS)), np.float32)
    categorical = np.zeros((len(layouts), width, 4), np.int64)
    categorical[..., 1:] = [
        self.lookup(field, PRIMARY_LABEL_TOKEN) for field in VOCABULARY_FIELDS
    ]
    for row, layout in enumerate(layouts):
        for column, element in enumerate(layout):
            geometry[row, column] = [element[key] for key in GEOMETRY_FIELDS]  # ty: ignore[invalid-key]
            categorical[row, column] = [
                element["clickable"],
                *(self.lookup(key, element[key]) for key in VOCABULARY_FIELDS),  # ty: ignore[invalid-key]
            ]

    element_ids = np.concatenate([self.discretize(geometry), categorical], axis=-1)
    return BatchFeature(
        {
            "num_elements": torch.tensor([len(layout) for layout in layouts]),
            "element_ids": torch.from_numpy(element_ids),
        }
    )

configuration_canvas_vae

Configuration for CanvasVAE layout models.

CanvasVAEField

Bases: StrEnum

Per-element fields of a CanvasVAE layout, in model column order.

Source code in models/canvas-vae/src/canvas_vae/configuration_canvas_vae.py
12
13
14
15
16
17
18
19
20
21
22
class CanvasVAEField(StrEnum):
    """Per-element fields of a CanvasVAE layout, in model column order."""

    left = auto()
    top = auto()
    width = auto()
    height = auto()
    clickable = auto()
    component = auto()
    icon = auto()
    text_button = auto()

CanvasVAEConfig

Bases: PretrainedConfig

Configuration for a CanvasVAE model trained on RICO.

A layout has a document-level element count and eight categorical per-element fields. Box coordinates and sizes are discretized into num_bins bins over [0, 1]; component, icon, and text_button use the lookup tables in vocabularies, whose index 0 is the out-of-vocabulary token.

Parameters:

Name Type Description Default
vocabularies Mapping[str, Sequence[str]] | None

Lookup tables for component, icon, and text_button, each starting with the out-of-vocabulary token. Required for a usable model; the default only exists so that Transformers can instantiate an empty config while serializing.

None
max_length int

Maximum number of elements per layout.

50
num_bins int

Number of discretization bins for box coordinates and sizes.

64
latent_dim int

Hidden and latent dimension.

256
num_blocks int

Number of transformer blocks in the encoder and decoder.

1
num_heads int

Number of attention heads.

8
dropout float

Dropout rate of position embeddings and blocks.

0.1
kl_weight float

Weight of the KL divergence term.

16.0
l2_weight float

Weight of the squared-norm penalty on dense and embedding weights.

1e-06
layer_norm_epsilon float

Layer-normalization epsilon.

0.001
batch_norm_epsilon float

Batch-normalization epsilon.

0.001
batch_norm_momentum float

Batch-normalization moving-average momentum.

0.99
**kwargs str | float | bool | None

Extra PretrainedConfig fields.

{}

Examples:

>>> config = CanvasVAEConfig(
...     vocabularies={
...         "component": ["[UNK]", "", "Text"],
...         "icon": ["[UNK]", ""],
...         "text_button": ["[UNK]", ""],
...     },
...     latent_dim=16,
...     num_heads=2,
... )
>>> config.field_sizes["component"], config.primary_label_id
(3, 1)
Source code in models/canvas-vae/src/canvas_vae/configuration_canvas_vae.py
 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
class CanvasVAEConfig(PretrainedConfig):
    """Configuration for a CanvasVAE model trained on RICO.

    A layout has a document-level element count and eight categorical
    per-element fields. Box coordinates and sizes are discretized into
    ``num_bins`` bins over ``[0, 1]``; ``component``, ``icon``, and
    ``text_button`` use the lookup tables in ``vocabularies``, whose index 0 is
    the out-of-vocabulary token.

    Args:
        vocabularies: Lookup tables for ``component``, ``icon``, and
            ``text_button``, each starting with the out-of-vocabulary token.
            Required for a usable model; the default only exists so that
            Transformers can instantiate an empty config while serializing.
        max_length: Maximum number of elements per layout.
        num_bins: Number of discretization bins for box coordinates and sizes.
        latent_dim: Hidden and latent dimension.
        num_blocks: Number of transformer blocks in the encoder and decoder.
        num_heads: Number of attention heads.
        dropout: Dropout rate of position embeddings and blocks.
        kl_weight: Weight of the KL divergence term.
        l2_weight: Weight of the squared-norm penalty on dense and embedding
            weights.
        layer_norm_epsilon: Layer-normalization epsilon.
        batch_norm_epsilon: Batch-normalization epsilon.
        batch_norm_momentum: Batch-normalization moving-average momentum.
        **kwargs: Extra ``PretrainedConfig`` fields.

    Examples:
        >>> config = CanvasVAEConfig(
        ...     vocabularies={
        ...         "component": ["[UNK]", "", "Text"],
        ...         "icon": ["[UNK]", ""],
        ...         "text_button": ["[UNK]", ""],
        ...     },
        ...     latent_dim=16,
        ...     num_heads=2,
        ... )
        >>> config.field_sizes["component"], config.primary_label_id
        (3, 1)
    """

    model_type = "canvas-vae"

    def __init__(
        self,
        vocabularies: Mapping[str, Sequence[str]] | None = None,
        max_length: int = 50,
        num_bins: int = 64,
        latent_dim: int = 256,
        num_blocks: int = 1,
        num_heads: int = 8,
        dropout: float = 0.1,
        kl_weight: float = 16.0,
        l2_weight: float = 1e-6,
        layer_norm_epsilon: float = 1e-3,
        batch_norm_epsilon: float = 1e-3,
        batch_norm_momentum: float = 0.99,
        **kwargs: str | float | bool | None,
    ) -> None:
        """Initialize the config; see the class docstring for arguments."""
        tables = {
            str(key): [str(token) for token in tokens]
            for key, tokens in (vocabularies or {}).items()
        }
        components = tables.get(CanvasVAEField.component, [])
        kwargs.pop("id2label", None)
        kwargs.pop("label2id", None)
        super().__init__(
            id2label=dict(enumerate(components)),
            label2id={token: index for index, token in enumerate(components)},
            **kwargs,  # ty: ignore[invalid-argument-type]
        )
        self.vocabularies = tables
        self.max_length = max_length
        self.num_bins = num_bins
        self.latent_dim = latent_dim
        self.num_blocks = num_blocks
        self.num_heads = num_heads
        self.dropout = dropout
        self.kl_weight = kl_weight
        self.l2_weight = l2_weight
        self.layer_norm_epsilon = layer_norm_epsilon
        self.batch_norm_epsilon = batch_norm_epsilon
        self.batch_norm_momentum = batch_norm_momentum

    @property
    def field_sizes(self) -> dict[str, int]:
        """Return the number of classes of every per-element field.

        Raises:
            ValueError: If a required vocabulary is missing.
        """
        missing = [key for key in VOCABULARY_FIELDS if key not in self.vocabularies]
        if missing:
            raise ValueError(f"CanvasVAEConfig is missing vocabularies: {missing}")

        sizes = {str(key): self.num_bins for key in GEOMETRY_FIELDS}
        sizes[CanvasVAEField.clickable] = NUM_CLICKABLE_CLASSES
        for key in VOCABULARY_FIELDS:
            sizes[key] = len(self.vocabularies[key])

        return sizes

    @property
    def primary_label_id(self) -> int:
        """Return the ``component`` id of the empty label used as background."""
        return self.vocabularies[CanvasVAEField.component].index(PRIMARY_LABEL_TOKEN)

field_sizes property

field_sizes: dict[str, int]

Return the number of classes of every per-element field.

Raises:

Type Description
ValueError

If a required vocabulary is missing.

primary_label_id property

primary_label_id: int

Return the component id of the empty label used as background.

__init__

__init__(
    vocabularies: Mapping[str, Sequence[str]] | None = None,
    max_length: int = 50,
    num_bins: int = 64,
    latent_dim: int = 256,
    num_blocks: int = 1,
    num_heads: int = 8,
    dropout: float = 0.1,
    kl_weight: float = 16.0,
    l2_weight: float = 1e-06,
    layer_norm_epsilon: float = 0.001,
    batch_norm_epsilon: float = 0.001,
    batch_norm_momentum: float = 0.99,
    **kwargs: str | float | bool | None,
) -> None

Initialize the config; see the class docstring for arguments.

Source code in models/canvas-vae/src/canvas_vae/configuration_canvas_vae.py
 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
def __init__(
    self,
    vocabularies: Mapping[str, Sequence[str]] | None = None,
    max_length: int = 50,
    num_bins: int = 64,
    latent_dim: int = 256,
    num_blocks: int = 1,
    num_heads: int = 8,
    dropout: float = 0.1,
    kl_weight: float = 16.0,
    l2_weight: float = 1e-6,
    layer_norm_epsilon: float = 1e-3,
    batch_norm_epsilon: float = 1e-3,
    batch_norm_momentum: float = 0.99,
    **kwargs: str | float | bool | None,
) -> None:
    """Initialize the config; see the class docstring for arguments."""
    tables = {
        str(key): [str(token) for token in tokens]
        for key, tokens in (vocabularies or {}).items()
    }
    components = tables.get(CanvasVAEField.component, [])
    kwargs.pop("id2label", None)
    kwargs.pop("label2id", None)
    super().__init__(
        id2label=dict(enumerate(components)),
        label2id={token: index for index, token in enumerate(components)},
        **kwargs,  # ty: ignore[invalid-argument-type]
    )
    self.vocabularies = tables
    self.max_length = max_length
    self.num_bins = num_bins
    self.latent_dim = latent_dim
    self.num_blocks = num_blocks
    self.num_heads = num_heads
    self.dropout = dropout
    self.kl_weight = kl_weight
    self.l2_weight = l2_weight
    self.layer_norm_epsilon = layer_norm_epsilon
    self.batch_norm_epsilon = batch_norm_epsilon
    self.batch_norm_momentum = batch_norm_momentum

conversion

Checkpoint conversion for CanvasVAE.

TensorFlow checkpoints written by the original Keras trainer store variables under object-graph keys such as encoder/seq2seq/seq2seq_0/attn/dense_query/kernel/.ATTRIBUTES/VARIABLE_VALUE. This module maps those keys to package state-dict keys, transposing Keras dense kernels, and rejects missing or unexpected model variables. Lightning checkpoints written by the package trainer store the model under model..

TensorFlowSource

Bases: NamedTuple

Source variable of one package state-dict entry.

Source code in models/canvas-vae/src/canvas_vae/conversion.py
28
29
30
31
32
class TensorFlowSource(NamedTuple):
    """Source variable of one package state-dict entry."""

    key: str
    transpose: bool

tensorflow_key_map

tensorflow_key_map(
    config: CanvasVAEConfig,
) -> dict[str, TensorFlowSource]

Return the TensorFlow source of every package state-dict entry.

Parameters:

Name Type Description Default
config CanvasVAEConfig

Model config.

required

Returns:

Type Description
dict[str, TensorFlowSource]

Source keys without the /.ATTRIBUTES/VARIABLE_VALUE suffix.

Examples:

>>> config = CanvasVAEConfig(
...     vocabularies={"component": ["[UNK]", ""], "icon": ["[UNK]"],
...                   "text_button": ["[UNK]"]}
... )
>>> tensorflow_key_map(config)["encoder.z_log_var.weight"]
TensorFlowSource(key='encoder/head/z_log_sigma/kernel', transpose=True)
Source code in models/canvas-vae/src/canvas_vae/conversion.py
 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
def tensorflow_key_map(config: CanvasVAEConfig) -> dict[str, TensorFlowSource]:
    """Return the TensorFlow source of every package state-dict entry.

    Args:
        config: Model config.

    Returns:
        Source keys without the ``/.ATTRIBUTES/VARIABLE_VALUE`` suffix.

    Examples:
        >>> config = CanvasVAEConfig(
        ...     vocabularies={"component": ["[UNK]", ""], "icon": ["[UNK]"],
        ...                   "text_button": ["[UNK]"]}
        ... )
        >>> tensorflow_key_map(config)["encoder.z_log_var.weight"]
        TensorFlowSource(key='encoder/head/z_log_sigma/kernel', transpose=True)
    """
    mapping = {
        "encoder.length_embedding.weight": TensorFlowSource(
            "encoder/input_layer/length/embeddings", False
        ),
        "encoder.position_embedding.embeddings.weight": TensorFlowSource(
            "encoder/input_layer/const/embeddings/embeddings", False
        ),
        "encoder.norm.weight": TensorFlowSource("encoder/norm/gamma", False),
        "encoder.norm.bias": TensorFlowSource("encoder/norm/beta", False),
        "encoder.norm.running_mean": TensorFlowSource(
            "encoder/norm/moving_mean", False
        ),
        "encoder.norm.running_var": TensorFlowSource(
            "encoder/norm/moving_variance", False
        ),
        "decoder.position_embedding.embeddings.weight": TensorFlowSource(
            "decoder/embedding_const/embeddings/embeddings", False
        ),
    }
    mapping |= _dense("encoder.z_mean", "encoder/head/z_mean")
    mapping |= _dense("encoder.z_log_var", "encoder/head/z_log_sigma")
    mapping |= _dense("decoder.length_head", "decoder/head/decoders/length")
    for field in CanvasVAEField:
        mapping[f"encoder.field_embeddings.{field}.weight"] = TensorFlowSource(
            f"encoder/input_layer/{field}/embeddings", False
        )
        mapping |= _dense(
            f"decoder.field_heads.{field}", f"decoder/head/decoders/{field}"
        )

    for index in range(config.num_blocks):
        for part in ("encoder", "decoder"):
            mapping |= _block(
                f"{part}.blocks.{index}", f"{part}/seq2seq/seq2seq_{index}"
            )

    return mapping

convert_tensorflow_variables

convert_tensorflow_variables(
    variables: Mapping[str, Shaped[ndarray, ...]],
    config: CanvasVAEConfig,
) -> dict[str, Float[torch.Tensor, ...]]

Convert TensorFlow checkpoint variables into a package state dict.

Parameters:

Name Type Description Default
variables Mapping[str, Shaped[ndarray, ...]]

Checkpoint variables keyed by object-graph key, with or without the /.ATTRIBUTES/VARIABLE_VALUE suffix. Optimizer and bookkeeping entries are ignored.

required
config CanvasVAEConfig

Model config.

required

Returns:

Type Description
dict[str, Float[Tensor, ...]]

Package state dict.

Raises:

Type Description
KeyError

If model variables are missing or unexpected.

Source code in models/canvas-vae/src/canvas_vae/conversion.py
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
def convert_tensorflow_variables(
    variables: Mapping[str, Shaped[np.ndarray, ...]],
    config: CanvasVAEConfig,
) -> dict[str, Float[torch.Tensor, ...]]:
    """Convert TensorFlow checkpoint variables into a package state dict.

    Args:
        variables: Checkpoint variables keyed by object-graph key, with or
            without the ``/.ATTRIBUTES/VARIABLE_VALUE`` suffix. Optimizer and
            bookkeeping entries are ignored.
        config: Model config.

    Returns:
        Package state dict.

    Raises:
        KeyError: If model variables are missing or unexpected.
    """
    model_variables = {
        key.removesuffix(VARIABLE_SUFFIX): value
        for key, value in variables.items()
        if not any(part in key for part in IGNORED_KEY_PARTS)
    }
    mapping = tensorflow_key_map(config)
    expected = {source.key for source in mapping.values()}
    missing = sorted(expected - model_variables.keys())
    unexpected = sorted(model_variables.keys() - expected)
    if missing:
        raise KeyError(f"missing TensorFlow variables: {missing}")

    if unexpected:
        raise KeyError(f"unexpected TensorFlow variables: {unexpected}")

    state_dict = {}
    for key, source in mapping.items():
        value = np.asarray(model_variables[source.key], dtype=np.float32)
        state_dict[key] = torch.from_numpy(
            np.ascontiguousarray(value.T if source.transpose else value)
        )

    return state_dict

load_lightning_state_dict

load_lightning_state_dict(
    checkpoint: str | Path,
) -> dict[str, Float[torch.Tensor, ...]]

Load the model state dict from a package Lightning checkpoint.

Parameters:

Name Type Description Default
checkpoint str | Path

Lightning .ckpt file.

required

Returns:

Type Description
dict[str, Float[Tensor, ...]]

Model state dict without the model. prefix.

Source code in models/canvas-vae/src/canvas_vae/conversion.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
def load_lightning_state_dict(
    checkpoint: str | Path,
) -> dict[str, Float[torch.Tensor, ...]]:
    """Load the model state dict from a package Lightning checkpoint.

    Args:
        checkpoint: Lightning ``.ckpt`` file.

    Returns:
        Model state dict without the ``model.`` prefix.
    """
    payload = torch.load(checkpoint, map_location="cpu", weights_only=True)
    return {
        key.removeprefix(LIGHTNING_MODEL_PREFIX): value
        for key, value in payload["state_dict"].items()
        if key.startswith(LIGHTNING_MODEL_PREFIX)
    }

metrics

Reconstruction and random-generation metrics for CanvasVAE layouts.

Reconstruction scores compare a decoded layout with its source layout per document: a unigram BLEU score per element field, their mean (total), and pixel accuracy and mean IoU of the component maps rasterized on the num_bins x num_bins grid. Random-generation scores compare normalized histograms of every field between a reference split and decoded prior samples by histogram intersection.

bleu1

bleu1(
    target_ids: Int[Tensor, "batch target_elements"],
    target_mask: Bool[Tensor, "batch target_elements"],
    predicted_ids: Int[Tensor, "batch predicted_elements"],
    predicted_mask: Bool[
        Tensor, "batch predicted_elements"
    ],
    num_classes: int,
) -> Float[torch.Tensor, "batch"]

Return the unigram BLEU score of every document for one field.

Parameters:

Name Type Description Default
target_ids Int[Tensor, 'batch target_elements']

Reference field ids.

required
target_mask Bool[Tensor, 'batch target_elements']

Valid reference elements.

required
predicted_ids Int[Tensor, 'batch predicted_elements']

Predicted field ids.

required
predicted_mask Bool[Tensor, 'batch predicted_elements']

Valid predicted elements.

required
num_classes int

Number of field classes.

required

Returns:

Type Description
Float[Tensor, 'batch']

Brevity-penalized square root of unigram precision, clipped to

Float[Tensor, 'batch']

[0, 1].

Examples:

>>> ids = torch.tensor([[1, 2]])
>>> mask = torch.tensor([[True, True]])
>>> bleu1(ids, mask, ids, mask, 3)
tensor([1.])
Source code in models/canvas-vae/src/canvas_vae/metrics.py
24
25
26
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
def bleu1(
    target_ids: Int[torch.Tensor, "batch target_elements"],
    target_mask: Bool[torch.Tensor, "batch target_elements"],
    predicted_ids: Int[torch.Tensor, "batch predicted_elements"],
    predicted_mask: Bool[torch.Tensor, "batch predicted_elements"],
    num_classes: int,
) -> Float[torch.Tensor, "batch"]:
    """Return the unigram BLEU score of every document for one field.

    Args:
        target_ids: Reference field ids.
        target_mask: Valid reference elements.
        predicted_ids: Predicted field ids.
        predicted_mask: Valid predicted elements.
        num_classes: Number of field classes.

    Returns:
        Brevity-penalized square root of unigram precision, clipped to
        ``[0, 1]``.

    Examples:
        >>> ids = torch.tensor([[1, 2]])
        >>> mask = torch.tensor([[True, True]])
        >>> bleu1(ids, mask, ids, mask, 3)
        tensor([1.])
    """
    target_bow = _bag_of_words(target_ids, target_mask, num_classes)
    predicted_bow = _bag_of_words(predicted_ids, predicted_mask, num_classes)
    target_length = target_mask.float().sum(dim=1) + 1e-9
    predicted_length = predicted_mask.float().sum(dim=1) + 1e-9
    match = torch.minimum(target_bow, predicted_bow).sum(dim=1).float()
    precision = match / predicted_length
    brevity = torch.exp(torch.clamp(1.0 - target_length / predicted_length, max=0.0))
    return (brevity * torch.sqrt(precision)).clamp(0.0, 1.0)

reconstruction_scores

reconstruction_scores(
    target_ids: Int[Tensor, "batch target_elements fields"],
    target_mask: Bool[Tensor, "batch target_elements"],
    predicted_ids: Int[
        Tensor, "batch predicted_elements fields"
    ],
    predicted_mask: Bool[
        Tensor, "batch predicted_elements"
    ],
    field_sizes: Mapping[str, int],
) -> dict[str, Float[torch.Tensor, "batch"]]

Return per-document BLEU scores of every field and their mean.

Parameters:

Name Type Description Default
target_ids Int[Tensor, 'batch target_elements fields']

Reference field ids in :class:CanvasVAEField order.

required
target_mask Bool[Tensor, 'batch target_elements']

Valid reference elements.

required
predicted_ids Int[Tensor, 'batch predicted_elements fields']

Predicted field ids in the same order.

required
predicted_mask Bool[Tensor, 'batch predicted_elements']

Valid predicted elements.

required
field_sizes Mapping[str, int]

Number of classes per field.

required

Returns:

Type Description
dict[str, Float[Tensor, 'batch']]

Scores keyed by field name plus total.

Source code in models/canvas-vae/src/canvas_vae/metrics.py
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
def reconstruction_scores(
    target_ids: Int[torch.Tensor, "batch target_elements fields"],
    target_mask: Bool[torch.Tensor, "batch target_elements"],
    predicted_ids: Int[torch.Tensor, "batch predicted_elements fields"],
    predicted_mask: Bool[torch.Tensor, "batch predicted_elements"],
    field_sizes: Mapping[str, int],
) -> dict[str, Float[torch.Tensor, "batch"]]:
    """Return per-document BLEU scores of every field and their mean.

    Args:
        target_ids: Reference field ids in :class:`CanvasVAEField` order.
        target_mask: Valid reference elements.
        predicted_ids: Predicted field ids in the same order.
        predicted_mask: Valid predicted elements.
        field_sizes: Number of classes per field.

    Returns:
        Scores keyed by field name plus ``total``.
    """
    scores = {
        str(field): bleu1(
            target_ids[..., index],
            target_mask,
            predicted_ids[..., index],
            predicted_mask,
            field_sizes[field],
        )
        for index, field in enumerate(CanvasVAEField)
    }
    scores[TOTAL_KEY] = torch.stack(tuple(scores.values()), dim=1).mean(dim=1)
    return scores

component_grid

component_grid(
    element_ids: Int[Tensor, "elements fields"],
    *,
    grid_size: int,
    background_id: int,
) -> Int[torch.Tensor, "grid grid"]

Rasterize one layout's component ids on the bin grid.

Each element fills rows top..min(top + height, grid_size - 1) and columns left..min(left + width, grid_size - 1) inclusively in element order; elements with an empty span are skipped.

Parameters:

Name Type Description Default
element_ids Int[Tensor, 'elements fields']

Valid elements of one layout in :class:CanvasVAEField order.

required
grid_size int

Number of geometry bins.

required
background_id int

component id painted where no element lies.

required

Returns:

Type Description
Int[Tensor, 'grid grid']

Label grid indexed by row and column.

Examples:

>>> element = torch.tensor([[0, 0, 1, 1, 0, 2, 0, 0]])
>>> component_grid(element, grid_size=3, background_id=1).tolist()
[[2, 2, 1], [2, 2, 1], [1, 1, 1]]
Source code in models/canvas-vae/src/canvas_vae/metrics.py
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
def component_grid(
    element_ids: Int[torch.Tensor, "elements fields"],
    *,
    grid_size: int,
    background_id: int,
) -> Int[torch.Tensor, "grid grid"]:
    """Rasterize one layout's ``component`` ids on the bin grid.

    Each element fills rows ``top..min(top + height, grid_size - 1)`` and
    columns ``left..min(left + width, grid_size - 1)`` inclusively in element
    order; elements with an empty span are skipped.

    Args:
        element_ids: Valid elements of one layout in :class:`CanvasVAEField`
            order.
        grid_size: Number of geometry bins.
        background_id: ``component`` id painted where no element lies.

    Returns:
        Label grid indexed by row and column.

    Examples:
        >>> element = torch.tensor([[0, 0, 1, 1, 0, 2, 0, 0]])
        >>> component_grid(element, grid_size=3, background_id=1).tolist()
        [[2, 2, 1], [2, 2, 1], [1, 1, 1]]
    """
    grid = torch.full((grid_size, grid_size), background_id, dtype=torch.long)
    component_index = list(CanvasVAEField).index(CanvasVAEField.component)
    for left, top, width, height, *rest in element_ids[
        :, : component_index + 1
    ].tolist():
        right = min(grid_size - 1, left + width)
        bottom = min(grid_size - 1, top + height)
        if top >= bottom or left >= right:
            continue

        grid[top : bottom + 1, left : right + 1] = rest[-1]

    return grid

layout_scores

layout_scores(
    target_ids: Int[Tensor, "batch target_elements fields"],
    target_mask: Bool[Tensor, "batch target_elements"],
    predicted_ids: Int[
        Tensor, "batch predicted_elements fields"
    ],
    predicted_mask: Bool[
        Tensor, "batch predicted_elements"
    ],
    *,
    grid_size: int,
    num_labels: int,
    background_id: int,
) -> dict[str, Float[torch.Tensor, "batch"]]

Return per-document pixel accuracy and mean IoU of component grids.

Parameters:

Name Type Description Default
target_ids Int[Tensor, 'batch target_elements fields']

Reference field ids.

required
target_mask Bool[Tensor, 'batch target_elements']

Valid reference elements.

required
predicted_ids Int[Tensor, 'batch predicted_elements fields']

Predicted field ids.

required
predicted_mask Bool[Tensor, 'batch predicted_elements']

Valid predicted elements.

required
grid_size int

Number of geometry bins.

required
num_labels int

Size of the component vocabulary.

required
background_id int

component id of the background.

required

Returns:

Type Description
dict[str, Float[Tensor, 'batch']]

layout_acc and layout_miou per document.

Source code in models/canvas-vae/src/canvas_vae/metrics.py
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
def layout_scores(
    target_ids: Int[torch.Tensor, "batch target_elements fields"],
    target_mask: Bool[torch.Tensor, "batch target_elements"],
    predicted_ids: Int[torch.Tensor, "batch predicted_elements fields"],
    predicted_mask: Bool[torch.Tensor, "batch predicted_elements"],
    *,
    grid_size: int,
    num_labels: int,
    background_id: int,
) -> dict[str, Float[torch.Tensor, "batch"]]:
    """Return per-document pixel accuracy and mean IoU of component grids.

    Args:
        target_ids: Reference field ids.
        target_mask: Valid reference elements.
        predicted_ids: Predicted field ids.
        predicted_mask: Valid predicted elements.
        grid_size: Number of geometry bins.
        num_labels: Size of the ``component`` vocabulary.
        background_id: ``component`` id of the background.

    Returns:
        ``layout_acc`` and ``layout_miou`` per document.
    """
    accuracies, mean_ious = [], []
    for index in range(target_ids.shape[0]):
        target = component_grid(
            target_ids[index][target_mask[index]].cpu(),
            grid_size=grid_size,
            background_id=background_id,
        )
        predicted = component_grid(
            predicted_ids[index][predicted_mask[index]].cpu(),
            grid_size=grid_size,
            background_id=background_id,
        )
        confusion = torch.zeros(num_labels, num_labels)
        confusion.index_put_(
            (predicted.flatten(), target.flatten()),
            torch.ones(grid_size * grid_size),
            accumulate=True,
        )
        intersection = confusion.diagonal()
        union = confusion.sum(dim=0) + confusion.sum(dim=1) - intersection
        valid = (union > 0).float()
        accuracies.append(intersection.sum() / confusion.sum())
        mean_ious.append((valid * intersection / (union + 1e-9)).sum() / valid.sum())

    return {
        "layout_acc": torch.stack(accuracies),
        "layout_miou": torch.stack(mean_ious),
    }

field_histograms

field_histograms(
    num_elements: Int[Tensor, "batch"],
    element_ids: Int[Tensor, "batch elements fields"],
    field_sizes: Mapping[str, int],
    max_length: int,
) -> dict[str, Float[torch.Tensor, "classes"]]

Return normalized histograms of element counts and of every field.

Parameters:

Name Type Description Default
num_elements Int[Tensor, 'batch']

Element count of every layout.

required
element_ids Int[Tensor, 'batch elements fields']

Field ids in :class:CanvasVAEField order.

required
field_sizes Mapping[str, int]

Number of classes per field.

required
max_length int

Maximum element count.

required

Returns:

Type Description
dict[str, Float[Tensor, 'classes']]

Histograms keyed by length and field name.

Examples:

>>> ids = torch.zeros(1, 2, 8, dtype=torch.long)
>>> sizes = {field: 2 for field in CanvasVAEField}
>>> field_histograms(torch.tensor([1]), ids, sizes, 3)["length"].tolist()
[1.0, 0.0, 0.0]
Source code in models/canvas-vae/src/canvas_vae/metrics.py
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
def field_histograms(
    num_elements: Int[torch.Tensor, "batch"],
    element_ids: Int[torch.Tensor, "batch elements fields"],
    field_sizes: Mapping[str, int],
    max_length: int,
) -> dict[str, Float[torch.Tensor, "classes"]]:
    """Return normalized histograms of element counts and of every field.

    Args:
        num_elements: Element count of every layout.
        element_ids: Field ids in :class:`CanvasVAEField` order.
        field_sizes: Number of classes per field.
        max_length: Maximum element count.

    Returns:
        Histograms keyed by ``length`` and field name.

    Examples:
        >>> ids = torch.zeros(1, 2, 8, dtype=torch.long)
        >>> sizes = {field: 2 for field in CanvasVAEField}
        >>> field_histograms(torch.tensor([1]), ids, sizes, 3)["length"].tolist()
        [1.0, 0.0, 0.0]
    """
    histograms = {
        LENGTH_KEY: torch.bincount(num_elements - 1, minlength=max_length).float()
    }
    positions = torch.arange(element_ids.shape[1], device=element_ids.device)
    mask = positions.unsqueeze(0) < num_elements.unsqueeze(1)
    for index, field in enumerate(CanvasVAEField):
        values = element_ids[..., index][mask]
        histograms[field] = torch.bincount(values, minlength=field_sizes[field]).float()

    return {key: value / value.sum() for key, value in histograms.items()}

histogram_scores

histogram_scores(
    reference: Mapping[str, Float[Tensor, "classes"]],
    generated: Mapping[str, Float[Tensor, "classes"]],
) -> dict[str, float]

Return histogram intersections per key and their mean.

Parameters:

Name Type Description Default
reference Mapping[str, Float[Tensor, 'classes']]

Histograms of a reference split.

required
generated Mapping[str, Float[Tensor, 'classes']]

Histograms of generated layouts.

required

Returns:

Type Description
dict[str, float]

Intersection per key plus total.

Examples:

>>> histogram_scores({"a": torch.tensor([0.5, 0.5])},
...                  {"a": torch.tensor([1.0, 0.0])})
{'a': 0.5, 'total': 0.5}
Source code in models/canvas-vae/src/canvas_vae/metrics.py
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
def histogram_scores(
    reference: Mapping[str, Float[torch.Tensor, "classes"]],
    generated: Mapping[str, Float[torch.Tensor, "classes"]],
) -> dict[str, float]:
    """Return histogram intersections per key and their mean.

    Args:
        reference: Histograms of a reference split.
        generated: Histograms of generated layouts.

    Returns:
        Intersection per key plus ``total``.

    Examples:
        >>> histogram_scores({"a": torch.tensor([0.5, 0.5])},
        ...                  {"a": torch.tensor([1.0, 0.0])})
        {'a': 0.5, 'total': 0.5}
    """
    scores = {
        key: torch.minimum(reference[key], generated[key].to(reference[key].device))
        .sum()
        .item()
        for key in reference
    }
    scores[TOTAL_KEY] = sum(scores.values()) / len(scores)
    return scores

modeling_canvas_vae

CanvasVAE encoder-decoder model.

CanvasVAEModelOutput dataclass

Bases: ModelOutput

Outputs of :class:CanvasVAEModel.

Attributes:

Name Type Description
loss Float[Tensor, ''] | None

Reconstruction plus weighted KL loss; present when layout inputs are given and the model is in training mode.

length_logits Float[Tensor, 'batch lengths'] | None

Logits over the zero-based element count.

element_logits dict[str, Float[Tensor, 'batch elements classes']] | None

Logits of every per-element field, keyed by field name.

mask Bool[Tensor, 'batch elements'] | None

Valid-element mask used by the decoder.

latents Float[Tensor, 'batch latent'] | None

Latent codes fed to the decoder.

z_mean Float[Tensor, 'batch latent'] | None

Posterior mean; present when layout inputs are given.

z_log_var Float[Tensor, 'batch latent'] | None

Posterior log-variance; present when layout inputs are given.

kl_divergence Float[Tensor, ''] | None

Unweighted KL divergence to the standard normal prior.

reconstruction_losses dict[str, Float[Tensor, '']] | None

Per-field reconstruction losses, including length; present together with loss.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@dataclass
class CanvasVAEModelOutput(ModelOutput):
    """Outputs of :class:`CanvasVAEModel`.

    Attributes:
        loss: Reconstruction plus weighted KL loss; present when layout inputs
            are given and the model is in training mode.
        length_logits: Logits over the zero-based element count.
        element_logits: Logits of every per-element field, keyed by field name.
        mask: Valid-element mask used by the decoder.
        latents: Latent codes fed to the decoder.
        z_mean: Posterior mean; present when layout inputs are given.
        z_log_var: Posterior log-variance; present when layout inputs are given.
        kl_divergence: Unweighted KL divergence to the standard normal prior.
        reconstruction_losses: Per-field reconstruction losses, including
            ``length``; present together with ``loss``.
    """

    loss: Float[torch.Tensor, ""] | None = None
    length_logits: Float[torch.Tensor, "batch lengths"] | None = None
    element_logits: dict[str, Float[torch.Tensor, "batch elements classes"]] | None = (
        None
    )
    mask: Bool[torch.Tensor, "batch elements"] | None = None
    latents: Float[torch.Tensor, "batch latent"] | None = None
    z_mean: Float[torch.Tensor, "batch latent"] | None = None
    z_log_var: Float[torch.Tensor, "batch latent"] | None = None
    kl_divergence: Float[torch.Tensor, ""] | None = None
    reconstruction_losses: dict[str, Float[torch.Tensor, ""]] | None = None

CanvasVAEBatchNorm

Bases: Module

Batch normalization over latent features with population variance.

Training mode normalizes with the batch mean and the biased batch variance and moves both moving statistics toward them; evaluation mode uses the moving statistics.

Parameters:

Name Type Description Default
num_features int

Feature dimension.

required
eps float

Variance epsilon.

required
momentum float

Moving-average momentum of the previous statistic.

required
Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
 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
class CanvasVAEBatchNorm(nn.Module):
    """Batch normalization over latent features with population variance.

    Training mode normalizes with the batch mean and the biased batch variance
    and moves both moving statistics toward them; evaluation mode uses the
    moving statistics.

    Args:
        num_features: Feature dimension.
        eps: Variance epsilon.
        momentum: Moving-average momentum of the previous statistic.
    """

    running_mean: Float[torch.Tensor, "features"]
    running_var: Float[torch.Tensor, "features"]

    def __init__(self, num_features: int, eps: float, momentum: float) -> None:
        """Create parameters and moving statistics."""
        super().__init__()
        self.eps = eps
        self.momentum = momentum
        self.weight = nn.Parameter(torch.ones(num_features))
        self.bias = nn.Parameter(torch.zeros(num_features))
        self.register_buffer("running_mean", torch.zeros(num_features))
        self.register_buffer("running_var", torch.ones(num_features))

    def forward(
        self, inputs: Float[torch.Tensor, "batch features"]
    ) -> Float[torch.Tensor, "batch features"]:
        """Normalize ``inputs``.

        Args:
            inputs: Pooled features.

        Returns:
            Normalized features.
        """
        if self.training:
            mean = inputs.mean(dim=0)
            variance = (inputs - mean.detach()).square().mean(dim=0)
            with torch.no_grad():
                decay = 1.0 - self.momentum
                self.running_mean.sub_((self.running_mean - mean) * decay)
                self.running_var.sub_((self.running_var - variance) * decay)
        else:
            mean = self.running_mean
            variance = self.running_var

        inverse = torch.rsqrt(variance + self.eps) * self.weight
        return inputs * inverse + (self.bias - mean * inverse)

__init__

__init__(
    num_features: int, eps: float, momentum: float
) -> None

Create parameters and moving statistics.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
89
90
91
92
93
94
95
96
97
def __init__(self, num_features: int, eps: float, momentum: float) -> None:
    """Create parameters and moving statistics."""
    super().__init__()
    self.eps = eps
    self.momentum = momentum
    self.weight = nn.Parameter(torch.ones(num_features))
    self.bias = nn.Parameter(torch.zeros(num_features))
    self.register_buffer("running_mean", torch.zeros(num_features))
    self.register_buffer("running_var", torch.ones(num_features))

forward

forward(
    inputs: Float[Tensor, "batch features"],
) -> Float[torch.Tensor, "batch features"]

Normalize inputs.

Parameters:

Name Type Description Default
inputs Float[Tensor, 'batch features']

Pooled features.

required

Returns:

Type Description
Float[Tensor, 'batch features']

Normalized features.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def forward(
    self, inputs: Float[torch.Tensor, "batch features"]
) -> Float[torch.Tensor, "batch features"]:
    """Normalize ``inputs``.

    Args:
        inputs: Pooled features.

    Returns:
        Normalized features.
    """
    if self.training:
        mean = inputs.mean(dim=0)
        variance = (inputs - mean.detach()).square().mean(dim=0)
        with torch.no_grad():
            decay = 1.0 - self.momentum
            self.running_mean.sub_((self.running_mean - mean) * decay)
            self.running_var.sub_((self.running_var - variance) * decay)
    else:
        mean = self.running_mean
        variance = self.running_var

    inverse = torch.rsqrt(variance + self.eps) * self.weight
    return inputs * inverse + (self.bias - mean * inverse)

CanvasVAEBlock

Bases: Module

Pre-norm transformer block conditioned on a global vector.

Parameters:

Name Type Description Default
config CanvasVAEConfig

Model config.

required
pooling bool

Whether to return the masked mean of ReLU outputs.

required
Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
class CanvasVAEBlock(nn.Module):
    """Pre-norm transformer block conditioned on a global vector.

    Args:
        config: Model config.
        pooling: Whether to return the masked mean of ReLU outputs.
    """

    def __init__(self, config: CanvasVAEConfig, *, pooling: bool) -> None:
        """Create attention, conditioning, and feed-forward layers."""
        super().__init__()
        dim = config.latent_dim
        self.pooling = pooling
        self.norm1 = nn.LayerNorm(dim, eps=config.layer_norm_epsilon)
        self.attention = MultiHeadSelfAttention(dim, config.num_heads)
        self.dropout1 = nn.Dropout(config.dropout)
        self.conditional = nn.Linear(dim, dim)
        self.norm2 = nn.LayerNorm(dim, eps=config.layer_norm_epsilon)
        self.mlp = nn.Sequential(
            nn.Linear(dim, 2 * dim), nn.ReLU(), nn.Linear(2 * dim, dim)
        )
        self.dropout2 = nn.Dropout(config.dropout)

    def forward(
        self,
        hidden_states: Float[torch.Tensor, "batch elements dim"],
        condition: Float[torch.Tensor, "batch dim"],
        mask: Bool[torch.Tensor, "batch elements"],
    ) -> Float[torch.Tensor, "batch elements dim"] | Float[torch.Tensor, "batch dim"]:
        """Apply the block.

        Args:
            hidden_states: Element sequence.
            condition: Global conditioning vector.
            mask: Valid-element mask.

        Returns:
            Updated sequence, or its masked mean when pooling.
        """
        attended = self.attention(self.norm1(hidden_states), mask)
        hidden_states = hidden_states + self.dropout1(attended)
        hidden_states = hidden_states + self.conditional(condition).unsqueeze(1)
        hidden_states = hidden_states + self.dropout2(
            self.mlp(self.norm2(hidden_states))
        )
        if not self.pooling:
            return hidden_states

        weights = mask.to(hidden_states.dtype).unsqueeze(-1)
        pooled = (torch.relu(hidden_states) * weights).sum(dim=1)
        return pooled / weights.sum(dim=1)

__init__

__init__(config: CanvasVAEConfig, *, pooling: bool) -> None

Create attention, conditioning, and feed-forward layers.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def __init__(self, config: CanvasVAEConfig, *, pooling: bool) -> None:
    """Create attention, conditioning, and feed-forward layers."""
    super().__init__()
    dim = config.latent_dim
    self.pooling = pooling
    self.norm1 = nn.LayerNorm(dim, eps=config.layer_norm_epsilon)
    self.attention = MultiHeadSelfAttention(dim, config.num_heads)
    self.dropout1 = nn.Dropout(config.dropout)
    self.conditional = nn.Linear(dim, dim)
    self.norm2 = nn.LayerNorm(dim, eps=config.layer_norm_epsilon)
    self.mlp = nn.Sequential(
        nn.Linear(dim, 2 * dim), nn.ReLU(), nn.Linear(2 * dim, dim)
    )
    self.dropout2 = nn.Dropout(config.dropout)

forward

forward(
    hidden_states: Float[Tensor, "batch elements dim"],
    condition: Float[Tensor, "batch dim"],
    mask: Bool[Tensor, "batch elements"],
) -> (
    Float[torch.Tensor, "batch elements dim"]
    | Float[torch.Tensor, "batch dim"]
)

Apply the block.

Parameters:

Name Type Description Default
hidden_states Float[Tensor, 'batch elements dim']

Element sequence.

required
condition Float[Tensor, 'batch dim']

Global conditioning vector.

required
mask Bool[Tensor, 'batch elements']

Valid-element mask.

required

Returns:

Type Description
Float[Tensor, 'batch elements dim'] | Float[Tensor, 'batch dim']

Updated sequence, or its masked mean when pooling.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
def forward(
    self,
    hidden_states: Float[torch.Tensor, "batch elements dim"],
    condition: Float[torch.Tensor, "batch dim"],
    mask: Bool[torch.Tensor, "batch elements"],
) -> Float[torch.Tensor, "batch elements dim"] | Float[torch.Tensor, "batch dim"]:
    """Apply the block.

    Args:
        hidden_states: Element sequence.
        condition: Global conditioning vector.
        mask: Valid-element mask.

    Returns:
        Updated sequence, or its masked mean when pooling.
    """
    attended = self.attention(self.norm1(hidden_states), mask)
    hidden_states = hidden_states + self.dropout1(attended)
    hidden_states = hidden_states + self.conditional(condition).unsqueeze(1)
    hidden_states = hidden_states + self.dropout2(
        self.mlp(self.norm2(hidden_states))
    )
    if not self.pooling:
        return hidden_states

    weights = mask.to(hidden_states.dtype).unsqueeze(-1)
    pooled = (torch.relu(hidden_states) * weights).sum(dim=1)
    return pooled / weights.sum(dim=1)

CanvasVAEPositionEmbedding

Bases: Module

Learned position embeddings with dropout.

Parameters:

Name Type Description Default
config CanvasVAEConfig

Model config.

required
Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
class CanvasVAEPositionEmbedding(nn.Module):
    """Learned position embeddings with dropout.

    Args:
        config: Model config.
    """

    def __init__(self, config: CanvasVAEConfig) -> None:
        """Create the embedding table."""
        super().__init__()
        self.embeddings = nn.Embedding(config.max_length, config.latent_dim)
        self.dropout = nn.Dropout(config.dropout)

    def forward(
        self, mask: Bool[torch.Tensor, "batch elements"]
    ) -> Float[torch.Tensor, "batch elements dim"]:
        """Return dropout-applied position embeddings shaped like ``mask``.

        Args:
            mask: Valid-element mask whose shape selects the output shape.

        Returns:
            Position embeddings tiled over the batch.
        """
        batch, elements = mask.shape
        weight = self.embeddings.weight[:elements]
        return self.dropout(weight.unsqueeze(0).expand(batch, -1, -1))

__init__

__init__(config: CanvasVAEConfig) -> None

Create the embedding table.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
185
186
187
188
189
def __init__(self, config: CanvasVAEConfig) -> None:
    """Create the embedding table."""
    super().__init__()
    self.embeddings = nn.Embedding(config.max_length, config.latent_dim)
    self.dropout = nn.Dropout(config.dropout)

forward

forward(
    mask: Bool[Tensor, "batch elements"],
) -> Float[torch.Tensor, "batch elements dim"]

Return dropout-applied position embeddings shaped like mask.

Parameters:

Name Type Description Default
mask Bool[Tensor, 'batch elements']

Valid-element mask whose shape selects the output shape.

required

Returns:

Type Description
Float[Tensor, 'batch elements dim']

Position embeddings tiled over the batch.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
def forward(
    self, mask: Bool[torch.Tensor, "batch elements"]
) -> Float[torch.Tensor, "batch elements dim"]:
    """Return dropout-applied position embeddings shaped like ``mask``.

    Args:
        mask: Valid-element mask whose shape selects the output shape.

    Returns:
        Position embeddings tiled over the batch.
    """
    batch, elements = mask.shape
    weight = self.embeddings.weight[:elements]
    return self.dropout(weight.unsqueeze(0).expand(batch, -1, -1))

CanvasVAEEncoder

Bases: Module

Encode a layout into posterior statistics.

Parameters:

Name Type Description Default
config CanvasVAEConfig

Model config.

required
Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
class CanvasVAEEncoder(nn.Module):
    """Encode a layout into posterior statistics.

    Args:
        config: Model config.
    """

    def __init__(self, config: CanvasVAEConfig) -> None:
        """Create input embeddings, blocks, normalization, and the head."""
        super().__init__()
        dim = config.latent_dim
        self.length_embedding = nn.Embedding(config.max_length, dim)
        self.position_embedding = CanvasVAEPositionEmbedding(config)
        self.field_embeddings = nn.ModuleDict(
            {key: nn.Embedding(size, dim) for key, size in config.field_sizes.items()}
        )
        self.blocks = nn.ModuleList(
            CanvasVAEBlock(config, pooling=index == config.num_blocks - 1)
            for index in range(config.num_blocks)
        )
        self.norm = CanvasVAEBatchNorm(
            dim, config.batch_norm_epsilon, config.batch_norm_momentum
        )
        self.z_mean = nn.Linear(dim, dim)
        self.z_log_var = nn.Linear(dim, dim)

    def forward(
        self,
        length_ids: Int[torch.Tensor, "batch"],
        element_ids: Int[torch.Tensor, "batch elements fields"],
    ) -> tuple[Float[torch.Tensor, "batch dim"], Float[torch.Tensor, "batch dim"]]:
        """Return the posterior mean and log-variance.

        Args:
            length_ids: Zero-based element counts.
            element_ids: Field ids in :class:`CanvasVAEField` order.

        Returns:
            Posterior mean and log-variance.
        """
        mask = length_mask(length_ids, element_ids.shape[1])
        condition = self.length_embedding(length_ids)
        hidden_states = self.position_embedding(mask)
        for index, embedding in enumerate(self.field_embeddings.values()):
            hidden_states = hidden_states + embedding(element_ids[..., index])

        for block in self.blocks:
            hidden_states = block(hidden_states, condition, mask)

        pooled = self.norm(hidden_states)
        return self.z_mean(pooled), self.z_log_var(pooled)

__init__

__init__(config: CanvasVAEConfig) -> None

Create input embeddings, blocks, normalization, and the head.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def __init__(self, config: CanvasVAEConfig) -> None:
    """Create input embeddings, blocks, normalization, and the head."""
    super().__init__()
    dim = config.latent_dim
    self.length_embedding = nn.Embedding(config.max_length, dim)
    self.position_embedding = CanvasVAEPositionEmbedding(config)
    self.field_embeddings = nn.ModuleDict(
        {key: nn.Embedding(size, dim) for key, size in config.field_sizes.items()}
    )
    self.blocks = nn.ModuleList(
        CanvasVAEBlock(config, pooling=index == config.num_blocks - 1)
        for index in range(config.num_blocks)
    )
    self.norm = CanvasVAEBatchNorm(
        dim, config.batch_norm_epsilon, config.batch_norm_momentum
    )
    self.z_mean = nn.Linear(dim, dim)
    self.z_log_var = nn.Linear(dim, dim)

forward

forward(
    length_ids: Int[Tensor, "batch"],
    element_ids: Int[Tensor, "batch elements fields"],
) -> tuple[
    Float[torch.Tensor, "batch dim"],
    Float[torch.Tensor, "batch dim"],
]

Return the posterior mean and log-variance.

Parameters:

Name Type Description Default
length_ids Int[Tensor, 'batch']

Zero-based element counts.

required
element_ids Int[Tensor, 'batch elements fields']

Field ids in :class:CanvasVAEField order.

required

Returns:

Type Description
tuple[Float[Tensor, 'batch dim'], Float[Tensor, 'batch dim']]

Posterior mean and log-variance.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
def forward(
    self,
    length_ids: Int[torch.Tensor, "batch"],
    element_ids: Int[torch.Tensor, "batch elements fields"],
) -> tuple[Float[torch.Tensor, "batch dim"], Float[torch.Tensor, "batch dim"]]:
    """Return the posterior mean and log-variance.

    Args:
        length_ids: Zero-based element counts.
        element_ids: Field ids in :class:`CanvasVAEField` order.

    Returns:
        Posterior mean and log-variance.
    """
    mask = length_mask(length_ids, element_ids.shape[1])
    condition = self.length_embedding(length_ids)
    hidden_states = self.position_embedding(mask)
    for index, embedding in enumerate(self.field_embeddings.values()):
        hidden_states = hidden_states + embedding(element_ids[..., index])

    for block in self.blocks:
        hidden_states = block(hidden_states, condition, mask)

    pooled = self.norm(hidden_states)
    return self.z_mean(pooled), self.z_log_var(pooled)

CanvasVAEDecoder

Bases: Module

Decode latent codes into per-element field logits in one pass.

Parameters:

Name Type Description Default
config CanvasVAEConfig

Model config.

required
Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
class CanvasVAEDecoder(nn.Module):
    """Decode latent codes into per-element field logits in one pass.

    Args:
        config: Model config.
    """

    def __init__(self, config: CanvasVAEConfig) -> None:
        """Create position embeddings, blocks, and output heads."""
        super().__init__()
        dim = config.latent_dim
        self.position_embedding = CanvasVAEPositionEmbedding(config)
        self.blocks = nn.ModuleList(
            CanvasVAEBlock(config, pooling=False) for _ in range(config.num_blocks)
        )
        self.length_head = nn.Linear(dim, config.max_length)
        self.field_heads = nn.ModuleDict(
            {key: nn.Linear(dim, size) for key, size in config.field_sizes.items()}
        )

    def forward(
        self,
        latents: Float[torch.Tensor, "batch dim"],
        mask: Bool[torch.Tensor, "batch elements"] | None = None,
    ) -> tuple[
        Float[torch.Tensor, "batch lengths"],
        dict[str, Float[torch.Tensor, "batch elements classes"]],
        Bool[torch.Tensor, "batch elements"],
    ]:
        """Decode latent codes.

        Args:
            latents: Latent codes.
            mask: Element mask to decode with. Defaults to the mask of the
                predicted element counts.

        Returns:
            Length logits, per-field logits, and the decoding mask.
        """
        length_logits = self.length_head(latents)
        if mask is None:
            mask = length_mask(length_logits.argmax(dim=-1))

        hidden_states = self.position_embedding(mask)
        for block in self.blocks:
            hidden_states = block(hidden_states, latents, mask)

        logits = {key: head(hidden_states) for key, head in self.field_heads.items()}
        return length_logits, logits, mask

__init__

__init__(config: CanvasVAEConfig) -> None

Create position embeddings, blocks, and output heads.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
267
268
269
270
271
272
273
274
275
276
277
278
def __init__(self, config: CanvasVAEConfig) -> None:
    """Create position embeddings, blocks, and output heads."""
    super().__init__()
    dim = config.latent_dim
    self.position_embedding = CanvasVAEPositionEmbedding(config)
    self.blocks = nn.ModuleList(
        CanvasVAEBlock(config, pooling=False) for _ in range(config.num_blocks)
    )
    self.length_head = nn.Linear(dim, config.max_length)
    self.field_heads = nn.ModuleDict(
        {key: nn.Linear(dim, size) for key, size in config.field_sizes.items()}
    )

forward

forward(
    latents: Float[Tensor, "batch dim"],
    mask: Bool[Tensor, "batch elements"] | None = None,
) -> tuple[
    Float[torch.Tensor, "batch lengths"],
    dict[
        str, Float[torch.Tensor, "batch elements classes"]
    ],
    Bool[torch.Tensor, "batch elements"],
]

Decode latent codes.

Parameters:

Name Type Description Default
latents Float[Tensor, 'batch dim']

Latent codes.

required
mask Bool[Tensor, 'batch elements'] | None

Element mask to decode with. Defaults to the mask of the predicted element counts.

None

Returns:

Type Description
tuple[Float[Tensor, 'batch lengths'], dict[str, Float[Tensor, 'batch elements classes']], Bool[Tensor, 'batch elements']]

Length logits, per-field logits, and the decoding mask.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
def forward(
    self,
    latents: Float[torch.Tensor, "batch dim"],
    mask: Bool[torch.Tensor, "batch elements"] | None = None,
) -> tuple[
    Float[torch.Tensor, "batch lengths"],
    dict[str, Float[torch.Tensor, "batch elements classes"]],
    Bool[torch.Tensor, "batch elements"],
]:
    """Decode latent codes.

    Args:
        latents: Latent codes.
        mask: Element mask to decode with. Defaults to the mask of the
            predicted element counts.

    Returns:
        Length logits, per-field logits, and the decoding mask.
    """
    length_logits = self.length_head(latents)
    if mask is None:
        mask = length_mask(length_logits.argmax(dim=-1))

    hidden_states = self.position_embedding(mask)
    for block in self.blocks:
        hidden_states = block(hidden_states, latents, mask)

    logits = {key: head(hidden_states) for key, head in self.field_heads.items()}
    return length_logits, logits, mask

CanvasVAEPreTrainedModel

Bases: PreTrainedModel

Base class handling CanvasVAE weight initialization.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
class CanvasVAEPreTrainedModel(PreTrainedModel):
    """Base class handling CanvasVAE weight initialization."""

    config_class = CanvasVAEConfig
    base_model_prefix = "canvas_vae"
    main_input_name = "element_ids"

    @torch.no_grad()
    def _init_weights(self, module: nn.Module) -> None:
        """Initialize Glorot-uniform kernels, zero biases, and uniform embeddings.

        Args:
            module: Module to initialize.
        """
        if isinstance(module, nn.Linear):
            nn.init.xavier_uniform_(module.weight)
            nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            nn.init.uniform_(module.weight, -EMBEDDING_INIT_RANGE, EMBEDDING_INIT_RANGE)
        elif isinstance(module, (nn.LayerNorm, CanvasVAEBatchNorm)):
            nn.init.ones_(module.weight)
            nn.init.zeros_(module.bias)

        if isinstance(module, CanvasVAEBatchNorm):
            module.running_mean.zero_()
            module.running_var.fill_(1.0)

CanvasVAEModel

Bases: CanvasVAEPreTrainedModel

CanvasVAE variational autoencoder over layout element sequences.

Given layout ids, the model encodes them, takes a posterior sample in training mode (or when posterior_noise is given) and the posterior mean otherwise, and decodes with the ground-truth element mask in training mode or the predicted element count in evaluation mode. Given only latents, it decodes them with the predicted element count.

Parameters:

Name Type Description Default
config CanvasVAEConfig

Model config with vocabularies.

required

Examples:

>>> config = CanvasVAEConfig(
...     vocabularies={
...         "component": ["[UNK]", "", "Text"],
...         "icon": ["[UNK]", ""],
...         "text_button": ["[UNK]", ""],
...     },
...     max_length=4,
...     num_bins=8,
...     latent_dim=16,
...     num_heads=2,
... )
>>> model = CanvasVAEModel(config).eval()
>>> output = model(latents=torch.zeros(2, 16))
>>> output.length_logits.shape
torch.Size([2, 4])
Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
class CanvasVAEModel(CanvasVAEPreTrainedModel):
    """CanvasVAE variational autoencoder over layout element sequences.

    Given layout ids, the model encodes them, takes a posterior sample in
    training mode (or when ``posterior_noise`` is given) and the posterior mean
    otherwise, and decodes with the ground-truth element mask in training mode
    or the predicted element count in evaluation mode. Given only
    ``latents``, it decodes them with the predicted element count.

    Args:
        config: Model config with vocabularies.

    Examples:
        >>> config = CanvasVAEConfig(
        ...     vocabularies={
        ...         "component": ["[UNK]", "", "Text"],
        ...         "icon": ["[UNK]", ""],
        ...         "text_button": ["[UNK]", ""],
        ...     },
        ...     max_length=4,
        ...     num_bins=8,
        ...     latent_dim=16,
        ...     num_heads=2,
        ... )
        >>> model = CanvasVAEModel(config).eval()
        >>> output = model(latents=torch.zeros(2, 16))
        >>> output.length_logits.shape
        torch.Size([2, 4])
    """

    def __init__(self, config: CanvasVAEConfig) -> None:
        """Create the encoder and decoder."""
        super().__init__(config)
        self.encoder = CanvasVAEEncoder(config)
        self.decoder = CanvasVAEDecoder(config)
        self.post_init()

    def forward(
        self,
        num_elements: Int[torch.Tensor, "batch"] | None = None,
        element_ids: Int[torch.Tensor, "batch elements fields"] | None = None,
        *,
        latents: Float[torch.Tensor, "batch dim"] | None = None,
        posterior_noise: Float[torch.Tensor, "batch dim"] | None = None,
    ) -> CanvasVAEModelOutput:
        """Encode and decode a layout batch, or decode latent codes.

        Args:
            num_elements: Element count of every layout.
            element_ids: Field ids in :class:`CanvasVAEField` order, padded to
                the longest layout in the batch.
            latents: Latent codes to decode instead of encoding a layout.
            posterior_noise: Standard-normal noise for the posterior sample.
                Drawn from the global generator in training mode when omitted.

        Returns:
            Model outputs; ``loss`` is set only in training mode.

        Raises:
            ValueError: If neither a layout nor latent codes are given.
        """
        if latents is not None:
            length_logits, logits, mask = self.decoder(latents)
            return CanvasVAEModelOutput(
                length_logits=length_logits,
                element_logits=logits,
                mask=mask,
                latents=latents,
            )

        if num_elements is None or element_ids is None:
            raise ValueError("pass num_elements and element_ids, or latents")

        length_ids = num_elements - 1
        z_mean, z_log_var = self.encoder(length_ids, element_ids)
        kl_divergence = -0.5 * torch.mean(
            1 + z_log_var - z_mean.square() - torch.exp(z_log_var)
        )
        if posterior_noise is None and self.training:
            posterior_noise = randn(
                z_mean.shape, device=z_mean.device, dtype=z_mean.dtype
            )

        latents = z_mean
        if posterior_noise is not None:
            latents = z_mean + torch.exp(0.5 * z_log_var) * posterior_noise

        mask = length_mask(length_ids, element_ids.shape[1]) if self.training else None
        length_logits, logits, mask = self.decoder(latents, mask)
        output = CanvasVAEModelOutput(
            length_logits=length_logits,
            element_logits=logits,
            mask=mask,
            latents=latents,
            z_mean=z_mean,
            z_log_var=z_log_var,
            kl_divergence=kl_divergence,
        )
        if not self.training:
            return output

        losses = {
            "length": nn.functional.cross_entropy(length_logits, length_ids),
            **{
                key: _sequence_cross_entropy(logits[key], element_ids[..., index], mask)
                for index, key in enumerate(CanvasVAEField)
            },
        }
        reconstruction = sum(losses.values(), torch.zeros((), device=z_mean.device))
        output.reconstruction_losses = losses
        output.loss = reconstruction + self.config.kl_weight * kl_divergence
        return output

__init__

__init__(config: CanvasVAEConfig) -> None

Create the encoder and decoder.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
380
381
382
383
384
385
def __init__(self, config: CanvasVAEConfig) -> None:
    """Create the encoder and decoder."""
    super().__init__(config)
    self.encoder = CanvasVAEEncoder(config)
    self.decoder = CanvasVAEDecoder(config)
    self.post_init()

forward

forward(
    num_elements: Int[Tensor, "batch"] | None = None,
    element_ids: Int[Tensor, "batch elements fields"]
    | None = None,
    *,
    latents: Float[Tensor, "batch dim"] | None = None,
    posterior_noise: Float[Tensor, "batch dim"]
    | None = None,
) -> CanvasVAEModelOutput

Encode and decode a layout batch, or decode latent codes.

Parameters:

Name Type Description Default
num_elements Int[Tensor, 'batch'] | None

Element count of every layout.

None
element_ids Int[Tensor, 'batch elements fields'] | None

Field ids in :class:CanvasVAEField order, padded to the longest layout in the batch.

None
latents Float[Tensor, 'batch dim'] | None

Latent codes to decode instead of encoding a layout.

None
posterior_noise Float[Tensor, 'batch dim'] | None

Standard-normal noise for the posterior sample. Drawn from the global generator in training mode when omitted.

None

Returns:

Type Description
CanvasVAEModelOutput

Model outputs; loss is set only in training mode.

Raises:

Type Description
ValueError

If neither a layout nor latent codes are given.

Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
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
def forward(
    self,
    num_elements: Int[torch.Tensor, "batch"] | None = None,
    element_ids: Int[torch.Tensor, "batch elements fields"] | None = None,
    *,
    latents: Float[torch.Tensor, "batch dim"] | None = None,
    posterior_noise: Float[torch.Tensor, "batch dim"] | None = None,
) -> CanvasVAEModelOutput:
    """Encode and decode a layout batch, or decode latent codes.

    Args:
        num_elements: Element count of every layout.
        element_ids: Field ids in :class:`CanvasVAEField` order, padded to
            the longest layout in the batch.
        latents: Latent codes to decode instead of encoding a layout.
        posterior_noise: Standard-normal noise for the posterior sample.
            Drawn from the global generator in training mode when omitted.

    Returns:
        Model outputs; ``loss`` is set only in training mode.

    Raises:
        ValueError: If neither a layout nor latent codes are given.
    """
    if latents is not None:
        length_logits, logits, mask = self.decoder(latents)
        return CanvasVAEModelOutput(
            length_logits=length_logits,
            element_logits=logits,
            mask=mask,
            latents=latents,
        )

    if num_elements is None or element_ids is None:
        raise ValueError("pass num_elements and element_ids, or latents")

    length_ids = num_elements - 1
    z_mean, z_log_var = self.encoder(length_ids, element_ids)
    kl_divergence = -0.5 * torch.mean(
        1 + z_log_var - z_mean.square() - torch.exp(z_log_var)
    )
    if posterior_noise is None and self.training:
        posterior_noise = randn(
            z_mean.shape, device=z_mean.device, dtype=z_mean.dtype
        )

    latents = z_mean
    if posterior_noise is not None:
        latents = z_mean + torch.exp(0.5 * z_log_var) * posterior_noise

    mask = length_mask(length_ids, element_ids.shape[1]) if self.training else None
    length_logits, logits, mask = self.decoder(latents, mask)
    output = CanvasVAEModelOutput(
        length_logits=length_logits,
        element_logits=logits,
        mask=mask,
        latents=latents,
        z_mean=z_mean,
        z_log_var=z_log_var,
        kl_divergence=kl_divergence,
    )
    if not self.training:
        return output

    losses = {
        "length": nn.functional.cross_entropy(length_logits, length_ids),
        **{
            key: _sequence_cross_entropy(logits[key], element_ids[..., index], mask)
            for index, key in enumerate(CanvasVAEField)
        },
    }
    reconstruction = sum(losses.values(), torch.zeros((), device=z_mean.device))
    output.reconstruction_losses = losses
    output.loss = reconstruction + self.config.kl_weight * kl_divergence
    return output

length_mask

length_mask(
    length_ids: Int[Tensor, "batch"],
    max_elements: int | None = None,
) -> Bool[torch.Tensor, "batch elements"]

Build a valid-element mask from zero-based element-count ids.

Parameters:

Name Type Description Default
length_ids Int[Tensor, 'batch']

Element count minus one for every layout.

required
max_elements int | None

Mask width. Defaults to the longest layout in the batch.

None

Returns:

Type Description
Bool[Tensor, 'batch elements']

Mask where True marks a valid element.

Examples:

>>> length_mask(torch.tensor([0, 2])).tolist()
[[True, False, False], [True, True, True]]
Source code in models/canvas-vae/src/canvas_vae/modeling_canvas_vae.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def length_mask(
    length_ids: Int[torch.Tensor, "batch"], max_elements: int | None = None
) -> Bool[torch.Tensor, "batch elements"]:
    """Build a valid-element mask from zero-based element-count ids.

    Args:
        length_ids: Element count minus one for every layout.
        max_elements: Mask width. Defaults to the longest layout in the batch.

    Returns:
        Mask where ``True`` marks a valid element.

    Examples:
        >>> length_mask(torch.tensor([0, 2])).tolist()
        [[True, False, False], [True, True, True]]
    """
    lengths = length_ids + 1
    width = int(lengths.max()) if max_elements is None else max_elements
    positions = torch.arange(width, device=length_ids.device)
    return positions.unsqueeze(0) < lengths.unsqueeze(1)

pipeline_canvas_vae

Unconditional layout generation pipeline for CanvasVAE.

OutputType

Bases: StrEnum

Return container of :meth:CanvasVAEPipeline.__call__.

Source code in models/canvas-vae/src/canvas_vae/pipeline_canvas_vae.py
25
26
27
28
29
class OutputType(StrEnum):
    """Return container of :meth:`CanvasVAEPipeline.__call__`."""

    dataclass = auto()
    dict = auto()

CanvasVAEPipeline

Bases: LayoutGenerationPipeline

Sample RICO layouts from the CanvasVAE prior.

Each call draws latent codes from the standard normal prior, decodes the element count and every element field with argmax, and returns normalized center xywh boxes clamped to [0, 1] with component labels. intermediates holds the clickable, icon, and text_button ids and the latent codes.

Parameters:

Name Type Description Default
model CanvasVAEModel

CanvasVAE model.

required

Examples:

>>> from canvas_vae import CanvasVAEConfig
>>> config = CanvasVAEConfig(
...     vocabularies={
...         "component": ["[UNK]", "", "Text"],
...         "icon": ["[UNK]", ""],
...         "text_button": ["[UNK]", ""],
...     },
...     max_length=4,
...     num_bins=8,
...     latent_dim=16,
...     num_heads=2,
... )
>>> pipe = CanvasVAEPipeline(model=CanvasVAEModel(config))
>>> out = pipe(batch_size=2, seed=0)
>>> out.bbox.shape[0], out.bbox.shape[-1]
(2, 4)
Source code in models/canvas-vae/src/canvas_vae/pipeline_canvas_vae.py
 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
class CanvasVAEPipeline(LayoutGenerationPipeline):
    """Sample RICO layouts from the CanvasVAE prior.

    Each call draws latent codes from the standard normal prior, decodes the
    element count and every element field with ``argmax``, and returns
    normalized center ``xywh`` boxes clamped to ``[0, 1]`` with ``component``
    labels. ``intermediates`` holds the ``clickable``, ``icon``, and
    ``text_button`` ids and the latent codes.

    Args:
        model: CanvasVAE model.

    Examples:
        >>> from canvas_vae import CanvasVAEConfig
        >>> config = CanvasVAEConfig(
        ...     vocabularies={
        ...         "component": ["[UNK]", "", "Text"],
        ...         "icon": ["[UNK]", ""],
        ...         "text_button": ["[UNK]", ""],
        ...     },
        ...     max_length=4,
        ...     num_bins=8,
        ...     latent_dim=16,
        ...     num_heads=2,
        ... )
        >>> pipe = CanvasVAEPipeline(model=CanvasVAEModel(config))
        >>> out = pipe(batch_size=2, seed=0)
        >>> out.bbox.shape[0], out.bbox.shape[-1]
        (2, 4)
    """

    config_class: ClassVar[type[CanvasVAEConfig]] = CanvasVAEConfig
    component_specs: ClassVar[dict[str, PipelineComponentSpec]] = {
        "model": PipelineComponentSpec(attribute_name="model", loader=_load_model)
    }

    def __init__(self, model: CanvasVAEModel) -> None:
        """Wrap a model; the pipeline config is the model config."""
        super().__init__(model.config)
        self.model = model

    @classmethod
    def _from_pretrained_components(
        cls,
        *,
        config: PretrainedConfig,
        components: Mapping[str, PipelineComponent | None],
    ) -> CanvasVAEPipeline:
        """Build the pipeline from a loaded model."""
        del config
        return cls(model=cast(CanvasVAEModel, components["model"]))

    @torch.no_grad()
    def __call__(  # ty: ignore[invalid-method-override]
        self,
        *,
        batch_size: int = 1,
        seed: int | None = None,
        generator: torch.Generator | None = None,
        condition_type: ConditionType | str = ConditionType.unconditional,
        labels: Shaped[torch.Tensor, "..."]
        | Shaped[np.ndarray, "..."]
        | list[list[int]]
        | None = None,
        bbox: Shaped[torch.Tensor, "..."]
        | Shaped[np.ndarray, "..."]
        | list[list[list[float]]]
        | None = None,
        mask: Shaped[torch.Tensor, "..."]
        | Shaped[np.ndarray, "..."]
        | list[list[bool]]
        | None = None,
        num_elements: int | list[int] | Int[torch.Tensor, "batch"] | None = None,
        box_format: Literal["xywh", "ltwh", "ltrb"] = "xywh",
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
        num_inference_steps: int | None = None,
        output_type: OutputType | str = OutputType.dataclass,
        return_intermediates: bool = False,
    ) -> LayoutGenerationOutput | dict[str, Shaped[torch.Tensor, "..."]]:
        """Generate layouts from prior samples.

        Args:
            batch_size: Number of layouts.
            seed: Seed for a local generator when ``generator`` is absent.
            generator: Generator for the prior samples; takes precedence over
                ``seed``.
            condition_type: Only ``unconditional`` is supported.
            labels: Unsupported; must be ``None``.
            bbox: Unsupported; must be ``None``.
            mask: Unsupported; must be ``None``.
            num_elements: Unsupported; the model predicts the element count.
            box_format: Only ``xywh`` output is supported.
            normalized: Only normalized output is supported.
            canvas_size: Unsupported; must be ``None``.
            num_inference_steps: Unsupported; decoding is a single pass.
            output_type: ``dataclass`` or ``dict``.
            return_intermediates: Whether to fill ``intermediates``.

        Returns:
            Generated layouts in the shared layout schema.

        Raises:
            NotImplementedError: If a condition or layout input other than
                unconditional generation is requested.
        """
        condition = normalize_condition_type(condition_type)
        if condition is not ConditionType.unconditional:
            raise NotImplementedError(
                f"CanvasVAE supports only unconditional generation, got {condition}"
            )

        given = {
            "labels": labels,
            "bbox": bbox,
            "mask": mask,
            "num_elements": num_elements,
            "canvas_size": canvas_size,
            "num_inference_steps": num_inference_steps,
        }
        unsupported = [name for name, value in given.items() if value is not None]
        if unsupported or BoxFormat(box_format) is not BoxFormat.xywh or not normalized:
            raise NotImplementedError(
                "CanvasVAE unconditional generation returns normalized xywh boxes "
                f"and takes no layout inputs; got {unsupported or box_format}"
            )

        generator = self.prepare_generator(generator=generator, seed=seed)
        device = self.model.device
        latents = randn(
            (batch_size, self.model.config.latent_dim),
            generator=generator,
            device=device,
            dtype=self.model.dtype,
        )
        was_training = self.model.training
        self.model.eval()
        try:
            output = self.model(latents=latents)
        finally:
            self.model.train(was_training)

        ids = {
            key: logits.argmax(dim=-1) for key, logits in output.element_logits.items()
        }
        geometry = torch.stack(
            [
                ids[CanvasVAEField.left],
                ids[CanvasVAEField.top],
                ids[CanvasVAEField.width],
                ids[CanvasVAEField.height],
            ],
            dim=-1,
        )
        boxes = clamp_boxes(
            ltwh_to_xywh(bins_to_ltwh(geometry, self.model.config.num_bins))
        )
        valid = cast(Bool[torch.Tensor, "batch elements"], output.mask)
        result = LayoutGenerationOutput(
            bbox=boxes * valid.unsqueeze(-1),
            labels=ids[CanvasVAEField.component] * valid,
            mask=valid,
            id2label=dict(self.model.config.id2label),
            intermediates=(
                {
                    "clickable": ids[CanvasVAEField.clickable],
                    "icon": ids[CanvasVAEField.icon],
                    "text_button": ids[CanvasVAEField.text_button],
                    "latents": latents,
                }
                if return_intermediates
                else None
            ),
        )
        if OutputType(output_type) is OutputType.dict:
            return dict(result)

        return result

__init__

__init__(model: CanvasVAEModel) -> None

Wrap a model; the pipeline config is the model config.

Source code in models/canvas-vae/src/canvas_vae/pipeline_canvas_vae.py
100
101
102
103
def __init__(self, model: CanvasVAEModel) -> None:
    """Wrap a model; the pipeline config is the model config."""
    super().__init__(model.config)
    self.model = model

__call__

__call__(
    *,
    batch_size: int = 1,
    seed: int | None = None,
    generator: Generator | None = None,
    condition_type: ConditionType
    | str = ConditionType.unconditional,
    labels: Shaped[Tensor, "..."]
    | Shaped[ndarray, "..."]
    | list[list[int]]
    | None = None,
    bbox: Shaped[Tensor, "..."]
    | Shaped[ndarray, "..."]
    | list[list[list[float]]]
    | None = None,
    mask: Shaped[Tensor, "..."]
    | Shaped[ndarray, "..."]
    | list[list[bool]]
    | None = None,
    num_elements: int
    | list[int]
    | Int[Tensor, "batch"]
    | None = None,
    box_format: Literal["xywh", "ltwh", "ltrb"] = "xywh",
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    num_inference_steps: int | None = None,
    output_type: OutputType | str = OutputType.dataclass,
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[str, Shaped[torch.Tensor, "..."]]
)

Generate layouts from prior samples.

Parameters:

Name Type Description Default
batch_size int

Number of layouts.

1
seed int | None

Seed for a local generator when generator is absent.

None
generator Generator | None

Generator for the prior samples; takes precedence over seed.

None
condition_type ConditionType | str

Only unconditional is supported.

unconditional
labels Shaped[Tensor, '...'] | Shaped[ndarray, '...'] | list[list[int]] | None

Unsupported; must be None.

None
bbox Shaped[Tensor, '...'] | Shaped[ndarray, '...'] | list[list[list[float]]] | None

Unsupported; must be None.

None
mask Shaped[Tensor, '...'] | Shaped[ndarray, '...'] | list[list[bool]] | None

Unsupported; must be None.

None
num_elements int | list[int] | Int[Tensor, 'batch'] | None

Unsupported; the model predicts the element count.

None
box_format Literal['xywh', 'ltwh', 'ltrb']

Only xywh output is supported.

'xywh'
normalized bool

Only normalized output is supported.

True
canvas_size tuple[int, int] | None

Unsupported; must be None.

None
num_inference_steps int | None

Unsupported; decoding is a single pass.

None
output_type OutputType | str

dataclass or dict.

dataclass
return_intermediates bool

Whether to fill intermediates.

False

Returns:

Type Description
LayoutGenerationOutput | dict[str, Shaped[Tensor, '...']]

Generated layouts in the shared layout schema.

Raises:

Type Description
NotImplementedError

If a condition or layout input other than unconditional generation is requested.

Source code in models/canvas-vae/src/canvas_vae/pipeline_canvas_vae.py
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
@torch.no_grad()
def __call__(  # ty: ignore[invalid-method-override]
    self,
    *,
    batch_size: int = 1,
    seed: int | None = None,
    generator: torch.Generator | None = None,
    condition_type: ConditionType | str = ConditionType.unconditional,
    labels: Shaped[torch.Tensor, "..."]
    | Shaped[np.ndarray, "..."]
    | list[list[int]]
    | None = None,
    bbox: Shaped[torch.Tensor, "..."]
    | Shaped[np.ndarray, "..."]
    | list[list[list[float]]]
    | None = None,
    mask: Shaped[torch.Tensor, "..."]
    | Shaped[np.ndarray, "..."]
    | list[list[bool]]
    | None = None,
    num_elements: int | list[int] | Int[torch.Tensor, "batch"] | None = None,
    box_format: Literal["xywh", "ltwh", "ltrb"] = "xywh",
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    num_inference_steps: int | None = None,
    output_type: OutputType | str = OutputType.dataclass,
    return_intermediates: bool = False,
) -> LayoutGenerationOutput | dict[str, Shaped[torch.Tensor, "..."]]:
    """Generate layouts from prior samples.

    Args:
        batch_size: Number of layouts.
        seed: Seed for a local generator when ``generator`` is absent.
        generator: Generator for the prior samples; takes precedence over
            ``seed``.
        condition_type: Only ``unconditional`` is supported.
        labels: Unsupported; must be ``None``.
        bbox: Unsupported; must be ``None``.
        mask: Unsupported; must be ``None``.
        num_elements: Unsupported; the model predicts the element count.
        box_format: Only ``xywh`` output is supported.
        normalized: Only normalized output is supported.
        canvas_size: Unsupported; must be ``None``.
        num_inference_steps: Unsupported; decoding is a single pass.
        output_type: ``dataclass`` or ``dict``.
        return_intermediates: Whether to fill ``intermediates``.

    Returns:
        Generated layouts in the shared layout schema.

    Raises:
        NotImplementedError: If a condition or layout input other than
            unconditional generation is requested.
    """
    condition = normalize_condition_type(condition_type)
    if condition is not ConditionType.unconditional:
        raise NotImplementedError(
            f"CanvasVAE supports only unconditional generation, got {condition}"
        )

    given = {
        "labels": labels,
        "bbox": bbox,
        "mask": mask,
        "num_elements": num_elements,
        "canvas_size": canvas_size,
        "num_inference_steps": num_inference_steps,
    }
    unsupported = [name for name, value in given.items() if value is not None]
    if unsupported or BoxFormat(box_format) is not BoxFormat.xywh or not normalized:
        raise NotImplementedError(
            "CanvasVAE unconditional generation returns normalized xywh boxes "
            f"and takes no layout inputs; got {unsupported or box_format}"
        )

    generator = self.prepare_generator(generator=generator, seed=seed)
    device = self.model.device
    latents = randn(
        (batch_size, self.model.config.latent_dim),
        generator=generator,
        device=device,
        dtype=self.model.dtype,
    )
    was_training = self.model.training
    self.model.eval()
    try:
        output = self.model(latents=latents)
    finally:
        self.model.train(was_training)

    ids = {
        key: logits.argmax(dim=-1) for key, logits in output.element_logits.items()
    }
    geometry = torch.stack(
        [
            ids[CanvasVAEField.left],
            ids[CanvasVAEField.top],
            ids[CanvasVAEField.width],
            ids[CanvasVAEField.height],
        ],
        dim=-1,
    )
    boxes = clamp_boxes(
        ltwh_to_xywh(bins_to_ltwh(geometry, self.model.config.num_bins))
    )
    valid = cast(Bool[torch.Tensor, "batch elements"], output.mask)
    result = LayoutGenerationOutput(
        bbox=boxes * valid.unsqueeze(-1),
        labels=ids[CanvasVAEField.component] * valid,
        mask=valid,
        id2label=dict(self.model.config.id2label),
        intermediates=(
            {
                "clickable": ids[CanvasVAEField.clickable],
                "icon": ids[CanvasVAEField.icon],
                "text_button": ids[CanvasVAEField.text_button],
                "latents": latents,
            }
            if return_intermediates
            else None
        ),
    )
    if OutputType(output_type) is OutputType.dict:
        return dict(result)

    return result

bins_to_ltwh

bins_to_ltwh(
    bins: Int[Tensor, "... 4"], num_bins: int
) -> Float[torch.Tensor, "... 4"]

Map left, top, width, and height bin ids to normalized values.

Parameters:

Name Type Description Default
bins Int[Tensor, '... 4']

Bin ids in [0, num_bins).

required
num_bins int

Number of bins.

required

Returns:

Type Description
Float[Tensor, '... 4']

Values bin / (num_bins - 1) in [0, 1].

Examples:

>>> bins_to_ltwh(torch.tensor([0, 3, 1, 2]), 4).tolist()
[0.0, 1.0, 0.3333333432674408, 0.6666666865348816]
Source code in models/canvas-vae/src/canvas_vae/pipeline_canvas_vae.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def bins_to_ltwh(
    bins: Int[torch.Tensor, "... 4"], num_bins: int
) -> Float[torch.Tensor, "... 4"]:
    """Map left, top, width, and height bin ids to normalized values.

    Args:
        bins: Bin ids in ``[0, num_bins)``.
        num_bins: Number of bins.

    Returns:
        Values ``bin / (num_bins - 1)`` in ``[0, 1]``.

    Examples:
        >>> bins_to_ltwh(torch.tensor([0, 3, 1, 2]), 4).tolist()
        [0.0, 1.0, 0.3333333432674408, 0.6666666865348816]
    """
    return bins.float() / (num_bins - 1)

processing_canvas_vae

RICO preparation and layout encoding for CanvasVAE.

RicoSplit

Bases: StrEnum

Dataset splits assigned from the content hash.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
53
54
55
56
57
58
class RicoSplit(StrEnum):
    """Dataset splits assigned from the content hash."""

    train = auto()
    val = auto()
    test = auto()

RicoDocument

Bases: TypedDict

One deduplicated RICO screen flattened into elements.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
61
62
63
64
65
66
67
class RicoDocument(TypedDict):
    """One deduplicated RICO screen flattened into elements."""

    id: int
    content_hash: str
    split: str
    elements: list[RicoElement]

RicoNode

Bases: TypedDict

Subset of a RICO semantic-annotation node read by the flattener.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
70
71
72
73
74
75
76
77
78
class RicoNode(TypedDict, total=False):
    """Subset of a RICO semantic-annotation node read by the flattener."""

    bounds: list[float]
    children: list[RicoNode]
    clickable: bool
    componentLabel: str
    iconClass: str
    textButtonClass: str

CanvasVAEProcessor

Bases: ProcessorMixin

Encode flattened layouts into CanvasVAE model ids.

Coordinates and sizes in [0, 1] map to num_bins equal-width bins whose float32 boundaries are linspace(0, 1, num_bins)[1:]; values at or above a boundary fall in the next bin. Strings map through the lookup tables with unknown values at id 0. Layouts are padded to the longest one in the batch with zero geometry, clickable=0, and the id of the empty string.

Parameters:

Name Type Description Default
vocabularies Mapping[str, Sequence[str]]

Lookup tables for component, icon, and text_button.

required
num_bins int

Number of geometry bins.

64

Examples:

>>> processor = CanvasVAEProcessor(
...     vocabularies={
...         "component": ["[UNK]", "", "Text"],
...         "icon": ["[UNK]", ""],
...         "text_button": ["[UNK]", ""],
...     },
...     num_bins=4,
... )
>>> element = {"left": 0.5, "top": 0.0, "width": 1.0, "height": 0.2,
...            "clickable": 1, "component": "Text", "icon": "", "text_button": ""}
>>> processor([[element]])["element_ids"].tolist()
[[[1, 0, 3, 0, 1, 2, 1, 1]]]
Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
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
class CanvasVAEProcessor(ProcessorMixin):
    """Encode flattened layouts into CanvasVAE model ids.

    Coordinates and sizes in ``[0, 1]`` map to ``num_bins`` equal-width bins
    whose float32 boundaries are ``linspace(0, 1, num_bins)[1:]``; values at or
    above a boundary fall in the next bin. Strings map through the lookup
    tables with unknown values at id 0. Layouts are padded to the longest one
    in the batch with zero geometry, ``clickable=0``, and the id of the empty
    string.

    Args:
        vocabularies: Lookup tables for ``component``, ``icon``, and
            ``text_button``.
        num_bins: Number of geometry bins.

    Examples:
        >>> processor = CanvasVAEProcessor(
        ...     vocabularies={
        ...         "component": ["[UNK]", "", "Text"],
        ...         "icon": ["[UNK]", ""],
        ...         "text_button": ["[UNK]", ""],
        ...     },
        ...     num_bins=4,
        ... )
        >>> element = {"left": 0.5, "top": 0.0, "width": 1.0, "height": 0.2,
        ...            "clickable": 1, "component": "Text", "icon": "", "text_button": ""}
        >>> processor([[element]])["element_ids"].tolist()
        [[[1, 0, 3, 0, 1, 2, 1, 1]]]
    """

    def __init__(
        self, vocabularies: Mapping[str, Sequence[str]], num_bins: int = 64
    ) -> None:
        """Store lookup tables and the bin count."""
        self.chat_template = None
        self.vocabularies = {key: list(tokens) for key, tokens in vocabularies.items()}
        self.token_ids = {
            key: {token: index for index, token in reversed(list(enumerate(tokens)))}
            for key, tokens in self.vocabularies.items()
        }
        self.num_bins = num_bins

    @property
    def bin_boundaries(self) -> Float[np.ndarray, "boundaries"]:
        """Return the float32 geometry bin boundaries."""
        return np.linspace(0.0, 1.0, self.num_bins)[1:].astype(np.float32)

    def discretize(
        self, values: Float[np.ndarray, "*shape"]
    ) -> Int[np.ndarray, "*shape"]:
        """Map normalized geometry values to bin ids.

        Args:
            values: Normalized coordinates or sizes.

        Returns:
            Bin ids in ``[0, num_bins)``.
        """
        return np.searchsorted(
            self.bin_boundaries, values.astype(np.float32), side="right"
        )

    def lookup(self, field: str, value: str) -> int:
        """Return the id of ``value`` in the ``field`` table, or 0 when unknown.

        Args:
            field: Vocabulary field name.
            value: String value.

        Returns:
            Lookup id.
        """
        return self.token_ids[field].get(value, 0)

    def __call__(self, layouts: Sequence[Sequence[RicoElement]]) -> BatchFeature:
        """Encode a batch of flattened layouts.

        Args:
            layouts: Element lists, each non-empty.

        Returns:
            ``num_elements`` with shape ``(batch,)`` and ``element_ids`` with
            shape ``(batch, elements, 8)`` in :class:`CanvasVAEField` order.

        Raises:
            ValueError: If a layout has no elements.
        """
        if any(not layout for layout in layouts):
            raise ValueError("every layout needs at least one element")

        width = max(len(layout) for layout in layouts)
        geometry = np.zeros((len(layouts), width, len(GEOMETRY_FIELDS)), np.float32)
        categorical = np.zeros((len(layouts), width, 4), np.int64)
        categorical[..., 1:] = [
            self.lookup(field, PRIMARY_LABEL_TOKEN) for field in VOCABULARY_FIELDS
        ]
        for row, layout in enumerate(layouts):
            for column, element in enumerate(layout):
                geometry[row, column] = [element[key] for key in GEOMETRY_FIELDS]  # ty: ignore[invalid-key]
                categorical[row, column] = [
                    element["clickable"],
                    *(self.lookup(key, element[key]) for key in VOCABULARY_FIELDS),  # ty: ignore[invalid-key]
                ]

        element_ids = np.concatenate([self.discretize(geometry), categorical], axis=-1)
        return BatchFeature(
            {
                "num_elements": torch.tensor([len(layout) for layout in layouts]),
                "element_ids": torch.from_numpy(element_ids),
            }
        )

bin_boundaries property

bin_boundaries: Float[ndarray, 'boundaries']

Return the float32 geometry bin boundaries.

__init__

__init__(
    vocabularies: Mapping[str, Sequence[str]],
    num_bins: int = 64,
) -> None

Store lookup tables and the bin count.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
337
338
339
340
341
342
343
344
345
346
347
def __init__(
    self, vocabularies: Mapping[str, Sequence[str]], num_bins: int = 64
) -> None:
    """Store lookup tables and the bin count."""
    self.chat_template = None
    self.vocabularies = {key: list(tokens) for key, tokens in vocabularies.items()}
    self.token_ids = {
        key: {token: index for index, token in reversed(list(enumerate(tokens)))}
        for key, tokens in self.vocabularies.items()
    }
    self.num_bins = num_bins

discretize

discretize(
    values: Float[ndarray, "*shape"],
) -> Int[np.ndarray, "*shape"]

Map normalized geometry values to bin ids.

Parameters:

Name Type Description Default
values Float[ndarray, '*shape']

Normalized coordinates or sizes.

required

Returns:

Type Description
Int[ndarray, '*shape']

Bin ids in [0, num_bins).

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def discretize(
    self, values: Float[np.ndarray, "*shape"]
) -> Int[np.ndarray, "*shape"]:
    """Map normalized geometry values to bin ids.

    Args:
        values: Normalized coordinates or sizes.

    Returns:
        Bin ids in ``[0, num_bins)``.
    """
    return np.searchsorted(
        self.bin_boundaries, values.astype(np.float32), side="right"
    )

lookup

lookup(field: str, value: str) -> int

Return the id of value in the field table, or 0 when unknown.

Parameters:

Name Type Description Default
field str

Vocabulary field name.

required
value str

String value.

required

Returns:

Type Description
int

Lookup id.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
369
370
371
372
373
374
375
376
377
378
379
def lookup(self, field: str, value: str) -> int:
    """Return the id of ``value`` in the ``field`` table, or 0 when unknown.

    Args:
        field: Vocabulary field name.
        value: String value.

    Returns:
        Lookup id.
    """
    return self.token_ids[field].get(value, 0)

__call__

__call__(
    layouts: Sequence[Sequence[RicoElement]],
) -> BatchFeature

Encode a batch of flattened layouts.

Parameters:

Name Type Description Default
layouts Sequence[Sequence[RicoElement]]

Element lists, each non-empty.

required

Returns:

Type Description
BatchFeature

num_elements with shape (batch,) and element_ids with

BatchFeature

shape (batch, elements, 8) in :class:CanvasVAEField order.

Raises:

Type Description
ValueError

If a layout has no elements.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
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
def __call__(self, layouts: Sequence[Sequence[RicoElement]]) -> BatchFeature:
    """Encode a batch of flattened layouts.

    Args:
        layouts: Element lists, each non-empty.

    Returns:
        ``num_elements`` with shape ``(batch,)`` and ``element_ids`` with
        shape ``(batch, elements, 8)`` in :class:`CanvasVAEField` order.

    Raises:
        ValueError: If a layout has no elements.
    """
    if any(not layout for layout in layouts):
        raise ValueError("every layout needs at least one element")

    width = max(len(layout) for layout in layouts)
    geometry = np.zeros((len(layouts), width, len(GEOMETRY_FIELDS)), np.float32)
    categorical = np.zeros((len(layouts), width, 4), np.int64)
    categorical[..., 1:] = [
        self.lookup(field, PRIMARY_LABEL_TOKEN) for field in VOCABULARY_FIELDS
    ]
    for row, layout in enumerate(layouts):
        for column, element in enumerate(layout):
            geometry[row, column] = [element[key] for key in GEOMETRY_FIELDS]  # ty: ignore[invalid-key]
            categorical[row, column] = [
                element["clickable"],
                *(self.lookup(key, element[key]) for key in VOCABULARY_FIELDS),  # ty: ignore[invalid-key]
            ]

    element_ids = np.concatenate([self.discretize(geometry), categorical], axis=-1)
    return BatchFeature(
        {
            "num_elements": torch.tensor([len(layout) for layout in layouts]),
            "element_ids": torch.from_numpy(element_ids),
        }
    )

stable_hash

stable_hash(content: bytes) -> int

Return the MD5 integer of the str form of raw JSON bytes.

Parameters:

Name Type Description Default
content bytes

Raw annotation file bytes.

required

Returns:

Type Description
int

Integer hash used for deduplication and split assignment.

Examples:

>>> stable_hash(b"{}") % 10
6
Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def stable_hash(content: bytes) -> int:
    """Return the MD5 integer of the ``str`` form of raw JSON bytes.

    Args:
        content: Raw annotation file bytes.

    Returns:
        Integer hash used for deduplication and split assignment.

    Examples:
        >>> stable_hash(b"{}") % 10
        6
    """
    return int(hashlib.md5(str(content).encode("utf-8")).hexdigest(), 16)

split_for_hash

split_for_hash(content_hash: int) -> RicoSplit

Assign a split from a content hash: 0 is val, 1 is test, the rest train.

Parameters:

Name Type Description Default
content_hash int

Value returned by :func:stable_hash.

required

Returns:

Type Description
RicoSplit

Assigned split.

Examples:

>>> split_for_hash(20), split_for_hash(31), split_for_hash(45)
(<RicoSplit.val: 'val'>, <RicoSplit.test: 'test'>, <RicoSplit.train: 'train'>)
Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def split_for_hash(content_hash: int) -> RicoSplit:
    """Assign a split from a content hash: 0 is val, 1 is test, the rest train.

    Args:
        content_hash: Value returned by :func:`stable_hash`.

    Returns:
        Assigned split.

    Examples:
        >>> split_for_hash(20), split_for_hash(31), split_for_hash(45)
        (<RicoSplit.val: 'val'>, <RicoSplit.test: 'test'>, <RicoSplit.train: 'train'>)
    """
    remainder = content_hash % 10
    if remainder == 0:
        return RicoSplit.val

    if remainder == 1:
        return RicoSplit.test

    return RicoSplit.train

flatten_rico_hierarchy

flatten_rico_hierarchy(
    node: RicoNode,
) -> Iterator[RicoElement]

Yield elements of a RICO view hierarchy in depth-first pre-order.

The root node is included. Coordinates are divided by the fixed 1440x2560 RICO canvas and rounded to float32.

Parameters:

Name Type Description Default
node RicoNode

Root or child node of a semantic annotation.

required

Yields:

Type Description
RicoElement

Flattened elements with absent attributes filled by empty values.

Examples:

>>> root = {"bounds": [0, 0, 1440, 2560], "class": "View"}
>>> next(flatten_rico_hierarchy(root))["width"]
1.0
Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
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
def flatten_rico_hierarchy(node: RicoNode) -> Iterator[RicoElement]:
    """Yield elements of a RICO view hierarchy in depth-first pre-order.

    The root node is included. Coordinates are divided by the fixed
    1440x2560 RICO canvas and rounded to float32.

    Args:
        node: Root or child node of a semantic annotation.

    Yields:
        Flattened elements with absent attributes filled by empty values.

    Examples:
        >>> root = {"bounds": [0, 0, 1440, 2560], "class": "View"}
        >>> next(flatten_rico_hierarchy(root))["width"]
        1.0
    """
    left, top, right, bottom = node["bounds"]
    yield {
        "left": float(np.float32(left / RICO_CANVAS_WIDTH)),
        "top": float(np.float32(top / RICO_CANVAS_HEIGHT)),
        "width": float(np.float32((right - left) / RICO_CANVAS_WIDTH)),
        "height": float(np.float32((bottom - top) / RICO_CANVAS_HEIGHT)),
        "class": str(node["class"]),  # ty: ignore[invalid-key]
        "clickable": int(node.get("clickable", False)),
        "component": node.get("componentLabel", ""),
        "icon": node.get("iconClass", ""),
        "text_button": node.get("textButtonClass", ""),
    }
    for child in node.get("children", []):
        yield from flatten_rico_hierarchy(child)

read_rico_archive

read_rico_archive(
    archive: str | Path,
    *,
    max_elements: int = RICO_MAX_ELEMENTS,
) -> list[RicoDocument]

Read, deduplicate, flatten, and split the RICO semantic annotations.

Files with identical bytes are kept once under the first file name in archive order; screens with more than max_elements elements are dropped.

Parameters:

Name Type Description Default
archive str | Path

Path to semantic_annotations.zip.

required
max_elements int

Maximum number of elements per screen.

RICO_MAX_ELEMENTS

Returns:

Type Description
list[RicoDocument]

Kept documents in archive order.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
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
def read_rico_archive(
    archive: str | Path, *, max_elements: int = RICO_MAX_ELEMENTS
) -> list[RicoDocument]:
    """Read, deduplicate, flatten, and split the RICO semantic annotations.

    Files with identical bytes are kept once under the first file name in
    archive order; screens with more than ``max_elements`` elements are dropped.

    Args:
        archive: Path to ``semantic_annotations.zip``.
        max_elements: Maximum number of elements per screen.

    Returns:
        Kept documents in archive order.
    """
    documents: list[RicoDocument] = []
    seen: set[int] = set()
    with zipfile.ZipFile(archive) as handle:
        for name in handle.namelist():
            if not name.endswith(".json"):
                continue

            content = handle.read(name)
            content_hash = stable_hash(content)
            if content_hash in seen:
                continue

            seen.add(content_hash)
            elements = list(flatten_rico_hierarchy(json.loads(content)))
            if len(elements) > max_elements:
                continue

            documents.append(
                {
                    "id": int(Path(name).stem),
                    "content_hash": f"{content_hash:032x}",
                    "split": split_for_hash(content_hash),
                    "elements": elements,
                }
            )

    return documents

count_values

count_values(
    documents: Sequence[RicoDocument],
) -> dict[str, dict[str, int]]

Count string field values over all documents.

Values are ordered by descending count, ties by ascending value.

Parameters:

Name Type Description Default
documents Sequence[RicoDocument]

Documents of every split.

required

Returns:

Type Description
dict[str, dict[str, int]]

Counts per field for class, component, icon, and

dict[str, dict[str, int]]

text_button.

Examples:

>>> element = {"class": "A", "component": "", "icon": "x", "text_button": ""}
>>> doc = {"id": 0, "content_hash": "", "split": "train", "elements": [element]}
>>> count_values([doc])["icon"]
{'x': 1}
Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
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
def count_values(documents: Sequence[RicoDocument]) -> dict[str, dict[str, int]]:
    """Count string field values over all documents.

    Values are ordered by descending count, ties by ascending value.

    Args:
        documents: Documents of every split.

    Returns:
        Counts per field for ``class``, ``component``, ``icon``, and
        ``text_button``.

    Examples:
        >>> element = {"class": "A", "component": "", "icon": "x", "text_button": ""}
        >>> doc = {"id": 0, "content_hash": "", "split": "train", "elements": [element]}
        >>> count_values([doc])["icon"]
        {'x': 1}
    """
    counters: dict[str, Counter[str]] = {key: Counter() for key in COUNTED_FIELDS}
    for document in documents:
        for element in document["elements"]:
            for key in COUNTED_FIELDS:
                counters[key][element[key]] += 1  # ty: ignore[invalid-key]

    return {
        key: dict(sorted(counter.items(), key=lambda item: (-item[1], item[0])))
        for key, counter in counters.items()
    }

build_vocabularies

build_vocabularies(
    counts: Mapping[str, Mapping[str, int]],
    min_frequency: Mapping[str, int] = RICO_MIN_FREQUENCY,
) -> dict[str, list[str]]

Build lookup tables from value counts in their stored order.

Parameters:

Name Type Description Default
counts Mapping[str, Mapping[str, int]]

Value counts per field, in lookup order.

required
min_frequency Mapping[str, int]

Minimum count for a value to get its own id.

RICO_MIN_FREQUENCY

Returns:

Type Description
dict[str, list[str]]

Lookup tables starting with the out-of-vocabulary token.

Examples:

>>> counts = {"component": {"": 3}, "icon": {"": 900, "x": 2}, "text_button": {}}
>>> build_vocabularies(counts)["icon"]
['[UNK]', '']
Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def build_vocabularies(
    counts: Mapping[str, Mapping[str, int]],
    min_frequency: Mapping[str, int] = RICO_MIN_FREQUENCY,
) -> dict[str, list[str]]:
    """Build lookup tables from value counts in their stored order.

    Args:
        counts: Value counts per field, in lookup order.
        min_frequency: Minimum count for a value to get its own id.

    Returns:
        Lookup tables starting with the out-of-vocabulary token.

    Examples:
        >>> counts = {"component": {"": 3}, "icon": {"": 900, "x": 2}, "text_button": {}}
        >>> build_vocabularies(counts)["icon"]
        ['[UNK]', '']
    """
    return {
        str(key): [OOV_TOKEN]
        + [value for value, count in counts[key].items() if count >= min_frequency[key]]
        for key in VOCABULARY_FIELDS
    }

prepare_rico_cache

prepare_rico_cache(
    archive: str | Path, output_dir: str | Path
) -> dict[str, int]

Write split JSON-lines files, vocabulary.json, and count.json.

Parameters:

Name Type Description Default
archive str | Path

Path to semantic_annotations.zip.

required
output_dir str | Path

Cache directory to write.

required

Returns:

Type Description
dict[str, int]

Number of documents per split.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
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
def prepare_rico_cache(archive: str | Path, output_dir: str | Path) -> dict[str, int]:
    """Write split JSON-lines files, ``vocabulary.json``, and ``count.json``.

    Args:
        archive: Path to ``semantic_annotations.zip``.
        output_dir: Cache directory to write.

    Returns:
        Number of documents per split.
    """
    root = Path(output_dir)
    root.mkdir(parents=True, exist_ok=True)
    documents = read_rico_archive(archive)
    sizes: dict[str, int] = {}
    for split in RicoSplit:
        rows = [document for document in documents if document["split"] == split]
        sizes[split] = len(rows)
        with (root / f"{split}.jsonl").open("w", encoding="utf-8") as handle:
            handle.writelines(json.dumps(row) + "\n" for row in rows)

    (root / "vocabulary.json").write_text(
        json.dumps(count_values(documents)), encoding="utf-8"
    )
    (root / "count.json").write_text(json.dumps(sizes), encoding="utf-8")
    return sizes

load_rico_split

load_rico_split(
    data_dir: str | Path, split: RicoSplit | str
) -> list[RicoDocument]

Load one prepared split.

Parameters:

Name Type Description Default
data_dir str | Path

Directory written by :func:prepare_rico_cache.

required
split RicoSplit | str

Split name.

required

Returns:

Type Description
list[RicoDocument]

Documents in stored order.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
279
280
281
282
283
284
285
286
287
288
289
290
291
def load_rico_split(data_dir: str | Path, split: RicoSplit | str) -> list[RicoDocument]:
    """Load one prepared split.

    Args:
        data_dir: Directory written by :func:`prepare_rico_cache`.
        split: Split name.

    Returns:
        Documents in stored order.
    """
    path = Path(data_dir) / f"{RicoSplit(split)}.jsonl"
    with path.open(encoding="utf-8") as handle:
        return [json.loads(line) for line in handle]

load_rico_vocabularies

load_rico_vocabularies(
    data_dir: str | Path,
) -> dict[str, list[str]]

Build lookup tables from a prepared vocabulary.json.

Parameters:

Name Type Description Default
data_dir str | Path

Directory containing vocabulary.json.

required

Returns:

Type Description
dict[str, list[str]]

Lookup tables for :class:CanvasVAEConfig.

Source code in models/canvas-vae/src/canvas_vae/processing_canvas_vae.py
294
295
296
297
298
299
300
301
302
303
304
def load_rico_vocabularies(data_dir: str | Path) -> dict[str, list[str]]:
    """Build lookup tables from a prepared ``vocabulary.json``.

    Args:
        data_dir: Directory containing ``vocabulary.json``.

    Returns:
        Lookup tables for :class:`CanvasVAEConfig`.
    """
    counts = json.loads((Path(data_dir) / "vocabulary.json").read_text("utf-8"))
    return build_vocabularies(counts)

training

Training utilities for CanvasVAE.

checkpoints

Validate CanvasVAE Lightning checkpoints against the configured final epoch.

validate_final_checkpoint

validate_final_checkpoint(
    checkpoint_path: str | Path,
    *,
    max_epochs: int,
    expected_global_step: int,
) -> tuple[int, int]

Require a Lightning checkpoint from the configured final epoch and step.

Source code in models/canvas-vae/src/canvas_vae/training/checkpoints.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
def validate_final_checkpoint(
    checkpoint_path: str | Path,
    *,
    max_epochs: int,
    expected_global_step: int,
) -> tuple[int, int]:
    """Require a Lightning checkpoint from the configured final epoch and step."""
    if (
        type(max_epochs) is not int
        or type(expected_global_step) is not int
        or max_epochs < 1
        or expected_global_step < 0
    ):
        raise ValueError(
            "max_epochs must be positive and global_step cannot be negative"
        )

    checkpoint_file = Path(checkpoint_path)
    if not checkpoint_file.is_file():
        raise ValueError(f"checkpoint does not exist: {checkpoint_file}")

    checkpoint = torch.load(checkpoint_file, map_location="cpu", weights_only=False)
    if not isinstance(checkpoint, dict):
        raise ValueError("checkpoint must contain a Lightning checkpoint mapping")

    epoch = checkpoint.get("epoch")
    global_step = checkpoint.get("global_step")
    if type(epoch) is not int or type(global_step) is not int:
        raise ValueError(
            "checkpoint must contain integer epoch and global_step metadata"
        )

    expected_epoch = max_epochs - 1
    if epoch != expected_epoch or global_step != expected_global_step:
        raise ValueError(
            f"expected final checkpoint at epoch={expected_epoch}, "
            f"global_step={expected_global_step}; found epoch={epoch}, "
            f"global_step={global_step}"
        )

    return epoch, global_step

main

main() -> int

Validate one checkpoint against its expected final training step.

Source code in models/canvas-vae/src/canvas_vae/training/checkpoints.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def main() -> int:
    """Validate one checkpoint against its expected final training step."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("checkpoint", type=Path)
    parser.add_argument("--max-epochs", required=True, type=int)
    parser.add_argument("--expected-global-step", required=True, type=int)
    args = parser.parse_args()

    try:
        epoch, global_step = validate_final_checkpoint(
            args.checkpoint,
            max_epochs=args.max_epochs,
            expected_global_step=args.expected_global_step,
        )
    except (OSError, RuntimeError, ValueError) as error:
        parser.error(str(error))

    print(f"final checkpoint passed: epoch={epoch} global_step={global_step}")
    return 0

datamodule

Lightning data module for CanvasVAE on prepared RICO splits.

RicoLayoutDataset

Bases: Dataset[list[RicoElement]]

Element lists of prepared RICO documents.

Parameters:

Name Type Description Default
documents Sequence[RicoDocument]

Prepared documents.

required
Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class RicoLayoutDataset(Dataset[list[RicoElement]]):
    """Element lists of prepared RICO documents.

    Args:
        documents: Prepared documents.
    """

    def __init__(self, documents: Sequence[RicoDocument]) -> None:
        """Store the documents."""
        self.documents = documents

    def __len__(self) -> int:
        """Return the number of documents."""
        return len(self.documents)

    def __getitem__(self, index: int) -> list[RicoElement]:
        """Return the elements of one document."""
        return self.documents[index]["elements"]
__init__
__init__(documents: Sequence[RicoDocument]) -> None

Store the documents.

Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
29
30
31
def __init__(self, documents: Sequence[RicoDocument]) -> None:
    """Store the documents."""
    self.documents = documents
__len__
__len__() -> int

Return the number of documents.

Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
33
34
35
def __len__(self) -> int:
    """Return the number of documents."""
    return len(self.documents)
__getitem__
__getitem__(index: int) -> list[RicoElement]

Return the elements of one document.

Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
37
38
39
def __getitem__(self, index: int) -> list[RicoElement]:
    """Return the elements of one document."""
    return self.documents[index]["elements"]

CanvasVAEDataModule

Bases: LightningDataModule

Serve RICO splits prepared by scripts/prepare_rico.py.

Training batches come from one endless stream of shuffled passes with ceil(train_size / batch_size) full batches per epoch. Validation scores the same number of full batches over a repeated ordered pass, and testing scores one ordered pass. Every batch is padded to its longest layout.

Parameters:

Name Type Description Default
data_dir str

Prepared split directory.

'.cache/canvas-vae/data/rico'
batch_size int

Batch size.

1024
num_workers int

Data-loader worker processes.

0
seed int | None

Seed of the training stream. Defaults to the global Torch seed.

None
Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
 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
class CanvasVAEDataModule(L.LightningDataModule):
    """Serve RICO splits prepared by ``scripts/prepare_rico.py``.

    Training batches come from one endless stream of shuffled passes with
    ``ceil(train_size / batch_size)`` full batches per epoch. Validation
    scores the same number of full batches over a repeated ordered pass, and
    testing scores one ordered pass. Every batch is padded to its longest
    layout.

    Args:
        data_dir: Prepared split directory.
        batch_size: Batch size.
        num_workers: Data-loader worker processes.
        seed: Seed of the training stream. Defaults to the global Torch seed.
    """

    def __init__(
        self,
        data_dir: str = ".cache/canvas-vae/data/rico",
        batch_size: int = 1024,
        num_workers: int = 0,
        seed: int | None = None,
    ) -> None:
        """Store loader settings."""
        super().__init__()
        self.data_dir = data_dir
        self.batch_size = batch_size
        self.num_workers = num_workers
        self.seed = seed
        self.splits: dict[RicoSplit, RicoLayoutDataset] = {}
        self.processor: CanvasVAEProcessor | None = None
        self.train_sampler: CrossEpochBatchSampler | None = None

    def setup(self, stage: str | None = None) -> None:
        """Load splits and vocabularies once.

        Args:
            stage: Lightning stage name.
        """
        del stage
        if self.processor is not None:
            return

        self.processor = CanvasVAEProcessor(load_rico_vocabularies(self.data_dir))
        self.splits = {
            split: RicoLayoutDataset(load_rico_split(self.data_dir, split))
            for split in RicoSplit
        }
        seed = torch.initial_seed() if self.seed is None else self.seed
        self.train_sampler = CrossEpochBatchSampler(
            len(self.splits[RicoSplit.train]),
            self.batch_size,
            torch.Generator().manual_seed(seed),
        )

    def _loader(
        self, split: RicoSplit, batches: CrossEpochBatchSampler | list[list[int]] | None
    ) -> DataLoader[list[RicoElement]]:
        if self.processor is None or batches is None:
            raise RuntimeError("call setup() before requesting data loaders")

        return DataLoader(
            self.splits[split],
            batch_sampler=batches,
            collate_fn=self.processor,
            num_workers=self.num_workers,
        )

    def train_dataloader(self) -> DataLoader[list[RicoElement]]:
        """Return the cross-epoch training stream."""
        return self._loader(RicoSplit.train, self.train_sampler)

    def val_dataloader(self) -> DataLoader[list[RicoElement]]:
        """Return full batches over a repeated ordered validation pass."""
        size = len(self.splits.get(RicoSplit.val, ()))
        return self._loader(RicoSplit.val, wrapping_batches(size, self.batch_size))

    def test_dataloader(self) -> DataLoader[list[RicoElement]]:
        """Return one ordered test pass."""
        size = len(self.splits.get(RicoSplit.test, ()))
        return self._loader(RicoSplit.test, sequential_batches(size, self.batch_size))
__init__
__init__(
    data_dir: str = ".cache/canvas-vae/data/rico",
    batch_size: int = 1024,
    num_workers: int = 0,
    seed: int | None = None,
) -> None

Store loader settings.

Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def __init__(
    self,
    data_dir: str = ".cache/canvas-vae/data/rico",
    batch_size: int = 1024,
    num_workers: int = 0,
    seed: int | None = None,
) -> None:
    """Store loader settings."""
    super().__init__()
    self.data_dir = data_dir
    self.batch_size = batch_size
    self.num_workers = num_workers
    self.seed = seed
    self.splits: dict[RicoSplit, RicoLayoutDataset] = {}
    self.processor: CanvasVAEProcessor | None = None
    self.train_sampler: CrossEpochBatchSampler | None = None
setup
setup(stage: str | None = None) -> None

Load splits and vocabularies once.

Parameters:

Name Type Description Default
stage str | None

Lightning stage name.

None
Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def setup(self, stage: str | None = None) -> None:
    """Load splits and vocabularies once.

    Args:
        stage: Lightning stage name.
    """
    del stage
    if self.processor is not None:
        return

    self.processor = CanvasVAEProcessor(load_rico_vocabularies(self.data_dir))
    self.splits = {
        split: RicoLayoutDataset(load_rico_split(self.data_dir, split))
        for split in RicoSplit
    }
    seed = torch.initial_seed() if self.seed is None else self.seed
    self.train_sampler = CrossEpochBatchSampler(
        len(self.splits[RicoSplit.train]),
        self.batch_size,
        torch.Generator().manual_seed(seed),
    )
train_dataloader
train_dataloader() -> DataLoader[list[RicoElement]]

Return the cross-epoch training stream.

Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
110
111
112
def train_dataloader(self) -> DataLoader[list[RicoElement]]:
    """Return the cross-epoch training stream."""
    return self._loader(RicoSplit.train, self.train_sampler)
val_dataloader
val_dataloader() -> DataLoader[list[RicoElement]]

Return full batches over a repeated ordered validation pass.

Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
114
115
116
117
def val_dataloader(self) -> DataLoader[list[RicoElement]]:
    """Return full batches over a repeated ordered validation pass."""
    size = len(self.splits.get(RicoSplit.val, ()))
    return self._loader(RicoSplit.val, wrapping_batches(size, self.batch_size))
test_dataloader
test_dataloader() -> DataLoader[list[RicoElement]]

Return one ordered test pass.

Source code in models/canvas-vae/src/canvas_vae/training/datamodule.py
119
120
121
122
def test_dataloader(self) -> DataLoader[list[RicoElement]]:
    """Return one ordered test pass."""
    size = len(self.splits.get(RicoSplit.test, ()))
    return self._loader(RicoSplit.test, sequential_batches(size, self.batch_size))

lightning_module

Lightning training module for CanvasVAE.

CanvasVAETrainingModule

Bases: LightningModule

Train a CanvasVAE model with its reconstruction, KL, and L2 losses.

The model config is built from the prepared vocabulary.json in data_dir. Every gradient tensor is clipped to clip_norm on its own before a :class:KerasAdam step; do not set Lightning's global gradient_clip_val. Validation decodes with the posterior mean and predicted element counts and logs per-document mean scores under val/, including val/total_score.

Parameters:

Name Type Description Default
data_dir str

Prepared split directory containing vocabulary.json.

'.cache/canvas-vae/data/rico'
latent_dim int

Hidden and latent dimension.

256
num_blocks int

Number of transformer blocks.

1
num_heads int

Number of attention heads.

8
dropout float

Dropout rate.

0.1
kl_weight float

KL divergence weight.

16.0
l2_weight float

Squared-norm penalty weight.

1e-06
learning_rate float

Adam learning rate.

0.001
clip_norm float

Maximum norm of each gradient tensor.

1.0
Source code in models/canvas-vae/src/canvas_vae/training/lightning_module.py
 19
 20
 21
 22
 23
 24
 25
 26
 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
class CanvasVAETrainingModule(L.LightningModule):
    """Train a CanvasVAE model with its reconstruction, KL, and L2 losses.

    The model config is built from the prepared ``vocabulary.json`` in
    ``data_dir``. Every gradient tensor is clipped to ``clip_norm`` on its own
    before a :class:`KerasAdam` step; do not set Lightning's global
    ``gradient_clip_val``. Validation decodes with the posterior mean and
    predicted element counts and logs per-document mean scores under ``val/``,
    including ``val/total_score``.

    Args:
        data_dir: Prepared split directory containing ``vocabulary.json``.
        latent_dim: Hidden and latent dimension.
        num_blocks: Number of transformer blocks.
        num_heads: Number of attention heads.
        dropout: Dropout rate.
        kl_weight: KL divergence weight.
        l2_weight: Squared-norm penalty weight.
        learning_rate: Adam learning rate.
        clip_norm: Maximum norm of each gradient tensor.
    """

    def __init__(
        self,
        data_dir: str = ".cache/canvas-vae/data/rico",
        latent_dim: int = 256,
        num_blocks: int = 1,
        num_heads: int = 8,
        dropout: float = 0.1,
        kl_weight: float = 16.0,
        l2_weight: float = 1e-6,
        learning_rate: float = 1e-3,
        clip_norm: float = 1.0,
    ) -> None:
        """Build the model from the prepared vocabulary."""
        super().__init__()
        self.save_hyperparameters()
        self.learning_rate = learning_rate
        self.clip_norm = clip_norm
        self.model = CanvasVAEModel(
            CanvasVAEConfig(
                vocabularies=load_rico_vocabularies(data_dir),
                latent_dim=latent_dim,
                num_blocks=num_blocks,
                num_heads=num_heads,
                dropout=dropout,
                kl_weight=kl_weight,
                l2_weight=l2_weight,
            )
        )
        self.validation_sums: dict[str, float] = {}
        self.validation_count = 0

    def training_step(
        self, batch: Mapping[str, Int[torch.Tensor, "..."]], batch_idx: int
    ) -> Float[torch.Tensor, ""]:
        """Return reconstruction, weighted KL, and L2 losses summed.

        Args:
            batch: Processor output.
            batch_idx: Batch index.

        Returns:
            Total loss.
        """
        del batch_idx
        output = self.model(
            num_elements=batch["num_elements"], element_ids=batch["element_ids"]
        )
        penalty = l2_penalty(self.model, self.model.config.l2_weight)
        loss = output.loss + penalty
        batch_size = int(batch["num_elements"].shape[0])
        for key, value in (output.reconstruction_losses or {}).items():
            self.log(
                f"train/{key}_loss",
                value,
                on_step=False,
                on_epoch=True,
                batch_size=batch_size,
            )

        self.log(
            "train/kl_divergence",
            output.kl_divergence,
            on_epoch=True,
            batch_size=batch_size,
        )
        self.log(
            "train/l2", penalty, on_step=False, on_epoch=True, batch_size=batch_size
        )
        self.log(
            "train/loss", loss, prog_bar=True, on_epoch=True, batch_size=batch_size
        )
        return loss

    def on_validation_epoch_start(self) -> None:
        """Reset validation accumulators."""
        self.validation_sums = {}
        self.validation_count = 0

    def validation_step(
        self, batch: Mapping[str, Int[torch.Tensor, "..."]], batch_idx: int
    ) -> None:
        """Accumulate per-document reconstruction scores.

        Args:
            batch: Processor output.
            batch_idx: Batch index.
        """
        del batch_idx
        num_elements = batch["num_elements"]
        element_ids = batch["element_ids"]
        output = self.model(num_elements=num_elements, element_ids=element_ids)
        predicted = torch.stack(
            [logits.argmax(dim=-1) for logits in output.element_logits.values()], dim=-1
        )
        target_mask = length_mask(num_elements - 1, element_ids.shape[1])
        config = self.model.config
        scores = reconstruction_scores(
            element_ids, target_mask, predicted, output.mask, config.field_sizes
        ) | layout_scores(
            element_ids,
            target_mask,
            predicted,
            output.mask,
            grid_size=config.num_bins,
            num_labels=config.field_sizes["component"],
            background_id=config.primary_label_id,
        )
        for key, values in scores.items():
            self.validation_sums[key] = self.validation_sums.get(key, 0.0) + float(
                values.sum()
            )

        self.validation_count += int(num_elements.shape[0])
        kl = float(output.kl_divergence)
        self.validation_sums["kl_divergence_batches"] = (
            self.validation_sums.get("kl_divergence_batches", 0.0) + kl
        )
        self.validation_sums["batches"] = self.validation_sums.get("batches", 0.0) + 1

    def on_validation_epoch_end(self) -> None:
        """Log per-document means of the accumulated scores."""
        if not self.validation_count:
            return

        sums = dict(self.validation_sums)
        batches = sums.pop("batches")
        kl = sums.pop("kl_divergence_batches") / batches
        for key, value in sums.items():
            name = f"val/{key}" if key.startswith("layout_") else f"val/{key}_score"
            self.log(name, value / self.validation_count, prog_bar=key == TOTAL_KEY)

        self.log("val/kl_divergence", kl)

    def configure_optimizers(self) -> Optimizer:
        """Return :class:`KerasAdam` over the model parameters."""
        return KerasAdam(self.model.parameters(), lr=self.learning_rate)

    def configure_gradient_clipping(
        self,
        optimizer: Optimizer,
        gradient_clip_val: float | None = None,
        gradient_clip_algorithm: str | None = None,
    ) -> None:
        """Clip every gradient tensor to ``clip_norm`` independently.

        Args:
            optimizer: Optimizer about to step.
            gradient_clip_val: Must be unset; global clipping is not used.
            gradient_clip_algorithm: Ignored.

        Raises:
            ValueError: If Lightning's global ``gradient_clip_val`` is set.
        """
        del optimizer, gradient_clip_algorithm
        if gradient_clip_val:
            raise ValueError("set clip_norm on the module instead of gradient_clip_val")

        clip_gradients_by_norm(self.model.parameters(), self.clip_norm)
__init__
__init__(
    data_dir: str = ".cache/canvas-vae/data/rico",
    latent_dim: int = 256,
    num_blocks: int = 1,
    num_heads: int = 8,
    dropout: float = 0.1,
    kl_weight: float = 16.0,
    l2_weight: float = 1e-06,
    learning_rate: float = 0.001,
    clip_norm: float = 1.0,
) -> None

Build the model from the prepared vocabulary.

Source code in models/canvas-vae/src/canvas_vae/training/lightning_module.py
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
def __init__(
    self,
    data_dir: str = ".cache/canvas-vae/data/rico",
    latent_dim: int = 256,
    num_blocks: int = 1,
    num_heads: int = 8,
    dropout: float = 0.1,
    kl_weight: float = 16.0,
    l2_weight: float = 1e-6,
    learning_rate: float = 1e-3,
    clip_norm: float = 1.0,
) -> None:
    """Build the model from the prepared vocabulary."""
    super().__init__()
    self.save_hyperparameters()
    self.learning_rate = learning_rate
    self.clip_norm = clip_norm
    self.model = CanvasVAEModel(
        CanvasVAEConfig(
            vocabularies=load_rico_vocabularies(data_dir),
            latent_dim=latent_dim,
            num_blocks=num_blocks,
            num_heads=num_heads,
            dropout=dropout,
            kl_weight=kl_weight,
            l2_weight=l2_weight,
        )
    )
    self.validation_sums: dict[str, float] = {}
    self.validation_count = 0
training_step
training_step(
    batch: Mapping[str, Int[Tensor, "..."]], batch_idx: int
) -> Float[torch.Tensor, ""]

Return reconstruction, weighted KL, and L2 losses summed.

Parameters:

Name Type Description Default
batch Mapping[str, Int[Tensor, '...']]

Processor output.

required
batch_idx int

Batch index.

required

Returns:

Type Description
Float[Tensor, '']

Total loss.

Source code in models/canvas-vae/src/canvas_vae/training/lightning_module.py
 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
def training_step(
    self, batch: Mapping[str, Int[torch.Tensor, "..."]], batch_idx: int
) -> Float[torch.Tensor, ""]:
    """Return reconstruction, weighted KL, and L2 losses summed.

    Args:
        batch: Processor output.
        batch_idx: Batch index.

    Returns:
        Total loss.
    """
    del batch_idx
    output = self.model(
        num_elements=batch["num_elements"], element_ids=batch["element_ids"]
    )
    penalty = l2_penalty(self.model, self.model.config.l2_weight)
    loss = output.loss + penalty
    batch_size = int(batch["num_elements"].shape[0])
    for key, value in (output.reconstruction_losses or {}).items():
        self.log(
            f"train/{key}_loss",
            value,
            on_step=False,
            on_epoch=True,
            batch_size=batch_size,
        )

    self.log(
        "train/kl_divergence",
        output.kl_divergence,
        on_epoch=True,
        batch_size=batch_size,
    )
    self.log(
        "train/l2", penalty, on_step=False, on_epoch=True, batch_size=batch_size
    )
    self.log(
        "train/loss", loss, prog_bar=True, on_epoch=True, batch_size=batch_size
    )
    return loss
on_validation_epoch_start
on_validation_epoch_start() -> None

Reset validation accumulators.

Source code in models/canvas-vae/src/canvas_vae/training/lightning_module.py
114
115
116
117
def on_validation_epoch_start(self) -> None:
    """Reset validation accumulators."""
    self.validation_sums = {}
    self.validation_count = 0
validation_step
validation_step(
    batch: Mapping[str, Int[Tensor, "..."]], batch_idx: int
) -> None

Accumulate per-document reconstruction scores.

Parameters:

Name Type Description Default
batch Mapping[str, Int[Tensor, '...']]

Processor output.

required
batch_idx int

Batch index.

required
Source code in models/canvas-vae/src/canvas_vae/training/lightning_module.py
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
def validation_step(
    self, batch: Mapping[str, Int[torch.Tensor, "..."]], batch_idx: int
) -> None:
    """Accumulate per-document reconstruction scores.

    Args:
        batch: Processor output.
        batch_idx: Batch index.
    """
    del batch_idx
    num_elements = batch["num_elements"]
    element_ids = batch["element_ids"]
    output = self.model(num_elements=num_elements, element_ids=element_ids)
    predicted = torch.stack(
        [logits.argmax(dim=-1) for logits in output.element_logits.values()], dim=-1
    )
    target_mask = length_mask(num_elements - 1, element_ids.shape[1])
    config = self.model.config
    scores = reconstruction_scores(
        element_ids, target_mask, predicted, output.mask, config.field_sizes
    ) | layout_scores(
        element_ids,
        target_mask,
        predicted,
        output.mask,
        grid_size=config.num_bins,
        num_labels=config.field_sizes["component"],
        background_id=config.primary_label_id,
    )
    for key, values in scores.items():
        self.validation_sums[key] = self.validation_sums.get(key, 0.0) + float(
            values.sum()
        )

    self.validation_count += int(num_elements.shape[0])
    kl = float(output.kl_divergence)
    self.validation_sums["kl_divergence_batches"] = (
        self.validation_sums.get("kl_divergence_batches", 0.0) + kl
    )
    self.validation_sums["batches"] = self.validation_sums.get("batches", 0.0) + 1
on_validation_epoch_end
on_validation_epoch_end() -> None

Log per-document means of the accumulated scores.

Source code in models/canvas-vae/src/canvas_vae/training/lightning_module.py
160
161
162
163
164
165
166
167
168
169
170
171
172
def on_validation_epoch_end(self) -> None:
    """Log per-document means of the accumulated scores."""
    if not self.validation_count:
        return

    sums = dict(self.validation_sums)
    batches = sums.pop("batches")
    kl = sums.pop("kl_divergence_batches") / batches
    for key, value in sums.items():
        name = f"val/{key}" if key.startswith("layout_") else f"val/{key}_score"
        self.log(name, value / self.validation_count, prog_bar=key == TOTAL_KEY)

    self.log("val/kl_divergence", kl)
configure_optimizers
configure_optimizers() -> Optimizer

Return :class:KerasAdam over the model parameters.

Source code in models/canvas-vae/src/canvas_vae/training/lightning_module.py
174
175
176
def configure_optimizers(self) -> Optimizer:
    """Return :class:`KerasAdam` over the model parameters."""
    return KerasAdam(self.model.parameters(), lr=self.learning_rate)
configure_gradient_clipping
configure_gradient_clipping(
    optimizer: Optimizer,
    gradient_clip_val: float | None = None,
    gradient_clip_algorithm: str | None = None,
) -> None

Clip every gradient tensor to clip_norm independently.

Parameters:

Name Type Description Default
optimizer Optimizer

Optimizer about to step.

required
gradient_clip_val float | None

Must be unset; global clipping is not used.

None
gradient_clip_algorithm str | None

Ignored.

None

Raises:

Type Description
ValueError

If Lightning's global gradient_clip_val is set.

Source code in models/canvas-vae/src/canvas_vae/training/lightning_module.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def configure_gradient_clipping(
    self,
    optimizer: Optimizer,
    gradient_clip_val: float | None = None,
    gradient_clip_algorithm: str | None = None,
) -> None:
    """Clip every gradient tensor to ``clip_norm`` independently.

    Args:
        optimizer: Optimizer about to step.
        gradient_clip_val: Must be unset; global clipping is not used.
        gradient_clip_algorithm: Ignored.

    Raises:
        ValueError: If Lightning's global ``gradient_clip_val`` is set.
    """
    del optimizer, gradient_clip_algorithm
    if gradient_clip_val:
        raise ValueError("set clip_norm on the module instead of gradient_clip_val")

    clip_gradients_by_norm(self.model.parameters(), self.clip_norm)

optim

Optimizer, gradient clipping, and weight penalty for CanvasVAE training.

clip_gradients_by_norm

clip_gradients_by_norm(
    parameters: Iterable[Parameter], max_norm: float
) -> None

Rescale every gradient tensor whose own L2 norm exceeds max_norm.

Each gradient becomes g * max_norm / max(norm(g), max_norm); unlike global-norm clipping, tensors are clipped independently.

Parameters:

Name Type Description Default
parameters Iterable[Parameter]

Parameters whose gradients are clipped in place.

required
max_norm float

Maximum norm of each gradient tensor.

required

Examples:

>>> weight = nn.Parameter(torch.zeros(2))
>>> weight.grad = torch.tensor([3.0, 4.0])
>>> clip_gradients_by_norm([weight], 1.0)
>>> weight.grad.tolist()
[0.6000000238418579, 0.800000011920929]
Source code in models/canvas-vae/src/canvas_vae/training/optim.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def clip_gradients_by_norm(parameters: Iterable[nn.Parameter], max_norm: float) -> None:
    """Rescale every gradient tensor whose own L2 norm exceeds ``max_norm``.

    Each gradient becomes ``g * max_norm / max(norm(g), max_norm)``; unlike
    global-norm clipping, tensors are clipped independently.

    Args:
        parameters: Parameters whose gradients are clipped in place.
        max_norm: Maximum norm of each gradient tensor.

    Examples:
        >>> weight = nn.Parameter(torch.zeros(2))
        >>> weight.grad = torch.tensor([3.0, 4.0])
        >>> clip_gradients_by_norm([weight], 1.0)
        >>> weight.grad.tolist()
        [0.6000000238418579, 0.800000011920929]
    """
    for param in parameters:
        if param.grad is None:
            continue

        norm = param.grad.square().sum().sqrt()
        clip = torch.tensor(max_norm, dtype=norm.dtype, device=norm.device)
        param.grad.copy_(param.grad * clip / torch.maximum(norm, clip))

l2_penalty

l2_penalty(
    model: Module, weight: float
) -> Float[torch.Tensor, ""]

Return weight * sum(w**2) over every dense and embedding parameter.

Normalization parameters and moving statistics are excluded.

Parameters:

Name Type Description Default
model Module

Model whose nn.Linear and nn.Embedding parameters are penalized.

required
weight float

Penalty weight.

required

Returns:

Type Description
Float[Tensor, '']

Scalar penalty.

Examples:

>>> l2_penalty(nn.Linear(1, 1, bias=False).requires_grad_(False), 0.0)
tensor(0.)
Source code in models/canvas-vae/src/canvas_vae/training/optim.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def l2_penalty(model: nn.Module, weight: float) -> Float[torch.Tensor, ""]:
    """Return ``weight * sum(w**2)`` over every dense and embedding parameter.

    Normalization parameters and moving statistics are excluded.

    Args:
        model: Model whose ``nn.Linear`` and ``nn.Embedding`` parameters are
            penalized.
        weight: Penalty weight.

    Returns:
        Scalar penalty.

    Examples:
        >>> l2_penalty(nn.Linear(1, 1, bias=False).requires_grad_(False), 0.0)
        tensor(0.)
    """
    terms = [
        weight * param.square().sum()
        for module in model.modules()
        if isinstance(module, (nn.Linear, nn.Embedding))
        for param in module.parameters(recurse=False)
    ]
    return torch.stack(terms).sum()

sampling

Batch index streams for CanvasVAE training and evaluation.

CrossEpochBatchSampler

Bases: Sampler[list[int]]

Yield fixed-size batches from one endless stream of shuffled passes.

The stream concatenates independent permutations of the records, and each epoch takes the next ceil(num_records / batch_size) batches from it, so batches straddle pass boundaries and every batch is full.

Parameters:

Name Type Description Default
num_records int

Number of training records.

required
batch_size int

Batch size.

required
generator Generator

Generator for the permutations.

required
ordered_indices Sequence[int] | None

Optional finite sample-index stream for deterministic replay. Without it, the sampler keeps drawing shuffled passes.

None

Examples:

>>> sampler = CrossEpochBatchSampler(5, 2, torch.Generator().manual_seed(0))
>>> [len(batch) for batch in sampler], len(sampler)
([2, 2, 2], 3)
Source code in models/canvas-vae/src/canvas_vae/training/sampling.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
class CrossEpochBatchSampler(Sampler[list[int]]):
    """Yield fixed-size batches from one endless stream of shuffled passes.

    The stream concatenates independent permutations of the records, and each
    epoch takes the next ``ceil(num_records / batch_size)`` batches from it, so
    batches straddle pass boundaries and every batch is full.

    Args:
        num_records: Number of training records.
        batch_size: Batch size.
        generator: Generator for the permutations.
        ordered_indices: Optional finite sample-index stream for deterministic
            replay. Without it, the sampler keeps drawing shuffled passes.

    Examples:
        >>> sampler = CrossEpochBatchSampler(5, 2, torch.Generator().manual_seed(0))
        >>> [len(batch) for batch in sampler], len(sampler)
        ([2, 2, 2], 3)
    """

    def __init__(
        self,
        num_records: int,
        batch_size: int,
        generator: torch.Generator,
        *,
        ordered_indices: Sequence[int] | None = None,
    ) -> None:
        """Store the stream configuration."""
        self.num_records = num_records
        self.batch_size = batch_size
        self.generator = generator
        if ordered_indices is not None and any(
            index < 0 or index >= num_records for index in ordered_indices
        ):
            raise ValueError("ordered indices must refer to records in the dataset")

        self._ordered_indices = ordered_indices
        self._ordered_position = 0
        self.buffer: list[int] = []

    def __len__(self) -> int:
        """Return the number of batches per epoch."""
        return math.ceil(self.num_records / self.batch_size)

    def __iter__(self) -> Iterator[list[int]]:
        """Yield the next epoch of batches from the stream."""
        for _ in range(len(self)):
            if self._ordered_indices is not None:
                start = self._ordered_position
                stop = start + self.batch_size
                if stop > len(self._ordered_indices):
                    raise RuntimeError(
                        "the injected sample order ended before the batch"
                    )

                batch = list(self._ordered_indices[start:stop])
                self._ordered_position = stop
                yield batch
                continue

            while len(self.buffer) < self.batch_size:
                self.buffer.extend(
                    randperm(self.num_records, generator=self.generator).tolist()
                )

            batch, self.buffer = (
                self.buffer[: self.batch_size],
                self.buffer[self.batch_size :],
            )
            yield batch
__init__
__init__(
    num_records: int,
    batch_size: int,
    generator: Generator,
    *,
    ordered_indices: Sequence[int] | None = None,
) -> None

Store the stream configuration.

Source code in models/canvas-vae/src/canvas_vae/training/sampling.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def __init__(
    self,
    num_records: int,
    batch_size: int,
    generator: torch.Generator,
    *,
    ordered_indices: Sequence[int] | None = None,
) -> None:
    """Store the stream configuration."""
    self.num_records = num_records
    self.batch_size = batch_size
    self.generator = generator
    if ordered_indices is not None and any(
        index < 0 or index >= num_records for index in ordered_indices
    ):
        raise ValueError("ordered indices must refer to records in the dataset")

    self._ordered_indices = ordered_indices
    self._ordered_position = 0
    self.buffer: list[int] = []
__len__
__len__() -> int

Return the number of batches per epoch.

Source code in models/canvas-vae/src/canvas_vae/training/sampling.py
54
55
56
def __len__(self) -> int:
    """Return the number of batches per epoch."""
    return math.ceil(self.num_records / self.batch_size)
__iter__
__iter__() -> Iterator[list[int]]

Yield the next epoch of batches from the stream.

Source code in models/canvas-vae/src/canvas_vae/training/sampling.py
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
def __iter__(self) -> Iterator[list[int]]:
    """Yield the next epoch of batches from the stream."""
    for _ in range(len(self)):
        if self._ordered_indices is not None:
            start = self._ordered_position
            stop = start + self.batch_size
            if stop > len(self._ordered_indices):
                raise RuntimeError(
                    "the injected sample order ended before the batch"
                )

            batch = list(self._ordered_indices[start:stop])
            self._ordered_position = stop
            yield batch
            continue

        while len(self.buffer) < self.batch_size:
            self.buffer.extend(
                randperm(self.num_records, generator=self.generator).tolist()
            )

        batch, self.buffer = (
            self.buffer[: self.batch_size],
            self.buffer[self.batch_size :],
        )
        yield batch

wrapping_batches

wrapping_batches(
    num_records: int, batch_size: int
) -> list[list[int]]

Return ceil(num_records / batch_size) full batches of a repeated pass.

Records are taken in order and the last batch wraps to the first records, so some records are scored twice.

Parameters:

Name Type Description Default
num_records int

Number of records.

required
batch_size int

Batch size.

required

Returns:

Type Description
list[list[int]]

Batches of record indices.

Examples:

>>> wrapping_batches(3, 2)
[[0, 1], [2, 0]]
Source code in models/canvas-vae/src/canvas_vae/training/sampling.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def wrapping_batches(num_records: int, batch_size: int) -> list[list[int]]:
    """Return ``ceil(num_records / batch_size)`` full batches of a repeated pass.

    Records are taken in order and the last batch wraps to the first records,
    so some records are scored twice.

    Args:
        num_records: Number of records.
        batch_size: Batch size.

    Returns:
        Batches of record indices.

    Examples:
        >>> wrapping_batches(3, 2)
        [[0, 1], [2, 0]]
    """
    steps = math.ceil(num_records / batch_size)
    indices = [index % num_records for index in range(steps * batch_size)]
    return [
        indices[start : start + batch_size]
        for start in range(0, len(indices), batch_size)
    ]

sequential_batches

sequential_batches(
    num_records: int, batch_size: int
) -> list[list[int]]

Return one ordered pass whose last batch may be smaller.

Parameters:

Name Type Description Default
num_records int

Number of records.

required
batch_size int

Batch size.

required

Returns:

Type Description
list[list[int]]

Batches of record indices.

Examples:

>>> sequential_batches(3, 2)
[[0, 1], [2]]
Source code in models/canvas-vae/src/canvas_vae/training/sampling.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def sequential_batches(num_records: int, batch_size: int) -> list[list[int]]:
    """Return one ordered pass whose last batch may be smaller.

    Args:
        num_records: Number of records.
        batch_size: Batch size.

    Returns:
        Batches of record indices.

    Examples:
        >>> sequential_batches(3, 2)
        [[0, 1], [2]]
    """
    return [
        list(range(start, min(start + batch_size, num_records)))
        for start in range(0, num_records, batch_size)
    ]