From e515ab96e445c47453850f5d2ec70f452274229e Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 17 Sep 2025 16:36:16 -0700 Subject: [PATCH 01/44] Add prototype of multimodal configuration --- examples/2d_flow_matching.ipynb | 6 +- examples/2d_multimodal_flow_matching.py | 358 ++++++++++++++++++++++++ flow_matching/utils/multimodal.py | 206 ++++++++++++++ 3 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 examples/2d_multimodal_flow_matching.py create mode 100644 flow_matching/utils/multimodal.py diff --git a/examples/2d_flow_matching.ipynb b/examples/2d_flow_matching.ipynb index 622f411..7ec836a 100644 --- a/examples/2d_flow_matching.ipynb +++ b/examples/2d_flow_matching.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "id": "rb5VSo4mNkVd" }, @@ -40,7 +40,7 @@ "# flow_matching\n", "from flow_matching.path.scheduler import CondOTScheduler\n", "from flow_matching.path import AffineProbPath\n", - "from flow_matching.solver import Solver, ODESolver\n", + "from flow_matching.solver import ODESolver\n", "from flow_matching.utils import ModelWrapper\n", "\n", "# visualization\n", @@ -131,7 +131,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/examples/2d_multimodal_flow_matching.py b/examples/2d_multimodal_flow_matching.py new file mode 100644 index 0000000..b76814a --- /dev/null +++ b/examples/2d_multimodal_flow_matching.py @@ -0,0 +1,358 @@ +# A simple 2D Multimodal Flow Matching model +# This notebook trains and evaluates a multimodal FM model that jointly handles +# a discrete modality (categorical data) and a continuous modality (real‑valued 2‑D data). + +# %% +# Imports and device setup +import time +import torch +from torch import nn, Tensor + +# flow_matching core components +from flow_matching.utils.multimodal import Flow +from flow_matching.path.scheduler import ( + PolynomialConvexScheduler, # discrete scheduler (training) + CondOTScheduler, # continuous scheduler (training) +) +from flow_matching.path import MixtureDiscreteProbPath, AffineProbPath + +# visualization +import matplotlib.pyplot as plt + +# %% +# Device +if torch.cuda.is_available(): + device = "cuda:0" + print("Using GPU") +elif torch.backends.mps.is_available(): + device = "mps" + print("Using MPS") +else: + device = "cpu" + print("Using CPU") +torch.manual_seed(42) + +# %% +# ------------------------------ +# 1️⃣ Discrete modality utilities +# ------------------------------ + + +def inf_train_gen_discrete( + n_grid_points: int = 128, + batch_size: int = 200, + device: str = "cpu", +) -> Tensor: + """ + Generate a batch of discrete (categorical) samples. + Returns a tensor of shape (batch, 2) with integer token IDs. + """ + assert n_grid_points % 4 == 0, "grid size must be divisible by 4" + n_grid_points //= 4 + + x1 = torch.randint(low=0, high=n_grid_points * 4, size=(batch_size,), device=device) + samples_x2 = torch.randint( + low=0, high=n_grid_points, size=(batch_size,), device=device + ) + + x2 = ( + samples_x2 + + 2 * n_grid_points + - torch.randint(low=0, high=2, size=(batch_size,), device=device) + * 2 + * n_grid_points + + (torch.floor(x1 / n_grid_points) % 2) * n_grid_points + ) + return torch.stack([x1, x2], dim=1).long() + + +class Swish(nn.Module): + """Swish activation (x * sigmoid(x)).""" + + def forward(self, x: Tensor) -> Tensor: + return torch.sigmoid(x) * x + + +class DiscreteMLP(nn.Module): + """ + Simple token-embedding MLP for the discrete modality. + Input: integer token IDs of shape (batch, 2) + Output: logits over the vocabulary for each token. + """ + + def __init__( + self, + vocab_size: int = 128, + time_dim: int = 1, + hidden_dim: int = 128, + length: int = 2, + ): + super().__init__() + self.input_dim = vocab_size + self.time_dim = time_dim + self.hidden_dim = hidden_dim + self.length = length + + self.time_embedding = nn.Linear(1, time_dim) + self.token_embedding = nn.Embedding(self.input_dim, hidden_dim) + + self.main = nn.Sequential( + Swish(), + nn.Linear(hidden_dim * length + time_dim, hidden_dim), + Swish(), + nn.Linear(hidden_dim, hidden_dim), + Swish(), + nn.Linear(hidden_dim, hidden_dim), + Swish(), + nn.Linear(hidden_dim, self.input_dim * length), + ) + + def sample_shape(self, batch_size: int) -> torch.Size: + """Return the shape of samples from the prior distribution.""" + return torch.Size((batch_size, self.length)) + + def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor: + """Sample from the prior distribution (uniform over vocabulary).""" + return torch.randint(low=0, high=self.input_dim, size=shape, device=device) + + def forward(self, x: Tensor, t: Tensor) -> Tensor: + """ + x : (B, 2) integer token IDs + t : (B,) time scalar + Returns logits of shape (B, 2, vocab_size) + """ + if t.ndim == 0: + t = t.unsqueeze(0).expand(x.shape[0]) + + t = self.time_embedding(t.unsqueeze(-1).float()) + x = self.token_embedding(x) + + B, N, d = x.shape + x = x.reshape(B, N * d) + + h = torch.cat([x, t], dim=1) + h = self.main(h) + + h = h.reshape(B, N, self.input_dim) + + return h + + +# ------------------------------ +# 2️⃣ Continuous modality utilities +# ------------------------------ + + +def inf_train_gen_continuous(batch_size: int = 200, device: str = "cpu") -> Tensor: + """ + Generate a batch of 2-D continuous points from a checkerboard-like distribution. + Returns a tensor of shape (batch, 2). + """ + x1 = torch.rand(batch_size, device=device) * 4 - 2 + x2_ = ( + torch.rand(batch_size, device=device) + - torch.randint(high=2, size=(batch_size,), device=device) * 2 + ) + x2 = x2_ + (torch.floor(x1) % 2) + data = torch.stack([x1, x2], dim=1) / 0.45 + return data.float() + + +class ContinuousMLP(nn.Module): + """ + Simple MLP that predicts the velocity field for the continuous modality. + Input: (B, 2) positions + (B, 1) time + Output: (B, 2) velocity vectors. + """ + + def __init__(self, input_dim: int = 2, time_dim: int = 1, hidden_dim: int = 128): + super().__init__() + self.input_dim = input_dim + self.time_dim = time_dim + self.hidden_dim = hidden_dim + + self.main = nn.Sequential( + nn.Linear(input_dim + time_dim, hidden_dim), + Swish(), + nn.Linear(hidden_dim, hidden_dim), + Swish(), + nn.Linear(hidden_dim, hidden_dim), + Swish(), + nn.Linear(hidden_dim, hidden_dim), + Swish(), + nn.Linear(hidden_dim, input_dim), + ) + + def sample_shape(self, batch_size: int) -> torch.Size: + """Return the shape of samples from the prior distribution.""" + return torch.Size((batch_size, self.input_dim)) + + def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor: + """Sample from the prior distribution (standard normal).""" + return torch.randn(shape, device=device) + + def forward(self, x: Tensor, t: Tensor) -> Tensor: + """ + x : (B, 2) positions + t : (B,) time scalar + Returns velocity vectors of shape (B, 2) + """ + sz = x.size() + x = x.reshape(-1, self.input_dim) + t = t.reshape(-1, self.time_dim).float() + + t = t.reshape(-1, 1).expand(x.shape[0], 1) + h = torch.cat([x, t], dim=1) + output = self.main(h) + + return output.reshape(*sz) + + +# ------------------------------ +# 3️⃣ Build multimodal components +# ------------------------------ + +# ---- Discrete side ------------------------------------------------- +vocab_size = 128 +added_token = 0 # uniform source distribution → no extra token +vocab_size += added_token +length = 2 # 2 tokens per sample + +discrete_model = DiscreteMLP( + vocab_size=vocab_size, time_dim=1, hidden_dim=128, length=length +).to(device) +discrete_path = MixtureDiscreteProbPath(scheduler=PolynomialConvexScheduler(n=2.0)) + +# ---- Continuous side ----------------------------------------------- +continuous_model = ContinuousMLP(input_dim=length, time_dim=1, hidden_dim=512).to( + device +) +continuous_path = AffineProbPath(scheduler=CondOTScheduler()) + +# ---- Assemble modalities dict --------------------------------------- +modalities = { + "discrete": { + "model": discrete_model, + "path": discrete_path, + # loss omitted → Flow will use MixturePathGeneralizedKL automatically + }, + "continuous": { + "model": continuous_model, + "path": continuous_path, + # loss omitted → Flow will use MSE loss automatically + }, +} + +# ------------------------------ +# 4️⃣ Instantiate the multimodal Flow model +# ------------------------------ + +flow = Flow(modalities=modalities) + +# Optimizer (optimises both modality models) +optimizer = torch.optim.Adam(flow.parameters(), lr=1e-3) + +# ------------------------------ +# 5️⃣ Training loop +# ------------------------------ + +lr = 1e-3 +batch_size = 4096 +iterations = 30001 +print_every = 3000 +epsilon = 1e-3 + +source_distribution = "uniform" # for the discrete modality + +start_time = time.time() +for i in range(iterations): + optimizer.zero_grad() + + # ---- Discrete data ------------------------------------------------- + x1_disc = inf_train_gen_discrete( + n_grid_points=vocab_size - added_token, + batch_size=batch_size, + device=device, + ) + if source_distribution == "uniform": + x0_disc = torch.randint_like(x1_disc, high=vocab_size) + else: # mask case (not used here) + raise NotImplementedError + + # ---- Continuous data ----------------------------------------------- + x1_cont = inf_train_gen_continuous(batch_size=batch_size, device=device) + x0_cont = torch.randn_like(x1_cont) # isotropic Gaussian prior + + # ---- Sample a common time tensor for both modalities --------------- + t = torch.rand(batch_size, device=device) * (1 - epsilon) + + # ---- Sample from each path to obtain x_t --------------------------- + disc_path_sample = discrete_path.sample(t=t, x_0=x0_disc, x_1=x1_disc) + cont_path_sample = continuous_path.sample(t=t, x_0=x0_cont, x_1=x1_cont) + + # ---- Build the inputs dict expected by Flow.training_loss ----------- + inputs = { + "discrete": (x1_disc, disc_path_sample.x_t, None), # dx_t is None for discrete + "continuous": (x1_cont, cont_path_sample.x_t, cont_path_sample.dx_t), + } + + # ---- Compute total loss and back‑propagate ------------------------- + loss = flow.training_loss(inputs=inputs, t=t) + loss.backward() + optimizer.step() + + # ---- Logging ------------------------------------------------------- + if (i + 1) % print_every == 0: + elapsed = time.time() - start_time + print( + f"| iter {i+1:6d} | {elapsed*1000/print_every:5.2f} ms/step | loss {loss.item():8.3f} " + ) + start_time = time.time() + +# ------------------------------ +# 6️⃣ Sampling from the trained multimodal model +# ------------------------------ + +flow.eval() # switch to eval mode for sampling +samples = flow.sample(batch_size=200_000, device=device, steps=1000) + +# ----------------------------------------------------------------- +# 7️⃣ Visualisation +# ----------------------------------------------------------------- + +# ---- Discrete modality ------------------------------------------------- +discrete_samples = samples["discrete"].cpu().numpy() # shape (N, 2) integer tokens +vocab = vocab_size + +# Plot a 2‑D histogram of the discrete samples +plt.figure(figsize=(6, 5)) +plt.hist2d( + discrete_samples[:, 0], + discrete_samples[:, 1], + bins=vocab, + cmap="viridis", +) +plt.title("Discrete modality samples (token histogram)") +plt.xlabel("Token 1") +plt.ylabel("Token 2") +plt.colorbar(label="Count") +plt.tight_layout() +plt.show() + +# ---- Continuous modality ----------------------------------------------- +continuous_samples = samples["continuous"].cpu().numpy() # shape (N, 2) + +# Plot a 2‑D histogram of the continuous samples +plt.figure(figsize=(6, 5)) +plt.hist2d( + continuous_samples[:, 0], + continuous_samples[:, 1], + bins=200, + cmap="viridis", +) +plt.title("Continuous modality samples (2-D density)") +plt.xlabel("x₁") +plt.ylabel("x₂") +plt.colorbar(label="Count") +plt.tight_layout() +plt.show() diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py new file mode 100644 index 0000000..054fd6f --- /dev/null +++ b/flow_matching/utils/multimodal.py @@ -0,0 +1,206 @@ +""" +Generic multimodal flow matching class. + +This class aggregates multiple modalities, each with its own model, path, +scheduler, and loss. It provides utilities for training (computing the total +loss) and inference (sampling) across all modalities. +""" + +import torch +from torch import nn, Tensor +from typing import Dict, Optional, Tuple, Callable, Any + +# Flow matching components +from flow_matching.path.scheduler import Scheduler +from flow_matching.path.scheduler.schedule_transform import ScheduleTransformedModel +from flow_matching.solver import MixtureDiscreteEulerSolver +from flow_matching.solver.ode_solver import ODESolver +from flow_matching.utils import ModelWrapper +from flow_matching.loss.generalized_loss import MixturePathGeneralizedKL + +# Attempt to import the discrete probability path class; if unavailable, the user +# must provide a compatible path object. +try: + from flow_matching.path.mixture import MixtureDiscreteProbPath +except Exception: # pragma: no cover + MixtureDiscreteProbPath = None # type: ignore + + +def _default_continuous_loss(pred: Tensor, target: Tensor) -> Tensor: + """Mean squared error loss for continuous modalities.""" + return torch.mean((pred - target) ** 2) + + +class Flow(nn.Module): + """ + Generic multimodal flow matching model. + + Parameters + ---------- + modalities : dict + Mapping from modality name to a dict with keys: + - "model": nn.Module (or ModelWrapper) that implements the velocity model. + - "path": a probability path object (e.g., MixtureDiscreteProbPath for discrete data, + or any continuous path implementation). + - "loss" (optional): a callable loss function. If omitted, a default loss is chosen + based on the path type. + training_scheduler (optional): Scheduler + Scheduler used during training. + inference_scheduler (optional): Scheduler + Scheduler used during inference (sampling). + """ + + def __init__( + self, + modalities: Dict[str, Dict[str, Any]], + training_scheduler: Optional[Scheduler] = None, + inference_scheduler: Optional[Scheduler] = None, + ) -> None: + super().__init__() + self.modalities = nn.ModuleDict() + self.paths: Dict[str, Any] = {} + self.loss_fns: Dict[str, Callable] = {} + self.training_scheduler = training_scheduler + self.inference_scheduler = inference_scheduler + + for name, spec in modalities.items(): + model = spec["model"] + if not isinstance(model, nn.Module): + raise TypeError(f"Model for modality '{name}' must be an nn.Module.") + self.modalities[name] = model + + path = spec["path"] + self.paths[name] = path + + # Choose loss function + loss_fn = spec.get("loss") + if loss_fn is None: + if MixtureDiscreteProbPath is not None and isinstance( + path, MixtureDiscreteProbPath + ): + loss_fn = MixturePathGeneralizedKL(path) + else: + loss_fn = _default_continuous_loss + self.loss_fns[name] = loss_fn + + def training_loss( + self, + inputs: Dict[str, Tuple[Tensor, Tensor]], + t: Tensor, + ) -> Tensor: + """ + Compute the total training loss across all modalities. + + Parameters + ---------- + inputs : dict + Mapping from modality name to a tuple ``(x_1, x_t)`` where ``x_1`` is the data at + time ``0`` and ``x_t`` is the data at the sampled time ``t``. + t : Tensor + Tensor of shape ``(batch,)`` containing the time values. + + Returns + ------- + Tensor + Scalar loss (sum of modality losses). + """ + total_loss = 0.0 + for name, (x_1, x_t, dx_t) in inputs.items(): + model = self.modalities[name] + path = self.paths[name] + loss_fn = self.loss_fns[name] + + if MixtureDiscreteProbPath is not None and isinstance( + path, MixtureDiscreteProbPath + ): + # Discrete case: model should output logits. + logits = model(x=x_t, t=t) + loss = loss_fn(logits, x_1, x_t, t) + else: + # Continuous case: model returns velocity field. + pred_vel = model(x=x_t, t=t) + loss = loss_fn(pred_vel, dx_t) + + total_loss = total_loss + loss + + return total_loss + + @torch.no_grad() + def sample( + self, + batch_size: int, + device: torch.device = torch.device("cpu"), + steps: int = 1000, + ) -> Dict[str, Tensor]: + """ + Generate samples for each modality using the inference scheduler. + + Parameters + ---------- + batch_size : int + Number of samples to generate. + device : torch.device, optional + Device on which to run the sampling. + steps : int, optional + Number of integration steps for the ODE solver. + + Returns + ------- + dict + Mapping from modality name to sampled tensor. + """ + xs: Dict[str, Tensor] = {} + for name, model in self.modalities.items(): + path = self.paths[name] + + # Maybe transform the schedule of each modality. + velocity_model = model + if ( + self.training_scheduler is not None + and self.inference_scheduler is not None + ): + velocity_model = ScheduleTransformedModel( + velocity_model=model, + original_scheduler=self.training_scheduler, + new_scheduler=self.inference_scheduler, + ) + + # Initialise samples for each modality. + assert hasattr( + model, "sample_shape" + ), f"Model for modality '{name}' must implement 'sample_shape' method." + assert hasattr( + model, "sample_prior" + ), f"Model for modality '{name}' must implement 'sample_prior' method." + x_shape = model.sample_shape(batch_size) + xs[name] = model.sample_prior(x_shape, device=device) + + # Set up ODE solver. + solver = ODESolver(velocity_model=velocity_model) + if MixtureDiscreteProbPath is not None and isinstance( + path, MixtureDiscreteProbPath + ): + + class WrappedModel(ModelWrapper): + """Wrap velocity model to output probabilities.""" + + def forward(self, x: torch.Tensor, t: torch.Tensor, **extras): + """Output class probabilities.""" + return torch.softmax(self.model(x, t, **extras), dim=-1) + + wrapped_probability_denoiser = WrappedModel(velocity_model) + solver = MixtureDiscreteEulerSolver( + model=wrapped_probability_denoiser, + path=path, + vocabulary_size=wrapped_probability_denoiser.model.input_dim, + ) + + # Solve ODE to obtain samples at time 1. + time_grid = torch.linspace(0.0, 1.0, steps, device=device) + xs[name] = solver.sample( + x_init=xs[name], + step_size=1.0 / steps, + time_grid=time_grid, + ) + + return xs From 16dc80e19615ce576aa93b27443e74667c9e0c57 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 17 Sep 2025 18:19:27 -0700 Subject: [PATCH 02/44] Share Transformer encoder across all modalities --- examples/2d_multimodal_flow_matching.py | 165 ++++++++++++++---------- 1 file changed, 100 insertions(+), 65 deletions(-) diff --git a/examples/2d_multimodal_flow_matching.py b/examples/2d_multimodal_flow_matching.py index b76814a..1ee82f9 100644 --- a/examples/2d_multimodal_flow_matching.py +++ b/examples/2d_multimodal_flow_matching.py @@ -73,69 +73,84 @@ def forward(self, x: Tensor) -> Tensor: return torch.sigmoid(x) * x -class DiscreteMLP(nn.Module): +class SharedTransformer(nn.Module): """ - Simple token-embedding MLP for the discrete modality. - Input: integer token IDs of shape (batch, 2) - Output: logits over the vocabulary for each token. + Shared Transformer trunk used by both modalities. """ + def __init__(self, hidden_dim: int = 128, nhead: int = 4, num_layers: int = 2): + super().__init__() + encoder_layer = nn.TransformerEncoderLayer(d_model=hidden_dim, nhead=nhead) + self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) + def forward(self, x: Tensor) -> Tensor: + """ + x: (seq_len, batch, hidden_dim) + Returns transformed tensor of same shape. + """ + return self.transformer(x) + + +class DiscreteTransformerModel(nn.Module): + """ + Model for the discrete modality with separate input and output heads, + sharing a common Transformer trunk. + """ def __init__( self, + shared_transformer: SharedTransformer, vocab_size: int = 128, time_dim: int = 1, hidden_dim: int = 128, length: int = 2, ): super().__init__() - self.input_dim = vocab_size + self.shared = shared_transformer + self.vocab_size = vocab_size self.time_dim = time_dim self.hidden_dim = hidden_dim self.length = length self.time_embedding = nn.Linear(1, time_dim) - self.token_embedding = nn.Embedding(self.input_dim, hidden_dim) - - self.main = nn.Sequential( - Swish(), - nn.Linear(hidden_dim * length + time_dim, hidden_dim), - Swish(), - nn.Linear(hidden_dim, hidden_dim), - Swish(), - nn.Linear(hidden_dim, hidden_dim), - Swish(), - nn.Linear(hidden_dim, self.input_dim * length), - ) + self.token_embedding = nn.Embedding(vocab_size, hidden_dim) + self.input_proj = nn.Linear(hidden_dim + time_dim, hidden_dim) + self.output_head = nn.Linear(hidden_dim, vocab_size) + self.activation = Swish() def sample_shape(self, batch_size: int) -> torch.Size: - """Return the shape of samples from the prior distribution.""" return torch.Size((batch_size, self.length)) def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor: - """Sample from the prior distribution (uniform over vocabulary).""" - return torch.randint(low=0, high=self.input_dim, size=shape, device=device) + return torch.randint(low=0, high=self.vocab_size, size=shape, device=device) def forward(self, x: Tensor, t: Tensor) -> Tensor: """ - x : (B, 2) integer token IDs - t : (B,) time scalar - Returns logits of shape (B, 2, vocab_size) + x: (B, length) integer token IDs + t: (B,) time scalar + Returns logits of shape (B, length, vocab_size) """ if t.ndim == 0: t = t.unsqueeze(0).expand(x.shape[0]) - t = self.time_embedding(t.unsqueeze(-1).float()) - x = self.token_embedding(x) + # Token embedding + x_emb = self.token_embedding(x) # (B, length, hidden_dim) - B, N, d = x.shape - x = x.reshape(B, N * d) + # Time embedding + t_emb = self.time_embedding(t.unsqueeze(-1).float()) # (B, time_dim) + t_emb = t_emb.unsqueeze(1).expand(-1, self.length, -1) # (B, length, time_dim) - h = torch.cat([x, t], dim=1) - h = self.main(h) + # Concatenate and project + h = torch.cat([x_emb, t_emb], dim=-1) # (B, length, hidden_dim+time_dim) + h = self.input_proj(h) # (B, length, hidden_dim) - h = h.reshape(B, N, self.input_dim) + # Transformer expects (seq_len, batch, hidden_dim) + h = h.permute(1, 0, 2) # (length, B, hidden_dim) + h = self.shared(h) # (length, B, hidden_dim) + h = h.permute(1, 0, 2) # (B, length, hidden_dim) - return h + # Output logits + h = self.activation(h) + logits = self.output_head(h) # (B, length, vocab_size) + return logits # ------------------------------ @@ -158,54 +173,64 @@ def inf_train_gen_continuous(batch_size: int = 200, device: str = "cpu") -> Tens return data.float() -class ContinuousMLP(nn.Module): +class ContinuousTransformerModel(nn.Module): """ - Simple MLP that predicts the velocity field for the continuous modality. - Input: (B, 2) positions + (B, 1) time - Output: (B, 2) velocity vectors. + Model for the continuous modality with separate input and output heads, + sharing a common Transformer trunk. """ - - def __init__(self, input_dim: int = 2, time_dim: int = 1, hidden_dim: int = 128): + def __init__( + self, + shared_transformer: SharedTransformer, + input_dim: int = 2, + time_dim: int = 1, + hidden_dim: int = 128, + ): super().__init__() + self.shared = shared_transformer self.input_dim = input_dim self.time_dim = time_dim self.hidden_dim = hidden_dim - self.main = nn.Sequential( - nn.Linear(input_dim + time_dim, hidden_dim), - Swish(), - nn.Linear(hidden_dim, hidden_dim), - Swish(), - nn.Linear(hidden_dim, hidden_dim), - Swish(), - nn.Linear(hidden_dim, hidden_dim), - Swish(), - nn.Linear(hidden_dim, input_dim), - ) + self.time_embedding = nn.Linear(1, time_dim) + self.position_proj = nn.Linear(input_dim, hidden_dim) + self.input_proj = nn.Linear(hidden_dim + time_dim, hidden_dim) + self.output_head = nn.Linear(hidden_dim, input_dim) + self.activation = Swish() def sample_shape(self, batch_size: int) -> torch.Size: - """Return the shape of samples from the prior distribution.""" return torch.Size((batch_size, self.input_dim)) def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor: - """Sample from the prior distribution (standard normal).""" return torch.randn(shape, device=device) def forward(self, x: Tensor, t: Tensor) -> Tensor: """ - x : (B, 2) positions - t : (B,) time scalar - Returns velocity vectors of shape (B, 2) + x: (B, input_dim) positions + t: (B,) time scalar + Returns velocity vectors of shape (B, input_dim) """ - sz = x.size() - x = x.reshape(-1, self.input_dim) - t = t.reshape(-1, self.time_dim).float() + if t.ndim == 0: + t = t.unsqueeze(0).expand(x.shape[0]) - t = t.reshape(-1, 1).expand(x.shape[0], 1) - h = torch.cat([x, t], dim=1) - output = self.main(h) + # Position projection + x_emb = self.position_proj(x) # (B, hidden_dim) - return output.reshape(*sz) + # Time embedding + t_emb = self.time_embedding(t.unsqueeze(-1).float()) # (B, time_dim) + + # Concatenate and project + h = torch.cat([x_emb, t_emb], dim=-1) # (B, hidden_dim+time_dim) + h = self.input_proj(h) # (B, hidden_dim) + + # Transformer expects (seq_len, batch, hidden_dim) with seq_len=1 + h = h.unsqueeze(0) # (1, B, hidden_dim) + h = self.shared(h) # (1, B, hidden_dim) + h = h.squeeze(0) # (B, hidden_dim) + + # Output velocity + h = self.activation(h) + velocity = self.output_head(h) # (B, input_dim) + return velocity # ------------------------------ @@ -218,15 +243,25 @@ def forward(self, x: Tensor, t: Tensor) -> Tensor: vocab_size += added_token length = 2 # 2 tokens per sample -discrete_model = DiscreteMLP( - vocab_size=vocab_size, time_dim=1, hidden_dim=128, length=length +# Shared transformer trunk +shared_transformer = SharedTransformer(hidden_dim=128, nhead=4, num_layers=2).to(device) + +discrete_model = DiscreteTransformerModel( + shared_transformer=shared_transformer, + vocab_size=vocab_size, + time_dim=1, + hidden_dim=128, + length=length, ).to(device) discrete_path = MixtureDiscreteProbPath(scheduler=PolynomialConvexScheduler(n=2.0)) # ---- Continuous side ----------------------------------------------- -continuous_model = ContinuousMLP(input_dim=length, time_dim=1, hidden_dim=512).to( - device -) +continuous_model = ContinuousTransformerModel( + shared_transformer=shared_transformer, + input_dim=length, + time_dim=1, + hidden_dim=128, +).to(device) continuous_path = AffineProbPath(scheduler=CondOTScheduler()) # ---- Assemble modalities dict --------------------------------------- From c622a554a03462061dfba8ada3dbe0363a49b333 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Thu, 18 Sep 2025 12:30:51 -0700 Subject: [PATCH 03/44] Add 2D multimodal flow matching example notebook, and remove unused imports in other examples --- examples/2d_discrete_flow_matching.ipynb | 8 +- examples/2d_flow_matching.ipynb | 5 +- examples/2d_multimodal_flow_matching.ipynb | 619 ++++++++++++++++++ examples/2d_multimodal_flow_matching.py | 393 ----------- ..._riemannian_flow_matching_flat_torus.ipynb | 10 +- .../2d_riemannian_flow_matching_sphere.ipynb | 5 +- examples/README.md | 2 +- 7 files changed, 639 insertions(+), 403 deletions(-) create mode 100644 examples/2d_multimodal_flow_matching.ipynb delete mode 100644 examples/2d_multimodal_flow_matching.py diff --git a/examples/2d_discrete_flow_matching.ipynb b/examples/2d_discrete_flow_matching.ipynb index d9d44d8..f5bbca0 100644 --- a/examples/2d_discrete_flow_matching.ipynb +++ b/examples/2d_discrete_flow_matching.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "id": "rb5VSo4mNkVd" }, @@ -45,14 +45,13 @@ "from flow_matching.loss import MixturePathGeneralizedKL\n", "\n", "# visualization\n", - "import numpy as np\n", "import matplotlib.cm as cm\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -67,6 +66,9 @@ "if torch.cuda.is_available():\n", " device = 'cuda:0'\n", " print('Using gpu')\n", + "elif torch.backends.mps.is_available():\n", + " device = \"mps\"\n", + " print(\"Using MPS\")\n", "else:\n", " device = 'cpu'\n", " print('Using cpu.')" diff --git a/examples/2d_flow_matching.ipynb b/examples/2d_flow_matching.ipynb index 7ec836a..e0c49ed 100644 --- a/examples/2d_flow_matching.ipynb +++ b/examples/2d_flow_matching.ipynb @@ -57,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -72,6 +72,9 @@ "if torch.cuda.is_available():\n", " device = 'cuda:0'\n", " print('Using gpu')\n", + "elif torch.backends.mps.is_available():\n", + " device = \"mps\"\n", + " print(\"Using MPS\")\n", "else:\n", " device = 'cpu'\n", " print('Using cpu.')" diff --git a/examples/2d_multimodal_flow_matching.ipynb b/examples/2d_multimodal_flow_matching.ipynb new file mode 100644 index 0000000..ddf31e4 --- /dev/null +++ b/examples/2d_multimodal_flow_matching.ipynb @@ -0,0 +1,619 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "381ea8d5", + "metadata": {}, + "source": [ + "# A simple 2D Multimodal Flow Matching model" + ] + }, + { + "cell_type": "markdown", + "id": "0c0a75af", + "metadata": {}, + "source": [ + "This notebook trains and evaluates a multimodal FM model that jointly handles\n", + "a discrete modality (categorical data) and a continuous modality (real‑valued 2‑D data).\n", + "\n", + "Dataset: 2D discrete/continuous checkerboard\n", + "Model (probability denoiser/velocity): MLPs for each modality and a shared Transformer trunk" + ] + }, + { + "cell_type": "markdown", + "id": "b5c941fc", + "metadata": {}, + "source": [ + "## Imports and init device" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e7758331", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import torch\n", + "from torch import nn, Tensor\n", + "\n", + "# flow_matching\n", + "from flow_matching.utils.multimodal import Flow\n", + "from flow_matching.path.scheduler import (\n", + " PolynomialConvexScheduler, # discrete scheduler (training)\n", + " CondOTScheduler, # continuous scheduler (training)\n", + ")\n", + "from flow_matching.path import MixtureDiscreteProbPath, AffineProbPath\n", + "\n", + "# visualization\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# To avoide meshgrid warning\n", + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\", category=UserWarning, module='torch')" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "10957ca3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using MPS\n" + ] + } + ], + "source": [ + "if torch.cuda.is_available():\n", + " device = \"cuda:0\"\n", + " print(\"Using GPU\")\n", + "elif torch.backends.mps.is_available():\n", + " device = \"mps\"\n", + " print(\"Using MPS\")\n", + "else:\n", + " device = \"cpu\"\n", + " print(\"Using CPU\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0491f488", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "torch.manual_seed(42)" + ] + }, + { + "cell_type": "markdown", + "id": "b2ff4e5f", + "metadata": {}, + "source": [ + "## Shared model" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1321dec5", + "metadata": {}, + "outputs": [], + "source": [ + "class SharedTransformer(nn.Module):\n", + " \"\"\"\n", + " Shared Transformer trunk used by both modalities.\n", + " \"\"\"\n", + " def __init__(self, hidden_dim: int = 128, nhead: int = 4, num_layers: int = 2):\n", + " super().__init__()\n", + " encoder_layer = nn.TransformerEncoderLayer(d_model=hidden_dim, nhead=nhead)\n", + " self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)\n", + "\n", + " def forward(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " x: (seq_len, batch, hidden_dim)\n", + " Returns transformed tensor of same shape.\n", + " \"\"\"\n", + " return self.transformer(x)" + ] + }, + { + "cell_type": "markdown", + "id": "af22ef56", + "metadata": {}, + "source": [ + "## Discrete modality dataset and model" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b2466329", + "metadata": {}, + "outputs": [], + "source": [ + "def inf_train_gen_discrete(\n", + " n_grid_points: int = 128,\n", + " batch_size: int = 200,\n", + " device: str = \"cpu\",\n", + ") -> Tensor:\n", + " \"\"\"\n", + " Generate a batch of discrete (categorical) samples.\n", + " Returns a tensor of shape (batch, 2) with integer token IDs.\n", + " \"\"\"\n", + " assert n_grid_points % 4 == 0, \"grid size must be divisible by 4\"\n", + " n_grid_points //= 4\n", + "\n", + " x1 = torch.randint(low=0, high=n_grid_points * 4, size=(batch_size,), device=device)\n", + " samples_x2 = torch.randint(\n", + " low=0, high=n_grid_points, size=(batch_size,), device=device\n", + " )\n", + "\n", + " x2 = (\n", + " samples_x2\n", + " + 2 * n_grid_points\n", + " - torch.randint(low=0, high=2, size=(batch_size,), device=device)\n", + " * 2\n", + " * n_grid_points\n", + " + (torch.floor(x1 / n_grid_points) % 2) * n_grid_points\n", + " )\n", + " return torch.stack([x1, x2], dim=1).long()\n", + "\n", + "\n", + "class Swish(nn.Module):\n", + " \"\"\"Swish activation (x * sigmoid(x)).\"\"\"\n", + "\n", + " def forward(self, x: Tensor) -> Tensor:\n", + " return torch.sigmoid(x) * x\n", + "\n", + "\n", + "class DiscreteTransformerModel(nn.Module):\n", + " \"\"\"\n", + " Model for the discrete modality with separate input and output heads,\n", + " sharing a common Transformer trunk.\n", + " \"\"\"\n", + " def __init__(\n", + " self,\n", + " shared_transformer: SharedTransformer,\n", + " vocab_size: int = 128,\n", + " time_dim: int = 1,\n", + " hidden_dim: int = 128,\n", + " length: int = 2,\n", + " ):\n", + " super().__init__()\n", + " self.shared = shared_transformer\n", + " self.input_dim = vocab_size\n", + " self.time_dim = time_dim\n", + " self.hidden_dim = hidden_dim\n", + " self.length = length\n", + "\n", + " self.time_embedding = nn.Linear(1, time_dim)\n", + " self.token_embedding = nn.Embedding(vocab_size, hidden_dim)\n", + " self.input_proj = nn.Linear(hidden_dim + time_dim, hidden_dim)\n", + " self.output_head = nn.Linear(hidden_dim, vocab_size)\n", + " self.activation = Swish()\n", + "\n", + " def sample_shape(self, batch_size: int) -> torch.Size:\n", + " return torch.Size((batch_size, self.length))\n", + "\n", + " def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor:\n", + " return torch.randint(low=0, high=self.input_dim, size=shape, device=device)\n", + "\n", + " def forward(self, x: Tensor, t: Tensor) -> Tensor:\n", + " \"\"\"\n", + " x: (B, length) integer token IDs\n", + " t: (B,) time scalar\n", + " Returns logits of shape (B, length, vocab_size or input_dim)\n", + " \"\"\"\n", + " if t.ndim == 0:\n", + " t = t.unsqueeze(0).expand(x.shape[0])\n", + "\n", + " # Token embedding\n", + " x_emb = self.token_embedding(x) # (B, length, hidden_dim)\n", + "\n", + " # Time embedding\n", + " t_emb = self.time_embedding(t.unsqueeze(-1).float()) # (B, time_dim)\n", + " t_emb = t_emb.unsqueeze(1).expand(-1, self.length, -1) # (B, length, time_dim)\n", + "\n", + " # Concatenate and project\n", + " h = torch.cat([x_emb, t_emb], dim=-1) # (B, length, hidden_dim+time_dim)\n", + " h = self.input_proj(h) # (B, length, hidden_dim)\n", + "\n", + " # Transformer expects (seq_len, batch, hidden_dim)\n", + " h = h.permute(1, 0, 2) # (length, B, hidden_dim)\n", + " h = self.shared(h) # (length, B, hidden_dim)\n", + " h = h.permute(1, 0, 2) # (B, length, hidden_dim)\n", + "\n", + " # Output logits\n", + " h = self.activation(h)\n", + " logits = self.output_head(h) # (B, length, vocab_size or input_dim)\n", + " return logits" + ] + }, + { + "cell_type": "markdown", + "id": "e1faf8fd", + "metadata": {}, + "source": [ + "# Continuous modality dataset and model" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a3517fbc", + "metadata": {}, + "outputs": [], + "source": [ + "def inf_train_gen_continuous(batch_size: int = 200, device: str = \"cpu\") -> Tensor:\n", + " \"\"\"\n", + " Generate a batch of 2-D continuous points from a checkerboard-like distribution.\n", + " Returns a tensor of shape (batch, 2).\n", + " \"\"\"\n", + " x1 = torch.rand(batch_size, device=device) * 4 - 2\n", + " x2_ = (\n", + " torch.rand(batch_size, device=device)\n", + " - torch.randint(high=2, size=(batch_size,), device=device) * 2\n", + " )\n", + " x2 = x2_ + (torch.floor(x1) % 2)\n", + " data = torch.stack([x1, x2], dim=1) / 0.45\n", + " return data.float()\n", + "\n", + "\n", + "class ContinuousTransformerModel(nn.Module):\n", + " \"\"\"\n", + " Model for the continuous modality with separate input and output heads,\n", + " sharing a common Transformer trunk.\n", + " \"\"\"\n", + " def __init__(\n", + " self,\n", + " shared_transformer: SharedTransformer,\n", + " input_dim: int = 2,\n", + " time_dim: int = 1,\n", + " hidden_dim: int = 128,\n", + " ):\n", + " super().__init__()\n", + " self.shared = shared_transformer\n", + " self.input_dim = input_dim\n", + " self.time_dim = time_dim\n", + " self.hidden_dim = hidden_dim\n", + "\n", + " self.time_embedding = nn.Linear(1, time_dim)\n", + " self.position_proj = nn.Linear(input_dim, hidden_dim)\n", + " self.input_proj = nn.Linear(hidden_dim + time_dim, hidden_dim)\n", + " self.output_head = nn.Linear(hidden_dim, input_dim)\n", + " self.activation = Swish()\n", + "\n", + " def sample_shape(self, batch_size: int) -> torch.Size:\n", + " return torch.Size((batch_size, self.input_dim))\n", + "\n", + " def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor:\n", + " return torch.randn(shape, device=device)\n", + "\n", + " def forward(self, x: Tensor, t: Tensor) -> Tensor:\n", + " \"\"\"\n", + " x: (B, input_dim) positions\n", + " t: (B,) time scalar\n", + " Returns velocity vectors of shape (B, input_dim)\n", + " \"\"\"\n", + " if t.ndim == 0:\n", + " t = t.unsqueeze(0).expand(x.shape[0])\n", + "\n", + " # Position projection\n", + " x_emb = self.position_proj(x) # (B, hidden_dim)\n", + "\n", + " # Time embedding\n", + " t_emb = self.time_embedding(t.unsqueeze(-1).float()) # (B, time_dim)\n", + "\n", + " # Concatenate and project\n", + " h = torch.cat([x_emb, t_emb], dim=-1) # (B, hidden_dim+time_dim)\n", + " h = self.input_proj(h) # (B, hidden_dim)\n", + "\n", + " # Transformer expects (seq_len, batch, hidden_dim) with seq_len=1\n", + " h = h.unsqueeze(0) # (1, B, hidden_dim)\n", + " h = self.shared(h) # (1, B, hidden_dim)\n", + " h = h.squeeze(0) # (B, hidden_dim)\n", + "\n", + " # Output velocity\n", + " h = self.activation(h)\n", + " velocity = self.output_head(h) # (B, input_dim)\n", + " return velocity" + ] + }, + { + "cell_type": "markdown", + "id": "d5378557", + "metadata": {}, + "source": [ + "## Build multimodal model" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9b0e8daa", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Discrete side -------------------------------------------------\n", + "vocab_size = 128\n", + "added_token = 0 # uniform source distribution → no extra token\n", + "vocab_size += added_token\n", + "length = 2 # 2 tokens per sample\n", + "\n", + "# Shared transformer trunk\n", + "shared_transformer = SharedTransformer(hidden_dim=128, nhead=4, num_layers=2).to(device)\n", + "\n", + "discrete_model = DiscreteTransformerModel(\n", + " shared_transformer=shared_transformer,\n", + " vocab_size=vocab_size,\n", + " time_dim=1,\n", + " hidden_dim=128,\n", + " length=length,\n", + ").to(device)\n", + "discrete_path = MixtureDiscreteProbPath(scheduler=PolynomialConvexScheduler(n=2.0))\n", + "\n", + "# ---- Continuous side -----------------------------------------------\n", + "continuous_model = ContinuousTransformerModel(\n", + " shared_transformer=shared_transformer,\n", + " input_dim=length,\n", + " time_dim=1,\n", + " hidden_dim=128,\n", + ").to(device)\n", + "continuous_path = AffineProbPath(scheduler=CondOTScheduler())\n", + "\n", + "# ---- Assemble modalities dict ---------------------------------------\n", + "modalities = {\n", + " \"discrete\": {\n", + " \"model\": discrete_model,\n", + " \"path\": discrete_path,\n", + " # loss omitted → Flow will use MixturePathGeneralizedKL automatically\n", + " },\n", + " \"continuous\": {\n", + " \"model\": continuous_model,\n", + " \"path\": continuous_path,\n", + " # loss omitted → Flow will use MSE loss automatically\n", + " },\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "b82a25cc", + "metadata": {}, + "source": [ + "## Instantiate the multimodal Flow model" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9f2ccedd", + "metadata": {}, + "outputs": [], + "source": [ + "flow = Flow(modalities=modalities)\n", + "\n", + "# Optimizer (optimises both modality models)\n", + "optimizer = torch.optim.Adam(flow.parameters(), lr=1e-3)" + ] + }, + { + "cell_type": "markdown", + "id": "2636f3a4", + "metadata": {}, + "source": [ + "## Training loop" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "646de9a8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "| iter 3000 | 9.83 ms/step | loss 9.204 \n", + "| iter 6000 | 10.00 ms/step | loss 9.772 \n", + "| iter 9000 | 10.02 ms/step | loss 9.799 \n", + "| iter 12000 | 10.02 ms/step | loss 8.503 \n" + ] + } + ], + "source": [ + "lr = 1e-3\n", + "batch_size = 1024 # adjust as needed to fit in memory\n", + "iterations = 12001\n", + "print_every = 3000\n", + "epsilon = 1e-3\n", + "\n", + "source_distribution = \"uniform\" # for the discrete modality\n", + "\n", + "start_time = time.time()\n", + "for i in range(iterations):\n", + " optimizer.zero_grad()\n", + "\n", + " # ---- Discrete data -------------------------------------------------\n", + " x1_disc = inf_train_gen_discrete(\n", + " n_grid_points=vocab_size - added_token,\n", + " batch_size=batch_size,\n", + " device=device,\n", + " )\n", + " if source_distribution == \"uniform\":\n", + " x0_disc = torch.randint_like(x1_disc, high=vocab_size)\n", + " else: # mask case (not used here)\n", + " raise NotImplementedError\n", + "\n", + " # ---- Continuous data -----------------------------------------------\n", + " x1_cont = inf_train_gen_continuous(batch_size=batch_size, device=device)\n", + " x0_cont = torch.randn_like(x1_cont) # isotropic Gaussian prior\n", + "\n", + " # ---- Sample a common time tensor for both modalities ---------------\n", + " t = torch.rand(batch_size, device=device) * (1 - epsilon)\n", + "\n", + " # ---- Sample from each path to obtain x_t ---------------------------\n", + " disc_path_sample = discrete_path.sample(t=t, x_0=x0_disc, x_1=x1_disc)\n", + " cont_path_sample = continuous_path.sample(t=t, x_0=x0_cont, x_1=x1_cont)\n", + "\n", + " # ---- Build the inputs dict expected by Flow.training_loss -----------\n", + " inputs = {\n", + " \"discrete\": (x1_disc, disc_path_sample.x_t, None), # dx_t is None for discrete\n", + " \"continuous\": (x1_cont, cont_path_sample.x_t, cont_path_sample.dx_t),\n", + " }\n", + "\n", + " # ---- Compute total loss and back‑propagate -------------------------\n", + " loss = flow.training_loss(inputs=inputs, t=t)\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " # ---- Logging -------------------------------------------------------\n", + " if (i + 1) % print_every == 0:\n", + " elapsed = time.time() - start_time\n", + " print(\n", + " f\"| iter {i+1:6d} | {elapsed*1000/print_every:5.2f} ms/step | loss {loss.item():8.3f} \"\n", + " )\n", + " start_time = time.time()" + ] + }, + { + "cell_type": "markdown", + "id": "e87e944d", + "metadata": {}, + "source": [ + "## Sampling from the trained multimodal model" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e2aab2d8", + "metadata": {}, + "outputs": [], + "source": [ + "flow.eval() # switch to eval mode for sampling\n", + "samples = flow.sample(batch_size=2_000, device=device, steps=1000)" + ] + }, + { + "cell_type": "markdown", + "id": "2bceb4bb", + "metadata": {}, + "source": [ + "## Visualization" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "43dc2909", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkcAAAHqCAYAAAAOKepaAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAaf9JREFUeJzt3Qm4E9XZB/A3l8smO1q4XHaRsu+ogFZAUQREKGorHxUEl6qIIn6KWBEQEVdAFAE30ApFsQJCFYsgILJviqIoSgFls1V2L8u98z3/0y9xEjK5k1kyycz/5xO5mcyemeTkvOc9J6RpmiZEREREpGT99x8iIiIiAhaOiIiIiHRYOCIiIiLSYeGIiIiISIeFIyIiIiIdFo6IiIiIdFg4IiIiItJh4YiIiIhIh4UjIiIiIh0WjtLAyJEjJRQKeb0bgYNzjnOfrKVLl6pl8W/YjTfeKLVq1XJ4D/2jQ4cO6pFqR48elUqVKsmMGTMcXS/e/zvvvFPSAa67q666ytJ1S2d64IEH5MILL/R6N8hjLBw5bPr06eoDKPwoUaKE5ObmSufOnWXixIly5MgRySRbt25VBYh//etfXu9KRjl+/Lg6b/wi8tazzz4rZcqUkeuvvz4y7b333rNUKA6qxx57TObOnStBMXjwYPn000/l3Xff9XpXyEMsHLnkkUcekb/+9a8yefJkGTRoUOSma9KkiXz22WdR8z700EPyyy+/SLoWjkaNGsXCUSFeeukl2bZtW1ThCOeNhSPvnDp1ShWObr75ZilSpEhU4QjvTdBccskl6nMG/yYjaIWjnJwc6dGjhzz99NNe7wp5KNvLjftZly5dpHXr1pHnw4YNkyVLlqjq76uvvlq+/PJLKVmypHotOztbPVLh9OnTUlBQIMWKFUvJ9oKiaNGiXu8CxViwYIH8+OOP8oc//MHrXUkLWVlZqiY7k2Bc9Ly8vMhnZargmrnuuuvku+++k3PPPTel26b0wJqjFLr00ktl+PDhsnPnTnnjjTcStjlatGiRXHzxxVK+fHkpXbq01KtXTx588MGoefChgWV/+9vfqg+9KlWqSK9eveTbb79Vr6O2B+vFL6AJEyZInTp1pHjx4qo2CL766iu59tprpWLFimp5FOb0VckIEeIDAjp27BgJFeprQ95//3353e9+J6VKlVLhi27duskXX3xhOvy4YsUKueuuu+Q3v/mNOtY///nPcvLkSTl48KD07dtXKlSooB7333+/+qDUO3bsmNx7771SvXp1dVw4RzjW2PlOnDgh99xzj9oG9hGF0++///6MfcL7cscdd6j14MP47LPPVsdvptZM3+YI82NbgBqK8HnDezVt2jT196ZNm+L+QkcNxw8//GC4HYRlUQOJbeGY0Z7m8ssvl40bN0bm+fjjj9V+16hRQ82D84Pjj62dxD7j2tq1a5cqtOPvqlWryqRJk9TrW7ZsUdcs3tuaNWvKzJkz476Hy5cvV+8bzlfZsmXV+/bzzz8Xes7wvowYMULOO++8yH7ifcb0ZO+FeFDbgfOE615/zOHj04e/k72m4nn00UdVAeS5555L6v4Ivw9433v27Kn+xvXzv//7v5Kfny9m4V664IIL1L2ML/TXX3+90DZH33zzjVxzzTWqtgTLVatWTYUgDx06FDlHOCevvfZa5Fxhf8NwHeOHIN537Pdll10mq1evPmPfUFvevn17dV9hGzhX4XtBf3+F20998MEH6vMI80+dOlW9hvlxPeKax3vTsGFDVTMfK7wOHGd4HaixDx/3O++8o57jeFu1ahX3XuzUqZP6d968eabPP/kLa45S7IYbblAf7P/85z/llltuiTsPPjxxczdt2lSF5/BBsH37dvnkk08i8+BDE/MsXrxYfZjdfffd6osTXySff/551BcCPlRQkLr11lvVulAYwjYuuugi9WWIBoj48H7rrbfUh/Pf//53+f3vf6+q31FwQVsp7HODBg3U+sL/ImzYr18/1Z7qiSeeUKEkfFjhiwwfOGYaKCPkiA9mFCLwofriiy+qL8GVK1eqL3cUGBAGeeqpp6Rx48bqixfwZYVCzkcffSQ33XSTNG/eXH2g3nfffepLZvz48ZFtIKyCwuj//M//SLt27VQNHr6kYq1bt05tF+cTH+D40MbxoCExCpRnnXWWqfcYX2xY7vbbb1fnEQVWwPtZu3ZtGThwoGog3KJFi6jlMA3bwnti5LbbbpO3335bNQbGl8N//vMf9aWImsiWLVuqeWbPnq3eC2wfBZa1a9eqL2wUCPGaHq4jfLnhvX7yySfVPmDduB7+8pe/SJ8+fdT+T5kyRZ37tm3bqmPQw/x4z1D4Q2gRx46CZvjLOB7UXuL9w77jusQ1hcIY3revv/46EsYxcy8YwXsZPidhKMTt2bNH3Se4fvWSuaZiITSOaxVf5OH7Opn7A+8D5kNDYBTGPvzwQ3nmmWfUfYz3sTA4J/ihg/3GNl999VVViMGXf6NGjeIugx8h2CYKo+H7EMeJGjf8OClXrpw6Btw/KHThfYLwZwveGxT8UDBCoRa1pzh+XMPLli2LNGrGOsM/rlCDjmvr5ZdfVu9lPLiGevfurd4rnEsUUAHnDseC9wg17fPnz1c/ZnAt4Z6KPR+437GOP/3pT+qcdu/eXV3H+CzDcjB27FhVS4RtomAbhmPHceI6ww8LCiCNHDVt2jT8xNTWrVtnOE+5cuW0Fi1aRJ6PGDFCLRM2fvx49fzHH380XMerr76q5hk3btwZrxUUFKh/d+zYoeYpW7asduDAgah5LrvsMq1JkyZaXl5e1HLt2rXT6tatG5k2e/ZstY6PPvooavkjR45o5cuX12655Zao6fv27VPHFzvd6Dx17tw5sr/Qtm1bLRQKabfddltk2unTp7Vq1app7du3j0ybO3euWv7RRx+NWu+1116rlt++fbt6vnnzZjXfHXfcETXf//zP/6jpOPdhx48fP2M/V61apeZ7/fXXI9NwLmLPSb9+/bSaNWtGnuO9i11/WO/evbXc3FwtPz8/Mm3jxo1qfpyXRHBuBw4cmHCeeMcxduxYdV527twZtc/Y5mOPPRaZ9vPPP2slS5ZU886aNSsy/auvvjrjeMLvYatWrbSTJ09Gpj/55JNq+rx58yLT8N7p37+//vWvWlZWlvbxxx9H7eeUKVPUsp988onpeyGeU6dOqWO49957z3gN5y/eR5/ZawowX/h9wDZwLNOnT7d0f4Tfh0ceeSRqXnxG4NwWBtcdll++fHlkGu734sWLRx1/7HW7adMm9Rz3eCKlSpVS+xirZ8+eWrFixbRvv/02Mm3Pnj1amTJltEsuuSQybdCgQer8YXth//nPf7SKFSuq7eNzKvZYFi5caOq6xufHueeeG/d8rFy5MjLtgw8+UNNwbevvgalTp8b9fIMrrrhCa9CgQcJzQ/7FsJoHUP2cKGsNv8LDVbr4VRQPanfOOeecSGNvvdhf66g2D4d54KefflK1J/jFhP3497//rR6ohcAvSVS1JwrtAH5549clfuGFl8cDYSH8YsSvbzPwS1e/v1gW3z2YHoZ1onoc8f8w1CZhOmq29BASwfIIZ4Tng9j5EJqKpW/XgMa8OB8I+eD90Iet7EINDGov9OcINTbYPt6rRLAva9asUcsb0R8HQiJ4X1BjhvMSL4SAmgH9+vFLHb/u9W11MA2v6d+DMNQo6NtcoaYDv+zD5z4e1GChtqh+/fpR1w/CJhA+N2buhXhwjeN4EZI1y+w1FYZpqDVDo2/UTKLGxs79gVpBPdTKxDvf8aAWEfOH4X7He5ZoedSOAGrHUKuVDNR0ofYbNc36NjkI7aPGBjWChw8fVtMWLlyoahxRExeG2mvUSsaDmkl8DiW6rhH2w/lEqA7HGA4D6s8HthkWrsXC9YUa6djp8c4Trh1sg4KJhSMPoO8VtD8w8sc//lGFvPClVblyZRXmQchL/+WAdkX48DPTkDs2DIIqZ3ywo/0TPkT1D7QBgQMHDiRcJwpQ4Q+b2HXgQ7Ow5cP0H1T6D2y0+Yidrm/HgrANukiIPY/hkB9eD/+L6nJ9mBHCVfV6aJPz8MMPR9qboPCJ48GXXOyHrx1oI4QvkXDfO3hf//a3v6kMmUTXBSD0hbAp9hGhDoSyYj/Y0YYIIRV8AYXbr+BLBGKPA+0u9AXn8LlGWDG2kB37HoTVrVs36jm2ieNL1FYL1w/CMrHXDtrPQfj6MXMvJGKmrVCy11QY2vSg/RJCligExR5fMvdHvPcBX85m2m7Fu4/MLI/PhSFDhqgQF651FEhwPGaudTR0R4Eq3n2E84X3Z/fu3ZHzhh8ZseJNC+9XPAhxoS0QCu4oNON8hduexe5zMp8rEO884dph/3PBxTZHKYZ2H7iRjT4Ywr+Q0MgVvy7/8Y9/qF9eb775pvqgxQerPi3ZjNhMj/AXCxp8xvuFBon2T78OtElAW4VYZrPvjI4l3vRkvuisQC0c2mehVgm/OvHBiQ9HfCEnU2tRGBwbfl0j/f+FF15QH/qoCULbiMKgNgc1BHPmzFHXAtpioT0LGpmi7RB+0aPwhZqToUOHqpoZfJmgJhAFptjjSOb8O/keYD/QKHbcuHFxXw9/iVm9F1AwxHtntnBhBQptmzdvlueff169L9im1fsj2XvaqfcL7ZpwXaBmDucTtWZoh4P2fyggeyFeZhp+DKKxN65nXDO4PpBxi9o+tAVz47rGtYNCIwUTC0cpFm4EalQoCUNtBz4M8MCHARp7ooEsviTw6wk1IQivIPyTbBp5uBocy4WzMowY/XIK18Qgc6SwdbgB2VNotIqwoP6XPjLwwq+H/8UHZ7imLUzfJ1EYGjojNIIvjDA0ZEfNUbIK+8WJ0Bq2g0alCNfgV3Bh10QYamXQoBQP1ECg0fGYMWNU4QiNmtGgGdlF4cbr4TCPW1BLgga3+prRvXv3SteuXQ2XwfWDjvZwfRd2rgq7F+JB4QPb2LFjxxmvGW3P7DWl/wGBmjw0QL7yyitVckR4Oa/vj2SgkIoHGpWjETsKfWi4jIwyo/OF6xUJCvHuI5wvvGfhAi7OG2qrY8WbZgT3CRqOI5tWXytkNnxvBa6dZs2aubZ+Sm8Mq6UQ2vmMHj1aVRsbxdsBv/pjheP14TRntE1BPBy/WpP9tYgPbHygI7MEX2LxqszDUOsAsQUEfJEjSwVfVCigJVqHG/DFi1qS2OPHr0h8mKOgAOF/kXGnh64N4v2qjD13CJkkk04dFs5sMypYIfsKD4Q00H4MtVOF1bZhP2LDB3gvEQoKXxfhX8b648DfaBfjFmQY6q8BZBWhP63wuY8HNS2ozULtWbzwJtpKmb0XjKD2b/369WdMN7qmzV5TengPUXuBbEFkQ4W7S/D6/jADbYLwPumhkISCjf7c4nzFnitcZ1dccYWqcdKHT/fv36+6fEBGHo4/fC5WrVqlatnC8L4mM6RLvOsa9wJqet2AdeMHFdrqUTCx5sglqA3ALyh8+OADAwUj/HrHryj8+knUGRtSlhFKQLo55kftAMIvqObGhw6gVgBtHtBmAKnaCLXgCwW/fFGjgPYriaBtAdaFD0Oky6I2CfuJDzGE/vCrPvxFhA8mhG7wgYG2OOG+RvAliK4JUHOBL3f8mkR7F4Q/8OszXsHNKfgiQm0FahDw4YxfeAgL4MMaYbHwL3fsP9qD4Pxh//Fhh1/48X61ImUcNXsIp6FBJ84FzifS4a2EBrAOhIDQjgYhF3RFgEcY3kOENsFMSA01GrgGkLKN40XbHuwfuiAI13Yh7IBjx3pR+MAXFApfboaXkBKOWp1wSjTONa4tpFwbwXWDtkNohIxf/7heUDDBPYPp4X5uzNwLRnAP4P1ETVq4LRMgvR0QQsIXN65vXL9mr6lYbdq0UfOgcIX3Bt0Q4Lx7eX+Ygc8kNChHn1g4P/iswvnC+dAnBuB84TpDrR0K4vhxh4bMqFkK90GFzxwU7vGDCwUr1KiFIc0fDdYR7kXoOpzKjxogFJLMtOtBQQxhNLxHSM9H7SQK1vgcivcDzy4cLwpihX2Oko95nS7nN+H05vADqa45OTna5Zdfrj377LPa4cOHz1gmNpV/8eLFWo8ePVS6N5bHv0j//vrrr89Ibf3LX/6i1a5dWytatKjaDtKOw6m14VT+p556Ku6+Yr6+ffuq5bB81apVtauuukp7++23o+Z76aWXVLpskSJFzkh7xd9Ip0V6cokSJbQ6depoN954o7Z+/XpLXR6Ez0Vs6jZSiZFSrId06XvuuUedH+w/uiDAseq7BoBffvlFu+uuu7Szzz5braN79+7a7t27z0hNRxp7//79tXPOOUcrXbq0Oi6ksCM1WJ/KbCaVH5BKjFRsvIfx0vr37t2rzulvf/tbzYwTJ05o9913n9asWTOVLo1jwd8vvPBC1Hxbt27VOnXqpI4Bx4K08U8//fSMrgLinVNAyn2jRo3OmI7j69at2xnv4bJly7Rbb71Vq1Chgtpmnz59VKp27Dr1qfyA9P8nnnhCbQtp51ge52vUqFHaoUOHkroXjM4Xjn/06NFR09E1BNLLf/Ob36gUc/29Z/aa0qfyh6HrguzsbO2Pf/xjpJsGM/eH0fsQ+7lgJPZ9MTrnsdftd999pw0YMEDtE/YNqfUdO3bUPvzww6j14B5Aaj7S4LG8/l5AFxQ4PrzvZ511llpen0IfhjT+3/3ud+p9Rrcc6Fpi4sSJan3o3qCwY4F3331Xa9q0qdrXWrVqqWsn3KVJbHcA8dYR7z0z+ozEe3jxxRfH3Q8KhhD+53UBjSiIEBZF+yFkyCFzMNOgh+z+/furmiv9UDnpBGFshF7QLspuo2dyFmrjUNOEWqB0em/27dunasdmzZrFmqMAY5sjIg8LFwglIfRC7kDvxvjyxRcdeSd26Br0IYYQHkJy6VQwCrdHRHMDFoyCjW2OiDxo64HhSJBhhk70zAyzQtagXZbZPrfIPWgcjyQQ9IGEto2vvPKKahCejjWmjz/+uNe7QGmAhSOiFEMj43DKtH6QUiK/QmN1dJWBzEY0wEYjdRSQMKYfUTpimyMiIiJyrSZu2LBhanD0eF2o6IcUQk0iMkXR6z4ypBP1leY2tjkiIiIix61bt041ukd/YImgJh1drmBMTYz/iOYGeGCoJK+w5oiIiIgcdfToURU+Rb9k6BMLfc4Z1RxhDEX007dgwYKo/sOwDHpr9wLbHP3/OEgY2wpd/3OgQSIicgPqItCZKzrTRE/kbsGwR+ic1SlanEF40SEwHkYGDhyoOm/F8DnhoWiMoMNddGishw5a0aGqV1g4ElEFo9jRmomIiNywe/du1wb2RcGods3Ssu9A8sMeJcr6PHr0aNS0ESNGyMiRI+POj64zNm7cqMJqZvuWqly5ctQ0PMd0r7BwJBIZLPJi6SrZktwgrpS5fun+a8eFJef/OgbXjifOj/xde6i5m5soaPeJ2WX0Ypc3WreVbVrZL6N1u7X903JKVsh7UQMbOw01RigY7dxQS8qWsV87dfhIgdRs9S9VoAuPlwdGtUaYD42vMbRMomGy0h0LR7pRp1Ewyg6xcBQU2UV/vXH173uW7obm9UBBZ3SfmF0manrM8kbrtrJNK/tltG63tq8GldJ957ipdJmQethVIP9dBwpG+sKRkQ0bNqi+xdDeKAyd3WKMRIwniLH3Yjv+zMnJUf1f6eE5pnuFhSNKa7/8/sKo5yXnrHF9+dxlmqnl9a8lu1/JLK+fb0/7Xz/s6gxe7em+WNkOpYbR+/TthDZxr/PY+cxMN7vNRMxs08o+J7s9q/PFfj4Utv8FeXkiQ+dJKuRrBZKvObOeZGAQ6i1btkRNwzBDGBR76NChcXtERyehGBAcQ8qEoeYJ073CwhERERE5okyZMtK4ceOoaaVKlZKzzz47Mr1v375StWpVGTt2rHqOMFz79u3lmWeeUY240WZp/fr1qtNQr7CfIyIiIp8pEM2xh9N27dole/fujTxv166dzJw5UxWGmjVrpnpTR6ZabCErlVhzRERERK5ZunRpwudw3XXXqUe6YCeQaI1/+LCUK1dOOkgPNsDNIPp4//JJv1a/XjLwVsO2A5nQZsaoHYMT+2x0/JlwXihzmLnOEr3m1nXutdPaKVkq8+TQoUOmGjfb+T7bs62aY9lqufW+d3Wf0xFrjoiIiHwmX9PUw4n1BBHbHBERERHpsOaIfKFzbrPI3yXFuBo9FVXsVroPSBRKS3abibZnN12bMo8+ldyo+wcr14B+vbFp9lbCZU5eg1a2n66hOKucakxd4EKD7EzAwhEREZHPoFCTz8KRZQyrEREREemw5ojSWqKqfyczWoy2YYWV5c1m9FjZpplwgd9CCiRxQ2luSra3ayf2zShkaDaUaLSffsCwmj2sOSIiIiLSYc0RERGRzzCV3x52AslOINNa7uroTse+faKBK1XiRtXw6cytUJjdLCbKTMleT15cJ06H5Zy8t8wM9ouBZ3cOfSglnUB+9WVlKeNAJ5BHjhRI/Qb7A9cJJMNqREREROlSOFq+fLl0795dcnNzJRQKqYHmwk6dOiVDhw6VJk2aqBF9MQ9G8t2zZ0/UOn766Sfp06ePKtGWL19ebrrpJjl69KgHR0NERJQekMbv1COIPG1zdOzYMTUC74ABA6RXr15Rrx0/flw2btwow4cPV/P8/PPPcvfdd8vVV18t69evj8yHghFG9120aJEqUPXv319uvfVWNcIvZb6PVzeMel5nzmpXxnLyOpSWqBrf6DW3whix683EkCMlL9nrycr1ZzcUF3v92Q0tGy2/p30o8neu2OvEUr/PGFttp6RGvvbfhxPrCSJPC0ddunRRj3gQM0WBR+/555+XCy64QHbt2iU1atSQL7/8UhYuXCjr1q2T1q1bq3mee+456dq1qzz99NOqtomIiIjIt22O0CAM4TeEz2DVqlXq73DBCDp16iRZWVmyZg0bkRIRUTAVOPgIooxJ5c/Ly1NtkHr37h1pMb9v3z6pVKlS1HzZ2dlSsWJF9ZqREydOqIe+dT+lJ7NhHH01eJ055tadqiwsMyG/RPuS7LE5nUXEUFpmsxtyNpquvy4TXSdudjDqVoetRuPEZVLnqQUSknwJObKeIMqImiO0JfrDH/4g6HVg8uTJttc3duxYFbYLP6pXr+7IfhIREVHmy8qUgtHOnTtVGyR9Pws5OTly4MCBqPlPnz6tMtjwmpFhw4apEF34sXv3blePgYiIKJUKNOceQZSdCQWjb775Rj766CM5++yzo15v27atHDx4UDZs2CCtWrVS05YsWSIFBQVy4YXGY+gUL15cPYiIiIjSqods9Ee0fft29XeLFi1k3Lhx0rFjR9VmqEqVKnLttdeqdP4FCxZI5cqVI8vh9WLFiqm/ke22f/9+mTJlSiSVHw20k0nlZw/Z6StVPeJaSVdP5/YGRG709uzmNq0sn2n3IFL5l8q8lPSQveaLHCntQA/ZR48UyIWN9gWuh2xPa47QXxEKQ2FDhgxR//br109Gjhwp7777rnrevHnzqOVQi9ShQwf194wZM+TOO++Uyy67TGWpXXPNNTJx4sSUHgcREVE6yXeoQXZ+QBtke1o4QgEnUcWVmUot1CKxw0ciIiIKRJsjIn1KrROMquHNpCHHLpMoLdpomWT3y+p8Tsq00AVFs/KeGS1jpSd3K9ePme3HzmcUGrdyb/rhmi/QQurhxHqCiIUjIiIin2FYzeep/ERERESpxJojSmuJqrT11ehWwm9Gvf3qq+TthgHMbt/JqnsrGX5mQw9+CDeQWL4ejAZkTSbMnOz2zQ62bBRKczKsaHY/00G+ZKmH/fUEE2uOiIiIiHRYc0REROQzmkMNsjU2yCZKf/pqbLODQxotn6rBVY22aTYjx8wyiY7FqOrfSseX6RY6oMKZfZ/NhIjsDupsdlBku/cGQ8FskG0Xw2pEREREOqw5IiIi8pl8LUs97K9HAomFI8ooZjJiElWjJ1utbjYMkGgZO9t3M0TgZiiR0jP8nEgqQk52t+Fm5pkV6RymK5CQFDgQHCqQYJaOGFYjIiIi0mHNERERkc+wQbY9LByR77hZ1W1m/Cgr+2M2o8itsILZTvvSOYxA7rxPdq8Nu+OxJWLUeauTYXKzy6dbVpxzbY40CSKG1YiIiIh0WHNERETkM/9tkG0/JFYQ0LAaa46IiIiIdEKaFtCAos7hw4elXLly0kF6SHaoqNe7Qw6m0qdD7D8V+2alXYeZdZldhtKLF/eAW+3xzG4zalBck73nmzlPTp7L09opWSrz5NChQ1K2bFlx8/ts9qf15awyRWyv7/iRfLmu2Veu7nM6Ys0RERGRz4QbZDvxSMbkyZOladOmqiCFR9u2beX99983nH/69OkSCoWiHiVKlBCvsc0REREROaJatWry+OOPS926dQWBqddee0169OghmzZtkkaNGsVdBoWobdu2RZ6jgOQ1Fo4orXndI26iEJNR+r1+euxrZlhZPtExJ3s+GEbLfFbew2QHSI4VFdYScyE2K4MfG+2bmUFxrYSMrQxqnQ73EHrH9qKH7O7du0c9HzNmjKpNWr16tWHhCIWhnJwcSScMqxEREflMvhZy7GFVfn6+zJo1S44dO6bCa0aOHj0qNWvWlOrVq6tapi+++EK8xpojIiIiKrSht17x4sXVI54tW7aowlBeXp6ULl1a5syZIw0bNow7b7169eTVV19V7ZTQ6Pvpp5+Wdu3aqQISQnReYeGIfMfJ6m19qCC26t5oQE+z4QGjTBs3B4Q1k1GUDiEBsicV11ZsiMrKulM9+LGbIbJ0u2/yJUs97K9HU/+iVkdvxIgRMnLkSMMCz+bNm1Vh5+2335Z+/frJsmXL4haQUIjS1yqhYNSgQQOZOnWqjB49WrzCwhEREZHPFGhZ6mF/PZr6d/fu3VGp/Ea1RlCsWDE577zz1N+tWrWSdevWybPPPqsKPIUpWrSotGjRQrZv3y5eYpsjIiIiSqjs/6fmhx+JCkexCgoK5MSJE6bbKSEsV6VKFfESa47Id5zsWM4odObEdpLNtHGz6j/dQgIkrl9bdjv7tNK5ot0OGdONmQFyvToWp8NqZg0bNky6dOkiNWrUkCNHjsjMmTNl6dKl8sEHH6jX+/btK1WrVpWxY8eq54888oi0adNG1TQdPHhQnnrqKdm5c6fcfPPN4iUWjoiIiMgRBw4cUAWgvXv3qp660dAaBaPLL79cvb5r1y7Jyvq10Pbzzz/LLbfcIvv27ZMKFSqoMNzKlSsNG3CnCgtHREREPlPw/+n8TqwnGa+88krC11GLpDd+/Hj1SDcsHFHGslsNb6XqP9n1xnJybDO3qus5tlowOP2+Wrkf3bqHnRybLREzmYD6ji4L8vJEhs6TzOoEMkuCKJhHTURERGSANUdEREQ+Y2XQWKP1BBELR5RR0qka3kjseo2W8fpYzK7L7vhXlD6shHzNjrnm9TiIXmSMmgml6ec5rZ2SnZIaBRJSDyfWE0TBLBISERERGWDNERERkc8wrGZPMI+aiIiIyABrjiijJJsK70S7CCeXd2sbdtsfGQ1CC2xnlHnMXA/pPKCqlXZOZo7Z6W4qjLaZDveMcz1kZ0kQsXBERETkMwVaSD2cWE8QBbNISERERGSANUcU2HBBqlLUU9HDdapCH0zrzwypuOYShajsXidO9rBtt+fuRJIN66USerZ2IiRWENA6FBaOiIiIfKZAy1IPJ9YTRME8aiIiIiIDrDmijOJkdbVRdX+i6nG3qs6trNfJKn2zx/y7Nlsjf+9JeiuUzpK9hhJdJ2YHbrW7X26FrzKxt+9Y+RJSDyfWE0QsHBEREfkMw2r2BPOoiYiIiAyw5ojSmt1O26wMtJloG2a2r8/UiQ0xuJVFlKrl97Q5bGs7lBpGoahUZVTtaR9KekDX3NVlI39/+0QDW/vJrEqEw5wJieVLMLHmiIiIiEiHNUdEREQ+wzZH9rBwRGnHbAdsRsvYDYvZ3WZsNb5+PrNjRrnFblYcZQYz4VunxxmLugfmJH/N6UO2JcXc2GiG209RKC3dOn7Uy9ey1MOJ9QRRMI+aiIiIyABrjoiIiHxGk5AUONAgW2M/R0Tpwcr4R0bZMU6O0RQ7nxVOjv9kJFG4xErmUjqHDig9s9XSdQzBRPeGm/dg2OlTeSLz50kqMKxmTzCPmoiIiMgAa46IiIh8pkALqYcT6wkiTwtHy5cvl6eeeko2bNgge/fulTlz5kjPnj0jr2uaJiNGjJCXXnpJDh48KBdddJFMnjxZ6tatG5nnp59+kkGDBsn8+fMlKytLrrnmGnn22WeldOnSHh0VOclsiMhsR3NGVedmq9cTZfsYsTJOlZl9toshMv9yq7NRpzthNJO9aeX6txL+dnI8NaPpp7VTkir5kqUeTqwniDw96mPHjkmzZs1k0qRJcV9/8sknZeLEiTJlyhRZs2aNlCpVSjp37ix5eXmRefr06SNffPGFLFq0SBYsWKAKXLfeemsKj4KIiIj8xNOaoy5duqhHPKg1mjBhgjz00EPSo0cPNe3111+XypUry9y5c+X666+XL7/8UhYuXCjr1q2T1q1bq3mee+456dq1qzz99NOSm5ub0uMhIiJKBwyr2ZO29WU7duyQffv2SadOnSLTypUrJxdeeKGsWrVKPce/5cuXjxSMAPMjvIaaJiMnTpyQw4cPRz2IiIiI0rpBNgpGgJoiPTwPv4Z/K1WqFPV6dna2VKxYMTJPPGPHjpVRo0a5st/kLLuDTuoHfXV6m6lKA/a6bZDX2yd7rAzCavd6Nnvf2W2P5+Qy+oFv/TDAcoFkqYcT6wmiQB71sGHD5NChQ5HH7t27vd4lIiIix+RrIcceQZS2haOcnBz17/79+6Om43n4Nfx74MCBqNdPnz6tMtjC88RTvHhxKVu2bNSDiIiIKK3DarVr11YFnMWLF0vz5s3VNLQNQlui22+/XT1v27atSvFHVwCtWrVS05YsWSIFBQWqbRJlPrMhJv18+ip9KynydnuO1ocxEoUyzHQrYIXTvYJTZnNyEFavrxk3Q852Q2len5tYbJCdwYWjo0ePyvbt26MaYW/evFm1GapRo4YMHjxYHn30UdWvEQpLw4cPVxlo4b6QGjRoIFdeeaXccsstKt3/1KlTcuedd6pMNmaqERFRUGlalhQ4MPSHFtDhQzwtHK1fv146duwYeT5kyBD1b79+/WT69Oly//33q76Q0G8RaoguvvhilbpfokSJyDIzZsxQBaLLLrss0gkk+kYiIiIisiKkoUOhgEO4Dt0EdJAekh0q6vXukIP0GShOZ6GYCct5MfAskRl2r1P9YM9mw3duhsXM9LZtdnkzx5/MfPqBZ9fMH64Sgdxq6xr+Prtp2R+kWGn732cnj56SV9q/5eo+p6Ng1pcRERERZVqDbCIiIrKmQHOmMXVBQGNLLByRLzq0M6rG3tMm+Wp7syEuK51Fmpkn3TqBJH+y27liosGezWZiOhlOtru8PkxodGx27/lUDjxb4FCD7IKANsgO5lETERERGWDhiIiIyGcKJOTYIxmTJ0+Wpk2bRjpYRn+E77//fsJlZs+eLfXr11eZ6E2aNJH33ntPvMawGqW12BCT0ZhNToaenA5jJZsFY2X7zHYju+OsJdspqpVsNzevZzPLJMpeNTMeXCaFvJ0a+iM/yXVUq1ZNHn/8cdU/IZLhX3vtNenRo4ds2rRJGjVqdMb8K1eulN69e6sxT6+66iqZOXOm6stw48aN0rhxY/EKa46IiIjIEd27d5euXbuqwtFvf/tbGTNmjJQuXVpWr47f1cOzzz6rOnO+7777VMfOo0ePlpYtW8rzzz8vXmLhiIiIyGfCDbKdeIT7T9I/Tpw4IYXJz8+XWbNmqc6cEV6LZ9WqVdKpU6eoaZ07d1bTvcSwmo1qaLeqh81uMxNDKcnuc6Ycl5NZQKnaZqpY6Vwv2WNLFO5wK0TjNDPbjB23T8/M51OieYy2mez0ZDi5bjPLJOoE1m+fR6q9kBOp/PLfdVSvXj1q+ogRI2TkyJFxl9myZYsqDOXl5alaozlz5kjDhg3jzrtv3z6pXLly1DQ8x3QvsXBERERECe3evTuqh+zixYsbzluvXj01Tip61X777bfVkGDLli0zLCClIxaOiIiIfEazkGlmtB4IZ5+ZUaxYMTnvvPPU361atZJ169aptkVTp049Y96cnBzZv39/1DQ8x3Qvsc0RERERuaagoMCwjRLCb4sXL46atmjRIsM2SqnCmqNCYv9W2hnZ7fnYTEppouWtSDaNt7DXnGo/kkkxfnKuLUeiNkjJbsNu+xUnByQ1u00j+s8G3huUCNobOTN8SCip+YcNGyZdunSRGjVqyJEjR1Rq/tKlS+WDDz5Qr/ft21eqVq2qUvfh7rvvlvbt28szzzwj3bp1Uw24169fLy+++KJ4iYUjIiIin/Fq+JADBw6oAtDevXulXLlyqkNIFIwuv/xy9fquXbskK+vXdbZr104VoB566CF58MEHVRcAc+fO9bSPI2DhiIiIiBzxyiuvJHwdtUixrrvuOvVIJywcWWQl9djJ0FNh++DUNq2EK8yGxYz2n+ECf7EbfrUS8vW6V3O39o33BqV7WM0vWDgiIiLyGSvjohmtJ4iYrUZERESkw5ojl3tKtRJiMju4o9H+6Jexu7zdEF+yWUeUOcxeJ072/m32OktF+IkDBFM6Y1jNHhaOiIiIfIaFI3sYViMiIiLSYc2Rzi/dW0t20RKuZZfEDhqp72AynTJazIb19POlKouI0oeV91Z/D1jp0DAVHZ+mcjtEbmHNkT2sOSIiIiLSYc0RERGRz7DmyB4WjnRKzl8v2aGirnVOaGWcNiudKLq1/06HPvQhFrtj2FHqJXr/jEJJdt9nK9e20b4kWkZ/bL9rszXy9542h03t5572objH7EXHkRRMmkN9FGkSTAyrEREREemw5oiIiMhnGFazh4WjOKx0wmjEzapytzLEUlW9r89WosyTKERm5rpxc3xCo2XMdraqP7Y9JraRaHmjfUnmtXgSZb8SsXBkD8NqRERERDqsOSIiIvIZ1hzZw5ojIiIiIh3WHBXCzfT3ZNv2JBpc04vUXydT8ZnGnHmstBlya/tWurzQszvAstMDUZtZnm2MKBHWHNnDwhEREZHPaFpIPZxYTxAxrEZERESkw5qjQgaetRs6slL1bzdF327VvdEyZtOV7W6HMo/T12ay94PdkHOi5VMRMje7DO8ZMgu9YzvRQ3aBA+vIRCwcERER+QzbHNnDsBoRERGRDmuO4gw860XoyMleuTMFM9Qym5MhW7vrTjSIq5n1Or1fboWzzYYSidgg2x4WjoiIiHyGYTV7GFYjIiIi0mHNUZxsNbtV1WYz3NwaONbJ6vVUDZybqm1S6pl5P82GiNwMaxlx8nrc0/7XX+F15iS/naCE3Mk+htXsYc0RERERkQ5rjoiIiHwGNT5OtBfSAlpzxMJRIdlq+mrsqCrxmHCZfr7cZVpaVumnKrsld3XZyN972hxOenmG0jKP3XHWvH7P3Ry30Ox4aOz4kZyEbyHNga8iTYKJYTUiIiIiHdYcERER+QyG/cB/TqwniFg4KoS+SlufXWIljOD0+E3p1GmfnpVQGvmLmevGbFan3fENzazLzXELrXweMGOT7GK2mj0MqxERERHpsOaIiIjIZ5CpFmIP2Zax5oiIiIhIhzVHFgd6dLr9jtftCqwcG9tFULoyahNo1GbJ6R66zdwbZj8PeJ+RFUjjdySVX5NAYuGIiIjIZ9gg28dhtfz8fBk+fLjUrl1bSpYsKXXq1JHRo0eLpivK4u+HH35YqlSpoubp1KmTfPPNN57uNxEREWWutK45euKJJ2Ty5Mny2muvSaNGjWT9+vXSv39/KVeunNx1111qnieffFImTpyo5kEhCoWpzp07y9atW6VEiehBZM0yUw2eqt6m7XJzENw6Q7+M/L0nZhBNCjajsFZU1xi66zHR/WQ0n5OD0Fq5fxPti5PrS9fPFkpvrDnycc3RypUrpUePHtKtWzepVauWXHvttXLFFVfI2rVrI7VGEyZMkIceekjN17RpU3n99ddlz549MnfuXK93n4iIyBPIMnPqkYyxY8fK+eefL2XKlJFKlSpJz549Zdu2bQmXmT59uoRCoaiH1cqNQBSO2rVrJ4sXL5avv/5aPf/0009lxYoV0qVLF/V8x44dsm/fPhVKC0Ot0oUXXiirVq3ybL+JiIiCaNmyZTJw4EBZvXq1LFq0SE6dOqUqNY4dO5ZwubJly8revXsjj507d4qX0jqs9sADD8jhw4elfv36UqRIEdUGacyYMdKnTx/1OgpGULly5ajl8Dz8WjwnTpxQjzBsw241uhcZJWa2abdHYbs9YWdK+JGS5+Q172SILNF8ZjJRYyUacNrM8k4eGzPXKN2z1RYuXHhGrRBqkDZs2CCXXHKJ4XKoLcrJyZF0kdY1R2+99ZbMmDFDZs6cKRs3blTtip5++mn1rx2o9kMNU/hRvXp1x/aZiIgoPQpHIQceYsuhQ4fUvxUrVkw439GjR6VmzZrq+xjNZL744gvxUloXju677z5Ve3T99ddLkyZN5IYbbpB77rlHFW4gXMrcv39/1HJ4nqgEOmzYMPWGhR+7d+92+UiIiIgy1+HDh6Me+uiLkYKCAhk8eLBcdNFF0rhxY8P56tWrJ6+++qrMmzdP3njjDbUcmtV8//334pW0DqsdP35csrKiy28Ir+HEAbLTUAhCu6TmzZuraXjT1qxZI7fffrvheosXL64emdCJo9MZMU5u325Hd5TZ0jl7M9nMr4RZmQYDTidaJlGYLRUD5BI5na1WPSbCMmLECBk5cmTCZdH26PPPP1dthRNp27ateoShYNSgQQOZOnWq6r7HC2ldOOrevbtqY1SjRg2Vyr9p0yYZN26cDBgwIBKjRKn00Ucflbp160ZS+XNzc1ULeSIiIrJv9+7dqtF0WGEVDHfeeacsWLBAli9fLtWqVUtqW0WLFpUWLVrI9u3bxStpXTh67rnnVGHnjjvukAMHDqhCz5///GfV6WPY/fffr1rB33rrrXLw4EG5+OKLVYMwr9MAiYiIvIKmQk6M/KH9/78oGOkLR4bza5oMGjRI5syZI0uXLlWVFslC8tWWLVuka9eu4pWQpu9uOqAQikPD7A7SQ7JDRU0t42YYIdMzUjJ9/ynzx+1Ldnkr2WZOZKgZrc/K8mY7fCXvnNZOyVKZp9q6milo2Pk+O/f1B6XIWfYrCfKP58l3fR8zvc+ozEASFdoPoS1RGPYJo1hA3759pWrVqpH2w4888oi0adNGzjvvPFXJ8dRTT6m+CpHh1rBhQ/FCWtccERERUeaYPHmy+rdDhw5R06dNmyY33nij+nvXrl1R7Yl//vlnueWWW1QXPBUqVJBWrVqpTqC9KhgBC0dERER+43RczSQzwSiE2/TGjx+vHumEhSOLnA4XmalStxLKs5Jdk6oO/fRhAD2GBDKf3Wwtt0JhZuc3s/92Q4SJtmHl84D3DUVxKFtNOLYaEREREbHmiIiIyGe8Gj7EL1g4SpNsKzPbsbIv6TTOW+x8DAMEg1vXoFsZok6v224HqewQktKhE8igYViNiIiISIc1R0RERH6DGh82yLaMNUdEREREOqw5iiOdYvlutn/yIn3fye4HKLNT980OaqxnlApvN60+Vd1cWJG7LKAtYskWNsi2h4UjIiIiv/GoE0i/YFiNiIiISIc1R2nSw7WV6n47+2hl3Rxsl6yExaxc28l2bWH22rRyb5pZxolrNtnz5Ob9SJmPqfz2sHBERETkRwENiTmBYTUiIiIinZBmZghdnzt8+LCUK1dOOkgPyQ4VlSAyMzimE9X2Rr39kn8FMWQaxGOmwp3WTslSmSeHDh2SsmXLuvp9Vn3qCMkqWcL2+gp+yZPdfx7l6j6nI9YcEREREemwzREREZHfMJXfFhaOXK4StzIga6qyU4xCXEb7op8/tnO6RJlDegyl+ZOVbC+z60u249DYZby4n83ew0bLMCxH9iHLzIlMs5AEEcNqRERERDqsOSIiIvIbhtVsYeHIoeptu504JtvpnROSDXHFjvHkxT5TenJ6bLJkr5tEnVA6eQ2mIkSYaD6G28g0Fo5SG1b7/vvv5ejRo2dMP3XqlCxfvtze3hARERFlSuFo7969csEFF0jNmjWlfPny0rdv36hC0k8//SQdO3Z0az+JiIjILAz74dQjgEyH1R544AHJysqSNWvWyMGDB9VzFIb++c9/SoUKFdQ87E8ycyXKokk2dGG2up8hgmBwK6vS7DbNZF8mWq+Z6zQ2k9PJrEwz9yZRLHwdO/GVrAX0a910zdGHH34oEydOlNatW0unTp3kk08+kSpVqsill16qao0gFApmCZOIiIgCWDhC1+HhGiIoXry4vPPOO1KrVi1Vg3TgwAG39pGIiIisNMh24hFApgtH5557rnz22WdR07Kzs2X27NnqtauuusqN/SMiIiJKzzZHXbp0kRdffFGuueaauAUkTEcmWyb7pXtryS5awrVUfLNtB5xsf5OovYTdNj92ewFmj8D+lbv61wEq97Q5bGtdZtrvWOmhO7ZrikRtiCL7Mif+NhPt4572vzY3yBX3uzUgUpxqTK2lf3MZVNCsW7dOzj777KjpaB/dsmVL+e6779wrHI0ZM0aOHz8efyXZ2fL3v/9dfvjhh6R3gIiIiJwV0v77cGI96e5f//qX5OfnnzH9xIkTlsslpgtHKACVLVs24etI8yciIiJy27vvvhv5+4MPPpBy5cpFnqOwtHjxYtUu2gr2kK1Tcv56yQ4VdXSdZkNMVpY3sy6zA8IabdNKWr6Z/bK6HUofid4/o1Ca0TJ2B451uvsIozCZlW4FjAZotoL3DJkWgB6ye/bsGcmU79evX9RrRYsWVQWjZ555xtK6WTgiIiLymwC0OSooKFD/1q5dW7U5OueccxxbNwtHRERElLF27Njh+DpZOCokWy3qdZuD0CbctonephP1wmsl9GAUCqsz9MvI33t02TlOhBgS7U+8/WLoIPPZDYt5EYoyWsbJ69zsNoksCUBYTQ/ti/BAn4vhGqWwV199VVJSOEJ63Nq1a+PuBMZcIyIiIg8FqHA0atQoeeSRR9QIHhi5w4nROpIuHM2fP1/69OmjBp1F9pp+J/A3C0dERESUKlOmTJHp06fLDTfc4Ng6ky4c3XvvvTJgwAB57LHH5KyzzpIgZat5XdWdqErfzICYsZ3eGS1jttM+K53zGW1fP5++0zx9p3uUXrzIRLSyXrc6ODUb8ra7zWTnIQpazdHJkyelXbt23gwfEoYOle666y7fFYyIiIgo89x8880yc+ZMR9eZdM1R586dZf369aq7biIiIkpDAUjlD8vLy1PDm3344YfStGlT1ceR3rhx48T1wlG3bt3kvvvuk61bt0qTJk3O2Imrr75a/MTNDLVkWam6N1u979b+Wwl96MefovSV6Jpxctw8J69NK+tKFJo2us/MbCdRx5dG27cSrqNgCtLwIZ999pk0b95c/f35559HvWa1cXbShaNbbrlF/YuW4bGwE/HGNyEiIiJyw0cffeT4OpNuc4TUfaMHC0ZERERp1CDbiUcSxo4dK+eff76UKVNGKlWqpIb42LZtW6HLzZ49W+rXry8lSpRQUan33ntPvJRtN86HA/EbK1XiqdgXu1XqiarxmSFGXkp0bboVpjYbYjMKpaUq88zK9om8smzZMhk4cKAqIJ0+fVoefPBBueKKK1RTnFKlSsVdZuXKldK7d29VsLrqqqtU42oUqjZu3CiNGzcudJsdO3ZMGD5bsmSJ+zVHqB0aPXq0VK1aVUqXLi3fffedmj58+HB55ZVXkt4BIiIi8oeFCxfKjTfeKI0aNZJmzZqp/od27dolGzZsMFzm2WeflSuvvFK1Z27QoIEqY7Rs2VKef/55U9tEeyNsK/xo2LChSu9H4Qq1UCmpORozZoy89tpr8uSTT0baHwFKdxMmTJCbbrrJ0o4QERGRM1CP4kiDbLHn0KFD6t+KFSsazrNq1SoZMmTIGZnxc+fONbWN8ePHx50+cuRI1WG1FUnXHL3++usqZQ69ZBcpUiQyHaW1r776ytJOEBERUfo6fPhw1OPEiROFLoO2yIMHD5aLLrooYXhs3759Urly5ahpeI7pdvzpT3+yNK6apZojdAJ53nnnxT0Jp06dEj/woqdaKwNyOrWNdEsRdrMXYLe6LEhVurVRW7dEvTXrOblvTvZQnaqen53sLTtVUtHmKnY7QRkE18z9lEhad7PgcD9H1atXj5o8YsQIVTOTCNoeIbV+xYoV4gXUSFltF5104QixvI8//lhq1qwZNf3tt9+WFi1aWNoJIiIiSt/hQ3bv3q3GUw0rXrx4wsXuvPNOWbBggSxfvlyqVauWcN6cnBzZv39/1DQ8x3QzevXqFb3LmiZ79+5VHVajPXRKCkcPP/yw9OvXT9UgobbonXfeUWl6CLfhRBAREZG/lC1bNqpwZAQFk0GDBsmcOXNk6dKlUrt27UKXadu2rSxevFiF4MIWLVqkpptRrly5qOdZWVlSr1491R8jMuWsCGk4kiSh5ggb/fTTT1VjJ7QqR6HJ6k54DfFTnNwO0iPhwLPkPaPq/lSFAaxsJ9WDsJrteZmCwesexu2uO9E2jcJiUV2T6MJdXt8bp7VTslTmqUbKZgoadr7Paj42RrIc6GqnIC9Pdj74F9P7fMcdd6hU/Hnz5qkCShj2qWTJkurvvn37qox3pO6HU/nbt28vjz/+uBqFY9asWWpwe7Op/G5Iuubo+++/l9/97neqVBdr9erV0qaNcXsHIiIi8u/wIZMnT1b/dujQIWr6tGnTVIo/ILUftTth7dq1UwWqhx56SPWLVLduXZWplmzBCN0FfPnll+pvdCVgp6lP0oUj1A6hcVVsWt4nn3yiSnwHDx60vDNERESUuTQTwSiE22Jdd9116mHFgQMH5Prrr1frLV++vJqGsgg6h0Qt1G9+8xv3C0eoGUIBCWOZoHtwQIOr7t27F9py3Qq0bRo6dKi8//77cvz4cZUphxJo69atI28EWs2/9NJL6mQgZRAlV5Q8yX+SzepzuhrdzPKpqrpP5xAJpQ8vBpK2cp1ZCZMnCicXNr/vOdwgO52hjdORI0fkiy++UJ1IAnrkRvvou+66S/72t7+538/Ryy+/LDVq1FCFIfRzgEISaozQBumee+4RJ/3888+qsFO0aFFVOMLBPvPMM1KhQoXIPOiMcuLEiTJlyhRZs2aN6p4cnUdhaBMiIqJA8mhsNa965X7hhRciBaNwZv2kSZNU2cGKpGuOECdENRUKRJdeeql89tlnqlEV0vac9sQTT6i+FVBTFKZv+Y5aI/TKjThljx491DRkzaHzKMQrUc1GRERE/lVQUKAqUWJhGl5zLVsNBaBYqMLCQHEoJN1+++2R6U2bNhWnoOSHWiA0Asdgdmjdjpbw4WFLMK5bnTp1ZNOmTWpslTC0esdzjNdiBrPVyIjX2S2xGAojO9eMPosrdkBbJ8O0Rut1836yO2C4W/uid/pUnqyZPzwl2Wq1H3EuW23Hw+az1byAyhE0q0H4LDc3N9IkByN5INKEbgVcqTlCQQMj3urLUeHnU6dOVcOJ4G9Mw8C0TkHhB+2HMOYKWrCvW7dOxQ+LFSumYonhrsWT7XYc4UB91+e4mIiIiCjzYIDaq6++WmrVqhXpyRudViLb7Y033rC0TlOFox07dogXUB2Ghtfo7wCQloeuyNG+CIUjqxAGHDVqlIN7SkRE5N/hQ9IZCkToE+nDDz+MjPGK9kedOnWyvE5ThaPYoUJSpUqVKiq0pocD/vvf/67+Dnctjm7GMW8YnuvDbLGGDRsWNQIwao5ix42h9JeKEJMT6022ut9sdo5bYzylWyiRzDG6hvR/10k+umCamcwxp68l/XWvDxOa3b5+Pic/T4yWRyeQKROAbLUlS5ao9s7oYxEhv8svv1w9AGFA9HWEyhT0zeh6thp8++23KnUOpTI8EOrCNKchUw1Dk+h9/fXXkcIaGmejgIRux/UFHWStJep2HGPChLtCN9slOhEREaUPJGShDXK873C0u/rzn/8s48aNs7TupAtHH3zwgarNWbt2rWp8jQcKIyihxes12w50DYASIcJq27dvVz1oon0TRvoFtHHCWCyPPvqovPvuu7JlyxbVLTkaZPXs2dPRfSEiIsq0HrKdeKQrDGF25ZVXGr6OPhnRa3ZKxlZDux9kkGEMFL0HHnhA/vnPf6q4n5MwmC3CYN98842qKUI4LJytpu8EEoUmtFa/+OKLVX8Hv/3tb01vg9lqmc9MB3CZ0nGiPlSQaGwohrvILVYyv6x04phO17Nb4zbql09lttq5Dz/mWLbad488mJbZaiVKlFDtkNE5dDyoVGnSpIn88ssv7vdzhHFL3nrrrTOmDxgwQFVxOe2qq65SDyOoPUIHlHgQERFRMFStWjVh4QjdEOnbI7saVsMYJZs3bz5jOqZVqlTJ0k4QERGRg5wKqWmStrp27SrDhw+POyIGaosQVUpUueJIWA01M//7v/8rTz/9tIwfP16F0TCSbnjQWfRmjZAXdjTTMKyWvlLVaZsV6RQSILLLr9ezF8dllD2KbLWlMi81YbWHHpMiDoTV8hFWezQ9w2rITG/ZsqUUKVJEZa3Vq1dPTUc6P4YOQb+LaOoT2xeio2E19At02223qcIPBpzFGGdoCwRoAI1BZ5G1RkREROQ2FHpWrlypRulAeSRc14PmNmgbjQKSlYJRUoUj/UaRRYYHhhABFJaIiIgoTQSgnyNA1z7vvfeeGqgeDbBRVqlbt27UAPVWJNUgGwUjPRaKiIiIyGsoDJ1//vmOrS+pwhHS42MLSLF++uknu/tElHQbAbvtCox62rXSW7UVifbfr21ByF/S9To1O/Ctk716G3W/gVR+mT9PUsGpPopCaV5z5JakCkdod4SGXkRERER+lVTh6Prrr2e6PhEREfma6cJRYeE0onTsFdtsVwBG1eBm122351wnw3d2uz/gwLOZz0pvz24NtmqWm4MnJ7uPTl7z+nVx4NnMkXS2GhEREaU3tjlKUeGooKDA5qaIiIiI0l/SY6sRpVKiEI+Vqm+jZaxU6ZsNV6Qq/JbsNtI1u4jssztwqpWwlJXtGGWJmmW0zT3tQ6ZC5sl+npgNWafNvRXQWh8nJD22GhEREZGfseaIiIjIb9gg2xYWjiit2Q1RmV3eSnaM2aw2o/2Mqvqfk/zydrkZYiFv2c38MtM5YqJlzDLb4WqyjEJpTmfk6ddtdM7ZCWRmYliNiIiISIc1R0RERH7DsJotLBxRoNjtUNFMdovZThSNsnPsduJoF8Nlmc9K5lcqMt/SreNJJ/ffKDTuVSeQDKvZw7AaERERkQ5rjoiIiPyGYTVbWDiitKbPAEmUeZOqTtfMbMfK+E1m99/KmFlETkmUxebFdWf3fnTyfk52va5j4cgWhtWIiIiIdFhzRERE5DNskG0Pa46IiIiIdFhzRGktUe++ZuL6ZtPijXq3tdJewGw7KSvYzojS5Tox2/u8mwMh2+2awy1pcT+yzZEtLBwRERH5DQtHtjCsRkRERKQT0jQtoOXCXx0+fFjKlSsnHaSHZIeKer07pON1urAX208UlnO6h+LC5k9mO+R/dge0dVomhJZjB55dM3+4HDp0SMqWLevq91n9ux6TIsVL2F5f/ok8+Wrig67uczpiWI2IiMhvGFazhWE1IiIicszy5cule/fukpubK6FQSObOnZtw/qVLl6r5Yh/79u0Tr7DmiNKa2YwYs5Jdfk/7UNRzo8ElnazeTxSuMJOVZ2Vwz0wIT5B9dsOyTofSjLLHzO5b1P3Z3l7Iz+jc2A0lBnHg2WPHjkmzZs1kwIAB0qtXL9PLbdu2LSp0V6lSJfEKC0dERER+42FYrUuXLuqRLBSGypcvL+mAYTUiIiIqtKH3Yd3jxIkTjm+jefPmUqVKFbn88svlk08+ES+x5ojSmtOdtiXbOZ7ZanS7mWOJ9sWtwTGNlme2mr+YueZiXzMz3QluhqDNcPIzINn1ZlrNUfXq1aMmjxgxQkaOHOnABkQViKZMmSKtW7dWha6XX35ZOnToIGvWrJGWLVuKF1g4IiIiooR2794d1R6oePHijq27Xr166hHWrl07+fbbb2X8+PHy17/+VbzAwhEREZHPoKl6yKH1AApGqezn6IILLpAVK1aIV1g4orRmt0ra6RCRfn36TBmjjhpjt5korGFnXxIdV7Jj0CUK6+kx3JYZ7IbLrITlzNwnZpkdH9HM8lYyOTNWhvdztHnzZhVu8woLR0REROSYo0ePyvbt2yPPd+zYoQo7FStWlBo1asiwYcPkhx9+kNdff129PmHCBKldu7Y0atRI8vLyVJujJUuWyD//+U/PjoGFIyIiIp/xsp+j9evXS8eOHSPPhwwZov7t16+fTJ8+Xfbu3Su7du2KvH7y5Em59957VYHprLPOkqZNm8qHH34YtY5U49hqHFvNF1LVOaSTWSh2s4iM1hUr2f00G+7zdUiCUj6eWrLh30TzmdlG7PKpGLcQnUAulXkpGVut0Z+dG1vti6nBG1uN/RwRERER6TCsRkRE5EeBjwtZx5ojIiIiIh3WHJEvONnOyOnUX6P1mU2xzl39a5z/2ycaJL18su0i2JYo8znZfsbNdkZ6bl2bRt0KQK4kd55S1X1BpjfI9gMWjoiIiPwmw/s58hrDakREREQ6rDmiwLKbIm+0Lrup+LHz72lz+Ncnvy98+8m8Rv5k5T136zpxqyf3wtYXb546cwqd3dJ+ObEdpzGsZg8LR0RERH7DsJotDKsRERER6bDmiNKamwPHmsn2sluNb1aqQl8cRJbckmxWZiwrgyKnQlTmWRqEy8xiWM0eFo6IiIj8hmE1WxhWIyIiItJhzRGlNaer0Z3MULPLrXBBolCgkx3dMRSXGVI1cGwm3A+xnUCaOR9Wzlla3CesOQpOzdHjjz8uoVBIBg8eHJmWl5cnAwcOlLPPPltKly4t11xzjezfv9/T/SQiIqLMlTGFo3Xr1snUqVOladOmUdPvuecemT9/vsyePVuWLVsme/bskV69enm2n0REROnSINuJRxBlRFjt6NGj0qdPH3nppZfk0UcfjUw/dOiQvPLKKzJz5ky59NJL1bRp06ZJgwYNZPXq1dKmza/VyeQ/ZjKvUpVtlmi/9OtzMsThVtU9O5T0F6PrzOlMULeWt3s/67kZVkyLUJoew2r+rzlC2Kxbt27SqVOnqOkbNmyQU6dORU2vX7++1KhRQ1atWmW4vhMnTsjhw4ejHkREREQZUXM0a9Ys2bhxowqrxdq3b58UK1ZMypcvHzW9cuXK6jUjY8eOlVGjRrmyv0RERF4LaZp6OLGeIErrwtHu3bvl7rvvlkWLFkmJEiUcW++wYcNkyJAhkeeoOapevbpj6ydJm6prs9XbdjtHNJvhZjfzJdkwhBPYcaR/7hMnQz9Oh+XMbsfoNbv3g91zlnb3A8Nq/g2rIWx24MABadmypWRnZ6sHGl1PnDhR/Y0aopMnT8rBgwejlkO2Wk5OjuF6ixcvLmXLlo16EBEREaV9zdFll10mW7ZsiZrWv39/1a5o6NChqranaNGisnjxYpXCD9u2bZNdu3ZJ27ZtPdprIiIib3H4EB8XjsqUKSONGzeOmlaqVCnVp1F4+k033aRCZBUrVlQ1QIMGDVIFI2aqERERke8KR2aMHz9esrKyVM0RstA6d+4sL7zwgte7RS5xq/1E1OCSNtN9E+2LlfY7RsecqrZAadeWglxPi7fblseoV+rYe8vJNoROthky3P8EA8+a2f7pU3ki8+dJSrDNUbAKR0uXLo16jobakyZNUg8iIiJiWM3XDbKJiIiIUi2kaQHtxEAHqfzlypWTDtJDskNFvd4d8oDZqvdUDRarZxSW8GLgWgq2RNdcJnT54PW1fVo7JUtlnhrdwa0s6fD3Wcvrx0iRYva7wMk/mScbZ/3F1X1ORxkXViMiIqLEGFazh2E1IiIiIh3WHJHvWAk32Z3PyjbNLmOUIWNmX5zOxEu7wTXJs/dJP4gy5C7TUr6fbvWYb2Z7setjtpq/sHBERETkQ0ENiTmBYTUiIiIiHdYcUUZxcqDJZLfn9DbNhsXMrDvReckVc4PimsFQWmYw1aFhghCr0fWk/ztRh4hW9jPZfTG7vBVmM++M9k0fctSfZ2SrpQwS0Z1IRteCWf3EmiMiIiIiHdYcERER+QxT+e1h4YjSmt3xn8yu2+kMMz2jKnYr6zUT7ojFUFiw2Q2FmcnCSrRNK9vxel1W5tNv025WqCOYrWYLw2pEREREOqw5IiIi8plQwX8fTqwniDi2GsdWCxQz4Suz40cxXEVkj5mQnZNjHQZpbLXzez4q2UXtj612+lSerJv7UODGVmNYjYiIiEiHhSMiIiKfZqs58UjW8uXLpXv37pKbmyuhUEjmzp1b6DJLly6Vli1bSvHixeW8886T6dOni5fY5oh8wWz1utFrZjLKClu3GekalkvX/SL7zGab2e2QMdl9ie2UUtpHj9XmRoZdqsLkaXE/edgJ5LFjx6RZs2YyYMAA6dWrV6Hz79ixQ7p16ya33XabzJgxQxYvXiw333yzVKlSRTp37ixeYOGIiIiIHNOlSxf1MGvKlClSu3ZteeaZZ9TzBg0ayIoVK2T8+PGeFY4YViMiIvIZL8NqyVq1apV06tQpahoKRZjuFdYcERERUaFZcHpoG4SHE/bt2yeVK1eOmobn2OYvv/wiJUuWlFRj4cghXseurQwumeo2CrGcPE9215WqHm3TtT2Pmz2Me9H+Ip1SvL1uf5KKwZKtLm938Fq7x+bW+5EW97nDPWRXr149avKIESNk5MiR4lcsHBEREfmM02Or7d69O6qfI6dqjSAnJ0f2798fNQ3PsT0vao2AhSMiIiJKqGzZsq51Atm2bVt57733oqYtWrRITfcKC0c6O544X7JKlLA0OKiZ3pZTFVYyqqo2GzowO7ik3QFRKbPZDV24GW5KdVgs0b3Fe4CClsp/9OhR2b59e1Sq/ubNm6VixYpSo0YNGTZsmPzwww/y+uuvq9eRwv/888/L/fffr9L/lyxZIm+99Zb84x//EK+wcEREROQzTofVkrF+/Xrp2LFj5PmQIUPUv/369VOdO+7du1d27doVeR1p/CgI3XPPPfLss89KtWrV5OWXX/YsjR9YOCIiIiLHdOjQQRIN2xqv92sss2nTJkkXHHjWoYFnzfawbEeqerT1IgvJKHzHkERmcjJ7MogDqpI/pXLg2bZXPuLYwLOrFj7MgWeJiIiIgoxhNSIiIp/xss2RH7BwpJOzuIwUK11M9rSJ7gnUDDMZbrGSrVa3MmiilU4cva7u93r7lPrsyVQNCJoqdjNBM/GYKc0UaP99OLGeAGJYjYiIiEiHNUdERER+4/DwIUHDwpHOjgn1VOv+krLGsWw1N6vHk+1cz+nQRTqNjUap52RWo5tjm+Wu/jXDRh8y9zp0x2ue3IQcUUfaHEkwMaxGREREpMOaIyIiIr/xcPgQP2DhSKfk/PWWO4HUh9LMdAAX+5oRs2ObJbveZOYjisduKM1Kp6RWljfKPrVy/VsJxVnpIDZV2yH/Yiq/PQyrEREREemw5oiIiMhvmK1mC2uOiIiIiHQ48KxuoL4Lu4/+byq/yXTjVLff8WL7dtOd3UzRpszgVsq81/djon3RD7abu8zcRyzvDf9L5cCzv+swQrKzHRh49nSefLx0VOAGnmVYjYiIyG8K/v/hxHoCiGE1IiIiIh3WHDmUyu9Fb7vJ9pBtJcRl91i8CBV43fMxGaeVW+lVW89KtwBm7gcr3Qok6n1eP9iu2WNJttsOXtuUSEjT1MOJ9QQRC0dERER+w2w1WxhWIyIiItJhzVESzFbd262ut7t9s6EHuz0P212X3WN2chmyJ/a9NNNDs9fZZlZ6ntbLFffCt2bCf8wEpYQ4fIgtLBwRERH5DIcPsYdhNSIiIiIddgKp6zSrg/RQ2WpBzHbSH7O+AzsrA1gG8fyRZMR14mSGmhVuDg7LgWfTXyo7gWzf9iHHOoFcturRwHUCyZojIiIiIh22OSIiIvKZUMF/H06sJ4hYOHIhWyudQklm90v/mj4LxworGTXpev7IPjMdJzqdeWW0zaiQ8ZzklzfTuWXsMnr6cdYSLaPfjn6ZROfF7BhuFBDMVrOFYTUiIiKiTCkcjR07Vs4//3wpU6aMVKpUSXr27Cnbtm2LmicvL08GDhwoZ599tpQuXVquueYa2b9/v2f7TERElDY9ZDvxCKC0zla78sor5frrr1cFpNOnT8uDDz4on3/+uWzdulVKlSql5rn99tvlH//4h0yfPl210L/zzjslKytLPvnkE8ez1bzOdPFCov1369gy/ZyR98x0Sur1dZaqUCIFM1utY+sHHctW+2j9Y4HLVkvrNkcLFy6Meo4CEGqQNmzYIJdccol6s1555RWZOXOmXHrppWqeadOmSYMGDWT16tXSpk38nm2JiIiIMjKsFguFIahYsaL6F4WkU6dOSadOnSLz1K9fX2rUqCGrVq0yXM+JEydU6Vr/ICIi8l2DbCceAZQxhaOCggIZPHiwXHTRRdK4cWM1bd++fVKsWDEpX7581LyVK1dWryVqy4Rqx/CjevXqru8/ERERZYa0DqvpodE12hutWLHC9rqGDRsmQ4YMiTxHzREKSL90by3ZRUuYHrjVDLuDxZplZl2J2jhY2Rcry7s12CylXqrecys9Pxutz24v0maOxWwv8063W+R9Q1FQ4eNEH0WaBFJGFI7QyHrBggWyfPlyqVatWmR6Tk6OnDx5Ug4ePBhVe4RsNbxmpHjx4upBRETkRyFNUw8n1hNEaR1WQyIdCkZz5syRJUuWSO3ataNeb9WqlRQtWlQWL14cmYZU/127dknbtm092GMiIiLKdGmdyn/HHXeoTLR58+ZJvXr1ItPRTqhkyZKRVP733ntPZbIhzXDQoEFq+sqVKy2n8puhr573aqDHZENpTle7m0mXtrIuhgcoiJy8nyg9pTKV/9LmD0h2EfsRktP5J2TJ5seZyp9OJk+erP7t0KFD1HSk6994443q7/Hjx6t+jdD5I7LQOnfuLC+88IIn+0tERJQWOHyIfwtHZiq1SpQoIZMmTVIPIiIiIl8XjtKZ02E0u9lmRtyskncyfJZOPReT/cwtt943s6GnVPTenojdzFaj7STKtrObiUc+g0y1kEPrCSAWjoiIiHyG2Wo+zlYjIiKizDNp0iSpVauWavpy4YUXytq1aw3nRUJVKBSKemA5L7HmqBBuddQYu75UhMjMhkHshi4SnTMzIcOoDvTmGO4y+Uii7E8rWVxu3SupGnDaaJlE54WhNEqXBtlvvvmm6mh5ypQpqmA0YcIElSyFrnYwPmo8yITD62EoIHmJNUdERETkmHHjxsktt9wi/fv3l4YNG6pC0llnnSWvvvqq4TIoDKHz5vADw4B5iYUjIiIiv3F44NnDMYO1o+uceDBqBQaF1w8Ij+528DzRgPBHjx6VmjVrqqG8evToIV988YV4iWG1QjiZOZaIFxlayY4hZ/aY7YYRGErLDE6GmOyGhMyGjM1mdJk5Niv3rJP3eaoy9ChDORxWqx4zQPuIESNk5MiRZ8z+73//W/Lz88+o+cHzr776Ku4m0MkzapWaNm2qOpt8+umnpV27dqqApB8yLJVYOCIiIqKEdu/eHdVDtpPjk2K4L/2QXygYNWjQQKZOnSqjR48WL7BwRERE5DcO93NUtmxZU8OHnHPOOVKkSBE1ALxeYQPC62HM1BYtWsj27dvFKywcpZDT1eDJLp9oHjPrSrT/URlmJjKNzO4zZT6nw0d2lsldZi7MkOz94GbI2ex9zvuJ0qGfo2LFiqlB4TEgfM+ePdW0goIC9RwDyZuBsNyWLVuka9eu4hUWjoiIiMgxQ4YMkX79+knr1q3lggsuUKn8x44dU9lr0LdvX6lataqMHTtWPX/kkUekTZs2ct5558nBgwflqaeekp07d8rNN9/s2TGwcEREROQ3HvZz9Mc//lF+/PFHefjhh2Xfvn3SvHlzWbhwYaSR9q5du1QGW9jPP/+sUv8xb4UKFVTN08qVK1U3AF4JaWZGd/U5pCWWK1dOOkgPyQ4Vtb0+J7NG9Nk1iUICRmEtN1npKM/MMhwjKjO42UGqHsfdI784rZ2SpTJPZWSZab9j5/usU53Bkl3EfqPp0/kn5MNvJ7i6z+mI/RwRERER6TCsRkRE5DcehtX8gDVHRERERDqsOUrzlFqzbW7M9CrtdFp9sr0Ixy6jf03/N9sZ+YuVbiL81P4oVT1pZ8r5oFRxqOZIgllzxMIRERGR3zCsZgvDakREREQ6rDlKono6VVXVbvWcnW77z6r/YHDrfU5V9wF2ORmyTnRv8X6iKAWo8dEcWk/wsHBERETkN1rBfx9OrCeAGFYjIiIi0mHNURxWqqeNerJ2KwssmRBBqtkd4JYyQya+f25df2YHnrWbhUZkGhtk28KaIyIiIiId1hwRERH5DRtk28LCkcWqb30Yzc2OC61UwzuZxWPU6Z7V7ZgJN9jdBqUXK2Ehtzp7TMV6Y6VqOxywmaIwrGYLw2pEREREOqw5IiIi8hsVVXOi5kgCiYUji2Krrc2EhcyOM5aqDLdk1+XENswc5572oaTGjKP0ZuY6T1WHjGbXa2Y/ne6E0u76GEqjKAyr2cKwGhEREZEOa46IiIj8pgA9Wxc4tJ7gYeEoDivV6GZCBFbGGUtVR3WpyhZj9lmwmQmlWgmnJrq2zWRxeRHyjp3u9ZiI5DMMq9nCsBoRERGRDmuOiIiI/IY1R7aw5oiIiIhIJ6RpAS0W6hw+fFjKlSsnHaSHZIeKmlomd3XZqOffPtHA9R6qiTKRlZ6b/XoP+PW4yJzT2ilZKvPk0KFDUrZs9HeI099nnSr2l+ysYrbXd7rgpHz40zRX9zkdMaxGRETkM5pWoB5OrCeIGFYjIiIi0mHNkc4v3VtLdtEShq9HpQfrwmipSv11ukddtwYQNbtfHGw2GMykz8e+5251YWF2vfpQYO4yLemBqM0s4zSG7CgKWswUsEG2VSwcERER+Y0q1LBwZBXDakREREQ6zFazmK3mhGTDSmar7s1Wr5uZj1X15IVMuO7MDpZrNlsvE46ZMidb7bIyfSQ75EC2mnZSFh+ZEbhsNdYcEREREemwzREREZHfsM2RLSwcOSQVGWpmO9Azu30nM+woeNy8ZpK9NmNDzmbuFbNhsWT3MZZ+X9zM0OM9THpaQYFoIfZzZBXDakREREQ6rDkiIiLyG4bVbGHhyKHqaSezvdyqHk/UieSe9qHI33XmxJ+nsPWZWYb8w8kwUKJ1G12bemZDzma2F7tvRuGzqP0yuf1Ex2JlDDo93ncUBR1Ahlg4sophNSIiIiId1hwRERH5jarxcaAxtRbMmiMWjjysnjbajpXtmwlxJVqvvkNJs9zaT2bd+IuZccrOyDabY+/aNGImXBb7mtH0XDE31qBeonCZk8dJpBVoojkQVtMCWjhiWI2IiIjIj4WjSZMmSa1ataREiRJy4YUXytq1a73eJSIiIm+gfyKnHin4Tp49e7bUr19fzd+kSRN57733xEu+KBy9+eabMmTIEBkxYoRs3LhRmjVrJp07d5YDBw54vWtERESB8maS38krV66U3r17y0033SSbNm2Snj17qsfnn38uXvHFwLMolZ5//vny/PPPq+cFBQVSvXp1GTRokDzwwANJDzxrpl1CqtrCZGL7m0zcZ0o9K6nrdq+tdOpaI5bdVH5Kf6kceLZD6PeODKR+GvuszUlqn5P9Tv7jH/8ox44dkwULFkSmtWnTRpo3by5TpkwRL2R8zdHJkydlw4YN0qlTp8i0rKws9XzVqlWe7hsREVGQwmonLXwnY7p+fkBNk5ff4Rmfrfbvf/9b8vPzpXLlylHT8fyrr76Ku8yJEyfUIwwlYjgtp1SHoqdP5UWVmiN/G0x3kxfbDOI+U+oV5CV/ndi9tqwsn6rr2cr5oMyivmNSlAEW/j5zZD3y3xopveLFi6uHE9/J+/btizs/pnsl4wtHVowdO1ZGjRp1xvQV8v8NwObPi7+g0XQ3ebHNIO4zpd7QX6+Tnam6tqwsn6rr2cr5oIx05MgRFfpyQ7FixSQnJ0dW7HOuQXPp0qVVWEwP7YlGjhwpfpXxhaNzzjlHihQpIvv374+ajue4QOIZNmyYaiwWdvDgQalZs6bs2rXLtQs2neEXAS783bt3uxYHT3c8BzwHQT9+4Dlw9xygxggFo9zcXHELsr127NihwltO7nco9OvQNxCv1sjqdzKmJzN/KmR84Qil5FatWsnixYtV6/Zw4y88v/POO+MuY1QdiIJRUD8QAMce5OMHngOeg6AfP/AcuHcOUvEDHAUkPDLlO7lt27bq9cGDB0emLVq0SE33SsYXjgC1QP369ZPWrVvLBRdcIBMmTFAt3/v37+/1rhEREQXKkEK+k/v27StVq1ZVTVzg7rvvlvbt28szzzwj3bp1k1mzZsn69evlxRdf9OwYfFE4Qhrgjz/+KA8//LBqwIX0v4ULF57RwIuIiIi8/U7etWuXymALa9euncycOVMeeughefDBB6Vu3boyd+5cady4sWfH4IvCEaC6zqjKrjAIsaFxmVEM1e+CfvzAc8BzEPTjB54DnoNUfCcvXbr0jGnXXXedeqQLX3QCSUREROSUjO8EkoiIiMhJLBwRERER6bBwRERERKQT+MLRpEmTpFatWqpPCAyWt3btWvErpE1iMMAyZcpIpUqVVB8U27Zti5onLy9PBg4cKGeffbbqFfWaa645o3Muv3j88cdVx2b6vjWCcPw//PCD/OlPf1LHWLJkSWnSpIlKmw1DM0RkmVSpUkW9jjGPvvnmG/ELDG0wfPhwqV27tjq+OnXqyOjRo6OGdPDbOVi+fLl0795ddT6Iax6ZQHpmjvenn36SPn36qL5/ypcvr0ZQP3r0qGT68Z86dUqGDh2q7oNSpUqpeZBqvmfPHt8cPyUv0IWjN998U/XHgMyEjRs3SrNmzdRgdwcOHBA/WrZsmfriX716tepgCx8KV1xxhep/Iuyee+6R+fPny+zZs9X8+IDo1auX+M26detk6tSp0rRp06jpfj/+n3/+WS666CIpWrSovP/++7J161bVt0iFChUi8zz55JMyceJENRr2mjVr1BcG7gsUHP3giSeekMmTJ6sRw7/88kv1HMf83HPP+fYc4B7H5xt+DMZj5nhRMPjiiy/UZwdGT0eB49Zbb5VMP/7jx4+rz38UmPHvO++8o340Xn311VHzZfLxkwVagF1wwQXawIEDI8/z8/O13NxcbezYsVoQHDhwAD+VtWXLlqnnBw8e1IoWLarNnj07Ms+XX36p5lm1apXmF0eOHNHq1q2rLVq0SGvfvr129913B+b4hw4dql188cWGrxcUFGg5OTnaU089FZmG81K8eHHtb3/7m+YH3bp10wYMGBA1rVevXlqfPn0CcQ5wPc+ZMyfy3Mzxbt26VS23bt26yDzvv/++FgqFtB9++EHL5OOPZ+3atWq+nTt3+u74yZzA1hxh3JkNGzao6uMwdEqF56tWrZIgOHTokPq3YsWK6l+cD9Qm6c9J/fr1pUaNGr46J6g9Qy+s+uMMyvG/++67qtda9CeC0GqLFi3kpZdeiryOMZnQaZv+HGC4A4Sc/XIO0OEchir4+uuv1fNPP/1UVqxYIV26dAnMOdAzc7z4F6EkXDthmB+fmahp8uNnI8JvOOYgHj/5qBPIZP373/9WbQ9ie9HG86+++kr8DmPdoK0NQizhXkjxAYlxccIfCPpzgtf8AN3So+ocYbVYQTj+7777ToWUEE5GT7Q4D3fddZc6bnT3Hz7OePeFX87BAw88oAYXRcEXA2Tic2DMmDEqbAJBOAd6Zo4X/6IwrZedna1+WPntnCCUiDZIvXv3joytFqTjp4AXjoIOtSeff/65+sUcFBhlG2P4oM2AV4MypkOhGL9+H3vsMfUcNUe4DtDWBIWjIHjrrbdkxowZariCRo0ayebNm9UPBTTEDco5oPhQc/yHP/xBNVDHjwgKrsCG1c455xz1qzE2EwnPc3JyxM/QpTsaFH700UdSrVq1yHQcN8KNBw8e9OU5QdgMje1btmypfvXhgUbXaIiKv/FL2c/HD8hGatiwYdS0Bg0aqLGOIHycfr4v7rvvPlV7dP3116sMpRtuuEE1xA8PghmEc6Bn5njxb2yiyunTp1UGl1/OSbhgtHPnTvUDKlxrFJTjp2iBLRwhjNCqVSvV9kD/qxrP27ZtK36EX0MoGM2ZM0eWLFmiUpn1cD6QxaQ/J8jawBenH87JZZddJlu2bFE1BeEHalEQTgn/7efjB4RRY7tvQNubmjVrqr9xTeDDXn8OEIJCuwq/nANkJ+kHvQT8UML9H5RzoGfmePEvfjTgB0YYPkNwztA2yS8FI3Rf8OGHH6puLvT8fvwUhxZgs2bNUhkZ06dPV9kIt956q1a+fHlt3759mh/dfvvtWrly5bSlS5dqe/fujTyOHz8emee2227TatSooS1ZskRbv3691rZtW/XwK322WhCOH1k42dnZ2pgxY7RvvvlGmzFjhnbWWWdpb7zxRmSexx9/XN0H8+bN0z777DOtR48eWu3atbVffvlF84N+/fppVatW1RYsWKDt2LFDe+edd7RzzjlHu//++317DpChuWnTJvXAx/64cePU3+FsLDPHe+WVV2otWrTQ1qxZo61YsUJlfPbu3VvL9OM/efKkdvXVV2vVqlXTNm/eHPXZeOLECV8cPyUv0IUjeO6559SXYbFixVRq/+rVqzW/wodCvMe0adMi8+DD8I477tAqVKigvjR///vfqw+JoBSOgnD88+fP1xo3bqx+GNSvX1978cUXo15Havfw4cO1ypUrq3kuu+wybdu2bZpfHD58WL3nuO9LlCihnXvuudpf/vKXqC9Cv52Djz76KO69j4Ki2eP9z3/+owoDpUuX1sqWLav1799fFToy/fhRQDb6bMRyfjh+Sl4I/4tXo0REREQURIFtc0REREQUDwtHRERERDosHBERERHpsHBEREREpMPCEREREZEOC0dEREREOiwcEREREemwcERERESkw8IREUX861//klAopMaaIyIKKhaOiHwGhZtEj5EjR0q6eeedd+SKK65QA36ycEZEXsv2egeIyFl79+6N/P3mm2/Kww8/LNu2bYtMK126tKSbY8eOycUXX6xGRr/lllu83h0iCjjWHBH5TE5OTuRRrlw5VRMTfl6pUiUZN26cVKtWTYoXLy7NmzeXhQsXGq4rPz9fBgwYIPXr15ddu3apafPmzZOWLVtKiRIl5Nxzz5VRo0bJ6dOnI8tgey+//LL8/ve/l7POOkvq1q0r7777bsJ9vuGGG1QhrlOnTg6eCSIia1g4IgqQZ599Vp555hl5+umn5bPPPpPOnTvL1VdfLd98880Z8544cUKuu+46FeL6+OOPpUaNGurfvn37yt133y1bt26VqVOnyvTp02XMmDFRy6LAhFogbKNr167Sp08f+emnn1J4pERE1rFwRBQgKBQNHTpUrr/+eqlXr5488cQTqvZowoQJUfMdPXpUunXrJj/++KN89NFH8pvf/CZS6HnggQekX79+qtbo8ssvl9GjR6tCkt6NN94ovXv3lvPOO08ee+wxtb61a9em9FiJiKximyOigDh8+LDs2bNHLrrooqjpeP7pp59GTUPBBqG3JUuWSMmSJSPTMd8nn3wSVVOE0FteXp4cP35chdGgadOmkddLlSolZcuWlQMHDrh4dEREzmHhiIjOgFDYG2+8IatWrZJLL700Mh01QKg96tWr1xnLoA1SWNGiRaNeQzukgoICl/eaiMgZLBwRBQRqb3Jzc1XNT/v27SPT8fyCCy6Imvf222+Xxo0bq/ZI//jHPyLzoyE2Mt8QLiMi8isWjogC5L777pMRI0ZInTp1VFujadOmqQbXM2bMOGPeQYMGqZDZVVddJe+//75KtUdGGZ6jcfa1114rWVlZKtT2+eefy6OPPmp5v9BYG9lwCPtBuOuBcJYdEVEqsXBEFCB33XWXHDp0SO69917VBqhhw4YqzR7p9vEMHjxYhcMQZkPKP7LbFixYII888ohqzI3wGdL8b775Zlv7hX3o379/5DkajAMKcunYaSUR+VtI0zTN650gIiIiShdM5SciIiLSYeGIiIiISIeFIyIiIiIdFo6IiIiIdFg4IiIiItJh4YiIiIhIh4UjIiIiIh0WjoiIiIh0WDgiIiIi0mHhiIiIiEiHhSMiIiIiHRaOiIiIiORX/wegj9ixya38OgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkYAAAHqCAYAAADh64FkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAWq1JREFUeJzt3QecVNX1wPGz9KIQUYpSFMVgQTFiAxPBiGLQiKaI/o2KBUuAqJioWLBGVCzYgpD8hSRKMMYgahSDKBL/2AA1oIGIDaKCmEhV2u78P+e6s5kdprw3777+++YzwZ2dee/Nmzc795577rlVmUwmIwAAAJAGYR8AAABAVNAwAgAAqEXDCAAAoBYNIwAAgFo0jAAAAGrRMAIAAKhFwwgAAKAWDSMAAIBaNIwAAABq0TBKuX79+pkbgjF79mypqqoy/7p13XXXmefm2m233WTIkCEWjzBZ9HzpeQvaa6+9Jk2aNJGPPvpI0nBt2vThhx+a45g8ebK1bb7zzjvSqFEjWbRokbVtIrloGAXovffek/PPP1923313adasmbRq1UoOP/xwufvuu+Wrr77ybb/6R0G/HPQPDpKN9zoarrrqKjn11FNl1113NT/X1NSYL/oTTjhBOnfuLC1btpQePXrITTfdJBs3bnTVcMnemjZtKu3btzcdm5tvvllWrVolSfX00097auDus88+ctxxx8no0aOtHheSqVHYB5AWf/nLX+THP/6x+WN2xhlnmD+Kmzdvlpdeekl+8YtfyNtvvy0TJ0707cvy+uuvN39ANcKQ669//asv+0QwlixZIg0aNHD0XiMYb775pjz33HMyd+7cuvu+/PJLOeuss+Swww6TCy64QNq1aycvv/yyXHvttTJr1ix5/vnnt4kGFvOzn/1MDj74YKmurjaNId2PbufOO++UP/7xj/Ld735X4kwbk9pRbNy4cb2G0f333++pcaTnfeDAgaaDuscee1g6WiQRDaMAfPDBB3LKKaeYD7z+Adx5553rfjds2DBZunSpaTiFQcP9iC9taCNaJk2aJF26dDGNoNzP2f/93/9Jnz596u4bOnSoabxmG0f9+/d3tP3vfOc78qMf/ajefW+99ZYcc8wx8sMf/tA0jnP/xsSNNhA1om6bnt8ddthBfvvb38oNN9xgfftIDobSAnDbbbfJ+vXr5X//938L/sHq1q2bXHTRRXU/b926VW688UbTq9EvPv3jeeWVV8qmTZvqPU/vP/74403U6ZBDDjF/THSY7ne/+13dYzR8r5EqdeSRR9aF4bN5BPk5RtlwvfY8f/nLX0qnTp3Mdo866ijTgHOS31Iob+mzzz6Tc845x4T+dXs9e/Y0f6Cc5DgUyjlYsWKF6YHr8ek50vM6aNCgskNIerzbbbedLFu2zJw7/e+OHTua3qhauHCh6XHrUIc2ZKdMmbLNNt5//31zTtu0aSMtWrQwX4CFGrb/+te/5MQTTzTb0gjBJZdcss17qP72t7+Z7emXqb4WHWrRxzoZXs19D0q912eeeabstNNOsmXLlm22oV+o3bt3L7mfd99913zpdujQwbx/et61sb9mzZp6DQI9d/pa9XXo8MX48eMLHrOeez2ugw46SJo3by777bdf3fv+5z//2fys++nVq5e88cYbBd9DfR8GDBhgzu8uu+xivuwymUzZc/bxxx/L2Wefba5FPc59991XHnzwwW0ed++995rf6XusX6h6rIWuh3yPP/64OQ+5ESBtGOU2irJOOukk8+8//vEP8UI/T+PGjZPVq1fLfffdV/bxTq9N9eqrr8qxxx4rrVu3Nueib9++ppFXKP9N/0bo+/ONb3zDPF4/oxotyzVz5kz59re/bR6j76Nee/r3rdjnXbeX/XzmDiXqe63Xkn7u8+nwpO5fUxeyNAKlf5emT59e9vwg3YgYBeDJJ580DZZCfxgLOffcc02jQXuFl156qfnDNGbMGPPHc9q0afUeq3+I9HHa6NAvP/0Dr39I9AtF/6gfccQRJvR+zz33mD8+e++9t3le9t9ibrnlFjNE8/Of/9x8+Wnj7rTTTjPH4pZ+wesfJD3W4cOHS9euXeXRRx81x6l/yHMbhU7pl7QOP44YMcL8cdSGl/7B1QZPuSEkHYL43ve+Z86Nvq6HH37YHJd+SWhuiL7OH/zgB/LAAw+YYc/evXubY1YrV64076P+sdfzuuOOO5r3SnNH/vSnP9V90elr1sakHo8+Tr+4f//735uIYT49F7q9Cy+80GxPE3f1S1m/vPR3TpV6r08//XTTYH722WdNoyS3ganHpFGLYnTIVxsg+sWp51sbR9q4eOqpp8z7p19AShtBes3pudBEV73uf/rTn5r8Go2M5tJr4X/+53/MF9dPfvITuf322+X73/++Oed67Po8pdf9ySefvM2Qob6H+mWtjVJ9D2fMmGFeg3YqSkUD9P3T5+gXq77nbdu2lWeeecZ8ftauXSsXX3yxedyvf/1rcy71s6XXp37R/v3vfzfXvx53MXpe9D0/8MADHbxjX59/pY1Wr7J/B3R4XDs1xbi5NvU+/azo3xM9v/oeZBvA2qDXDlkufa/0s6Lv24IFC+Q3v/mNaXjdeuut5vf6mdXrb//99zfvkzZM9VrIb2jl0mvkk08+MZ9vPc4sfQ/12tH3/z//+Y/pqGTptafvp/4+l74ObRjp7zTHEygoA1+tWbNGu7CZQYMGOXr8m2++aR5/7rnn1rv/5z//ubn/+eefr7tv1113NffNmTOn7r7PPvss07Rp08yll15ad9+jjz5qHvfCCy9ss7++ffuaW5Y+Rh+79957ZzZt2lR3/913323uX7hwYb39n3nmmWW3OW7cOPPchx56qO6+zZs3Z3r37p3ZbrvtMmvXrq237/zj/OCDD8z9kyZNMj9/8cUX5uexY8dm3NLj1efefPPNdffp9po3b56pqqrKTJ06te7+xYsXm8dee+21dfddfPHF5r6//e1vdfetW7cu07Vr18xuu+2Wqa6urvea//jHP9Y9bsOGDZlu3bpt8xq//PLLbY5zzJgx5ng++uijuvv0OPI/svnvQbH3Wo+rU6dOmcGDB9e7/8477zT7ef/994ueszfeeMNsU7ddSqHXMWDAgMzuu+++zTHr9ubOnVt337PPPmvu0/ch9zVPmDBhm9eTfQ9HjBhRd19NTU3muOOOyzRp0iSzatWquvvz379zzjkns/POO2c+//zzesd0yimnZFq3bl33GvTzuu+++2bceu6558w+n3zySUeP79+/f6ZVq1bmGiwn+/ko9T707Nkzs8MOO5TcjtNrU8/pnnvuad5D/e8sPUd6vR999NHbXJtnn312vX2ddNJJmR133LHu57vuuss8Lvc9ypf/eVfDhg3b5tpXS5YsMfePHz++3v0nnHCC+TzmHreaMmWKefyrr75a8hwh3RhK85n2TNT222/v6PGaZKhGjhxZ736NHKn8IRsdrtCcgyztAWtoWocZvNAQeG7+UXYflWxXX5NGGXSWTm5YW3urOsT44osvutqeDr3osenQyxdffCGV0Khclob09ZxpxEh7vFl6n/4u9zXra9Fesg4FZOlwwHnnnWeGADS/I/s4Hd7LzQXRYQh9XKHXk7Vhwwb5/PPPTVRKv9fzh5EqpT19jYQ98cQTsm7durr7NVqm+8pGxArJRoQ02pQ/LFLsdWiUUV+HDrvo+csdcstetxqJyzr00EPNvxqJ0CHF/PsLXXca8cnKRoA0uqWJz4Xo+XzsscdMZEr/W48ve9OImB6jRjmUvu8asXv99dfFjX//+9/mXx16K0dnkumxanRW92eDXou5728hTq9NTSLXIVSNkOnryp4rvUY14jRnzhwTDcxPcM6lfzf0udm/g9nXqVGb/OdW4pvf/Ka5RvQ6ztLokUYB9XrPT2jPvi/6OoBiaBj5LBuuLffHKkvrnuiXmOYd5dKGhf5Rya+Lkvslkvvhr7TBUGy72T8olWxXj3nPPfesNxSiskM9bmu9aPhdQ/P6x0/zRLJDYtlhiXI0d0UbkPlf/po3k/+HVO/Pfc16rIXycfJfi/6r72H+9go9V4c0dFhRhwL0i02PTRsUKr9B4YUOC+owSnY4Voen5s+fb4bZStFGkzbUdVhEh3y0EaE5H/nHpsMhmuCqDUy9VvV1ZHNH8h+bf31lG1+aX1Xo/vzrTq8lHZ7O/5JUxfLMdAaXDv3p7E89ttybdgSUDsmqyy+/3LwX2gjWa1eHAksN9+Qrl+v0yCOPyNVXX22GvnQINZdex7k3N6U8tKNRrhPm9NrURpHSIfr886XXgg6tlntf8/9uDB482JQo0Y6JfnY1T03zGb00kvS61vcm+9nT4WfNpSt0XWffF6czAJFONIwCaBjpGL7bwmJOP7gNGzYseL+TJFSv2y12jJr/UQk329NckH/+858ml0EbOtdcc41pnDiJsBR7bX6dy1L0tR199NEmEqhfxpq4q7kU2cRTG73q3CiN5lg89NBD5mf9VyNvuVGyYu644w6TY6MNHf2i1mif5hNpVEXpFGiNImhPXKeN6+vR16FJvYVeRxjvQfYYNO9Ej63QTb+0lV5L2nCcOnWqiQ5qpEn/LZWLpTRHrFwHQvejX+ZaV0dzqvJpNCf3po0oJ7QxoJ+J/E6V1/M1duzYoudLG49u3j+NKmqkSSNl2nDRa0obS/oZqPTvhjauNAKdjRrpda2J8oU6Idn3xUZOF5KL5OsAaLKh9lK1bknu8EEhOhNK/yBpby03QVqTRrW3my0Y54ZfvSPtDeox5dOeW25vXo9Z/wDq68qNGi1evLju99ntqfxtFoso6aw9HWLUm56vAw44wHyBZ7/4/aDHql+Y+fJfi/6rjWH9Qsg9//nP1Vlw+mWmCdz6ZZmlXzqVKPde6z40+vPpp5+aGVb65exk2EfpTDG9aaRDa+doI0K/2LVIoSa7agRBh+pyowYvvPCC+EGvJR1ey0aJlJ5HVSz5XiMdGk3RL2AnU+M18qVf2nrTITpNyNek5lGjRhWdTr7XXnvVlegoRJO3NUFfv7g1UqJJ6vny33ttgDqhyf/aaNWIXilOr81srR/t3DktJeCE/g3QRrTetBGtQ4o66UGvlWL7KXVda6RVr2NtGOnwmUaPdIZeIfq+6P5zrxsgHxGjAFx22WXmj6yGj7WBk09721r9WmkBMpX/wdY/IEr/ALil+1aFGjFe6B/OV155xXxpZOlMpeXLl9d7nL4mHRLI7fnq7CGdeaU9zuywkf7B1h6n9ihz/epXv6r3s+a55FcL1mPRL71iU45t0deis8a0kZulORfa8NUvZI3KZB+nM2n0yyr3uPOLeGZ72LkREf3v7PVg+73WPC/9ktGZVtqwyJ+1U4jmh+j7lUsbSPoFkz3fhV6HDrPoDCa/5E5L1/3qzxo50C/cQvQYdTajRn8KRXBzK0dnc4WyNLKm763up1DJgywt/aDDgfPmzdvmdzqrVD+/ep3o5yQ3JyuXNg5yb05qEmkdI42iaiM3fwZgPqfXpkYX9XOlMwZ1iC5fJZW2Nf8nn3ZoVKnPbrnrWqNPmt+nxXL1fdYoUiE6dKwNzewQLVAIEaMA6B8X7Z1rz1OjQLmVr7XnnZ26nq1HomP6+kdK/whoo0G/iDWioHVHtD6NW/qHR/9YaF6Ofllpjk623owX2tDTP646bVqHY7SBp9Ga/KqymtQ5YcIE8xr1D5N+Mejzsj27bE6E/rHSOjzaYNIvb92OfoFk8z5yIwP65af71C8r7XVr3ow2Oov9QbTliiuukD/84Q9mCrMOJ2lvVd8b7YnqF242IqbF+/SLWt9rfc365aZTjTXJNT/CoK9TyyLoVG/tnet2Ks0RK/dea9RE3y+95jQPyElDW6dsa2Kzvjfa09ZGkr6WbEMjWwtJGw+a2KzTq/WLVKe86341OmWbRmx0ir5+VjT5VvPNdPhOh/ry88dyaaKzRib0Ofoe6fWjX9aadK3DO9kvbn09mtenUTHNhdFGjb6fer7K5fBoXR29HnMjMppjqJEcfV/1yzt/EoVeA+WiyVk6TV47Bhr50gacfo40UqefH92vHncpTq9NvZY1l0ivdW1MaB6WNvz0OtVzqNeqRgrd0Cn62vHR86gdIf1sa8dH8/tyJzTk00aa0s+cnsf8xo9uT4cx9brW4y30t00btDrRI1sKAigq7GlxafLPf/4zM3ToUDONVKcVb7/99pnDDz88c++992Y2btxY97gtW7Zkrr/+ejMltnHjxpnOnTtnRo0aVe8x2WnPOkW53HR59etf/9pMm27YsGG9KbnFpuvnTwkuNIVW3XHHHZmOHTuaEgH6WubNm1dw/ytXrsycddZZmZ122sm89v3222+bbSmdxvvDH/4w06JFCzPt+Pzzz88sWrSo3r51qrVO391rr70yLVu2NNOsDz300HrTj4vRqd76nELnrND07ELn+L333sv86Ec/ynzjG9/INGvWLHPIIYdknnrqqW2eq9POddqwvhZ93RdddFFmxowZ20w/f+edd8y0bS1doI/Ta+Stt97a5nw7ma5f6r3O0vOk95933nkZJ3Qqv07D3mOPPczrbdOmTebII480U9NzPfHEE5n999/fPEav8VtvvTXz4IMPmn3p9VPqnCp9nL6vha673NIM2fdQ34djjjnGnN/27dub85Mtl1Bsun72WtT96OdKP18dOnTIHHXUUZmJEyfWKxNwxBFHmKnmem3ra//FL35hym+Us2DBgm1KOmRfR7FbobIX+bKfzexNj71t27bmOH/5y1+aUh1OOb02s+UafvCDH9SdC33/Tj755MysWbO2uTbzp+Hr9Zv7/utztBTCLrvsYv4O6L+nnnqq+duYf65yr/2tW7ea8gz6erW8RKGvrp/+9Kfmfp2SX8gzzzxjfv/uu+86Pk9Ipyr9v+LNJgBJo1OlNfqoPffcUg9xoZFHjTgWGt6JCo1oZgsnIhia6K+rC+iwfX70S+k1rxG8/CK5QD5yjICU0SEuTY4vNXQBbzShWHPq3JaiQGV0aFGH8XVot1CjSIdCdVhel1oCyiHHCEgJnXquswM1v0WTu6nl4h/NYcqdlAB/aI6S5oZpBFHzrYotL6S5nfkTCIBiaBgBKaEz0nQWoBYVJAEVSaAz0XSKviZb6xqB2RlugBfkGAEAAM/Gjx9vbtnq8zqbcfTo0WamYDE6k1AL9OpztMq8zqjNlq0JCzlGAADAs06dOpmSGFoGQmt5aakQLV/x9ttvF3y8lqvRSLZGsXXVAk2Q15vblSJsI2IEAAB80aZNG7OsjDZ+8mltPy2Qq4nxWYcddpgZEi20XE5QUpVjpMsIaMVXLdBG4ikAIAgaf9Ain1rCIX8xbT9m6NlO/M/kLR+jtHis3orRAqQ6TKYNn2LFS3UFAV2iKJcW8NQ1I8OUqoaRNoryV+8GACAIulySDjf52Sjquut2suKzyhbkLWa77bbbpm6YLqh83XXXbfNYXf9RG0J6LPo8rRuVXSopn9ac0sryufRnvT9MqWoYZUv5f1sGSiNpHPbhAAB89J+zDzX/tnnw1VCPY6tskZfk6bLLyXilkSJtFH00fzdptb2dyNTadTWya68PTaNOl4HJKhYt6t69u7z55ptmSSIto6DL9uhSLMUaR1GUqoZRNhSojaJGVTSMACDJ2k1a8PV/hP33vjaTN6gUju22rzI3G2rk6+1ooyi3YVSMrpnYrVu3ujXuXn/9dVM3TdfLzKfr+uUvrK4/l1vvz2/MSgMAIEGqMzVWb15zezdt2lTwdzrkNmvWrHr3zZw50/GCyn5JVcQIiJp/n9/H/LvjhLlhHwpQ8fWruIYxatQoU7OoS5cuJtl8ypQpMnv2bHn22WfN78844wzp2LGjjBkzxvyslcr79u0rd9xxhxx33HGmOr9O8584cWKor4OGEQAACVIjGXOztS03S7Ro4+fTTz+V1q1by/77728aRUcffbT5/bJly+rNyuvTp49pPF199dVy5ZVXmgKPOiOtR48eEqZU1TFau3atebP6ySByjBBrRJoQtesvztek38e+NbNFZst0k5DsJE/H63fciiVdrCZfd+i+zPdjjxIiRgAAJEiN+Z+9baUNDSMghuLYK0eyr784X5NxPvZCqjMZc7O1rbRhVhoAAEAtIkaAS3HOpQCSiM9kNJKvk4KGEQAACaKNmWoaRhWjYQS4RK8UaY3ARDUyU+x4onq8iDYaRgAAJAhDad7QMELqhNmL9LrvKBx7WPtPiyhEOYrt28sxrXl6T/Nv64Hv+nbd5Z87L8ebu1+v20K80DACACBBmK7vDQ0jAAASREsy2ivwmD4sCYLUqnTIIgpDHV7E/fiRruvMyxBuVJLIg14SZPE/2sv2lpYEWbeuRvbaeyVLggAAgHiqtjhdv5rkayA9Ku0t2uxl+tFzLbdNP3rJUemZIzpsRXfcbCc/YdrWscVNdebrm61tpQ1LggAAANQixwioUH4Pt1CP12nExO3U4CAjMUR94IdKPhu2r8Ggru2gc4zefKed1RyjA/b5jBwjAAAQTzVSJdVSZW1baUPDCLCcq1Ooh1uuuJ3bHmuQ0Zv8iFih+4gmIYhlPJxEaZ3IPq/RoFVf3zHB/XO55pOLhhEAAAlSk/n6ZmtbaUPDCKlVrvdZaRSn0MyYYpGiYr3PSpdPcMNt75seMmxwe52Vuu6K/a7Y5yf/s1nJ54uZl8lHwwgAgASptphjVJ3CHKPYzkq75ZZbZNSoUXLRRRfJuHHjHD2HWWkIu6fndp82FrKkxhAqVe4asTFjLIh9uD0WG/vKfV1Bz0qb+/bOsp2lWWnr19VIn30/TdWstFjWMXr99ddlwoQJsv/++4d9KAAAIEFiN5S2fv16Oe200+TXv/613HTTTWEfDhIojGrU5Xg5liiuBUe0KthzV+n5Lvd4JzPHKt2HjWvEba2kSmqQFRPmtV2TqTI3W9tKm9hFjIYNGybHHXec9O/fv+xjN23aZEKLuTcAANKQY2TrljaxihhNnTpVFixYYIbSnBgzZoxcf/31vh8X4qncrBivNVJynxvFXme52Xh+RnWIFMXr3Lm5FtzmzxWbzVlqO+Vmc26d3tbRMRTbbqn9E+1MvthEjJYvX24SrR9++GFp1qyZo+docrYmjGVvug0AAJKsWhpYvaVNbGalPf7443LSSSdJw4YN6+6rrq6WqqoqadCggRk2y/1dIcxKg82eX5C5Hl72FcT6azZmzyF6yl0T2QhNqZpBflzrbmtwhR3lCXpW2vOLOludlfbdHstTNSstNkNpRx11lCxcuLDefWeddZbstddecvnll5dtFAEAkAYZi8nXmRQmX8emYbT99ttLjx496t3XsmVL2XHHHbe5H6iErV5nqTyFMGaI+VHZ2muVcMSjFk+555eqHO02V62SPKZKP7NOKsuHHWXyggKP3qRv8BAAACDuOUY2kGOUXDZzXPJntvjRY/QjJyfIHm6ce9MovJ6flyhPoW34KT/i4zZyVG5GXKltxSHH6Jm/d5WWlnKMNqyrke/t/wE5RgAAIJ5qpEpqLA0I1UhqYid1iBgh9SrNgXAzMybKEZYoH1scji+Jyq1O72XmYqNBq+pFZLM/V7Kv/Mc4yR0K4/oLOmL0l7/vLi23tzMhacO6ajlu//eJGAEAgHgi+dobIkZIvShHJIodW5SPuZI6M4gmP983G7W5spx+PsK6DoOOGE17a0+rEaOTer6bqogRs9IAAABqETECQlyhO4werB95GECUooI2IpZOP7tZpbYddMTosbe+aTVi9MOe/yRiBAAAkEYkXwNlOM1TKNRjLPfc7IwcmSCBcRspshlhilJUAf5UWPd6Xdm43krVJ7JVNTwq+UuF1Fhc/LUmhdP1aRgBAJAg1ZkG5mZnWxlJGxpGgEN+rJVms05Mpcrt02YuUhR609hWGGv4ZesYidS/vrysX1ZsDb/s/X7OluTaTg4aRgAAJIgOpVH5unI0jIA8bnKICuVG7DjQ/zyNMHq6bvIvopRvAfvCel/LRXqK/d7p8eY+7tlP3jL/Dtilp8RNdabK3GxtK22YlQYAAFCLiBGQp9y6S8XqlzjJxXFT+8TJMbqJ4ng9NjfbJ1IEt9xEGd3OGKtkXwddf+HXj5H4XcvVFmelVTOUBgAA4qwm08Dc7GwrI2lDwwiJ4EdOi9MZYzZ7um65WXncr9ovSC4bnyu3+UA2911MGJ9VxAcNIwAAEoShNG9oGCFSKu0l2uzduZ2V5kdv2gu326x0Bg+SL4g1/ir5XHndP7MmUQoNIwAAEqTG4jT7GkkfGkaIFFs9uFJ5Ml5ntMQll8PpNuk1w0/l1gN0c+37Wbk6SVEnuwUeG0japO8VAwAAFEHECInkphfntRdaSc/RVj2jQtuw1ZMttZ0o95ZRObdrkTnaVplK8G725ba2lpfr1Gldryh+FuwuIttA0oaGEQAACVIjVeZma1tpU5XJpKd609q1a6V169bSTwZJo6rGYR8OYsqPuivkSiDt/Pj8ROXa3prZIrNluqxZs0ZatWrl+3fcPfMPk+bb2Yl7fLV+q/ys1yu+H3uUEDECACBBGErzhoYR4JKXekZB5i8Vy4GwUbco6j102FXJ+5pda3Dr9LaOnuvl82OrxpiTtQe5xosbM2aM/PnPf5bFixdL8+bNpU+fPnLrrbdK9+7diz5n8uTJctZZZ9W7r2nTprJx40YJCw0jAAASxG7l6waOH/viiy/KsGHD5OCDD5atW7fKlVdeKcccc4y888470rJly6LP0yG6JUuW1P1cVRVuXhM5RkCEBNkbLVbriZ4w0jJTMajXE3SO0W2vf8dqjtFlB/+tomNftWqVtGvXzjSYjjjiiKIRo4svvlhWr14tUZG+wUMAAOC7NWvWmH/btGlT8nHr16+XXXfdVTp37iyDBg2St99+W8LEUBpSx2atFtvCmOkW5jptiMdnwMn7GuX3PGmvx0m16mrLla/Xrl27TR6Q3oo+r6bGRIIOP/xw6dGjR9HHaf7Rgw8+KPvvv79pSN1+++0mN0kbR506dZIwEDECACBBajINrN6URnN0mC5700TrUjTXaNGiRTJ16tSSj+vdu7ecccYZcsABB0jfvn1N8nbbtm1lwoQia8gEgIgRUieKa4dVGmkpNYvG6eu0GZWKcy87TWxdG05mcZV6jpNjqmQf5bbtxwzSpFu+fHm9HKNS0aLhw4fLU089JXPmzHEd9WncuLF861vfkqVLl0pYaBgBAJAg1VJlbra2pbRRVC75WudyjRgxQqZNmyazZ8+Wrl27ilvV1dWycOFCGThwoISFhhFgqYfo5Pm267BUMpvGZpSH3nQ82YqGVPJ8p/W1bEZsstuad+148++AXXo6Ps44yh0Cs7Etp3T4bMqUKTJ9+nTZfvvtZcWKFeZ+HXrTukZKh806duxYNxR3ww03yGGHHSbdunUzM9PGjh0rH330kZx77rkSFhpGAADAs/Hjv2549uvXr979kyZNkiFDhpj/XrZsmTRo8N/G1hdffCFDhw41jagddthBevXqJXPnzpV99tlHwkIdIyDCKlrNPMY9XYQjiGvHj314nWGan+/k1+sPuo7R6Ff7S7Pt7HzHbVy/RW449LlUrZXGrDQAAIBaDKUBZfjRq8yuI9V64LvW6hp5XX8tqN4zoqdcXk8l10a5XDYv0Z5iM9vyldtnUq/xsHKMkoKGEQAACVKdaWButraVNjSMkAqFaqGEURk6q1ykyI9jKfd6y/WuvdSsSWrPPGlsVl4vplxUykk0p9A16Ren54BrPTloGAEAkCAZqZIaS3WMMpa2EyfMSgMC6B0H0SOnxwq/RS0Pze3x2Kzk7WbfQc9K+8Xc46SppVlpm9ZvkbF9/sKsNAAAgDRiKA2w1PutZMaYzR53pXk/TmfI+YEoV7S4zUPzYx9uuJ3R1mjQqq//o8j6pG6OKcrXbE2mytxsbSttaBgBAJAg1dLA3GxtK23IMUKsOZ3hElc2XkdcqxojurysC5j/+0pqI7l5TqFjCfp6DTrH6OL/O8FqjtG4w59IVY4RESMAABKEoTRviBghtbz2Git5vp/7DLIXTIQoGSp9Hws9L4zPU1wEHTH62UuDrEaM7vm2/8ceJUSMAABIkBppYG62tpU2RIwAH/MQotQL9vNYilUgjsLrhnt+XPtutxnG56+SOkZOth10xOjCv/3AasRo/Hf+nKqIUfqaggAAAEUQMUKiBREl8aO2S5QiTXGsiIz4XgNh1tXyS9ARo/Pn/NBqxGjCEY+lKmJEjhEAAAmSyTSQmkwDa9tKGyJGQAi1XfyMrCSxxw0718TW6W3rVYB2e43kb4fIoLPPfdARo/Ne/LE0sRQx2rx+i0zs+ygRIwAAEE/VUmVutraVNjSM4Luo5su4jd6Ui8Q4eX22KnSXOhYiRfHkx+ekbpsDs9t8t+RaYeWO4b/X1ruReN1RqHwftb9r8I6GEQAACVKTsVexuiY1yTb/RY4REMGZY35umxwkpO2acbpum1/Rn6BzjM584RRpsl0TK9vcvH6z/PbIqanKMUpfujkAAEARDKUhFqJQMyjIXAKbVXeL55s4fHwIORRuKhDDf8UiRcUqnof9vuVfu1H6bAehRqrMzda20oaGEQAACVKdqTI3W9tKG3KMgAA5jcLYyGeyHfGxEcWJ6gzFKInSOYpCRfZS112UzlWUcoz+5/n/sZpjNOW7U1KVY0TECACABKmxWPm6JoWVr2kYIdHcVKOOUs2T/MdVcmxR7EWHNSsoTqJ0DnypqeRym6UeH6VzFbkcI1vT9SV9Q2npawoCAAAUQcQoQqIwCyiM3AE/91lJNeooikJEJez3KYqi8L5EZXZXuXPhNloY19pJUbgmMhZnpWWIGAEAAKQXEaMIiWs0IArrFUWhl+YnJ68rCefAzWtIW4Q1DG5eX6Xnovy6bMGxMfMyCteE5hfZWxKkStKGhhEAAAnCrDRvqGMUA0nrnZZ6PUl7rWlHVCdZ4ro+YNrqGJ008yxp3NJOHaMtGzbLtKMnUccIAADEE0Np3tAwAgAgQVgrLSVDaWPGjJE///nPsnjxYmnevLn06dNHbr31VunevXvih9KiJqkh76S+rqTg/UmWNL2fQQ+lff+v51gdSnvymP9N1VBabLKqXnzxRRk2bJi88sorMnPmTNmyZYscc8wxsmHDhrAPDQCAyA2l2bqlTWyG0mbMmFHv58mTJ0u7du1k/vz5csQRR4R2XDamd0Y90dFpkTe3C6RmhbkwaqXHAX/er1KPD3M5kTRFN4ISZkJ+mMcQBHKMUhIxyqdhPdWmTZuwDwUAACREbHKMctXU1MgJJ5wgq1evlpdeeqno4zZt2mRuueOvnTt3tpJjVMnipOV6K+W26Wd0ygt607B1LeR/Rpxsq9J9Fvo8VRpR4DPgn7hG5cPMMRrwzHlWc4ye/d5EcoyiTnONFi1aJFOnTi2bsK0XSfamjSIAAIDERIyGDx8u06dPlzlz5kjXrl1LPtbPiFHUxaEHG4djRPicRlKjuPSMzWPj8xJfQUeMjn76fKsRo5kDJ6QqYhSb5Gttv40YMUKmTZsms2fPLtsoUk2bNjU3AADSImOx/lBG0ic2EaOf/vSnMmXKFBMtyq1dpK1jrWvkBHWMnItqPhPiJUrRHjfczrC0ObMyqucE8YkY9X/6fGnU0k5QYOuGTfIcEaNoGj9+vPm3X79+9e6fNGmSDBkyJKSjAgAgWpiun5KIkQ1+R4yi0NPzOkPH5rFH4XwgXsKKwCT1Wo3j64rjMUctYtTvqQutRoxmHz8+VRGjWM5KAwAA8AMRo5T2jLzsO4k9OiQ/x23N03uaf1sPfNfx/t3u28k+nNY3C3MGHOIdMTriyZ9ajRjN+f6viBgBAACkUWySr5PA6ZpjxR5vY59ZbtYzK/dcP2fwuOnlI7rcvveVrOFX7lrZOr1t7fPalt2H02s8//7sPkTedfy6i/3O7ee+UASK6FE6kXztDQ0jAAASJJOpMjdb20obcowAJEoUK2ETxUm3oHOMDp8+3GqO0f8Nui9VOUZEjAAASBCtem2r8nWNpe3ECQ2jCkUt98VWLzmI3jazZ5LDae5NkPsO8lj8qF5ta5Ya0oscI2+YlQYAAFCLiFGFgowUeZnZYmPbxZ6Tv+8g1oCi1xwtTmdUhbnKvM19lttHpbNAK9kmUAzJ194QMQIAIIFDabZuTo0ZM0YOPvhg2X777aVdu3Zy4oknypIlS8o+79FHH5W99tpLmjVrJvvtt588/fTTEiYiRgGqtOdX6PHFque6jdYU+9lNHaNy9zs9tlKPQTyu23LXmZdZXEFUgi4WDXUbnSq172L7KLfNcsfMZwdhe/HFF2XYsGGmcbR161a58sor5ZhjjpF33nlHWrZsWfA5c+fOlVNPPdU0qo4//niZMmWKaVAtWLBAevToIWGgYQQAQIKENZQ2Y8aMej9PnjzZRI7mz58vRxxxRMHn3H333XLsscfKL37xC/PzjTfeKDNnzpT77rtPHnjgAQkDdYxiNEPMTWTFj961254uPVn4odLrKirXYxC5eEh3HaNej11itY7R/B/eJcuXL6937E2bNjW3UpYuXSp77rmnLFy4sGj0p0uXLjJy5Ei5+OKL6+679tpr5fHHH5e33npLwkCOEQAACZKxmF+UqY0Yde7c2TS6sjcd+iqlpqbGNHYOP/zwkkNiK1askPbt29e7T3/W+8PCUFoZlfTebEVpvMxscRvNKTaLqNRj3B5D1Go/IZ7iECkqNYut3Iw9IkXwSoeBbI0FZWr/LRQxKkVzjRYtWiQvvfSSxA0NIwAAUFKrVq0cDwMOHz5cnnrqKZkzZ4506tSp5GM7dOggK1eurHef/qz3h4WGURl+9t7CrC4dRn5D/srjQBCCXDPNxj7JMYKNZTz0f7a25ZSmLI8YMUKmTZsms2fPlq5du5Z9Tu/evWXWrFn1cow0+VrvDwsNIwAAEiSsWWnDhg0z0+2nT59uahll84Q0J6l58+bmv8844wzp2LFjXY7SRRddJH379pU77rhDjjvuOJk6darMmzdPJk6cKGGhYWRpTahCv/PrGJzsq9JjCSJC5mW2HRDmLC8/ozlRjBRF8ZgQXePHjzf/9uvXr979kyZNkiFDhpj/XrZsmTRo8N95X3369DGNqauvvtrUPdJZbDojLawaRoqGEQAACaKzyapCWEQ24yDjW4fY8v34xz82t6igjlEIktYLc1vN2EmtJGokxZvbyuxhC6Kqth/bQDwEXceoxx9/IQ1b2KljVP3lJll08ljfjz1KiBgBAJAgGu6wNl0/I6lDxCjmvT2bs2Hifi7SctxI9vsa9+NH+BGjfaZeZjVi9M4pt6UqYkTlawAAgFoMpVUoKr25MKr5lqvIHZceb9SPD3ZrdDl9vt/Io0NSp+snBQ0jAAASJKxZaUlBwyhEcauWSw8XfnFz7Xi9vhoNWvX1f0yQULBWGhBtNIwAAEgQZqV5Q8MoRDZ7hrZ7mZVUo45ST5foVbzzgiq5/spVp8/e33pgNNbqc3q85e6vZB9Itq8bRrZyjCR1mJUGAABQizpGIbLZM3S7Dz/25fQY6MWiGDfXhtfryOYafVzTiFIdo26/HyUNWzSzss3qLzfK0tPHUMcIAAAgjcgxiuAstCBXAY9qpCiMtasQ/ozL/Gul1Da8RkGjnOMHeKHDQLaGgjKSPjSMAABIEAo8ekPDKEDlerhOH+9lX35EXPxYr83tNuixR4vXvJ8gr/2orklIFBQIBw0jAACShLE0T2gYhSCKlW69zBgrlhfCLDS4FbVrpFieXP7vs8hbQiRYHEqTFA6lMSsNAACgFhGjEIRZU6iYUvv2OsvHj1l2RKHixUZkxc/oTLHrqdjPxfICw7Dm6T3rVfTO/px7H9KFJUG8oWEEAECCMCvNGxpGCYkk2YqgFNqO7ShNqbyNOK7LhvKCnKnoJU/Or8f7UbG72NpvRIkAb2gYAQCQJBrlIfm6YjSMIrxGmpuepa0ISqkojq013SqpfE2ECOVEIe/MxjGU2wZ5doC/aBgBAJAgJF97U5XJpOdlZ1ce7ieDpFFV49COo9zsGj96gjYqW8ehZxqnY0VxQcyscrIemx/7KrcP6oElz9bMFpkt031foT77Hbfrr6+RBi2aWdlmzZcb5aOhN/p+7FFCHSMAAIBaRIwS3uPeOr2t773MbE+20aBVBXv49HTTy+l7X8k1EsR15bV2UqFjLJZLWOzzg/gLOmLUZeJoqxGjZefdkKqIETlGAAAkTWpCHvYRMULFORFEgpA0lV7T5Z5XSa0uJEcoEaPmliJGXxExAgAAMUbla29oGAUgjut8BVk7qRJxOIeIP6c5RpWsI8g1DEQTDSMAAJJEE2RsJclkJHVoGAWg3IrdkIorYdPrTj6/3mMn1d2L7fPZT94y/w7YpaejfRTaDtcs/KPDX7aGwKokbahjBAAAUIuIUYQFWafFz/ox2ZpKXuqzVJrbgfjn23mNDhZ7npcq8AN2cfZ4rk+EgqE0T2gYAQCQJDSMPKFhFGGlepu2okmVPN9tD55KvsjyErWJQj0tp/uwcUxOayMRlQLsomEEAECSaO0hW/WHMulLvqZhFFO28iy89Dor7anS000fm+95sRmKfu6z2Lb9nHlq87iDXDsR4dP1LGytaZFJ4VAas9IAAABqETFKiXKRIj9zIpz28JFcYbznfu7TzyiU0207eVzdNgdmH0u+XyqQfO0JESMAAIBaRIwiwM/ZJ05ruPhZx8iPvBJEm5/vV1Krnvt5rpAyKUu+3n333eX111+XHXfcsd79q1evlgMPPFDef/99V9ujYQQAQIJUZb6+2dpW1H344YdSXV29zf2bNm2Sjz/+2PX2aBhFQCUrc1caGQqiJ29T0qICaWFr1qTXxwYljGPyI08QiJMnnnii7r+fffZZad26dd3P2lCaNWuW7Lbbbq63S8MIAIAkSUny9Yknnmj+raqqkjPPPLPe7xo3bmwaRXfccYfr7VZlMumpUrB27VrTouwng6RRVeOwDwc+KldnBohrZChMUTqWONma2SKzZbqsWbNGWrVq5ft3XOe7bpQGzZtZ2WbNVxtl+SXX+H7sXnTt2tXkGO20005WtkfECAAAxNYHH3xgdXs0jGIaBYlTjzWMXiY9WiQldypK13KUjqWU1Ee2UjKUlkvzifT22WefSU1NTb3fPfjgg+IGDSMAAJIkZQ2j66+/Xm644QY56KCDZOeddzY5R16QYxQhqe/lxDQ6lUacZ+84h+kReI7RHZZzjC6Ndo6RNoZuu+02Of30061sj4gRAABJkrKI0ebNm6VPn/oTblK1JMj9999vpuA1a9ZMDj30UHnttdckKbTnSO/RG85hvM6zRk3yZxDa2oaNbXs9hlLScq0G+T4gnc4991yZMmWKte3FKmL0yCOPyMiRI+WBBx4wjaJx48bJgAEDZMmSJdKuXbuwDw8AgPClbEmQjRs3ysSJE+W5556T/fff39QwynXnnXcmN8dIG0MHH3yw3HfffeZnzTzv3LmzjBgxQq644orY5xjBHvI37OFcBoPznNxzHHSOUZfbbrKaY7TssqsjnWN05JFHFv2dJmI///zzyYwY6Rji/PnzZdSoUXX3NWjQQPr37y8vv/xyqMcGAADC8cILL1jdXkUNo3nz5skll1wiDRs2lMsuu0wGDhxo7j/ppJNk2rRp4ofPP//crH3Svn37evfrz4sXLy74HF1ATm+5rWmkQ/76UYV+F0d+9nijVEenVM2ucufAyzkKM2oT5+syLlJzjkNMvp4zZ46MHTvWBDI+/fRT0ybILt1RyOzZswtGfPS5HTp0kDBU1DC68MIL5cYbbzT/rUNYTz31lNx7772yevVqiZIxY8aY+gYAAMB/GzZskJ49e8rZZ58tP/jBDxw/T3OFc4fq3OQNa8OqVO2iQIbSmjdvLscee6z57+9+97vys5/9TL73ve/Jl19+KX7RNVA0QrVy5cp69+vPxVqVOuymydq5ESPNSQIAAPZ973vfMze3tCH0jW98o6J9HnDAAfV+3rJli7z55puyaNGibRaX9a1hpLk9K1asMA2SJk2amFli99xzj1x66aXiF91Pr169TMnvbFhOk6/15+HDhxd8TtOmTc0tLki+DHZILY7n289jDXPoKX87pbZXbl9ejiVO1wJQjMZOqiwNpVVJMLRxo6kvPXr0kOuuu04OP/xwx8+96667Ct6v21m/fr3/DSNNcvrLX/5iGiq5NGq0bt068ZNGf7T1p2W/DznkEDNdX8N2Z511lq/7BQAgzdbm5ejaCjxo1WoNruj3ujaMfvOb30i/fv3k1VdflQMPPNDTtn/yk5+YtsLtt9/ub8NIh9C0EXTzzTfXS4zWxslLL70kV111lfhl8ODBsmrVKhk9erSJWGkLc8aMGdskZMcVvVX/IhVhnVu/o1JhLSxse59c+0C06xh1zktDufbaa01Exqvu3bubW5ZWsH7vvfdMFOj3v/+9p23rjHUtBh1IxOiMM86QmTNnmkqTH3zwgZxzzjnyzW9+04zp+U2HzYoNnQEAkHo+zEpbvnx5veRoP9NUNMqjgRan8pO8tTyjzmrTGfTXXHON/w0jbc1pA+iCCy4wYS7N89EZajpt3+uKtkgHG1EUGxGGIBaq9TsSQqQFaWY7Iht2BDbKWrVqFViBR21j6BCbU1rUMj8PWqNQN9xwgxxzzDHBJF//85//NC2xTp06ySeffGKm2emMtJYtW1ayOQAAkIA6RuvXr5elS5fW/ayjStrQadOmjXTp0sXMFv/444/ld7/7nfm95gp37dpV9t13X7O0h+YY6fT6v/71r473OWnSJLHJdcPolltuMWOL5513ninipCfg9NNPN+uTPPTQQ9K7d2+rB4jkiXtPzObxx3FmHBDVWadeP0eFnp8/ozUOn1WdkWZtVlrG3eM1aJJbsDFbMkcnTk2ePNkMcS1btqzeqhY6o10bSy1atDBtCV3zrNQyH8VoUcl//OMf5r+1ofWtb31LAmkY3X333fL444/X1SnQqXW6wv2VV15pMslzK00DAID06Nevn8nxKUYbR7k0DUdvXnz22WdyyimnmCra2VpIWnBaG1dTp06Vtm3b+tswWrhwoSm2mEtXstXo0fHHH+92c4ArNnqGUYrSuO3xRnW5iyCOH+Gdy/x9ldp3mMfl9hicPC6W12qIQ2lh0IXktVzQ22+/LXvvvbe575133jFRKp1F/4c//MHfhlF+oyhX37593W4OAADYlLKG0YwZM8zwW7ZRpPbZZx+5//77g0u+BsIShR6gHz3iIOoxRaHn68fCr2kV5Llys7Cw0+Pyc3Zq0hYYRmk6O15HrvLpffo7txq4fgYAAIh88rWtW9Tpmq0XXXSRmSWfpcncl1xyiRx11FGut1eVKZUllcCS5lrvoJ8MkkZV27YuAds9yHKP8doLLVR3xa+eLTVekFR+R4O2ZrbIbJkua9as8bUWUPY7rusNv5QGFVR8LqRm40b5YPRVvh+7F1p88oQTTjA5RtkK3XqfTg574oknTGkhNxhKAwAgSXxYEiTKtDG0YMECk2e0ePFic5/mG/Xv37+i7RExAkLgdyTJy76jum3Em81ro1htoTCvv1L7DjxidN3NdiNG110ZyYiRFoLUJcJeeeWVbY5Nj1dX6tAFar/zne+42i45RgAAIHbGjRsnQ4cOLdhg0wbi+eefL3feeafr7RIxQuqkLarhtnJv2s4P/Mmry3J7HUXt+rNxPEFHjHa/1m7E6P3roxkx2nXXXc1U/dxp+rl0WE2n6+dW2naCHCMAAJIkJXWMVq5cWXCaflajRo1k1apVrrdLwwip49dsrbCrcRfbhts8JjdVhKPWu4c/3LzP5R6z5uk9zb+tB77reh9urzc/ayUhfB07dpRFixZJt27dCv7+73//u+y8886ut0uOEQAASWKzhlFGImvgwIFyzTXXyMaNG7f53VdffWUWvK9kqTJyjIAIrPtV6baoLYS48PPz4jWvKX+7Trbh5vUEnmN09c3S0FKOUbXmGN0UzRwjHUo78MADpWHDhmZ2Wvfu3etyi3Q5kOrqajONv3379q62y1AaAACInfbt28vcuXPlwgsvlFGjRkk2zlNVVSUDBgwwjSO3jSLzfCJGSCI3vTmveQtRy7OxfTxRe31AmIp9HqJUx2j3qyxHjH4ZzYhRri+++EKWLl1qGkd77rmn7LDDDhVvi4gRAACItR122EEOPvhgK9siYgQEWI3aay5RsV5qqW0S8UGQbF1vlcxSc7rvYjPj/BJ0xGiPK+1GjN67OfoRI5uYlQYAAFCLoTSkjtt6P/m/D2OtsWL7slFXJkqIbsVfkO9do0G1xfsmuNt3NlLkR+0kxB8NIwAAkiQlla/9Qo4RUq+SWSZet+30eVlB9lbpIadX0t77KNQaCyPHqNsVdnOMlt5CjhEAAEAqMZSG1CuXv1Oup1hqhkulPdViFX2DkJRoAbZV7lp2GjUNMs/Or2rUbo8jdrM+UzMWZB8RIwAAgFrkGAEBCGNVcMApm/l0WeTFhZhjdPnN0rCppRyjTRtl6a3pyjFiKA0AgASpynx9s7WttKFhBATAbU/WRj5D1HvRiA4b14jXvDgv12sY1zifr+SiYQQAQJJQx8gTGkZAGWHMzCnGTYXeIGfwVCoKxwC76wQ6nc1Z7nqt5Nrw+ll1MwPOTb5g9eaNIg9Ol6AwlOYNs9IAAABqETECKuwBV5LnU+nq306eZyuPyQu3vXwiRdFQaWV2P9+/SuoYeVlTsNzjKv0s6+91VlqgGErzhIYRAABJQsPIE+oYIdEq6dmWqmRtax9et+Vln25fnw2V9raRPmHUQyp0Pdq8RoOuY/TNkXbrGP3zTuoYAQCAmCL52hsaRki0Snp7bvJ6Kt1HVrG8JS+zaMo9t1ikyM8oTv42iRRFk5droNLnFnteJTlGlQpyX4g+GkYAACQJOUaekGMERCjHJozcG/J90suP9z7O13Chqt028uKCzjHqfpHdHKMld6crx4g6RgAAALUYSgNC6J16rbdi47iC7NkTlYqWIPPJ3B5LJcdmO6/Jxj7CRPK1NzSMAABIEnKMPCHHCIhRRCXMyAszdhA3NqJRbvMBC90fdI7RXiPs5hgtvjddOUZEjAAASBCG0ryhYQREYC0xt2s5ler5+hVVIkqUHFGOUNrcp436WVFYg9A1htI8YVYaAABALSJGgE+VfAs9t9LepJv1o7zuw8nzmWUWbzZWm/eLmxXuozizNBKIGHlCxAgAAKAWESPAQfVbr9vK7126XeE+iN6pn3VjEK6kRPiKzQBz+zwnM8ncRGmjpqr2ZmtbaUPDCACAJGEozRMaRki0UmsfFeO1Z+jk+U4jRV7yfmzkSFXyfERPFGef2WDreAttJ4oV5BEMGkYAACQIdYy8oWGERItqL85pVMZL3o/TSr1Ot4d4qKRCua21xpweVyX79MLPfXuN0PqCoTRPmJUGAABQi4gR4CO3la699DrLPdfWqudetgn/BVGHykYFaT8jLPnbLnUNV7rNyH8WUhjpsYWIEQAAQK2qTCaTmnZlduXhfjJIGlU1DvtwgLL8jCDZEKm8CiCi1d63ZrbIbJnu+wr12e+4HufdLA2bNLOyzerNG2XRxCt9P/YoYSgNAIAkIfnaExpGQAiCiLSUy+lwewyFZjsRKYr2tWGrtlW57ZfaZrF9ljsWJ8daLGeo0tl4lczoQ31z5syRsWPHyvz58+XTTz+VadOmyYknniilzJ49W0aOHClvv/22dO7cWa6++moZMmSIhIUcIwAAEljHyNbNjQ0bNkjPnj3l/vvvd/T4Dz74QI477jg58sgj5c0335SLL75Yzj33XHn22WclLOQYIVGCzHkJYzX6MHJ6mI0GL7xGdWxEccLO1Qs6x2i/c+zmGC3838pyjKqqqspGjC6//HL5y1/+IosWLaq775RTTpHVq1fLjBkzJAxEjAAAQChefvll6d+/f737BgwYYO4PCzlGSBQ/a6I0GrSq3jpnfq5Gv+bpPevty8l2ij2nUsw4gx/Xhtvn2Mgx8pJjFWT9pSgvCbJ27dp69zdt2tTcvFqxYoW0b9++3n36s+7vq6++kubNm0vQiBgBAJDEWWm2biImKVqH6bK3MWPGSFIRMQLKqOsZTpDAciKKRX1K9Va3Tm9b+192IkaIP7+iG1GJuLid0eZ0pmapbUc5UuSn5cuX18sxshEtUh06dJCVK1fWu09/1n2FES1SNIwAAEgSH+oYtWrVypfE8d69e8vTTz9d776ZM2ea+8NCwygCKq3zgWix8X55WY/Jae/Z6XFy3cVfmO9huaiNn3/fnO6jVESJv7+VWb9+vSxdurTedHydht+mTRvp0qWLjBo1Sj7++GP53e9+Z35/wQUXyH333SeXXXaZnH322fL888/LH//4RzNTLSw0jAAASBA/kq+dmjdvnqlJlKWFG9WZZ54pkydPNkUfly1bVvf7rl27mkbQJZdcInfffbd06tRJfvOb35iZaWGhjhE8S1vPqtjsLxuzwsI8l2l7H5Mqex3anKEY5vXjJmfPz6rhXgRdx6jnGXbrGL31u3StlcasNAAAgFoMpcGzuEcYnPYM6x43sPDj3MwKy6+/UunaY6V60257vFHMR4F7QUaJcvn13vlZLywq0SnbqjIZc7O1rbSJRcToww8/lHPOOceMRer0vT322EOuvfZa2bx5c9iHBgBA4usYpUksIkaLFy+WmpoamTBhgnTr1s2sqTJ06FCzWN3tt98e9uEh5mxFVGz2HL3MHIvjLMcoHhOin7tjex9BRqcQXbFoGB177LHmlrX77rvLkiVLZPz48TSMAACIyKy0JIhFw6gQzZDXugilbNq0ydyy8td68UuUe+YI533Of6yf0ac4RYpsSPrri1skpdw+3VarrgTXAhKfY5RPi0fde++9cv7555d8nK7lkru2i671AgBAopFjFN86RldccYXceuutJR/zj3/8Q/baa6+6n7ViZt++faVfv36mCJTbiJE2jqhjZAc9dal49ln+7zmHX+N8JOecxPW4/RB0HaMDT/2l1TpGC/5wVarqGIU6lHbppZfKkCFDSj5G84myPvnkE1NRs0+fPjJx4sSy29dF7mwtdAcAAJIvNpWvNVKkjaJevXrJQw89JA0bNnS9DSpfI6495Lj0vuNynEkS13Putn5Y7mPLRWIl7RGjUyxHjKYSMYpko0iHznbddVczC23VqlV1v+vQoUOoxwYAQJQwKy0FDaOZM2eahGu96QJzuWIS8EIKVDI7zea2k16bJgr7jKK4vn6ns9MKPS6urxnxEItZaZqHpA2gQjcAAJCDWWnJjxj5gd4mnEpqfZUoHUslwqycHMW/H34cUxh5c0Qd7UjjEFiqIkYAAABBiM2sNBuCmpWW5F5IUNJyDt2+zrScl6AFeV6j8B5G4Rj8WPE+qoKeldbrxzdJo8Z2ZqVt3bJR5j96dapmpRExAgAASHuOEQAAScR0fW8YSouBMKdCB73fIM+N7QTbOA1PBIVzAgQ/lHbQD+0Opc17jKE0AACAVCJilPKeOD16JFXcr223S2YUelzcz0FSBB0xOvgkuxGj16elK2JEjhEAAEliszBjRlKHhlFM2eoB+rmMhV/bQXLkXxNxK6ZZ7vi9vB6nzyn1OD5rgHs0jAAASBBmpXlDwwiRik7ZQnQqXE7PfyXLQdiOYOZuy+223b6+QvvPf4zTY1jz9J7m39YD33V0rEgRTR22lT6cSV/LiFlpAAAAtZiVFlNERJB2Nj8DlW4rjEVW+ezHT9Cz0g79/o1WZ6W9+uQ1qZqVRsQIAACgFjlGMWWrt+hn5ei4SdrrSTqb75PTbeXn9QQZKcri+kRZTNf3hIYRAAAJwqw0b2gYpSxSke3xbp3e1vM+49RzdbL2W5xeD8LJ78l+bkSYCQYkFQ0jAACShOn6ntAwioAgZ538t8fr/77CUOz1JOX1JZWb6zCIa9ZrVNHL+mVcq/CKoTRvmJUGAABQizpGCee2d+1Hb9xNPRbbx5u0iFhUReE8O60EnZtvls/pmmdRqKGE+Ai6jlHvY2+wWsfo5RmjqWMEAACQRkSMUpJnEaVeabHed+595Z6D8jhnziNGcT5HvM/RF3TEqM8AuxGjuc+mK2JE8jUAAElSk/n6ZmtbKUPDKELc9PjisFK90xliTrZLb9g9zlnh6zAOnx03eJ8Bu2gYAQCQJCwJ4gkNo4T0GvNn5ATZSw6idhB5FOlQyfsc5bpAXK8IQ5XF+kNVkj7MSgMAAKhFxMilSiraBtFrzK/dYnOfjQat+vo/JhT+fRAz4/yoH4Po8XJtVFpHK/d3xWoclXqum2MotS2uaVjDkiCe0DACACBBWBLEGxpGLpXq1VXac630cUFxWkm43PHaeD1ROScItlp1IU6jNjYilOW26SXPiQgSEC00jAAASBJmpXlCw6hC2V5dXf6Ng16v0x5gJfk0tmsNuRHGjDckQyWRIlu5bYWeH2Tl9TBzEgEUR8MIAIAEqcpkzM3WttKGhpHXXt2E4I+l5PG43Jaf0Sgb6D2jUsXygwr93mt+UiV5Qlzb8E1N7c3WtlKGOkYAAAC1qjKZ9MTJsisP95NB0qiqscSZ7SiOk+1FMd8niscEu9L6HldaKylt5ykOtma2yGyZ7vsK9dnvuCO+M1oaNWpmZZtbt26UOX+7wfdjjxKG0gAASBJmpXlCwyimbNc+sbk2le3nlXouveNkKHVtxPk9rqRSfqWvO87nCYgSGkYAACQJS4J4QsMoIWzVXylViTi7jexj3M7I8xKVCqJ+DJIt/30vtVaa01piNtb/A2xjSRBvmJUGAABQi4hRCGxGLMqtHO62inapSsR12xjo7bjd1Hip9HWUQk89eoJ4TyrJT/N6HcZtlicSgqE0T4gYAQAAa+6//37ZbbfdpFmzZnLooYfKa6+9VvSxkydPlqqqqno3fV6YiBj5KIiZVMV6sE73WSynyElVYFuc5HoEdSyIXrXqSq/dID5nXq5xt2u9ZddlzL5eIk4opqrm65utbbnxyCOPyMiRI+WBBx4wjaJx48bJgAEDZMmSJdKuXbuCz9H6SPr7LG0chYmIEQAASRxKs3Vz4c4775ShQ4fKWWedJfvss49pILVo0UIefPDBos/RhlCHDh3qbu3bt5cwUfk6ZVVz6WUirfzM7atk7cEsPovJF3Tl636HXGW18vXs134py5cvr3fsTZs2NbdcmzdvNo2gP/3pT3LiiSfW3X/mmWfK6tWrZfr06QWH0s4991zp2LGj1NTUyIEHHig333yz7LvvvhIWIkYAACSx8rWtm4h07tzZNLqytzFjxmyz288//1yqq6u3ifjozytWrCh4qN27dzfRJG00PfTQQ6Zx1KdPH/nXv/4lYSHHKIargnudDRM0olRwy48cNz9ngQZ9HEApVZmMudnalioUMbKhd+/e5paljaK9995bJkyYIDfeeKOEgYYRAAAoqVWrVmWHAXfaaSdp2LChrFy5st79+rPmDjnRuHFj+da3viVLly6VsNAwiiA/84G85EQUq5WUnS2zdXpb348fyebkmnFau6vc8yqJSpVb1yyIGmVOn2frOBBDIdUxatKkifTq1UtmzZpVl2OkQ2P68/Dhwx1tQ4fiFi5cKAMHDpSw0DACAABWjBw50iRbH3TQQXLIIYeY6fobNmwws9TUGWecYRKtszlKN9xwgxx22GHSrVs3k6A9duxY+eijj0xCdlhoGEWYl2hOtsZLNoqTv003az+VXf27bs20+vVk3NZpcfMcJJObatTl7vdzvT23x+akLlEl27L5PCSIBnks1TESl4GnwYMHy6pVq2T06NEm4fqAAw6QGTNm1CVkL1u2TBo0+O+8ry+++MJM79fH7rDDDibiNHfuXDPVPyw0jAAASBA/kq/d0GGzYkNns2fPrvfzXXfdZW5RQh2jAMU158ZW3RU/1j0DnPLjenOb1+THPhB9Qdcx+u63rpBGDS3VMareKM+/cYvvxx4lRIwAAEgSU3/IVvK1pA4NowC47fHZ7GXmC7P+ShDrntG7jjabERa327KxTT/riXldH5BrH2HPSksKKl8DAACkOWL0n7MPlXaTFgS2vzB6cFHuNVbSs3X6nCi/blT2/lWam1bJtWArfy4KEVmkmM5Is7VAfY2kTiobRgAAJFXYs9LiLpUNozYPvioSwqy0SkU1d6DS47LZk2emW/rWC/RzBli543FaQ6nU87hGgWhLZcMIAIDEIvnaExpGMaiJEvbsrUorDfuh2EwjeuHRZDOqWGnUJkyFjimKxwngv2gYAQCQJESMPKHytUVpi1rYqojt9zaRLMU+Z27vd7PtSn9faM3CcseRtr8jaRB05euj9r5UGjVsamWbW6s3yax/3JGqytfUMQIAAKjFUJpHNleGDzJvyUbv2o8eLb3kdPJy7WejMjsOrJ9vluVmm+Vy1pzWTqp7ft0xtXU8k5K8OXhGHSNPaBgBAJAg1DHyhoaRR5X05oLMo3E7gycqvVPb68vZjOzBvmKRy0K/KxaVKfb4MK67Us8vFhGyEcUF4B0NIwAAkoRZaZ4wK61M3kLrge9KEhV7fWH0TonmIKoq/TyUel65iLHT3/NZiY+gZ6X13+Niq7PSnntvHLPSAAAA0ih2Q2mbNm2SQw89VN566y1544035IADDvBlP1GIFPm5Cn2x11fqebZ6z6XWwQLKRRG9ro/nZv0yt9d6NhIr04s/xunMtkp/DzCUlrKI0WWXXSa77LJL2IcBAAASKFYRo2eeeUb++te/ymOPPWb+O2psj/3bXIU+X7E8hlKvodLXVa4XXiiCRB5FutmoUl3u8b7UIKubIfeu42NkNhrssxgxkvRFjGLTMFq5cqUMHTpUHn/8cWnRooXjYTe95SamAQCQaAylJb9hpBPnhgwZIhdccIEcdNBB8uGHHzp63pgxY+T666+XoFTas3PaMywUWam0l+mmjpEfM3OcHIvNfSBavLxvla415jV/yM2+3OzDVk0xPgtAAnKMrrjiCqmqqip5W7x4sdx7772ybt06GTVqlKvt6+N1imH2tnz5ct9eCwAAkVCTsXtLmVDrGK1atUr+/e9/l3zM7rvvLieffLI8+eSTpqGUVV1dLQ0bNpTTTjtNfvvb31qvYxQEt5VvczmdkeM12lPJc4EwlKs95qS2ULGZk358BsgtSo/A6xh1+ak0amCpjlHNJnlu2a9SVcco1KG0tm3bmls599xzj9x00011P3/yyScyYMAAeeSRR8zUfQAAgNRWvtYco65du7quYxS1iFEY/OiNuu1d2zwGet1wKshrwkntLq7N9Ag8YtT5QrsRo+XjUxUxil0dIwAAgFTPSsu32267mZlqiEZv2e02bR6DrRk9SI9K1kF0+/nxYwYc4JhJmLb0HVmTvu/aWDaMAABAEdQx8oSGUYTzFSqpmmtrzScgbpzO6gxjHURmeQLxQcMIAIAkMSNptiJGkjo0jCoURF2TStZXinJvtJLcDiCI6tOVbtvG85lBCesYSvOEWWkAAABxrmNUKZt1jAr18oKolusXeq1IGqdV4cOqwYX0CLyOUbtzpVGDJla2ubVmszz32W9SVceIoTQAAJKEoTRPaBhVqFCPMYq9yGwPt9GgVSXze6J47IDNWkI2Hut0jcIg1kzkMwv4g4YRAABJQsTIExpGMeBlnaW6x02QWHDaEyfXI31svdeFPk+2j8XN9el0ZinXOhAMGkYAACQJS4J4QsMIAIAEyWRqzM3WttKGhlGAKh3+sVEczu2+w1rCwGlyK8MKiGtxVgDRRsMIAIAk0YRpW0NgGYbS4CObU33dJpDajFIFIez9A25wvSJSTGOGhlGlWBIEAACgFhGjEJRboqBYYbfc+8PsoTJVHnES9es16seHGKqpEamylDSdSV/yNREjAACAWkSMQuR0JkvUepIUX0ScRP06jPrxIYbIMfKEhhEAAAmSqamRjKWhtEwKh9JoGKWsp+hnNGfN03t+ve2B9IABAPFEwwgAgCRhKM0TGkY+ikKuTZAVeVsPfNe3bQNREIXPNFCWFnesomFUKWalAQAA1CJi5CO365L50Qu1WW0bSDs+P4gFE+WxVccoI2lDwwgAgATJ1GQkY2koLUPDCG7ZWIU+iF4oPV2kVRDRUiKyQHKQYwQAQJJo7SGbN5fuv/9+2W233aRZs2Zy6KGHymuvvVby8Y8++qjstdde5vH77befPP300xImIkYe0UMEoo2ILBCcRx55REaOHCkPPPCAaRSNGzdOBgwYIEuWLJF27dpt8/i5c+fKqaeeKmPGjJHjjz9epkyZIieeeKIsWLBAevToEcprqMqkaABx7dq10rp1a+kng6RRVeOwDwcAkAJbM1tktkyXNWvWSKtWrfz/jqs6ydp33FY99sw0x8eujaGDDz5Y7rvvPvNzTU2NdO7cWUaMGCFXXHHFNo8fPHiwbNiwQZ566qm6+w477DA54IADTOMqDAylwVUeRW5OldfHAQCSM5S2efNmmT9/vvTv37/uvgYNGpifX3755YLP0ftzH680wlTs8UFI1VBaNji2VbZYKwqaJtWbN9b1IGw8DgDSwHznBDjDy+Z33NbaY9doVK6mTZuaW67PP/9cqqurpX379vXu158XL15ccPsrVqwo+Hi9PyypahitW7fO/PuShJvYFVsPTrf7OABI2XeQDnX5pUmTJtKhQwd5aYXd77jtttvODIfluvbaa+W6666TJEpVw2iXXXaR5cuXy/bbby9VVVUSR9pq1wtUX4efY9Vpw3n1B+fVH5zXeJ1XjRRpo0i/g/yks7o++OADM6RlUyaT2eY7Mz9apHbaaSdp2LChrFy5st79+rM22ArR+908PgipahjpWGenTp0kCfRDyx9E+ziv/uC8+oPzGp/z6mekKL9xpLcwNGnSRHr16iWzZs0yM8uyydf68/Dhwws+p3fv3ub3F198cd19M2fONPeHJVUNIwAA4J+RI0fKmWeeKQcddJAccsghZrq+zjo766yzzO/POOMM6dixo5mery666CLp27ev3HHHHXLcccfJ1KlTZd68eTJx4sTQXgMNIwAAYMXgwYNl1apVMnr0aJNArdPuZ8yYUZdgvWzZMjN6k9WnTx9Tu+jqq6+WK6+8Uvbcc095/PHHQ6thpGgYxYyO62rSW6HxXVSO8+oPzqs/OK/+4LzaocNmxYbOZs+evc19P/7xj80tKlJV4BEAAKAUCjwCAADUomEEAABQi4YRAABALRpGCbBp0yaT+a8FuN58882wDyfWPvzwQznnnHOka9eu0rx5c9ljjz1MMqbtgmlpcP/998tuu+1maqrowpKvvfZa2IcUazq9WRfn1AK1ukq51onRFcth1y233GL+lubW1UG60DBKgMsuu8z3iqppoev5aEGyCRMmyNtvvy133XWXWeFZp5HCuUceecTUM9FG5YIFC6Rnz55mYcjPPvss7EOLrRdffFGGDRsmr7zyiimAt2XLFjnmmGNMjRjY8frrr5vP/v777x/2oSBEzEqLuWeeecZ8AT322GOy7777yhtvvGGiR7Bn7NixMn78eHn//ffDPpTY0AiRRjfuu+8+87M2NnWphREjRsgVV1wR9uElgtaK0ciRNpiOOOKIsA8n9tavXy8HHnig/OpXv5KbbrrJ/B3V4oRIHyJGMabryQwdOlR+//vfS4sWLcI+nMRas2aNtGnTJuzDiA0ddpw/f77079+/7j4t6KY/v/zyy6EeW9KuS8W1aYdG47Tycu51i3SiwGNMaaBvyJAhcsEFF5jS65obA/uWLl0q9957r9x+++1hH0psfP7551JdXV1X6TZLf9ahSninETjNgTn88MNDrRCcFLoMhQ756lAaQMQoYnSYQRP/St30y0W/rHW15lGjRoV9yIk6r7k+/vhjOfbYY01FVo3MAVGKbixatMh8ocOb5cuXm/W6Hn744dAWX0W0kGMUwbyBf//73yUfs/vuu8vJJ58sTz75pPlCz9JeesOGDeW0006T3/72twEcbfLOq64OrT755BPp16+fHHbYYTJ58uR6a/ug/FCaDu3+6U9/qlthW+nCkqtXr5bp06eHenxxp0st6DmcM2eOmT0Jb3RdrpNOOsn87cz9W6p/W/Vzr7N+c3+H5KNhFFO6EN/atWvrftYvcp31o19GmvjaqVOnUI8vzjRSdOSRR0qvXr3koYce4o9iBfQa1JW1NbKZHfrp0qWL+VIn+boy+qdak9enTZtm1pvSxTbhnUbeP/roo3r36Urwe+21l1x++eUMVaYQOUYxpV8yubbbbjvzr9bdoVHkrVGkkaJdd93V5BVppCmrQ4cOoR5bnOhMSY0Qaf6bNpB0do9OK9cvHFQ+fKarkGu0SGsZ6crlqnXr1qbmFiqj5zK/8dOyZUvZcccdaRSlFA0jIIfWh9GEa73lNzAJrjo3ePBg06gcPXq0+QLXqc8zZszYJiEbzmnJCKUN91yTJk0yEzEA2MFQGgAAQC0ySgEAAGrRMAIAAKhFwwgAAKAWDSMAAIBaNIwAAABq0TACAACoRcMIAACgFg0jAACAWjSMAAAAatEwAgAAqEXDCAAAoBYNIwD16OKvHTp0kJtvvrnuvrlz50qTJk1k1qxZoR4bAPiNhhGAetq2bSsPPvigXHfddTJv3jxZt26dnH766TJ8+HA56qijZMSIEdKmTZttVnkHgCSoymQymbAPAkD0DBs2TJ577jk56KCDZOHChfL6669L06ZNZdGiRbJs2TK57bbbZPbs2WEfJgBYRcQIQEG33367bN26VR599FF5+OGHTaNI9ejRQ1q0aBH24QGAL2gYASjovffek08++URqamrkww8/DPtwACAQjYLZDYA42bx5s/zkJz+RwYMHS/fu3eXcc881w2nt2rUL+9AAwFdEjABs46qrrpI1a9bIPffcI5dffrl885vflLPPPjvswwIA35F8DaAeTag++uij5YUXXpBvf/vb5j4dSuvZs6fccsst5r8feugh+c9//iMdO3aU559/Xrp06RL2YQOAFTSMAAAAajGUBgAAUIuGEQAAQC0aRgAAALVoGAEAANSiYQQAAFCLhhEAAEAtGkYAAAC1aBgBAADUomEEAABQi4YRAABALRpGAAAAtWgYAQAAyNf+H9xjQZP3m/43AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ---- Discrete modality -------------------------------------------------\n", + "discrete_samples = samples[\"discrete\"].cpu().numpy() # shape (N, 2) integer tokens\n", + "vocab = vocab_size\n", + "\n", + "# Plot a 2‑D histogram of the discrete samples\n", + "plt.figure(figsize=(6, 5))\n", + "plt.hist2d(\n", + " discrete_samples[:, 0],\n", + " discrete_samples[:, 1],\n", + " bins=vocab,\n", + " cmap=\"viridis\",\n", + ")\n", + "plt.title(\"Discrete modality samples (token histogram)\")\n", + "plt.xlabel(\"Token 1\")\n", + "plt.ylabel(\"Token 2\")\n", + "plt.colorbar(label=\"Count\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# ---- Continuous modality -----------------------------------------------\n", + "continuous_samples = samples[\"continuous\"].cpu().numpy() # shape (N, 2)\n", + "\n", + "# Plot a 2‑D histogram of the continuous samples\n", + "plt.figure(figsize=(6, 5))\n", + "plt.hist2d(\n", + " continuous_samples[:, 0],\n", + " continuous_samples[:, 1],\n", + " bins=200,\n", + " cmap=\"viridis\",\n", + ")\n", + "plt.title(\"Continuous modality samples (2-D density)\")\n", + "plt.xlabel(\"x₁\")\n", + "plt.ylabel(\"x₂\")\n", + "plt.colorbar(label=\"Count\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "flow_matching", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/2d_multimodal_flow_matching.py b/examples/2d_multimodal_flow_matching.py deleted file mode 100644 index 1ee82f9..0000000 --- a/examples/2d_multimodal_flow_matching.py +++ /dev/null @@ -1,393 +0,0 @@ -# A simple 2D Multimodal Flow Matching model -# This notebook trains and evaluates a multimodal FM model that jointly handles -# a discrete modality (categorical data) and a continuous modality (real‑valued 2‑D data). - -# %% -# Imports and device setup -import time -import torch -from torch import nn, Tensor - -# flow_matching core components -from flow_matching.utils.multimodal import Flow -from flow_matching.path.scheduler import ( - PolynomialConvexScheduler, # discrete scheduler (training) - CondOTScheduler, # continuous scheduler (training) -) -from flow_matching.path import MixtureDiscreteProbPath, AffineProbPath - -# visualization -import matplotlib.pyplot as plt - -# %% -# Device -if torch.cuda.is_available(): - device = "cuda:0" - print("Using GPU") -elif torch.backends.mps.is_available(): - device = "mps" - print("Using MPS") -else: - device = "cpu" - print("Using CPU") -torch.manual_seed(42) - -# %% -# ------------------------------ -# 1️⃣ Discrete modality utilities -# ------------------------------ - - -def inf_train_gen_discrete( - n_grid_points: int = 128, - batch_size: int = 200, - device: str = "cpu", -) -> Tensor: - """ - Generate a batch of discrete (categorical) samples. - Returns a tensor of shape (batch, 2) with integer token IDs. - """ - assert n_grid_points % 4 == 0, "grid size must be divisible by 4" - n_grid_points //= 4 - - x1 = torch.randint(low=0, high=n_grid_points * 4, size=(batch_size,), device=device) - samples_x2 = torch.randint( - low=0, high=n_grid_points, size=(batch_size,), device=device - ) - - x2 = ( - samples_x2 - + 2 * n_grid_points - - torch.randint(low=0, high=2, size=(batch_size,), device=device) - * 2 - * n_grid_points - + (torch.floor(x1 / n_grid_points) % 2) * n_grid_points - ) - return torch.stack([x1, x2], dim=1).long() - - -class Swish(nn.Module): - """Swish activation (x * sigmoid(x)).""" - - def forward(self, x: Tensor) -> Tensor: - return torch.sigmoid(x) * x - - -class SharedTransformer(nn.Module): - """ - Shared Transformer trunk used by both modalities. - """ - def __init__(self, hidden_dim: int = 128, nhead: int = 4, num_layers: int = 2): - super().__init__() - encoder_layer = nn.TransformerEncoderLayer(d_model=hidden_dim, nhead=nhead) - self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) - - def forward(self, x: Tensor) -> Tensor: - """ - x: (seq_len, batch, hidden_dim) - Returns transformed tensor of same shape. - """ - return self.transformer(x) - - -class DiscreteTransformerModel(nn.Module): - """ - Model for the discrete modality with separate input and output heads, - sharing a common Transformer trunk. - """ - def __init__( - self, - shared_transformer: SharedTransformer, - vocab_size: int = 128, - time_dim: int = 1, - hidden_dim: int = 128, - length: int = 2, - ): - super().__init__() - self.shared = shared_transformer - self.vocab_size = vocab_size - self.time_dim = time_dim - self.hidden_dim = hidden_dim - self.length = length - - self.time_embedding = nn.Linear(1, time_dim) - self.token_embedding = nn.Embedding(vocab_size, hidden_dim) - self.input_proj = nn.Linear(hidden_dim + time_dim, hidden_dim) - self.output_head = nn.Linear(hidden_dim, vocab_size) - self.activation = Swish() - - def sample_shape(self, batch_size: int) -> torch.Size: - return torch.Size((batch_size, self.length)) - - def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor: - return torch.randint(low=0, high=self.vocab_size, size=shape, device=device) - - def forward(self, x: Tensor, t: Tensor) -> Tensor: - """ - x: (B, length) integer token IDs - t: (B,) time scalar - Returns logits of shape (B, length, vocab_size) - """ - if t.ndim == 0: - t = t.unsqueeze(0).expand(x.shape[0]) - - # Token embedding - x_emb = self.token_embedding(x) # (B, length, hidden_dim) - - # Time embedding - t_emb = self.time_embedding(t.unsqueeze(-1).float()) # (B, time_dim) - t_emb = t_emb.unsqueeze(1).expand(-1, self.length, -1) # (B, length, time_dim) - - # Concatenate and project - h = torch.cat([x_emb, t_emb], dim=-1) # (B, length, hidden_dim+time_dim) - h = self.input_proj(h) # (B, length, hidden_dim) - - # Transformer expects (seq_len, batch, hidden_dim) - h = h.permute(1, 0, 2) # (length, B, hidden_dim) - h = self.shared(h) # (length, B, hidden_dim) - h = h.permute(1, 0, 2) # (B, length, hidden_dim) - - # Output logits - h = self.activation(h) - logits = self.output_head(h) # (B, length, vocab_size) - return logits - - -# ------------------------------ -# 2️⃣ Continuous modality utilities -# ------------------------------ - - -def inf_train_gen_continuous(batch_size: int = 200, device: str = "cpu") -> Tensor: - """ - Generate a batch of 2-D continuous points from a checkerboard-like distribution. - Returns a tensor of shape (batch, 2). - """ - x1 = torch.rand(batch_size, device=device) * 4 - 2 - x2_ = ( - torch.rand(batch_size, device=device) - - torch.randint(high=2, size=(batch_size,), device=device) * 2 - ) - x2 = x2_ + (torch.floor(x1) % 2) - data = torch.stack([x1, x2], dim=1) / 0.45 - return data.float() - - -class ContinuousTransformerModel(nn.Module): - """ - Model for the continuous modality with separate input and output heads, - sharing a common Transformer trunk. - """ - def __init__( - self, - shared_transformer: SharedTransformer, - input_dim: int = 2, - time_dim: int = 1, - hidden_dim: int = 128, - ): - super().__init__() - self.shared = shared_transformer - self.input_dim = input_dim - self.time_dim = time_dim - self.hidden_dim = hidden_dim - - self.time_embedding = nn.Linear(1, time_dim) - self.position_proj = nn.Linear(input_dim, hidden_dim) - self.input_proj = nn.Linear(hidden_dim + time_dim, hidden_dim) - self.output_head = nn.Linear(hidden_dim, input_dim) - self.activation = Swish() - - def sample_shape(self, batch_size: int) -> torch.Size: - return torch.Size((batch_size, self.input_dim)) - - def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor: - return torch.randn(shape, device=device) - - def forward(self, x: Tensor, t: Tensor) -> Tensor: - """ - x: (B, input_dim) positions - t: (B,) time scalar - Returns velocity vectors of shape (B, input_dim) - """ - if t.ndim == 0: - t = t.unsqueeze(0).expand(x.shape[0]) - - # Position projection - x_emb = self.position_proj(x) # (B, hidden_dim) - - # Time embedding - t_emb = self.time_embedding(t.unsqueeze(-1).float()) # (B, time_dim) - - # Concatenate and project - h = torch.cat([x_emb, t_emb], dim=-1) # (B, hidden_dim+time_dim) - h = self.input_proj(h) # (B, hidden_dim) - - # Transformer expects (seq_len, batch, hidden_dim) with seq_len=1 - h = h.unsqueeze(0) # (1, B, hidden_dim) - h = self.shared(h) # (1, B, hidden_dim) - h = h.squeeze(0) # (B, hidden_dim) - - # Output velocity - h = self.activation(h) - velocity = self.output_head(h) # (B, input_dim) - return velocity - - -# ------------------------------ -# 3️⃣ Build multimodal components -# ------------------------------ - -# ---- Discrete side ------------------------------------------------- -vocab_size = 128 -added_token = 0 # uniform source distribution → no extra token -vocab_size += added_token -length = 2 # 2 tokens per sample - -# Shared transformer trunk -shared_transformer = SharedTransformer(hidden_dim=128, nhead=4, num_layers=2).to(device) - -discrete_model = DiscreteTransformerModel( - shared_transformer=shared_transformer, - vocab_size=vocab_size, - time_dim=1, - hidden_dim=128, - length=length, -).to(device) -discrete_path = MixtureDiscreteProbPath(scheduler=PolynomialConvexScheduler(n=2.0)) - -# ---- Continuous side ----------------------------------------------- -continuous_model = ContinuousTransformerModel( - shared_transformer=shared_transformer, - input_dim=length, - time_dim=1, - hidden_dim=128, -).to(device) -continuous_path = AffineProbPath(scheduler=CondOTScheduler()) - -# ---- Assemble modalities dict --------------------------------------- -modalities = { - "discrete": { - "model": discrete_model, - "path": discrete_path, - # loss omitted → Flow will use MixturePathGeneralizedKL automatically - }, - "continuous": { - "model": continuous_model, - "path": continuous_path, - # loss omitted → Flow will use MSE loss automatically - }, -} - -# ------------------------------ -# 4️⃣ Instantiate the multimodal Flow model -# ------------------------------ - -flow = Flow(modalities=modalities) - -# Optimizer (optimises both modality models) -optimizer = torch.optim.Adam(flow.parameters(), lr=1e-3) - -# ------------------------------ -# 5️⃣ Training loop -# ------------------------------ - -lr = 1e-3 -batch_size = 4096 -iterations = 30001 -print_every = 3000 -epsilon = 1e-3 - -source_distribution = "uniform" # for the discrete modality - -start_time = time.time() -for i in range(iterations): - optimizer.zero_grad() - - # ---- Discrete data ------------------------------------------------- - x1_disc = inf_train_gen_discrete( - n_grid_points=vocab_size - added_token, - batch_size=batch_size, - device=device, - ) - if source_distribution == "uniform": - x0_disc = torch.randint_like(x1_disc, high=vocab_size) - else: # mask case (not used here) - raise NotImplementedError - - # ---- Continuous data ----------------------------------------------- - x1_cont = inf_train_gen_continuous(batch_size=batch_size, device=device) - x0_cont = torch.randn_like(x1_cont) # isotropic Gaussian prior - - # ---- Sample a common time tensor for both modalities --------------- - t = torch.rand(batch_size, device=device) * (1 - epsilon) - - # ---- Sample from each path to obtain x_t --------------------------- - disc_path_sample = discrete_path.sample(t=t, x_0=x0_disc, x_1=x1_disc) - cont_path_sample = continuous_path.sample(t=t, x_0=x0_cont, x_1=x1_cont) - - # ---- Build the inputs dict expected by Flow.training_loss ----------- - inputs = { - "discrete": (x1_disc, disc_path_sample.x_t, None), # dx_t is None for discrete - "continuous": (x1_cont, cont_path_sample.x_t, cont_path_sample.dx_t), - } - - # ---- Compute total loss and back‑propagate ------------------------- - loss = flow.training_loss(inputs=inputs, t=t) - loss.backward() - optimizer.step() - - # ---- Logging ------------------------------------------------------- - if (i + 1) % print_every == 0: - elapsed = time.time() - start_time - print( - f"| iter {i+1:6d} | {elapsed*1000/print_every:5.2f} ms/step | loss {loss.item():8.3f} " - ) - start_time = time.time() - -# ------------------------------ -# 6️⃣ Sampling from the trained multimodal model -# ------------------------------ - -flow.eval() # switch to eval mode for sampling -samples = flow.sample(batch_size=200_000, device=device, steps=1000) - -# ----------------------------------------------------------------- -# 7️⃣ Visualisation -# ----------------------------------------------------------------- - -# ---- Discrete modality ------------------------------------------------- -discrete_samples = samples["discrete"].cpu().numpy() # shape (N, 2) integer tokens -vocab = vocab_size - -# Plot a 2‑D histogram of the discrete samples -plt.figure(figsize=(6, 5)) -plt.hist2d( - discrete_samples[:, 0], - discrete_samples[:, 1], - bins=vocab, - cmap="viridis", -) -plt.title("Discrete modality samples (token histogram)") -plt.xlabel("Token 1") -plt.ylabel("Token 2") -plt.colorbar(label="Count") -plt.tight_layout() -plt.show() - -# ---- Continuous modality ----------------------------------------------- -continuous_samples = samples["continuous"].cpu().numpy() # shape (N, 2) - -# Plot a 2‑D histogram of the continuous samples -plt.figure(figsize=(6, 5)) -plt.hist2d( - continuous_samples[:, 0], - continuous_samples[:, 1], - bins=200, - cmap="viridis", -) -plt.title("Continuous modality samples (2-D density)") -plt.xlabel("x₁") -plt.ylabel("x₂") -plt.colorbar(label="Count") -plt.tight_layout() -plt.show() diff --git a/examples/2d_riemannian_flow_matching_flat_torus.ipynb b/examples/2d_riemannian_flow_matching_flat_torus.ipynb index 8a80135..0f93e5d 100644 --- a/examples/2d_riemannian_flow_matching_flat_torus.ipynb +++ b/examples/2d_riemannian_flow_matching_flat_torus.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "id": "rb5VSo4mNkVd" }, @@ -25,14 +25,13 @@ "import time\n", "import torch\n", "import math\n", - "import numpy as np\n", "\n", "from torch import nn, Tensor\n", "\n", "# flow_matching\n", "from flow_matching.path import GeodesicProbPath\n", "from flow_matching.path.scheduler import CondOTScheduler\n", - "from flow_matching.solver import ODESolver, RiemannianODESolver\n", + "from flow_matching.solver import RiemannianODESolver\n", "from flow_matching.utils import ModelWrapper\n", "from flow_matching.utils.manifolds import FlatTorus, Manifold\n", "\n", @@ -44,7 +43,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -59,6 +58,9 @@ "if torch.cuda.is_available():\n", " device = 'cuda:0'\n", " print('Using gpu')\n", + "elif torch.backends.mps.is_available():\n", + " device = \"mps\"\n", + " print(\"Using MPS\")\n", "else:\n", " device = 'cpu'\n", " print('Using cpu.')" diff --git a/examples/2d_riemannian_flow_matching_sphere.ipynb b/examples/2d_riemannian_flow_matching_sphere.ipynb index c5d8a58..5b3cac2 100644 --- a/examples/2d_riemannian_flow_matching_sphere.ipynb +++ b/examples/2d_riemannian_flow_matching_sphere.ipynb @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -59,6 +59,9 @@ "if torch.cuda.is_available():\n", " device = 'cuda:0'\n", " print('Using gpu')\n", + "elif torch.backends.mps.is_available():\n", + " device = \"mps\"\n", + " print(\"Using MPS\")\n", "else:\n", " device = 'cpu'\n", " print('Using cpu.')" diff --git a/examples/README.md b/examples/README.md index 3b6af05..c607c2e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,4 +18,4 @@ | [2d_discrete_flow_matching.ipynb](2d_discrete_flow_matching.ipynb) | 2D discrete flow matching example on the checkerboard dataset using the flow_matching library. | | [2d_riemannian_flow_matching_flat_torus.ipynb](2d_riemannian_flow_matching_flat_torus.ipynb) | 2D Riemannian flow matching on a flat torus on the checkerboard dataset and the flow_matching library. | | [2d_riemannian_flow_matching_sphere.ipynb](2d_riemannian_flow_matching_sphere.ipynb) | 2D Riemannian flow matching on a sphere on the checkerboard dataset and the flow_matching library. | - +| [2d_multimodal_flow_matching.ipynb](2d_multimodal_flow_matching.ipynb) | 2D multimodal (discrete-continuous) flow matching on the checkerboard dataset and the flow_matching library. | From f71b2b10da37dfadb99e0cf0cfda0f071caa5de4 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Thu, 18 Sep 2025 12:43:57 -0700 Subject: [PATCH 04/44] Clean up multimodal.py --- flow_matching/utils/multimodal.py | 113 +++++++++++++----------------- 1 file changed, 49 insertions(+), 64 deletions(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 054fd6f..7c39b10 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -1,33 +1,34 @@ -""" -Generic multimodal flow matching class. - -This class aggregates multiple modalities, each with its own model, path, -scheduler, and loss. It provides utilities for training (computing the total -loss) and inference (sampling) across all modalities. -""" +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the CC-by-NC license found in the +# LICENSE file in the root directory of this source tree. import torch from torch import nn, Tensor from typing import Dict, Optional, Tuple, Callable, Any -# Flow matching components +# flow_matching +from flow_matching.loss.generalized_loss import MixturePathGeneralizedKL +from flow_matching.path.mixture import MixtureDiscreteProbPath from flow_matching.path.scheduler import Scheduler from flow_matching.path.scheduler.schedule_transform import ScheduleTransformedModel from flow_matching.solver import MixtureDiscreteEulerSolver from flow_matching.solver.ode_solver import ODESolver from flow_matching.utils import ModelWrapper -from flow_matching.loss.generalized_loss import MixturePathGeneralizedKL - -# Attempt to import the discrete probability path class; if unavailable, the user -# must provide a compatible path object. -try: - from flow_matching.path.mixture import MixtureDiscreteProbPath -except Exception: # pragma: no cover - MixtureDiscreteProbPath = None # type: ignore def _default_continuous_loss(pred: Tensor, target: Tensor) -> Tensor: - """Mean squared error loss for continuous modalities.""" + """ + Mean squared error loss for continuous modalities. + + Args: + pred (Tensor): predicted velocity field. + target (Tensor): target velocity field. + + Returns: + Tensor: mean squared error loss. + """ return torch.mean((pred - target) ** 2) @@ -35,19 +36,20 @@ class Flow(nn.Module): """ Generic multimodal flow matching model. - Parameters - ---------- - modalities : dict - Mapping from modality name to a dict with keys: - - "model": nn.Module (or ModelWrapper) that implements the velocity model. - - "path": a probability path object (e.g., MixtureDiscreteProbPath for discrete data, - or any continuous path implementation). - - "loss" (optional): a callable loss function. If omitted, a default loss is chosen - based on the path type. - training_scheduler (optional): Scheduler - Scheduler used during training. - inference_scheduler (optional): Scheduler - Scheduler used during inference (sampling). + This class aggregates multiple modalities, each with its own model, path, + scheduler, and loss. It provides utilities for training (computing the total + loss) and inference (sampling) across all modalities. + + Args: + modalities (dict): + Mapping from modality name to a dict with keys: + - "model": nn.Module (or ModelWrapper) that implements the velocity model. + - "path": a probability path object (e.g., MixtureDiscreteProbPath for discrete data, + or any continuous path implementation). + - "loss" (optional): a callable loss function. If omitted, a default loss is chosen + based on the path type. + training_scheduler (Scheduler, optional): Scheduler used during training. + inference_scheduler (Scheduler, optional): Scheduler used during inference (sampling). """ def __init__( @@ -75,9 +77,7 @@ def __init__( # Choose loss function loss_fn = spec.get("loss") if loss_fn is None: - if MixtureDiscreteProbPath is not None and isinstance( - path, MixtureDiscreteProbPath - ): + if isinstance(path, MixtureDiscreteProbPath): loss_fn = MixturePathGeneralizedKL(path) else: loss_fn = _default_continuous_loss @@ -91,18 +91,13 @@ def training_loss( """ Compute the total training loss across all modalities. - Parameters - ---------- - inputs : dict - Mapping from modality name to a tuple ``(x_1, x_t)`` where ``x_1`` is the data at - time ``0`` and ``x_t`` is the data at the sampled time ``t``. - t : Tensor - Tensor of shape ``(batch,)`` containing the time values. - - Returns - ------- - Tensor - Scalar loss (sum of modality losses). + Args: + inputs (dict): Mapping from modality name to a tuple ``(x_1, x_t)`` where ``x_1`` is the data at + time ``0`` and ``x_t`` is the data at the sampled time ``t``. + t (Tensor): Tensor of shape ``(batch,)`` containing the time values. + + Returns: + Tensor: scalar loss (sum of modality losses). """ total_loss = 0.0 for name, (x_1, x_t, dx_t) in inputs.items(): @@ -110,9 +105,7 @@ def training_loss( path = self.paths[name] loss_fn = self.loss_fns[name] - if MixtureDiscreteProbPath is not None and isinstance( - path, MixtureDiscreteProbPath - ): + if isinstance(path, MixtureDiscreteProbPath): # Discrete case: model should output logits. logits = model(x=x_t, t=t) loss = loss_fn(logits, x_1, x_t, t) @@ -135,19 +128,13 @@ def sample( """ Generate samples for each modality using the inference scheduler. - Parameters - ---------- - batch_size : int - Number of samples to generate. - device : torch.device, optional - Device on which to run the sampling. - steps : int, optional - Number of integration steps for the ODE solver. - - Returns - ------- - dict - Mapping from modality name to sampled tensor. + Args: + batch_size (int): Number of samples to generate. + device (torch.device, optional): Device on which to run the sampling. + steps (int, optional): Number of integration steps for the ODE solver. + + Returns: + dict: mapping from modality name to sampled tensor. """ xs: Dict[str, Tensor] = {} for name, model in self.modalities.items(): @@ -177,9 +164,7 @@ def sample( # Set up ODE solver. solver = ODESolver(velocity_model=velocity_model) - if MixtureDiscreteProbPath is not None and isinstance( - path, MixtureDiscreteProbPath - ): + if isinstance(path, MixtureDiscreteProbPath): class WrappedModel(ModelWrapper): """Wrap velocity model to output probabilities.""" From c313c43dca5328cd3b192489b2b1ef6130cab3d2 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Thu, 18 Sep 2025 13:11:13 -0700 Subject: [PATCH 05/44] Finish documenting new example --- README.md | 2 +- docs/Makefile | 1 + docs/source/_images/multimodal.png | Bin 0 -> 73099 bytes docs/source/dummy.rst | 1 + docs/source/notebooks.rst | 6 ++++++ examples/README.md | 2 +- 6 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 docs/source/_images/multimodal.png diff --git a/README.md b/README.md index 0e7b5b7..903cf32 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ pip install -e . #### I want to train a Flow Matching model, where can I find the training code? -We provide [training examples](examples). Under this folder, you can find synthetic data for [continuous](examples/2d_flow_matching.ipynb), [discrete](examples/2d_discrete_flow_matching.ipynb), and [Riemannian](examples/2d_riemannian_flow_matching_flat_torus.ipynb) Flow Matching. We also provide full training [examples](examples/image) (continuous and discrete) on CIFAR10 and face-blurred ImageNet, and a scalable discrete Flow Matching example for [text modeling](examples/text). +We provide [training examples](examples). Under this folder, you can find synthetic data for [continuous](examples/2d_flow_matching.ipynb), [discrete](examples/2d_discrete_flow_matching.ipynb), [multimodal](examples/2d_multimodal_flow_matching.ipynb), and [Riemannian](examples/2d_riemannian_flow_matching_flat_torus.ipynb) Flow Matching. We also provide full training [examples](examples/image) (continuous and discrete) on CIFAR10 and face-blurred ImageNet, and a scalable discrete Flow Matching example for [text modeling](examples/text). #### Do you release pre-trained models? diff --git a/docs/Makefile b/docs/Makefile index 05c2bb7..ef4da3c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -13,6 +13,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) links: mkdir -p source/notebooks && ln -sfn $(ROOT_DIR)/../examples/standalone_flow_matching.ipynb source/notebooks/standalone_flow_matching.ipynb mkdir -p source/notebooks && ln -sfn $(ROOT_DIR)/../examples/2d_discrete_flow_matching.ipynb source/notebooks/2d_discrete_flow_matching.ipynb + mkdir -p source/notebooks && ln -sfn $(ROOT_DIR)/../examples/2d_multimodal_flow_matching.ipynb source/notebooks/2d_multimodal_flow_matching.ipynb mkdir -p source/notebooks && ln -sfn $(ROOT_DIR)/../examples/2d_riemannian_flow_matching_flat_torus.ipynb source/notebooks/2d_riemannian_flow_matching_flat_torus.ipynb mkdir -p source/notebooks && ln -sfn $(ROOT_DIR)/../examples/2d_riemannian_flow_matching_sphere.ipynb source/notebooks/2d_riemannian_flow_matching_sphere.ipynb ln -sfn $(ROOT_DIR)/../assets/teaser.png source/_images/teaser.png diff --git a/docs/source/_images/multimodal.png b/docs/source/_images/multimodal.png new file mode 100644 index 0000000000000000000000000000000000000000..7260fd1c1b3b53f5ea34a706a9e16ec5ca24646e GIT binary patch literal 73099 zcmZs@by$>b*F8)LDD5cSC?Pq3bSfcAh_s{(A<_)p2-4}$t#nIw#}LvWDK)^*-T7VU zeLwH>9KY}2FJ#oaHUjZi~d<_x;ws=<`R zBMl59cu0f6n|nVvP*S;PgW$f@Jt?Wk032n_Na;s}L@Vq#@qr<|M{&qAt@N+QSsdu(r>Xpj^y5w7GVDGr{0Enzy9*iBXQ}_5_!qne7{Hj=OsSF zG7`f$|My-4A=-4gjCGqw_T8EU2O|IffT?j9q= zENALW^w#J2LUYCZd;}B_XXpLBcvvVD@E#kEls0U zobM}g1@vsHb$=N6Sk^51r#@C9jOVvb?Wf3*nmyfOh!8O}FRG07IPcT9swh@3FqWlu zG0G6DcaUjz+cwvq`IUS#{izanPTd8Qwc)gJw=%_X+6Qy*}Mvek-axyD5+Qg=ekn~K=S)tyY6Z%+++R{eIE(ETkK zk5-^}VFFv6b3vv^i|g0ee1KSaT_I>@@(YST@&C40frzbqytrEe{|I*RRrLb3s~_4$ zu|-W68+=YXIx}v&joZsGrKRerpQNqwi}w>7lTvJGp6(Y{M-uS;$8YUvPSg%bZT zO>?pH9;In=^TH1$Ue}vU^v!#1I3W-oy0_mS`gonkd6gax2$#Gw6?#nVaWs6=PIEIf zAbxxG%`z4Zgf@ELC++qMdGf2eY5Zm_;wG1rr$1G6#&O1yA8~a(35hr^$|IjY!khjE zErf_t1}vxDtfy_^Sv&h${%FONmS9yc*2++w?Of`eL>+w**d(`#y=vpe=wDD}7dVgL5?5|=QK1#}ux zE!;VH@}b`Y){~f@%G#w*drNE3 z^_SFd9V0rrv`0iHd@XU0Pgz@|AEa*xz)7M6!^H{afzSl^ylGyKNC z$B7|>>SOJBnABCX)_k+sY|(suQm*8&q2kzfpJ|fjW|3yPe(sn2i8sRf-bO_+dl)E zl7skm9^Y=1+0p5DO>Ml_>T^48p07e6^uJ0x7Ni=*jm zY02n(!IyxXXX9$GkvbN$jK1gadb#=bdUxBY`BGQBAU>)|L)-Fs(sT1)p%#}v_%1%< za4=)id+n2WfeY#Id5hK5wX&Rp31~icimG;bgHu`EY{sKlJXY)HuZw?sUT#xO$!TF@ z`&?kp+QO+ipS`v6ugNnt9{vzOWg<&y>vxUxy2+uNF$`hf5Vm*=W9NPA%dzsw8~ew! zpY0S%fvTD%;aSA(S%e|CGcv^~b!eIYq;c0{u(;{s6Xfy3=G##v@y7Tqt%{FPx{g!E zpt|jcUI$EGhOk8IoUS|PbKcEAG@Z$QNx}|T6x2Lz)URCz%BaR&Ybq(X69z$x* z2rGiTPiLV@Zml?a2KrY^BuO?*G^3=b3gj#JRy4~cuHyw;*SlU`rE#aBJOlB$i@>R1 zuPRr-?gGW>*abB%Judly6u&(`X+Vk&@&PCN67F>c-~VcQQ)&;IQQ(`(6o@JWUVj42 z6bUNtbeeN9U5sN|{W;gX{FD>Sk%`-}cK}Pf>d-r_n;!0^8pu_>Fr z+ni{=sid;&rt1s}g-ro_u@jRjAab#madPT)d)oO`efpkEMGQs1<|#-?u7LcFi9xe6u#~FK|3I=-Y#1 z)i?8u%PCno5`i%Q@k;YnBuI0(wR3@_`Knk;W>$RF- zN^EyqZnN0-T}j7fC8qNi+Mb~{Dt!A}0JU8u8?t7PcRh>K!)%x{2!;q1As;_ym-0|5 z7s5cr?gs1QUd5gNrd4FmDYtOBwZY?lPie}ag6xLhz zJvZBJJpS1qYa#MwP_Y-c(Cs4|2hgPn6vB zZw}((ZivaGzFi)dcu_F|c`}^<+kFc{!!sv*3hYuVX>+EUjb$KfnDd&$)jpg8X+MB^ z^^1%oYXWy!XEc8x2#17&tr#|@#n+VZCsi`_eghK8W2Cscl*d^+4c`DH`K18zddpP& zrSa!n@l>Wp6t76+qR1i<1JjI~JAiWwdo1Z?8{D2v~y% zLR(^DU~6@XWcoi0ByG3X9YwsA7^ld1z}Z*gx5!&k7<%E847P^|&SHm9c7Ze=F}Vo3 z3kNv&TK1y|)H6a7q4iRe&x_KWu2H4j2?wv-wktKj)S+d(zjw%kZ_N^mrHIhNEEv55 zDMIhb4nLe@~b}zql2Eu z=8eiQzS}#!TeHqGl~cGJ(Y2EM5~xe=5v_!BDrHe*f` z85v#q*}~-`GgRAgta>P*D87;af zZ!?s`($0TfII!!BXt6b1!jmU;-%DCP^)7Mj@b+txp7{Zxtu|;yLvK!Ihn0`@JRtz{ zoGnncdc#nO9V>iV89?osbX=LZ^W84D1Tc>)t96>gE-V_{>>(f0z$QEQ5i$>TT{~Y1 znh$EID=ei&-cHm*#P(Ic9cNyA@f=)!ilm=@m1g`kt_J7J9+t?>F2Hj-9Z9O{xPs)w zGxRo)W>1imv5@loz#Gz^A(FKG+55>9eKEwqfp@}a+7GjSvzlhIAMh zO=ddxfII!Fr9yh=Lk;Wq!!G3nA=q87EmLSG)~HLl$WPSze;4mI zCxJ)q}dx+rH$bC1ost8-1H81&T?r%M3Q`?gVvS~}-@a&2+bTSan z=N%YKDJ5u@t0`7$dCye3{HPnW+rke~5X$l26zVTbB_2Ily7bkT+j)OIffxYnGnVg&07w~@Hnkr@p!ZsZg-U`J4hFp2??(y)}L=f2eX z=98e=;aF4%={^HusikvZf5ksmP4^5=G)qRUL$+OC*^JVGR~;KbUP421%%91oR(*PP@< z_MQYtJ}-!<5s+?-U(I{u3&IS?y$ON^F>JX^xB3xWIC$c$Wp#keE?Q(LEP3++Z#sKL z{3>RGL@#rybGdS{_T4W&!q-6*+?XQr0|p_KLpuNV0CVK1Q`{EfOYNq*?R;~7x?~bDj}>pXgUEjHbik`307~ld zwW&RmX5bn;g_}fFr0{f238RU zb>~N*7GKJXaG4lBWq}>)P}Ick(GJ3eE6#Jouk*lji0Ky1=tJu%U%_57*pNSPK4$Z~ zxU=wF-)lM)*7lg{3<&I-9e=jPqc_=O#(3&gBUVxGZ^-xOh{`eS8MC$2_?NP4-VEMe z5iCYOgrIaehf2GkbO`vBl7h-a2#sFcdl`;Z5PD__wnB(`BiaN$A5fN-`;>)!1(TI= zFHQe^V8RIQ>lKdLPmi~yfwVoVuk(PpQSL2%Acc@!@FUT8yqwqib~%dz_9-C`cg$)` z7k%s~=N)){t%yk0=Z1K0NTp)cgbr$w$;}0MHZA&75DRPRU?TZnc%35rQVBKb0U~x5R8Cl50`tSOnoyUD=m6|IcS_Slg=cSOp0qpfl}uc zZEBSbsH>L-6S}q<2%f>**>RE&6#YFTs5%1#zKOs$ol95zLT4@adV)#RC2B%_ z?%Q!_a`-}D28)8UmWO?FT$2}~WDUzY4wEffeVN!P2^c3D3N;3Lu9_})lKhB3@5cs9 z^AD8RM21_%asp^LA@z^p{!HErZQdKF;R4iC`@-fcK9UxiZhf3#sLKF_R8_#$kJ zzjl*NDxHFw;-kaq&YBL0B|uG6v4%xzY+aT^jcalYo^zNjL6_dk z{}jVCk?->TsJ<#ovpo`;GrU;$poE@U2*e7C8q>(qt`2P?jj&G8{oT~lZoC5{-5lb8 z2aEMS6JnGcb*nDhr>gR#K5}#v`o(UJ#mxvKoga$mc^Mi~%m2N(`u*UnlkzJ3OI0Mv zeIITNUNcqy*J;MMnE9Bm3k4RSOaIzk2czsx3AK`t1)5;m)WaYztMudrK z*=%Hb;6B2U?4h|J=(Eieen=uT)_6O1&YqseaN$zS7BgzPal}%MkfYYl(4>2ebsaq_ z=KOnJcFJ~vUeBN3Q{}anY;<)wvY`c!RO$ZuCN932(zNVe%tL*l58v~)oI0!{#kN_4 zT4H)8#$*$4sg^$Hbbv(Xo_F{$qO)D#X^#)Rfhg{$?|z5xV(u&-a3`+|7yVkE_nryD z!iau^rc2xM4DILi@9uBtw$zL7Q&ail?HAo81YH%^1yNk;q4YxA%*P>07QF*>-35X1 zwVorzxp2W~k(Ir=j+!rzpU#qUI~q9Fqh6ka`}wR$0e%(bejEH6*zZfTA8OV%?}98jY1MBb`AqWZ8)S z=BFo&a{G%AZuK?pF36SKyzz6;_RAc6Tiv1&;mfV^uoxi`fmKwau`C+drTET+eEa%K z4vX~+hl}ipUFWx?rS$#I!4oVM6tGNWm>#^YBkkdJBIIQ~GvWg~K}X+3B~6^V`N8f( z;V42IAl{Z=;Rf=&9PIG}sT99Xea)rPFE1=^rAOa)|4R>M>K<~-&hDy{+iI=5yfEo8 zk+JGynyMXN(F)^}pRpMe;hIy**(!{uu=fd`AZ~Tb(&&`OeDWjUbLko9!O-gJAlHu+ zO3cn>?e7fL$A8!bW@T+(fhb^7@~T{|t?FX24|ra6#NJ4XFXpk5IK^&{VxB0eq*nQA zM&?Ar_#JQqeJ$^IsLNQ0(lTCH@i*UN{Q15p)`2H7XHKZ2g99B}HMWnzG(9AC;NV3g z*s!l0V0W?|PBKF&*vxg_h7!s&d&D%L;;^wDfDacr?t3DrwCDVwix2;T|Jt9Oa7}0~ z^BX0#pbMGe5f$U;9@AZMtc4NJyF}jgGEz~b`u(xhC!zBqr*~dd%|+@ip*m^8d3_Gr zMHe@k()}l#RsG=m9qp2g-+I-_>b;P)RV&NJNQKjJR;&WWF7C!Lcs}pkdRy>^s}`-& zPUjH!$CdT|ik+1O&*sXwn?1QPSL)u&ggX7m;sob8{?amites2>FRS}WBaopY6PghP z)%_Ajbj&^6>R31EzRJUzx5vf(5G2pa5`!JjyvRSi)0Ev&p}Oo0zF}PDHZ)`>ufD_s z@2r~?Qf$5&WfL%1`7WNGtzT}QVe$D_L`atiOK`6&u-%V_NhK1KUga<5iHXRHurDwM zQ9Ow~=1yphW7>=oq18zpnb}h8tS);g`HKYd^T@D)u>IIbi)yTH) zGS#`u_WLPJkG3!TX6%NI-hH`GcQOhEXM4RJAQ`L?NOfKc>a5SxZ0y+oV58_F-knxg zW1YIjrdwyfWNyb1k$lrGh2xQY_AZ#D(y{r=+QevKO9uX)ZNi~?!PVrD1^^|e?I?ah z(iYAE5?di-VRNe2V!8DkU>7({6qBuCH=robw&mBSqEM3S81{R{%?zHtlsx+5=*5Lr z@(31KmQX6vsclraif}JA=H3w=!C(t)CX9eLRDK@Vylc@VD-jBl*!DJANE{+Ch zR1gk4rKTWvgIs70z>6TP5U^gC-?B>_p2*W;WaxQ9F5)hdjAz5gn?$--b#)3c(OJ9F zMKByvq7R>;8R=*&vg}Xan>%dJ#3ZmJYtIr|UzYz3=7G3T2Um{fMEqJ$x@rmx3)}`&mtvO^W%j-S(TOevEoHLjurqA4a)E zA$g*w*!@-gBziS})x^~k$7qKF14&KAb%Sc{_@{(skwHQImm%Q2@ql=yewg+L^yd?JWx6m&sA^50>4p#&3bHuU2#3h1f0FT<2Hm>Xn?GQ}@M}@Q-fMFo z;I!yO&hAGaB{W}QUQc(PCwx7c?#CmxvkG5GA#}({OP!BAele?h@J@%(e0K02iH<>& z@q}?j&^DiKCkE1}i>gp{&vIqgF3*&`4xbM2r$t*Fk@tW*K~ zP@zm0NnV1ly#TvPJG&lHI!jrm#srgrg0OPWis|*z2RDQnQKnwsu_f!WXJPY?_3KvX z%rOqlM|BnP8tkRE@@I*0$>UWQL+II6>9G&_52NtRy)Jyyg6? zt%Kk)uXYmbY>@Zlc!4J6+0yT)$;-8J%YBb#8xA{h6lXiutCA}OhKDelRNKC)p723m zb}yr_f&yc5aCMHfN?SD0&d?Gr*RYV7DV!vGomlGBL!NrQeG4HWVodfMPd(uQk|?e^ zlzkkbBF9EgNl8ej+w`m$lQesLOB4e`s7c-}eN(h~v$;%q@Fda)tF-cP@HH_+7m$<3LL&axvlN3bHLDBy?!6Q@BVCv7H-l$3|`8%NdI{$ueAvx~>#02zJJbuTdv z{h@FbF6LCbqPQuck5$S%nyYcgGgi#?o9KRrngy5%D=~Tapr#lbJL;FY;CFZ z4Immr$1fZ)zVgMJ@R%#Dfkyq?3us&>m>%*;1Q1yGS78+@>)ho5(B7CQ#R17a9VCJ<+eu-uXvn4(6&pVq4TJJus!SsiT4UW<#=Y$t{_;`$z*i=ixFqAkkSn_&@;}~T5%D7;P$ys#28;o=kTUD&ogBqaWFnG zKrd8SCkLeWylK5dDZkGpW-_6JPSMJpEN4&R$;N?A5N8UQctIx47K;^M^UK%NnRtw) z>~JUz(y%LVQpzjYTu>GP(xIh(ZELxNDYU_tV$!w-ND8>E$*^L4?VKmDPDxh+#iKf^3;{odP^kX^D|f^P@6*>) z1`5hjimL~M9mSE!&|uDDFk`kjcix8M?C6g=e_PaMwJzJxl7vE_mPaNIK7cYx>juQU z0a}YDOHPAlEDwe0b*ib5IzH?$@lf`uu-5a>0g>{n73C!}{QErCb0P?*{>(=ZgRbaM zuXWSV{^4~$0fK|1RnL1meLI)ES1VoR#eLS1a*hWE=t@EJ4DbY7t@qgtgn@UV|9w~@ zkx53LP;h@Nr3cH3QLr?&zXMwC-=r~!$IN7?SUlP8LJse5-fSVWv1wglp(+r~4M?K$ zG9@a&-s@8!zF_$VPe&Kt(h6(4Cq|{SHx_EV#Fo@W!}R7x~ZdnMcdYg8+ zy#A2MA~yEm$k)-f)pYOmb+K$x>6iLYyCV^T(`{wSmq!rY=rslpO@Vc7}C$h5?6~B=2*b1WiMSI2oJR^ z6Jc5A@3W3;rQrWi$oG9VXdXrB+vE#x(S294f(K?a@RTC#$2t&)84U@+Q%?d%h%C9e*;%}w9d=<9%<((th}W4eID2)@8!Jb?tp=aKoRokoHQSoU(?Dkf z!Z*Q+2qhsM1?wHz-Ch$ak!8V$w&`;QrL+xA-DQ*Amoq+nQ9F8BXEiSsXN#wOL5Xug zS&#FPVTF(sMA58&pm=2xHR@?JSy8|Hl7zyd)F;2IJo@F4`HpCUJ!9HKCL(vQK!j+CS=+e3G^jZ>5*grZ0?~@e2`Rx*^CX5lkY;5?2gr|B`&1Dyx zs}%05LyW%X&41jD!9(CXQzZ3fJ31`^IL9ANsGTZZ9}pQl$5etk6Fe%v6cLSYv&nWC zFQi-w0`=YIIJ^vKuq_lbOu|$m_1G0FxIfvPhUXFdK=aoC_ccqt-uQz*!nYO_qJ?h{ zS>HKZqRyac7xfg3-_4{XxM;Hw!;tl|!g}iwU!*Oku&T2H8o~P7gz@5!#0a(629xwc z9d0_pySrGamu7TaC>S+nChJ0Pw5LYJy`SYYJh;bIPPC9!)ZiNyqu)T|^K_v>ILN88 z-@<6j^{-GZ{xt?La0qyArpJvThnsku9V1b~+o|E;NA#)ez7XaPf8ri14G#rypVYsF zm{-^|9%VplVDM&QZ=j4PF+N?h>is#T(SNgX)TpXL9Qlm;J%;Aj0b#f^=fhpCg?rQP zPd|P{S2qZwYw0ZlL~Dh-1$MplZ_f-L?$#Q*(ZWL2E}ntz^l`jCCRc|88rRpfaE~+BzI}rI z6u4nWfQB0&K5=$dfR3uGe*PJr8uOiMF@7C8_ZpZX?sGJE$*2ili-!HKsvgE9(%p)c zlmhqPWo&yNs6b{TAFnp*IL+4Pe& zA02Je=*ycOOcFV5L=5_BJYNW82Y=s6rBaF{kFQc@) zLa321)u!$K)EOC}F7Q?LEKt~P^R1^gAcM%r?Pf@OLRs{)Tk4Hhrv8ZdD59{T%Wnhy z@6Yubx(>AF#0GBzd9R!XLBF?VWMPj8<0mf&G3ieX0iQ*Ri=498)ruKKG(Mz|r9Ir8+ z#egCcKwcHGuiC#J4+&=pqiW0u7wT=gTH&PNtmIJpYLiR2ck}AjqO7@3g9nMX$V@qY zw|iUY+PBBeq+^+bR_?a_i-v0xv?JH)*>LI5A|WaY%ewV!$96pa$>$R~kUJH@wt}@Q zY(J*nv!YsG?710lxJAJ@`K@_XevwH*l@t%A@3?xkT1uO(zVY#Rzgd?7(X{yXOktIy zW9s2Nb_0jwTCDOX8V5ryMQVpSIoak+s2DS}cio<$dOX-%q2PGzc~1Ay`rzTOk6J2H zf$sYZ50&~Nb2>t(Y25cD5h+scLD+Rm!K_<=#E@@L3e$!yQ zFUq;d=#nUKaJ1T5rKF|!P$-rz#?|RzP;^@}m(D`7OJF14FndWW!C+_`ZLgu`~m+@R9*RPy)RT+oWcsKfT=#(0s2>$N1UQ*y&D z(0W_fc`_(?N)F7L79IfG%%N<2RVu>7q|S7@D>6MM2fuiQw76Rn+EamdiU}|{elsFN zxUOc`{e1F)d9cuZ#|Y6AuGkNUQvGHDRE}JF!3sBbD$Cb9lM`N3#&HT?_3e6|`n!L9 zOZH!SUwa=36$t(5s5CZ+u>sC|nj--Ydj}i$<^)M32LA7ah?8bd!pqC93f#Z|&w>PKTOCg+98qF8uUqY10065DP z(79l^!@ocCK0-%L&Arnmfr@)Lb!u$rm6iZGU>R_uKdxEqF{;2nb#;D76mkcb&oHuo z%#nDWJo6R8lPnMxl9sRBmcuP8-hs4o8dr{gOxQ_>V6{qLA z-r+}d0w~+clu3cN2WC7ae~)q1NIw8c-xsAPnLm9A4YsR>rm}{3>Q;v$kG?lp30*|+ zfn`QYx1P#!^5rlOo(BtM?jxm|RPwczay+x<+0bec%N|(N4zEthQN@xQB3d)_52Eah zl@$vWcjjHjLe_PtlyibA;86!_Gmrr5qtspa_oi$OP3Z0E^S6RD*l77u|KyAod71QD z}y@_vo{XH1LBs1)m@RE?{#{Hp!uplAJwsoWV{Q6ZV&e0FkF+F zVR(9u46Ht)c%dKksBa_lch2D&>q$;BWr`+&OAhlcCH2JhvR&AT$QO{I!~e# zuEk7EU=|-SQ5$bF$tYK(v3b9Ed%|E*7Ir#*B|URFUWzc81PEK{-?+f`QI_^b?LXKL z%Fw$no)PS`1c-wEwIQHjbETT`Gv;fDbGi>a8Oa=$7n=T5@Xr@f{|4npQlIsc8<_uM zRDCPMkaJjVjIf{!0@Z$v>oke-m$R@%>KIn2H)9wkZcY9su1}4zpM8bNW{^tsgaV=@ zbYmKMB*KUkY-D!Qb&iI~tV%V7Hc+Ohw&_*2SXs2>XRwRSs{_RuIP3)@r&hoOu zcP+=oF3j=aFd>7*M=QqbswHvGzvB+eE}kT%-tkaZYK_X*e`C#r3++a3lF|8b=wD*> zHx^ocFbs7#GhdLNobXYlF@`0|sNRrEFFf6??WE7rel0SukY(}Gw$A!AR?q=aAV(3= z<1$cPO_e$-x#PL8+!x5rbS3?4G|u9PXb-V{Rf_J!ExzOMo*#ha5LI|rb&p^P1mxh} zmjFFQk8oX1`93@IzAGaYff8|$&s(RcJP7$RzduX`QPv-2?P}zgMid_@{fABwqA`U0 z0ZyJW345{{#;`gJMUxi>HAHXi{}T${gt-CU)>`Y1Gji3AQb-5bj=yVV&VUu-v~KZ? z=okxi@5=?k!$xn^lh5?V67!e`&BnJOoY&vcjxzwk^pJb_r-Z?4rm*6MgV!_G&DRz? zv$elIRc=5^$G6``{Q`}wO{?)hG{2qE9sNW;t< z9kOl57bPrqX#-R0h%r^F10Hwmvwi=GApFKx4i<*`jJj~fjlIiIKZ2ZG4gT$_y^jcO zwZ&*b=@j0ScKWXRVYjBV#AEhm_n(C<8`_a}Y{1>X5Zyunwh&&Sc72(v9V=b0{buj< zxdpdp1dE2L;*R6eC;_Uqa?Ul|gvKnvtOv5@(k6AZog|u-#=+$-E%IzQ+mqi$E0S;5 z?>3!$Bk<_P2Sj->OQ}4jieRE65P|C^fzk$W4-w)fRSxKlB-e?$F8kz z0fA7ES83>bIFvrM1X4S#_7EVQGAk%73)Tf(2-139e=rz$#7|xf^-%sA7ILR7uRjJK zljmU)b%}KSD*6eywV(nj+6m~+ilAOrfH<@1OkP7i$EmbOBr=7;SA>%TgVLu$e^`l@ z(`m=mCT>OG#5j5s&IGK5+TXRv7qF)|?UpAOy*Wd43Wv|_`SU;8<^0}yI`7q7RQc=u z6Q+;jY8sUQAsl5+B4i9=N_B3d_B>mZi_T>heuCXmM3o8sP#V_3k^}37&d9xM)H`gd zyhrU3<&+=XIdd=Kq_*PHK6SK!70W57j&Fp58l!=x1){plu+nR1H($0Uve9W@$Z!H3 zQ7%d16-!R6h|vE6z1^z_pRB5|$UkH)H14CIy^?p({xrcU{a+L(nzqF`JIIzuY)|c7 zX9aOsMWEKq-N+NAi3BMMv`*fvRsiDo(=8SJzpI%ph_Nj9cZd85P}ONA zYP;Q~U%n3ygKe_a+dzEZ=?>PXLgSFmx{b*mOm-!GGZ;>8Baz6?FA};K4fy6T)!sw>^89QlEJZ==Uvg}e*hTN2a@(&*6~FG)Q`p{$CZhXd!^5~)s#jgiQ6 zg%_zM?V`jJoixiX`^vH{768;yyDSsc1A?;$XuZNaH5soC9e5c-?_3#EfE~j0PJ3^8 zez4N%7VNAsyz6-?JZ4$m&sXkoG6lyM-r}miGl03xU#3#IEJvMOOf=7}4y2f0SRWCc zoy!SpXX=Dpr3mlkw!6Ke?<|WiR*@ZJ(mnNHti6ERxyyc6P*hHRb60n`ejT*HSfhyA zNGCdOR1%6f*{>$D{H~}bG|m4_Vmx&7aWplz)hw3-k-w}pI-m(b{Z0iEl}l;O ze+6l_DB2(X4LB|eEdiU@Xj9Q>h0mwhe?H>#P3kW5`9CDO_goeGXQSay`eSk(tCO%;_B?Tu^As>Z)9hZ@ek@;*Z8DWlucDi`y#Zqavb)%r&N!>XcBK{H1u5T zQ*8EFf~7fSYn{J)DwRJAkLDRi@MN)|WtRQVejcP2uS6eQs4Ro;JZi zE5!Fsv|zhQpUwAkbZ{^am{+t60<~4?`+4Q+-{h3*+xQBZFLj^=;PXeWLh>OksYKJt+?BG~Y6 zedDtzX@J2H^d{F$A;RF$RHysSmesiWyrfk0bhg}bCe3;2zT4Si0F$yd1Ixd;oAd*Q zD-}-G2?MUQ#y9hkT-He=02^9z>Hly!!aHAK`G8Pgar2F<@S-2+({r3gLFj7y=*4D< zexKrN9&}W7R0%WlD9J)@?~wyX7mH*a62NA%X=jmMcagHTNYdMx*eksMTQSb{o1^P9 zeKJ%VUv-Z4v`ko&gvHzXkMGh_ox%xne@1L(8oPt>dSh}f%;vTi#_(3`-_;(BVgbE3 zg*1pDyY|ECw8-$VE8qkwxofdoG3C9Mz08f9nSPNH_1!~WHieh!*ih&f4RFHv*C&Zb z9NarJN9;q{W5@SdQ--BzsgdzbcMMNjaGmk;{!KFd=TiKU($JzTMJpUxPT66x>AMJYUR9R@X?h^&a zJo<;Vb8Y|SyV+C{mgKxrFd0jFwSTGpUqXa|_9m9C6pO}vsA}HRP1|8y4d>rx1IGy9 zS4tZ??zl5o0&EM`eUwJ%OpdA!l29_$Fa%at{h9N7Z}ua5$Il-SymanDK4M4qRebH3 z`IcP8fu5PR%Io40;|{t#E2$+*R+|m4HH>{_(_s*@j6+01xv5%&N(IAqRc0Uaal&e! zg&lWE2xqqBPG4;ta!Z%{v|Uh~I>`A913*&Y9n!p;TqQXr9m2!gY8CkA02m^2+X#A`N2 zqPbF`kA0?2=zNv`epc~a%s}VL%=-nw6swhv_cGvtwR-spW5J}4N)+_9>>?Yb4*e__ z3Zuh}vC_38?IGSY5NEaMBZ3^~WQfp70W%Yo2aJ>9uBXvqUlC?k&5BUwoRwp2pAC+4i4zovz?y!=H z%!eb;H^w+F9JH?_O4+h?IeYEQt|at!ss?m9#GEk%I20Ry&Fwgs<-=Ku`YboQmRzmk zE<#);%uHddgGJ%N4mFn1VXy1%vL;!KBmZOMGX=q~|H0co716hs5~zUMoEGCBYG4ku z@yDLqbN?rVK4NT)SG!w0HV@tyXD<36X)yd<+HpjV3Gag4Z z)y6wL)w3qvXchX}=QRE3Q6A738(g<%-Obsxg4oqR{#Ew4>NWb9UEI=t6*{DH7>N`l z;HpFy2!jH7m{GOHPFKlt|d4E>;ujJ0G#`KK4KP=J=~^f0c3!-m33_RR+CxOtqD z*PiNxhp=pp#>OuVa_tm>x}t*cUDceVvv)MTN-`}G`n|g5*0DHg z>pzWNgJA>_vk`xbuLc`&2K>2CORp zq0s8%b(ZvB|tmYMHEI6+wR9nVe^7_&mGqpgxS4F?HP_eF8 z48XK;YpQwZJjSk5;Q=QA*K72V#S$OmBG?riAX;id)xC5~B?mFAn|xKt;>%|4X4ENl zV-aT?zeLGh`(+b~m-oqC1&OUTn|_-nI@NBvQSV%L3zogwVbw^5$UoUE`{JWWnvI}`6Vh7w?b9v`$ z%eIUR=~WS!Xxr)!$8=NPW@|8psgn&;2(tX%+|g^wL-Y5(rZx{vpJ zx_{2spK)^a16M@Dyy7hxa}8&ygd?S*`zh?o;i)RoPOxx?%=I-+_K@>jq-|@))!$}V zNm5su$|r=K^ZV6vorY+Q7-bG}F${rUipLqO6QS_tXWPfEeWD4<#4Hyfe9C$;scxO| zg8O<5IhwLD^T~P@K|*G1%AB#%=c-v6lh3LeP}>GoDmk++nx~ zGvjm$$9v?E{jC3=QslYXc6`1oum)<~1$uNtN*K%9K<9-lOu}6YQnIe-Epj}jVhL2$ zRa51rBo%j*y}IeRs4xWJT6ceiL8_pn$yHtd-oM)h;L_S7A3gYsJdd2?h`J^q8!TWy zllm92GQ3+{H;YVZ&bwtTdcHorvtdpZ=p8?s@w&a<2FmpT|3Qq=k*qiU>p(%$yODp_ z1zLRJt%eao;>X4{9gF83t?*Hw`$ zW_qdS`4@ZMqav=oWD)id*IHh_C_7i>o$W8Of@VgF%QI1%Le@DY2a7LTBB&ay?2mT2 z^Ub~Hci5r9o`J0_$Kz(=O549VXRDpsdW5BuNJ-2ld}JHrvwyPmj`H3lkCf$gm)qB| zY&^LElx%~mgK5?j)(xeWWj%~1iv(ViciePpsWYV93Q$590h9?;0hAG@4r>2my+H@z z+nSf&f15oR3gjQLfzBlJZ2rI3b`rNlnqt4ovU6uX#&POfnWi|ziUKz8F22g-|BtV) zj*Dsw+f_uspbjb`45_4qfXECfNJ=_{N(v$k5<`cGiZlp_G*Uw&-JvL{bhpw2L+4O; zZIl!De)l_ndH5N7uf6wL@B7r7008i>0t5PIAR1~oSH5L8L>nyZA!CS^dxzBxuUh|a zn-g);e#AX(T$F}Fl?ApKP-Dg1Tt)m0yRXsq@l1|3W3#>Zi$gr(3eKK$ou!FI9xe1O z^xQI6ny1-<5fDP`{UA=Y>A0{=?wDaV>`74#GiixD`KV0a)ONAjq?%NN-sf;Eo+YN<%^+JRT!O{xfEF0 z$|~g2tzY#SNV<%gJ)8LVu5l|xH-99bNwuAREl%R4UCYspw!Uj*)Wqs78yVXFnnW$F zuwJZoG_j!IN#t#)q^}?$$-rBv{=lY>s_3HpESE3B|IXnoY{P%`JUO`l(F2|n~dgH%x6 z&EcKagLk|(?Ypi6pF(}&Wq8+G7$kAGhzaIbA^)qM4b>s1JutJc!Wb;P`^fC_ zpnZ#Bit76{ZWZUoXxoD6F=g8;&t#;eu(x z{8=w;7#xo3&7m9R#q`#EM?XDjmool_W?9J|-a70TyD)sAvAry7=%@zpZI`kF27(&! zy8x7fxb@@}g$9gMyDa4C-GrU5EPfGEbK_t$--1F+oY1PO>0-ZM9hKKFAk{#+@2SZs zU9b>W^loRx377#wN5IE+=Js`~3-f84@Vzp3X2t#t7N~63Ytu9f>&}|wFxI+}r{x(z z8bK_E2cF&1?Oy}dE3<0TrPMKVpkj9(wJ%zp;jlYYmntj0Gu^oiqZ`e)G7(wG`J@g2 zSol>ql&?g9=NysQd)7HC;=bFByHt9QTnkb%t}?skv^mH2KJHCjs-N6;D~^a-DD6g; zlg$x$9iwFM;fjMxuKk;$3(hSBipWQfl_dhumxjf}SWSa#w~|rAlH;9x9BEfh_j3JH zI(V5v-B4Pyml_|qbLb+(hys%OrwyUz&=0Hm9sId$z*pU4oy>5kFrfbdTGtw+`y!G$ zIIO^`*}crV-EpD)Abe0WB4Yl&z%`;GpEv~{@^?(&yQzOme;!MF^o=-`T!RyX=Q zNvj3n;?)Yf>5N1Ab;Us6k#>4Rx;GU-f^bph?yVkadMg&+VQ~z?q)Qa%H-D9KUfZIR z8I8QL+$)L;s(A~4FrrIgDa|jETp!d3SDbwzw<(-}X%vN=hNA;hZYZYU&2Bi zfLlqY^yCYNTLRWjrWj)FTBDO{2-s&&76PgQ`0${MP)?|aA-2IuRUw)50>ebFcv1;K zztt;^eC6_)r8_?=N1Q7P>Nb((3z0UZHeH-+?dz&OMXi<7JJ0<@cX9-!cImrYEkR#MKvjwXc#l3SnF`tQLoj`P zTvH$^V>xomz61&WBGRgI%GUuL{T2{04C%UJjec^chY;m20~d0YVIO zkP5iHD9_tJEL$xQ5x3|4E4n|7iV1T{(j{GF6v^)C`OtLGV>v5Dsq^&0m7AY9O9sA_ z3_hbayxe)bDZam%Vc%(1YysGesKnxa-O2~GwhAe0szM)Y>Q4FUF_wHA$vq#iFYCGm1-WaHef+72s_&1`AeAY@zLsa9&&{HCowE$XjwYyP{>(2!kbkJ5k~Xq1S`EGTt5~h8u2WG zdR((|d(5>^;D8Ir;y}-K6!r4O5F50RSFZs|C&xgsvx8sW>jeFSHnj`Kng%)v{of^$ zZ=^0$JNdEzDCJ@nvkTi=GA^xwM5b>f@F_AVPRw$9)UgywV$uZ(OT)%udxJ@C?1vzg z>JsYQe>39hzKbW0o`2~ueIVYfDQb@y#!h5dRysJrOH|DCSb||w6-(Xgzv_L(D<`L> zX|$TwP0d-4QR9APXV^ikl92N?&uH$#?VXkuX}vz@3lI`#SQ*uQtC1!QWeZ5ip|g~- z)t5n#;P31}ZT<1xHb)Bp1Gi!#E@zhvnL!~PAjxg3u;7d#F8475NIEqc#{v6j&`4)l?G3zZaP@5yURTgE47|N~xVOwb z2l9?$o`2RTDwCftn+z(1#90n|*wW$6em!ezw@GR&*hdOVtEM?A1iY0%``__4OZ4P= z!Orwu0l&xWa&Yc{?8Jdcghcv&T^nbQ`VBDAv6T zYOQB|ez5iBZ9HcPS+IjUcXy7Ly0*q*aPgH+CGc;8mGi^RjFf+ z!pk(UruFKTB9eWHr7-^R)=J^E9^;!#*y)|uTk(~Fcf;@MF3}9yCr(cWJ{)T*dy74q zY%@DFbbU+(GPv}=)F(;#Tm1a;nqn7<@A=lkSkutZk^*JZeG>{~zLZA)Q7)P7l#crG z0r-aG^jUz<|4|m5e8#d+%pTs`0tqNTQULrEC|~t!-I4r-p=t}@qhyt=(_|3j?w(gG zS^N@6Y-ZeuA!^#pG!Q4X9i?gCeO|sETRjCRY#ed8&=!}YE}@qieFfNoq#eavl`Nf_rRu!Ihfseb4g zekU%-h96t0>Dk(-k)M{{|M2O_IRHJFjC;qdQjaKiM+ygW%$DZrw&||@yekf>6uX<3eE(D& zUeaV&3*YCMyjba$6}M%Cp3VmnqI6<#JptG80*s@tQvN)n$8=7D^e!M+2v?pC(c%B< z3+IQK2WUy|-&b5oM;g&>CgrgzccV5R2(_bEooX~|m*@baEj=@}`I*gbZH;#cu~J~$ z3<%_)2xLH;(Q}-5N_zj(ie^Mo9={IDvmqGU&=in!0%2El_?Wd%1(!?T`gFJAO1yXI z8IW#7^nK%#n-ShfK0UE>4IRhX5wu-itFzM5bli$VjR}WZKMPoluzwbV?k3*+>bmGm z6?=3~A4({qQs;6Wg>po7WI0_~tLzoSZNkPe+pnd}$yjjIB`W6=7MOVB? z2LOR;4yEK-0FJz9*s9X(SAGE&_*;z;41RIX6nm^?UNz2rxg|@ICf&}2sJ#v8LNJCbC zSSE;xRSZ{__nfSFXr%YM-osr>k3A{TpyRroh#%@;+({hgMwI_AO%2!C8AH0g4{>g5 z&0A6JZVkERGuMujJOM>q3dt=fa0*wUlun1{8<7K$cU9pY3@9rorsetlUsV0m&VpAL zNaxBgpM^V%Y5mF~F@PBrJRIX1y(jd5&Q!te8?hPXeRb!@G7(lc?;H9W$&=E0RWv5I zt+vfO!E~3Zl+1Pdo!c4~R&4vx36k;Kwl)kCM0IHUE|YQh?P5}pDe5rcMfg|ib%Tgb zIhMGm%YbdSfC{&q(y<``XmAg@Z;u++FwXz)>X+AJ*L_+}_P`Gezk7v?UJ5_{rc*}A zKjdT0p;qBLKRx*O)6XvgK|uXd?a`sZ{pbat=0<;@slZ{fE|AA^nhGthe5W)8^=S)x z)+LUx^QrH|l!aGciR`b`ho#?BgcPX%+Ic~9lEQIbcqewG^=Eaiz+6KOGNuTozExh( zLuk1i(N%#{ZJfFqu|SA7Zv%`zJ(`uGKd0M}dj$`*`<8=FA9T;v1(UXZ(Od+uo|vj z{=6`Buew|_APO0do*yU$C<@Slvna~MWZDB$Zh9I3uKO+pvJ?1e5d~brJFoFu zzk+-7TjX0R?$fi{p>%DNA`g<4yyw&sxC2ONm%2p zRC|w;UB|kWbSq<9*Wz%`-JL*pDM{#8@I>wyudMINz5DG}QJ6Fcc~$cIe1=q5<@u51 zSnC9-=uRT#<^PCa9u+>jtfw{UZ?2Sfbn+-c3xECh(Q)#4^&1#@z~4u0SrmF})ee$8 z)6k4be%0)80Kb+XI0A`{mVn4*TiY++Uu(4l80WXj;3j`*>eD5S_gadXF*_w!oqGGi zO(+g#rc{0Gd;B-|h25-T`74YrVGW%(q!90WIE{3jw5OPoLtht$8Bs2}*vgtlk13->cZE z7}}jpu)Z_qlnkYkLK~^j#EUa3r4XJ4Wi1K;bZZg3$;BYi*VO^4(icF3BGJAvz9B81 z^SiV6dVt^1ppk!>n9M_-Bjg`*2~b161o#qjgVau4|6Bgbd>-)4&Vg;Y5a?TS=^t;j zIo1@)dV+LedUK%Lsl>vToqV7T3~JM_g(*PdaVU6M3hoKn5{6rKw|IvQgGDS3_jkAa zG^@S<0Uf{Jz5hOKaUy(ZHP5OV%7Fr|L-UbS4M;k&>;Fs&O1UZ$;|KyZnNl{GL2ysF z-mWRZY22BqgCPE%3`Yz4Z3^tWa~-tN?-t(mPf;uUmls zZafC5(O&vs1;J&u?Z^*J28&q%*>(oR9!NzU{k(5#9PgZVFmtDtP1nL50?^mvw}rW{ z(AnVs65!}6pHb0}KD>Q>wo??-A1V`<$v^yyargaYry~DNs-i;jNTHRWSZA3%#{^Swrb zTU~!*G1+Tzg+H3J+)!G5*RPB=&{sZ#Y+Z#;Es(Su`d7W!WPnO-&@W%(4F9yE>Zxi* zde@VkhL6VUa$unURjnVwJOGzn589P26)c54>$(yGw7Ttk{UNdard7jVvNqf~&R1!Q zxCyFSr?8I$iSQ`U*5?Gwx=HICF~+Q$ge}x|#@j1#<1%9!^r(7^YazkNw8euX)F$&{ zjg3xQN=a4jFFyj^msn9uADalc01OjPw(>6#u37BkjlI0u0s4M(gWHn1OAa-Uu&14G z#y!_y1+Rr{=@HQsPJ;WkBZd#H?CIs=d&4*;iw{jV4Ifq;TiWi)74CxrH)c2gH<^z= z6dTgzl9@ZhxuF{rcgIgT?%#i}!|C5Qebo#I2l#t@WIhLy;Q6URyEBh4QTC_zx258W zFg-}+V(27&)*i3Z0es*FDqbB>xkRYVMSS4R^M+imOgp?*QM5$u3SBhKI^D{;$=sjwQ<_hxC`NCwr;lJ z7!uy)Th4J<4`po2XnXn!#4H%A4UMYBM)R{vJ}CHjm)=T`0ah!2#XEN3&}QlOm2Oa^ zR%q@O(f)FfZxvpiUU?{H+Mn@j0IoitVeyP9WO(cx`IO45nN+8`#zmwfgaZShO^o&+elHB#S(ZaHW#0+B#4~)g9R|r?Y@OXw4g;r zxb|l08tpCD)7+!84|(7#2BlJ3^hH8-|Kt4*!F(n z!?#p@CexG_=uqx}t-z71?Vo%CZ}7eTS03;_)BY5Tm??;b3a(zzSuTLK#~`sor(uw! zjSWXCvGqoReb397%*6JFIELeg7}6Y-NnL4w_FnEAHSQn%8I{yASCt?8?0L&nD|HP> zU@ZXRAT&C!K_aR$>ad9n7NpPf=AUv7QV!BaYbv=bW#q+<7|L%(P~xGWZV(opa^gVI ze??}3kqX}VI7lkC|4GW_moE#MX4l$uD;B^z1u}H*Jp*YPX>I_@90qb=c!tSx^|ruN z*ej^C;z5$DR34Y{B1}O2Du;n5lG_2&PJl4mOh|^hJLv=GMWxq?4G9&f3Oy&{Qpt}J z*-9udvw+K}D(FCMF8mv&KwDUsq#NwYxRuy=PpMAlp87Vn z7Q>;|xWUM2k^ z;L}NuQCcwJeQy<<-*{@-*ezpOwe5}omx1=?#(+uq>Kwm=;x&}thmDgS=sVA`luWC{ zVh|r@_x;v0s*6y@I3%Q61UP0s2$;uirQNA+_5i~LjEipk%L@X)yJyh%{#`~U|F}jc zc=aMPN|z`gQ0y#6eCB4;>KWd{+x)ZHj1s!i5Zr|Y5!xBm-~_y*aUGJjHdOkz*!Ke? zn=2^0nJ@po+W@J=VU|_JQv#P!q)AE|EaK`{RY}z66;OlppUnEDd2G$l!=}!)`wrJ+ z(75{N+K9k%ii09|^+vW{#f9d}b z@^h3<=|-ld)Zx+vC*V>v7T)b{hja3|A*PEl5oAEn_MA$%MUKgiQO`$M*GXyI{fiyq zT`&nAjX7GoM|VjW9T`>`x1K)B>lCh;+KEb{Lrhy#jW{di$uDhwwsTn>nV7vX3#V>2 zX2uh#ZG9`Ur;KDFDzL=1aO4$%+}3WVQu198sXh3akWRY{J{2N)@lhz%kzJ3f%pe43 z16_L30r<%CL50x=Z8So_q}zCZQ<><)bo^IRyck_fo5-W~=Dp7NyM4XEJHxx#c5Z2B z2;z!9Qj9w6I6PiL^~1U1aJ0+^N<;m=Ru+bBR&D#k7yT*@l=f_1J8Eg>ij3kG8giR$ z$ZINmKi2$MF*WNl*$MJxJTbI&P~agqUxz@7!Vgz}RZD}Z7C!Drd*h{$bzK*PuTo-9 zd~Qwuck}uF>D!Fc%)8#A+aJZ#cOG~I^TuH`vO|+=APOa(1=sIu1bG@6r za@0ff54hDzmy|Y!dPjZpRJ+b9r_T&CbKl<2s!c{;&KTYSYCt@V(e7E10@V zr(?j4p(*>0V582oe*T&`m(3e{^MR-Cqhpfh1H&KR@g`U4{{8LhzoGAdSd8yv1I*DG zz|Y7UesUP|h>$((oYd@2IxwM6`uB(wl^}7Ne*RFdJR_Vus7c&|a~=cbz%ea#sTize zF_|Dx(^@pb9E)!EJ*@A0Y|aUl(SA7|#Q`$fzBzw&eB$@!yslEZ+HocQJ1txdoW4|+ zFL65U3Ty58ucA%(8VNEN{9Qg);JVC%PdQ0oZx5S4zsb=5m3zwBhJi;N=&{oAoyQMP zbF*;)^(*1 z!If3dWXt=cBH_bx~^IyeTGTYV3b0PNo`I1Yz#Iij6Kw#7EJzvkTdE8uioWKV!<^zr13|@v1QSyZw-25_UpiOh2 zQ-%%-QI4?$w%NhNe`MSm<@I=;4}OH%qI+I10I0G}XGu5$?NLp$Brv+x;~0_PQ1SIl zarZhxyS==1VFfzFB9!=uI!Nia`>?BDe2_o0NIgfyMn=Perq&#=&#C(uSfHz zKA8&V-}TaEp!XnN2vyZS`AG#xh|73bTEHdwq50Vv>%CAJT2o5x=U(o!LgV>h1Ia5m zJocCg#lr8OS``2v&5*dr0RmrWAgRhFm;jW9OBZcNkx$!p6 zqz@D2EBcD!p%(azDHm+wSedZ4vtLg;kHIT|>NEL*a}8w>T{Z?pnph z!Him~MsaxMpK9+l(@w0lwy_1B@35Q!BHbavt+~^m^8t+IYglrudjLGm4u_gHrGZzb zM|R((EoBd-1oN2G8@ciiYX(F~Dj4;Nf+Mf_%)FD1BTfq>zu;}k-lxjd4T6I)w)JntD@A*9V+D#ASgMk0^ENRz6Nn&{8sp z&5vgdp2x^0u>II|kxq{48R2O7o3} z!AQgB2TJxFJO*Nu6cqr(G9*)d^?rC*E_$3FFuAu4H5IIB+gWuz#i4QN~%6T zNofn~3Vl#MJ8ZK}zG*^n$*4Y_S=VG>l4fQHLABk}37aX|Vggf_HqWTYY7?xOGbRZ$O%f834HWRDRpe*!3=T2lf~Nq8=;RIP_B#%1o18 zilO5y-n{&&_%unlW`+kS*y7jkR;#r20S1}GA*8j_OE_V2P8__B@GODE0^Vk6!qY)Q zYIh89k;paz&ydJ&W%_pX{i|ml0i_C~@p2k_HO^5qO zrEsH80xs6J;+?WJJ|@T+gHg0D(y)qS4%BG4_ZQR*)}10#xh#?B394-fC(#RE1Afqx zhv;?4bX*#2i2eGRc^A+^DB(*#{`Srs0~bN$D-owq#6rE?>%%#urQVUdfxPyKtQ_}~ z&qPE_KRsrqa<$hw#a~R4+@|6@q~Dek^Rmsd=m?y0K$Q&_cPnlRFW>sC)7Pe1I=w_U zmP0yt&MOMCb${vfw5WvzVCn8YtvNVP3!uUsyUYR`m{Ska@}<(+kRX>^pj%=Ea^_NG z(PzEuC0~JHp~5teX&Qx1y(3K2Bl0HyC`I5F-KGIYvM@SWsmRym77X&0~qnJ4fo>S2Qv~>f6bOl*6j$(IXNB~?j4Xj z6lQQM599sCTI6J?fjF`@J^vgRKV;it06W>c*Bdq)LMxVW47R_K^B_ig|Eknq4mLd3oAw_HER z5~~lnA#?H@u9bj-bMQ1FjehBvi@DZ%9)O0nAj6;r@CFZ?W#ue>xxj5<-mQEVYzGuN zyT`pK7zOC};23GTLA2xNrNps)nXr=!phn4D^V-gs;q_e7M$vZ*g4+fB%eHRf*p^Nk z>eZijUrgfl`Vxt-_HG&~Q-!}rJKM)reP3T{orW*rzz1ADEa(W!<7-5M0cx#3b!TWE zU3QLx$5nXPZoRwUl}1(ug|UE;pHJWXDThJFXrL^*n*xT<>23k9;c6g!y9 z+{Y~ta+LVN#Gn93SFBowHY&k8a{9@U%QY>LSP2{Kb-l-SnY;$neqsHDj*nWh+`bi< zPyzvvZzCJ-#%N%zi8m>lbsIOKnrp9v|K@)1aU1X2DD09YS>cjJBm*YV^((Y-~(WtaX+3&@QMFyb-DB_=c-jx0FQ^DOn&E}j4i z&};pRXexfcRO?oVfY9covlGCPlFyX47mZA}b-or}#yyWzrELZaT>*T9F5&QLdu?6K z_D!fTruaTQU&66%qC*EaT^}JmMiR<^dSC*~ zE-H})T-d#+q3fU=ZuQ$gmR=Qz0W*0qwtO}|%x`PV)w+n)^_0u-Qjl~;o;Z{8@koy23-MfnYukSsysVkke73}ls-_XPgz(= zRre0eef_dDx@T`2zPk#TY^uRg6&*X2|VkPoeP)y7u~R zM7J$slde9*Zpl^VZ4^4D=^m3`-czQB)glg5Ndm@wh zN&4+PP0L%4UEU2o_xjd9n2gB%oSc_mhGb6DA&|^z-ChnpIVxQhIR}tBcFn5>89?%| zGr`LLuP>a)%YΞba%AkkJR`!7HYH*^eF>1KU7~&hyV_x8j!F8lf>~*|cY&Nr)m~ z9?*4`x58`ZKEI#<+D@K{Or<|{nRqSv!zY~PemIyp)8X{fM52xL3{Nq4bQa}<4qH@Z zTx30R*lNA@2C<(PNTkM}s|Oqb%5vXvG8Sjs(}vzNL8EO!zke*$XzmSbX>4c+ zIG1eKF4E6cyKg&ek{tLTB4Vu)%+!G<{e?}rITP<*|9OfJloe+jMy~uCmY=X`JvTqc zJaiB45^C^>D{eiaJMW#0UP_jI^+gfLF}01|{{%1YR^Q#YSGRXjxL=~g88K}iQb`i) z#tmW}=6nR1fHiw*W82@d&3Z%XnhwjF5?73zwj~zkm7fn#@@g3-(vvjgA2HGgU?fE&~=>!0ez|UXD8AZj|^yDDknhFPiq7Es0tWgL7lQkAlR2<9+ z%mD#gdaPurfP} zV4m9!@;o2HEA~GNV}))$ToUD8+>PArp|893s>%UGAH+;;P_xd4ft3h~9iZ=0e?>>6 z(}W6@vUR@PNbBd2l4U`lBQ4yKxL}zg3Ma?YfovpyP3-G1t~^v2W2-@52W<9-+%A8k z&Sxs7)|6FNadU{?;?Q`qtN3_h+{m`QrdR`-4Beal^A)w-K!e_B4>(y(jYiEP^~Xdn z>OOd0DLEY^7W#WVE>hz8Ndu@JN5l{3`l5LdjrK_cvhYS5H4gCR9PjfA^gL;lPn7jP zew`rGe|@S)F;KYNry^g`@-4%I$D#_(L;GLab=9dJz+f~zy^?oig0w)zw?r-*tQ@^O zX6M>!6=aTHMP=$;LAA-;S&iI{v;fEaR2h*TA93@?c>c#VZspZrVjH)p;&$w;aHH

3>`9>4^{RS$tX2zrRemv`o#xca|@9r`$W9+2h$ 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 Date: Fri, 19 Sep 2025 12:07:33 -0700 Subject: [PATCH 06/44] Run linter for multimodal.py --- flow_matching/utils/multimodal.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 7c39b10..e62a738 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -4,9 +4,10 @@ # This source code is licensed under the CC-by-NC license found in the # LICENSE file in the root directory of this source tree. +from typing import Any, Callable, Dict, Optional, Tuple + import torch from torch import nn, Tensor -from typing import Dict, Optional, Tuple, Callable, Any # flow_matching from flow_matching.loss.generalized_loss import MixturePathGeneralizedKL @@ -21,7 +22,7 @@ def _default_continuous_loss(pred: Tensor, target: Tensor) -> Tensor: """ Mean squared error loss for continuous modalities. - + Args: pred (Tensor): predicted velocity field. target (Tensor): target velocity field. @@ -41,15 +42,18 @@ class Flow(nn.Module): loss) and inference (sampling) across all modalities. Args: - modalities (dict): + modalities (Dict[str, Dict[str, Any]]): Mapping from modality name to a dict with keys: - "model": nn.Module (or ModelWrapper) that implements the velocity model. - "path": a probability path object (e.g., MixtureDiscreteProbPath for discrete data, or any continuous path implementation). - "loss" (optional): a callable loss function. If omitted, a default loss is chosen based on the path type. - training_scheduler (Scheduler, optional): Scheduler used during training. - inference_scheduler (Scheduler, optional): Scheduler used during inference (sampling). + training_scheduler (Optional[Scheduler]): Scheduler used during training. + inference_scheduler (Optional[Scheduler]): Scheduler used during inference (sampling). + + Raises: + TypeError: if any model is not an instance of nn.Module. """ def __init__( @@ -92,7 +96,7 @@ def training_loss( Compute the total training loss across all modalities. Args: - inputs (dict): Mapping from modality name to a tuple ``(x_1, x_t)`` where ``x_1`` is the data at + inputs (Dict[str, Tuple[Tensor, Tensor]]): Mapping from modality name to a tuple ``(x_1, x_t)`` where ``x_1`` is the data at time ``0`` and ``x_t`` is the data at the sampled time ``t``. t (Tensor): Tensor of shape ``(batch,)`` containing the time values. @@ -134,7 +138,7 @@ def sample( steps (int, optional): Number of integration steps for the ODE solver. Returns: - dict: mapping from modality name to sampled tensor. + Dict[str, Tensor]: mapping from modality name to sampled tensor. """ xs: Dict[str, Tensor] = {} for name, model in self.modalities.items(): From be86f36fb7e41432456c10f0b51c683d5b091f9c Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 23 Sep 2025 11:29:46 -0700 Subject: [PATCH 07/44] Add files via upload --- flow_matching/solver/multimodal_solver.py | 194 ++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 flow_matching/solver/multimodal_solver.py diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py new file mode 100644 index 0000000..3c0fd45 --- /dev/null +++ b/flow_matching/solver/multimodal_solver.py @@ -0,0 +1,194 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the CC-by-NC license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Any, Callable, Dict, List, Optional, Sequence, Union + +import torch +from torch import Tensor + +from flow_matching.path import MixtureDiscreteProbPath +from flow_matching.solver.solver import Solver +from flow_matching.utils import categorical, ModelWrapper + + +class MultimodalSolver(Solver): + """Solver for multiple continuous and discrete data modalities. + + This solver handles an arbitrary number of modalities, which can be either + continuous or discrete. Each modality has its own state tensor. + All modalities share the same time discretization and are updated + simultaneously at each step. + + For continuous modalities, an Euler integration step is used. For discrete + modalities, the update follows the procedure from `MixtureDiscreteEulerSolver`. + + Args: + model (Union[ModelWrapper, Callable]): + A model that receives a sequence of state tensors + (one per modality) as ``x`` and a scalar time tensor ``t``, + and returns a sequence of output tensors. For continuous modalities, + the output is a velocity. For discrete modalities, it is the + posterior probability `p_1t`. + modality_configs (List[Dict[str, Any]]): + A list of configuration dictionaries, one for each modality. + Each dictionary must have a ``'type'`` key, which is either + ``'continuous'`` or ``'discrete'``. Discrete modality configs must + also provide a ``'path'`` key with a `MixtureDiscreteProbPath` object. + + Raises: + TypeError: If `model` is not callable. + """ + + def __init__( + self, + model: Union[ModelWrapper, Callable], + modality_configs: List[Dict[str, Any]], + ): + super().__init__() + if not callable(model): + raise TypeError(f"model must be callable, got {type(model)}") + self.model = model + self.modality_configs = modality_configs + self._validate_configs() + + def _validate_configs(self): + """Validates the modality configurations.""" + if not isinstance(self.modality_configs, list): + raise TypeError("modality_configs must be a list of dictionaries.") + for i, config in enumerate(self.modality_configs): + if not isinstance(config, dict): + raise TypeError(f"Config for modality {i} must be a dictionary.") + if "type" not in config: + raise ValueError(f"Config for modality {i} must have a 'type' key.") + if config["type"] not in ["continuous", "discrete"]: + raise ValueError( + f"Unsupported modality type '{config['type']}' for modality {i}." + ) + if config["type"] == "discrete": + if "path" not in config: + raise ValueError( + f"Discrete modality {i} requires a 'path' in its config." + ) + if not isinstance(config["path"], MixtureDiscreteProbPath): + raise TypeError( + f"'path' for discrete modality {i} must be a MixtureDiscreteProbPath instance." + ) + + def sample( + self, + x_init: Sequence[Tensor], + step_size: Optional[float], + method: str = "euler", + time_grid: Tensor = torch.tensor([0.0, 1.0]), + return_intermediates: bool = False, + enable_grad: bool = False, + **model_extras: dict, + ) -> Union[Sequence[Tensor], Sequence[List[Tensor]]]: + """Sample all modalities simultaneously. + + Args: + x_init (Sequence[Tensor]): Initial states for each modality. + step_size (Optional[float]): Fixed step size for uniform discretization. + If ``None``, the discretization is taken from ``time_grid``. + method (str): Numerical integration method. Currently only ``"euler"`` is + supported, representing a single forward step. + time_grid (Tensor): Tensor of time points defining the interval. + return_intermediates (bool): If ``True``, returns a list of tensors for + each modality containing the state at each intermediate time step. + enable_grad (bool): Whether to enable gradient tracking during integration. + **model_extras (dict): Additional arguments passed to the model. + + Raises: + NotImplementedError: If an unsupported integration method is specified. + ValueError: If the number of initial states does not match the number of + modality configurations. + TypeError: If the model's output does not match the expected format. + + Returns: + Union[Sequence[Tensor], Sequence[List[Tensor]]]: If ``return_intermediates`` is + ``False`` (default), returns a list of final state tensors, one per + modality. If ``True``, returns a list where each element is another + list of tensors representing the trajectory for a modality. + """ + if len(x_init) != len(self.modality_configs): + raise ValueError( + "Number of initial states must match the number of modality configurations." + ) + + device = x_init[0].device + time_grid = time_grid.to(device) + + if step_size is None: + t_discretization = time_grid + n_steps = len(time_grid) - 1 + else: + t_init, t_final = time_grid[0].item(), time_grid[-1].item() + n_steps = int( + torch.ceil(torch.tensor((t_final - t_init) / step_size)).item() + ) + t_discretization = torch.linspace( + t_init, t_final, n_steps + 1, device=device + ) + + states: List[Tensor] = [x.clone() for x in x_init] + intermediates: List[List[Tensor]] = ( + [[x.clone()] for x in x_init] if return_intermediates else [] + ) + + if method != "euler": + raise NotImplementedError( + f"Method '{method}' is not implemented for MultimodalSolver." + ) + + with torch.set_grad_enabled(enable_grad): + for i in range(n_steps): + t = t_discretization[i : i + 1] + h = t_discretization[i + 1] - t_discretization[i] + + outputs = self.model(x=states, t=t, **model_extras) + + if not isinstance(outputs, (list, tuple)) or len(outputs) != len( + states + ): + raise TypeError( + "The model must return a sequence of tensors matching the number of modalities." + ) + + new_states = [] + for idx, config in enumerate(self.modality_configs): + current_state = states[idx] + model_output = outputs[idx] + + if config["type"] == "continuous": + new_state = current_state + h * model_output + elif config["type"] == "discrete": + p_1t = model_output + dtype = config.get("dtype_categorical", torch.float32) + + if i == n_steps - 1: + new_state = categorical(p_1t.to(dtype)) + else: + path: MixtureDiscreteProbPath = config["path"] + x_1 = categorical(p_1t.to(dtype)) + scheduler_output = path.scheduler(t=t) + p_th_t = path.conditional_probability( + x_1=x_1, + x_t=current_state, + t=t, + h=h, + alpha_t=scheduler_output["alpha_t"], + sigma_t=scheduler_output["sigma_t"], + ) + new_state = categorical(p_th_t.to(dtype)) + + new_states.append(new_state) + states = new_states + + if return_intermediates: + for idx, s in enumerate(states): + intermediates[idx].append(s.clone()) + + return intermediates if return_intermediates else states From dbcb9cfed754b88a256ce7af0b3cf7c6ff326c65 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 23 Sep 2025 14:34:08 -0700 Subject: [PATCH 08/44] Begin creating robust multimodal flow with integrated sampling support --- flow_matching/solver/multimodal_solver.py | 2 +- flow_matching/utils/multimodal.py | 210 +++++++++++++--------- 2 files changed, 122 insertions(+), 90 deletions(-) diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 3c0fd45..76a0b71 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -148,7 +148,7 @@ def sample( t = t_discretization[i : i + 1] h = t_discretization[i + 1] - t_discretization[i] - outputs = self.model(x=states, t=t, **model_extras) + outputs = self.model(states, t, **model_extras) if not isinstance(outputs, (list, tuple)) or len(outputs) != len( states diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index e62a738..531df4f 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -4,7 +4,7 @@ # This source code is licensed under the CC-by-NC license found in the # LICENSE file in the root directory of this source tree. -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union import torch from torch import nn, Tensor @@ -12,11 +12,10 @@ # flow_matching from flow_matching.loss.generalized_loss import MixturePathGeneralizedKL from flow_matching.path.mixture import MixtureDiscreteProbPath -from flow_matching.path.scheduler import Scheduler -from flow_matching.path.scheduler.schedule_transform import ScheduleTransformedModel -from flow_matching.solver import MixtureDiscreteEulerSolver -from flow_matching.solver.ode_solver import ODESolver -from flow_matching.utils import ModelWrapper +from flow_matching.solver.multimodal_solver import MultimodalSolver + + +MULTIMODAL_METHOD = Literal["euler"] def _default_continuous_loss(pred: Tensor, target: Tensor) -> Tensor: @@ -42,39 +41,31 @@ class Flow(nn.Module): loss) and inference (sampling) across all modalities. Args: + model (nn.Module): + A model that receives a sequence of state tensors + (one per modality) as ``x`` and a scalar time tensor ``t``, + and returns a sequence of output tensors. For continuous modalities, + the output is a velocity. For discrete modalities, it is the + posterior probability `p_1t`. modalities (Dict[str, Dict[str, Any]]): Mapping from modality name to a dict with keys: - - "model": nn.Module (or ModelWrapper) that implements the velocity model. - "path": a probability path object (e.g., MixtureDiscreteProbPath for discrete data, or any continuous path implementation). - "loss" (optional): a callable loss function. If omitted, a default loss is chosen based on the path type. - training_scheduler (Optional[Scheduler]): Scheduler used during training. - inference_scheduler (Optional[Scheduler]): Scheduler used during inference (sampling). - - Raises: - TypeError: if any model is not an instance of nn.Module. """ def __init__( self, + model: nn.Module, modalities: Dict[str, Dict[str, Any]], - training_scheduler: Optional[Scheduler] = None, - inference_scheduler: Optional[Scheduler] = None, ) -> None: super().__init__() - self.modalities = nn.ModuleDict() + self.model = model self.paths: Dict[str, Any] = {} self.loss_fns: Dict[str, Callable] = {} - self.training_scheduler = training_scheduler - self.inference_scheduler = inference_scheduler for name, spec in modalities.items(): - model = spec["model"] - if not isinstance(model, nn.Module): - raise TypeError(f"Model for modality '{name}' must be an nn.Module.") - self.modalities[name] = model - path = spec["path"] self.paths[name] = path @@ -89,107 +80,148 @@ def __init__( def training_loss( self, - inputs: Dict[str, Tuple[Tensor, Tensor]], - t: Tensor, - ) -> Tensor: + x_1: Sequence[Tensor], + x_t: Sequence[Tensor], + dx_t: Sequence[Tensor], + t: Sequence[Tensor], + **model_extras: dict, + ) -> Tuple[Sequence[Tensor], Dict[str, Tensor]]: """ Compute the total training loss across all modalities. Args: - inputs (Dict[str, Tuple[Tensor, Tensor]]): Mapping from modality name to a tuple ``(x_1, x_t)`` where ``x_1`` is the data at - time ``0`` and ``x_t`` is the data at the sampled time ``t``. - t (Tensor): Tensor of shape ``(batch,)`` containing the time values. + x_1 (Sequence[Tensor]): Sequence of tensors, one per modality, + containing the data at time 1. + x_t (Sequence[Tensor]): Sequence of tensors, one per modality, + containing the data at time t. + dx_t (Sequence[Tensor]): Sequence of tensors, one per modality, + containing the velocity field at time t. + t (Sequence[Tensor]): Sequence of tensors, one per modality, + containing the time values. + **model_extras (dict): Additional keyword arguments to pass to the model. Returns: - Tensor: scalar loss (sum of modality losses). + Tuple[Sequence[Tensor], Dict[str, Tensor]]: + Scalar loss (sum of modality losses) and a dictionary + of individual modality losses. """ + assert ( + len(x_1) == len(x_t) == len(dx_t) == len(t) == len(self.paths) + ), "Input sequences must match the number of modalities." + + loss_dict = {} total_loss = 0.0 - for name, (x_1, x_t, dx_t) in inputs.items(): - model = self.modalities[name] + + logits = self.model(x_t, t, **model_extras) + + for i, name in enumerate(self.paths): path = self.paths[name] loss_fn = self.loss_fns[name] if isinstance(path, MixtureDiscreteProbPath): # Discrete case: model should output logits. - logits = model(x=x_t, t=t) - loss = loss_fn(logits, x_1, x_t, t) + assert x_t[i].dtype == torch.long, ( + f"Expected integer tensor for discrete modality '{name}', " + f"got {x_t[i].dtype}", + ) + loss = loss_fn(logits[i], x_1[i], x_t[i], t[i]) else: # Continuous case: model returns velocity field. - pred_vel = model(x=x_t, t=t) - loss = loss_fn(pred_vel, dx_t) + assert x_t[i].dtype == torch.float32, ( + f"Expected float tensor for continuous modality '{name}', " + f"got {x_t[i].dtype}", + ) + loss = loss_fn(logits[i], dx_t[i]) + loss_dict[name] = loss.detach() total_loss = total_loss + loss - return total_loss + return total_loss, loss_dict - @torch.no_grad() def sample( self, - batch_size: int, + x_init: Sequence[Tensor], + time_grid: Optional[Tensor] = None, device: torch.device = torch.device("cpu"), steps: int = 1000, - ) -> Dict[str, Tensor]: + step_size: Optional[float] = None, + method: MULTIMODAL_METHOD = "euler", + return_intermediates: bool = False, + enable_grad: bool = False, + **model_extras: dict, + ) -> Union[Sequence[Tensor], Sequence[List[Tensor]]]: """ Generate samples for each modality using the inference scheduler. Args: - batch_size (int): Number of samples to generate. + x_init (Sequence[Tensor]): + Sequence of tensors, one per modality, containing the initial states at time 0. + For continuous modalities, this is typically Gaussian noise. + For discrete modalities, this is typically samples from a uniform categorical distribution. + time_grid (Optional[Tensor]): Optional tensor of time points defining the interval. + If provided, it overrides the uniform discretization defined by `steps`. device (torch.device, optional): Device on which to run the sampling. steps (int, optional): Number of integration steps for the ODE solver. + step_size (Optional[float]): Fixed step size for uniform discretization. + If ``None``, the step size is computed from ``steps``. + method (MULTIMODAL_METHOD): Numerical integration method. Currently only ``"euler"`` is + supported, representing a single forward step. + return_intermediates (bool): If ``True``, returns a list of tensors for + each modality containing the state at each intermediate time step. + enable_grad (bool): Whether to enable gradient tracking during integration. + **model_extras (dict): Additional keyword arguments to pass to the model. Returns: - Dict[str, Tensor]: mapping from modality name to sampled tensor. + Union[Sequence[Tensor], Sequence[List[Tensor]]]: A list where each element corresponds to a modality. + Each element is either a tensor of shape ``(batch_size, ...)`` containing the samples, + or a list of tensors (if `return_intermediates` is True in `MultimodalSolver.sample`). """ - xs: Dict[str, Tensor] = {} - for name, model in self.modalities.items(): + # Validate samples for each modality. + for i, name in enumerate(self.paths): path = self.paths[name] - # Maybe transform the schedule of each modality. - velocity_model = model - if ( - self.training_scheduler is not None - and self.inference_scheduler is not None - ): - velocity_model = ScheduleTransformedModel( - velocity_model=model, - original_scheduler=self.training_scheduler, - new_scheduler=self.inference_scheduler, - ) - - # Initialise samples for each modality. - assert hasattr( - model, "sample_shape" - ), f"Model for modality '{name}' must implement 'sample_shape' method." - assert hasattr( - model, "sample_prior" - ), f"Model for modality '{name}' must implement 'sample_prior' method." - x_shape = model.sample_shape(batch_size) - xs[name] = model.sample_prior(x_shape, device=device) - - # Set up ODE solver. - solver = ODESolver(velocity_model=velocity_model) if isinstance(path, MixtureDiscreteProbPath): - - class WrappedModel(ModelWrapper): - """Wrap velocity model to output probabilities.""" - - def forward(self, x: torch.Tensor, t: torch.Tensor, **extras): - """Output class probabilities.""" - return torch.softmax(self.model(x, t, **extras), dim=-1) - - wrapped_probability_denoiser = WrappedModel(velocity_model) - solver = MixtureDiscreteEulerSolver( - model=wrapped_probability_denoiser, - path=path, - vocabulary_size=wrapped_probability_denoiser.model.input_dim, + assert x_init[i].dtype == torch.long, ( + f"Expected integer tensor for discrete modality '{name}', " + f"got {x_init[i].dtype}", + ) + else: + assert x_init[i].dtype == torch.float32, ( + f"Expected float tensor for continuous modality '{name}', " + f"got {x_init[i].dtype}", ) - # Solve ODE to obtain samples at time 1. - time_grid = torch.linspace(0.0, 1.0, steps, device=device) - xs[name] = solver.sample( - x_init=xs[name], - step_size=1.0 / steps, - time_grid=time_grid, - ) - - return xs + x_init[i] = x_init[i].to(device) + + # Set up Euler solver for each modality. + modality_configs = { + name: { + "type": ( + "discrete" + if isinstance(path, MixtureDiscreteProbPath) + else "continuous" + ), + "path": path, + } + for name, path in self.paths.items() + } + solver = MultimodalSolver( + model=self.model, + modality_configs=modality_configs, + ) + + # Solve to obtain multimodal samples at time 1. + step_size = step_size or (1.0 / steps) + time_grid = time_grid or torch.linspace(0.0, 1.0, steps, device=device) + + samples = solver.sample( + x_init=x_init, + step_size=step_size, + method=method, + time_grid=time_grid, + return_intermediates=return_intermediates, + enable_grad=enable_grad, + model_extras=model_extras, + ) + + return samples From e5d6c49b79f8b531f465f8d9b9b3beac08f7074f Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 23 Sep 2025 15:06:18 -0700 Subject: [PATCH 09/44] Add new example for multimodality --- examples/2d_multimodal_flow_matching.py | 451 ++++++++++++++++++++++++ 1 file changed, 451 insertions(+) create mode 100644 examples/2d_multimodal_flow_matching.py diff --git a/examples/2d_multimodal_flow_matching.py b/examples/2d_multimodal_flow_matching.py new file mode 100644 index 0000000..68cf9ee --- /dev/null +++ b/examples/2d_multimodal_flow_matching.py @@ -0,0 +1,451 @@ +# %% [markdown] +# # A simple 2D Multimodal Flow Matching model + +# %% [markdown] +# This notebook trains and evaluates a multimodal FM model that jointly handles +# a discrete modality (categorical data) and a continuous modality (real‑valued 2‑D data). +# +# Dataset: 2D discrete/continuous checkerboard +# Model (probability denoiser/velocity): MLPs for each modality and a shared Transformer trunk + +# %% [markdown] +# ## Imports and init device + +# %% +import time + +# To avoide meshgrid warning +import warnings +from typing import Any, Dict, List, Sequence + +# visualization +import matplotlib.pyplot as plt +import torch +from flow_matching.path import AffineProbPath, MixtureDiscreteProbPath +from flow_matching.path.scheduler import ( + CondOTScheduler, # continuous scheduler (training) + PolynomialConvexScheduler, # discrete scheduler (training) +) + +# flow_matching +from flow_matching.utils.multimodal import Flow +from torch import nn, Tensor + +warnings.filterwarnings("ignore", category=UserWarning, module="torch") + +# %% +if torch.cuda.is_available(): + device = "cuda:0" + print("Using GPU") +elif torch.backends.mps.is_available(): + device = "mps" + print("Using MPS") +else: + device = "cpu" + print("Using CPU") + +# %% +torch.manual_seed(42) + +# %% [markdown] +# ## Shared model + +# %% + + +class SharedTransformer(nn.Module): + """ + Shared Transformer trunk used by both modalities. + + Args: + hidden_dim (int): The hidden dimension of the model. + nhead (int): The number of attention heads. + num_layers (int): The number of TransformerEncoder layers. + """ + + def __init__(self, hidden_dim: int = 128, nhead: int = 4, num_layers: int = 2): + super().__init__() + encoder_layer = nn.TransformerEncoderLayer(d_model=hidden_dim, nhead=nhead) + self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) + + def forward(self, x: Tensor) -> Tensor: + """ + Forward pass through the shared Transformer. + + Args: + x (Tensor): Input tensor of shape (sequence_length, batch_size, hidden_dim). + + Returns: + Tensor: Output tensor of the same shape as input. + """ + return self.transformer(x) + + +# %% [markdown] +# ## Datasets + +# %% +def inf_train_gen_discrete( + n_grid_points: int = 128, + batch_size: int = 200, + device: str = "cpu", +) -> Tensor: + """ + Generate a batch of discrete (categorical) samples. + Returns a tensor of shape (batch, 2) with integer token IDs. + + Args: + n_grid_points (int): Number of grid points along one axis (should be divisible by 4). + batch_size (int): Number of samples to generate. + device (str): Device to place the tensor on. + + Returns: + Tensor: A tensor of shape (batch_size, 2) with integer token IDs. + """ + assert n_grid_points % 4 == 0, "grid size must be divisible by 4" + n_grid_points //= 4 + + x1 = torch.randint(low=0, high=n_grid_points * 4, size=(batch_size,), device=device) + samples_x2 = torch.randint( + low=0, high=n_grid_points, size=(batch_size,), device=device + ) + + x2 = ( + samples_x2 + + 2 * n_grid_points + - torch.randint(low=0, high=2, size=(batch_size,), device=device) + * 2 + * n_grid_points + + (torch.floor(x1 / n_grid_points) % 2) * n_grid_points + ) + return torch.stack([x1, x2], dim=1).long() + + +def inf_train_gen_continuous(batch_size: int = 200, device: str = "cpu") -> Tensor: + """ + Generate a batch of 2-D continuous points from a checkerboard-like distribution. + Returns a tensor of shape (batch, 2). + + Args: + batch_size (int): Number of samples to generate. + device (str): Device to place the tensor on. + + Returns: + Tensor: A tensor of shape (batch_size, 2) with continuous values. + """ + x1 = torch.rand(batch_size, device=device) * 4 - 2 + x2_ = ( + torch.rand(batch_size, device=device) + - torch.randint(high=2, size=(batch_size,), device=device) * 2 + ) + x2 = x2_ + (torch.floor(x1) % 2) + data = torch.stack([x1, x2], dim=1) / 0.45 + return data.float() + + +# %% [markdown] +# ## Unified Multimodal Model + +# %% +class Swish(nn.Module): + """Swish activation (x * sigmoid(x)).""" + + def forward(self, x: Tensor) -> Tensor: + """Forward pass through the Swish activation.""" + return torch.sigmoid(x) * x + + +class TransformerModel(nn.Module): + """ + A unified Transformer-based model for handling multiple modalities. + + This model processes a sequence of modalities, each with its own input + and output heads, while sharing a central Transformer trunk. It is designed + to be flexible for both discrete (categorical) and continuous data types. + + Args: + shared_transformer (SharedTransformer): The shared TransformerEncoder module. + modality_configs (List[Dict[str, Any]]): A list of dictionaries, each configuring a modality. + Required keys per config: + - 'type': 'discrete' or 'continuous'. + - 'length': The sequence length for this modality's tokens. + If 'type' is 'discrete': + - 'vocab_size': The size of the vocabulary. + If 'type' is 'continuous': + - 'input_dim': The feature dimension of the continuous data. + time_dim (int): The dimension of the time embedding. + hidden_dim (int): The hidden dimension of the model and transformer. + + Raises: + ValueError: If an unknown modality type is provided. + """ + + def __init__( + self, + shared_transformer: SharedTransformer, + modality_configs: List[Dict[str, Any]], + time_dim: int = 1, + hidden_dim: int = 128, + ): + super().__init__() + self.shared = shared_transformer + self.modality_configs = modality_configs + self.seq_lengths = [config["length"] for config in modality_configs] + + self.input_embedders = nn.ModuleList() + self.time_embedders = nn.ModuleList() + self.input_projectors = nn.ModuleList() + self.output_heads = nn.ModuleList() + self.activations = nn.ModuleList() + + for config in self.modality_configs: + self.time_embedders.append(nn.Linear(1, time_dim)) + self.input_projectors.append(nn.Linear(hidden_dim + time_dim, hidden_dim)) + self.activations.append(Swish()) + + if config["type"] == "discrete": + self.input_embedders.append( + nn.Embedding(config["vocab_size"], hidden_dim) + ) + self.output_heads.append(nn.Linear(hidden_dim, config["vocab_size"])) + elif config["type"] == "continuous": + self.input_embedders.append(nn.Linear(config["input_dim"], hidden_dim)) + self.output_heads.append(nn.Linear(hidden_dim, config["input_dim"])) + else: + raise ValueError(f"Unknown modality type: {config['type']}") + + def forward( + self, x_modalities: Sequence[Tensor], t_modalities: Sequence[Tensor] + ) -> Sequence[Tensor]: + """ + Forward pass for multiple modalities. + + Args: + x_modalities (Sequence[Tensor]): A sequence of input tensors, one for each modality. + Shape for discrete: (batch, length) + Shape for continuous: (batch, input_dim) + t_modalities (Sequence[Tensor]): A sequence of time tensors, one for each modality. + Shape for all: (batch, 1) + + Returns: + Sequence[Tensor]: A sequence of output tensors, one for each modality. + """ + embeddings = [] + + # 1. Process each modality through its specific input head + for i, (x, t, config) in enumerate( + zip(x_modalities, t_modalities, self.modality_configs) + ): + # Embed time and expand to match sequence length + t_emb = self.time_embedders[i](t) + t_emb = t_emb.unsqueeze(1).expand(-1, config["length"], -1) + + # Embed input based on modality type + if config["type"] == "discrete": + x_emb = self.input_embedders[i](x) # (B, length, hidden_dim) + else: # continuous + x_emb = self.input_embedders[i](x) # (B, hidden_dim) + x_emb = x_emb.unsqueeze(1) # (B, 1, hidden_dim) + + # Combine, project, and activate + combined = torch.cat([x_emb, t_emb], dim=-1) + h = self.input_projectors[i](combined) + h = self.activations[i](h) + + # Prepare for transformer (seq_len, batch, hidden_dim) + embeddings.append(h.permute(1, 0, 2)) + + # 2. Concatenate all modality embeddings and pass through shared transformer + full_sequence = torch.cat(embeddings, dim=0) + transformer_out = self.shared(full_sequence) + + # 3. Split the output and process through specific output heads + output_chunks = torch.split(transformer_out, self.seq_lengths, dim=0) + results = [] + for i, chunk in enumerate(output_chunks): + # (length, B, hidden_dim) -> (B, length, hidden_dim) + chunk = chunk.permute(1, 0, 2) + output = self.output_heads[i](chunk) + + # Squeeze sequence dimension if it's 1 (for continuous case) + if output.size(1) == 1: + output = output.squeeze(1) + results.append(output) + + return results + + +# %% [markdown] +# ## Instantiate modalities and model + +# %% +# ---- General Hyperparameters ----------------------------------------- +length = 2 # 2 tokens per sample +vocab_size = 128 +added_token = 0 # uniform source distribution → no extra token +vocab_size += added_token +hidden_dim = 128 + +# ---- Shared transformer trunk ---------------------------------------- +shared_transformer = SharedTransformer(hidden_dim=hidden_dim, nhead=4, num_layers=2).to( + device +) + +# ---- Model and Path Configuration ------------------------------------ +modality_configs = [ + { + "type": "discrete", + "vocab_size": vocab_size, + "length": length, + }, + { + "type": "continuous", + "input_dim": length, + "length": 1, # This modality is treated as a single token in the sequence + }, +] + +# A unified model that handles all modalities +model = TransformerModel( + shared_transformer=shared_transformer, + modality_configs=modality_configs, + time_dim=1, + hidden_dim=hidden_dim, +).to(device) + +# Path definitions remain distinct per modality +discrete_path = MixtureDiscreteProbPath(scheduler=PolynomialConvexScheduler(n=2.0)) +continuous_path = AffineProbPath(scheduler=CondOTScheduler()) + +# ---- Assemble modalities dict for Flow ------------------------------- +modalities = { + "discrete": { + "path": discrete_path, + # loss omitted → Flow will use MixturePathGeneralizedKL automatically + }, + "continuous": { + "path": continuous_path, + # loss omitted → Flow will use MSE loss automatically + }, +} + +# %% [markdown] +# ## Instantiate the multimodal Flow model + +# %% +flow = Flow(model=model, modalities=modalities) + +# Optimizer (optimises both modality models) +optimizer = torch.optim.Adam(flow.parameters(), lr=1e-3) + +# %% [markdown] +# ## Training loop + +# %% +lr = 1e-3 +batch_size = 1024 # adjust as needed to fit in memory +iterations = 12001 +print_every = 3000 +epsilon = 1e-3 + +source_distribution = "uniform" # for the discrete modality + +start_time = time.time() +for i in range(iterations): + optimizer.zero_grad() + + # ---- Discrete data ------------------------------------------------- + x1_disc = inf_train_gen_discrete( + n_grid_points=vocab_size - added_token, + batch_size=batch_size, + device=device, + ) + if source_distribution == "uniform": + x0_disc = torch.randint_like(x1_disc, high=vocab_size) + else: # mask case (not used here) + raise NotImplementedError + + # ---- Continuous data ----------------------------------------------- + x1_cont = inf_train_gen_continuous(batch_size=batch_size, device=device) + x0_cont = torch.randn_like(x1_cont) # isotropic Gaussian prior + + # ---- Sample a common time tensor for both modalities --------------- + t = torch.rand(batch_size, device=device) * (1 - epsilon) + + # ---- Sample from each path to obtain x_t --------------------------- + disc_path_sample = discrete_path.sample(t=t, x_0=x0_disc, x_1=x1_disc) + cont_path_sample = continuous_path.sample(t=t, x_0=x0_cont, x_1=x1_cont) + + # ---- Build the inputs expected by Flow.training_loss ----------- + x_1 = [x1_disc, x1_cont] + x_t = [disc_path_sample.x_t, cont_path_sample.x_t] + dx_t = [None, cont_path_sample.dx_t] # NOTE: dx_t is None for discrete + ts = [t, t] + + # ---- Compute total loss and back‑propagate ------------------------- + loss, _ = flow.training_loss(x_1=x_1, x_t=x_t, dx_t=dx_t, t=ts) + loss.backward() + optimizer.step() + + # ---- Logging ------------------------------------------------------- + if (i + 1) % print_every == 0: + elapsed = time.time() - start_time + print( + f"| iter {i+1:6d} | {elapsed*1000/print_every:5.2f} ms/step | loss {loss.item():8.3f} " + ) + start_time = time.time() + +# %% [markdown] +# ## Sampling from the trained multimodal model + +# %% +x_init = [ + torch.randint_like( + x1_disc, high=vocab_size + ), # discrete initial state (uniform categorical) + torch.randn_like(x1_cont), # continuous initial state (Gaussian noise) +] + +flow.eval() # switch to eval mode for sampling +samples = flow.sample(x_init=x_init, device=device, steps=1000) + +# %% [markdown] +# ## Visualization + +# %% +# ---- Discrete modality ------------------------------------------------- +discrete_samples = samples[0].cpu().numpy() # shape (N, 2) integer tokens +vocab = vocab_size + +# Plot a 2‑D histogram of the discrete samples +plt.figure(figsize=(6, 5)) +plt.hist2d( + discrete_samples[:, 0], + discrete_samples[:, 1], + bins=vocab, + cmap="viridis", +) +plt.title("Discrete modality samples (token histogram)") +plt.xlabel("Token 1") +plt.ylabel("Token 2") +plt.colorbar(label="Count") +plt.tight_layout() +plt.show() + +# ---- Continuous modality ----------------------------------------------- +continuous_samples = samples[1].cpu().numpy() # shape (N, 2) + +# Plot a 2‑D histogram of the continuous samples +plt.figure(figsize=(6, 5)) +plt.hist2d( + continuous_samples[:, 0], + continuous_samples[:, 1], + bins=200, + cmap="viridis", +) +plt.title("Continuous modality samples (2-D density)") +plt.xlabel("x₁") +plt.ylabel("x₂") +plt.colorbar(label="Count") +plt.tight_layout() +plt.show() From 32d505e7f978261dee13e5c1a0625c84471b5062 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 23 Sep 2025 17:50:32 -0700 Subject: [PATCH 10/44] Revise multimodal_solver.py according to discrete_solver.py --- examples/2d_multimodal_flow_matching.py | 2 +- flow_matching/solver/multimodal_solver.py | 183 +++++++++++++++++----- flow_matching/utils/multimodal.py | 6 + 3 files changed, 155 insertions(+), 36 deletions(-) diff --git a/examples/2d_multimodal_flow_matching.py b/examples/2d_multimodal_flow_matching.py index 68cf9ee..b02de18 100644 --- a/examples/2d_multimodal_flow_matching.py +++ b/examples/2d_multimodal_flow_matching.py @@ -380,7 +380,7 @@ def forward( x_1 = [x1_disc, x1_cont] x_t = [disc_path_sample.x_t, cont_path_sample.x_t] dx_t = [None, cont_path_sample.dx_t] # NOTE: dx_t is None for discrete - ts = [t, t] + ts = [t, t] # NOTE: For now, both modalities share the same time # ---- Compute total loss and back‑propagate ------------------------- loss, _ = flow.training_loss(x_1=x_1, x_t=x_t, dx_t=dx_t, t=ts) diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 76a0b71..bd57546 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -4,15 +4,27 @@ # This source code is licensed under the CC-by-NC license found in the # LICENSE file in the root directory of this source tree. +from contextlib import nullcontext +from math import ceil from typing import Any, Callable, Dict, List, Optional, Sequence, Union import torch from torch import Tensor +from torch.nn import functional as F + from flow_matching.path import MixtureDiscreteProbPath from flow_matching.solver.solver import Solver +from flow_matching.solver.utils import get_nearest_times from flow_matching.utils import categorical, ModelWrapper +try: + from tqdm import tqdm + + TQDM_AVAILABLE = True +except ImportError: + TQDM_AVAILABLE = False + class MultimodalSolver(Solver): """Solver for multiple continuous and discrete data modalities. @@ -37,6 +49,7 @@ class MultimodalSolver(Solver): Each dictionary must have a ``'type'`` key, which is either ``'continuous'`` or ``'discrete'``. Discrete modality configs must also provide a ``'path'`` key with a `MixtureDiscreteProbPath` object. + source_distribution_p (Optional[Tensor], optional): Source distribution, must be of shape [vocabulary_size]. Required only when divergence-free term for the probability velocity is non-zero. Defaults to None. Raises: TypeError: If `model` is not callable. @@ -46,12 +59,15 @@ def __init__( self, model: Union[ModelWrapper, Callable], modality_configs: List[Dict[str, Any]], + source_distribution_p: Optional[Tensor] = None, ): super().__init__() if not callable(model): raise TypeError(f"model must be callable, got {type(model)}") self.model = model self.modality_configs = modality_configs + self.source_distribution_p = source_distribution_p + self._validate_configs() def _validate_configs(self): @@ -81,10 +97,12 @@ def sample( self, x_init: Sequence[Tensor], step_size: Optional[float], + div_free: Union[float, Callable[[float], float]] = 0.0, method: str = "euler", time_grid: Tensor = torch.tensor([0.0, 1.0]), return_intermediates: bool = False, enable_grad: bool = False, + verbose: bool = False, **model_extras: dict, ) -> Union[Sequence[Tensor], Sequence[List[Tensor]]]: """Sample all modalities simultaneously. @@ -93,18 +111,24 @@ def sample( x_init (Sequence[Tensor]): Initial states for each modality. step_size (Optional[float]): Fixed step size for uniform discretization. If ``None``, the discretization is taken from ``time_grid``. + div_free (Union[float, Callable[[float], float]]): The coefficient + of the divergence-free term in the probability velocity + (for discrete modalities). Can be either a float or a time + dependent function. Defaults to 0.0. method (str): Numerical integration method. Currently only ``"euler"`` is supported, representing a single forward step. time_grid (Tensor): Tensor of time points defining the interval. return_intermediates (bool): If ``True``, returns a list of tensors for each modality containing the state at each intermediate time step. enable_grad (bool): Whether to enable gradient tracking during integration. + verbose (bool): If ``True``, displays a progress bar during sampling. **model_extras (dict): Additional arguments passed to the model. Raises: - NotImplementedError: If an unsupported integration method is specified. ValueError: If the number of initial states does not match the number of modality configurations. + NotImplementedError: If an unsupported integration method is specified. + ImportError: If ``verbose`` is ``True`` but ``tqdm`` is not installed. TypeError: If the model's output does not match the expected format. Returns: @@ -117,36 +141,67 @@ def sample( raise ValueError( "Number of initial states must match the number of modality configurations." ) + if method != "euler": + raise NotImplementedError( + f"Method '{method}' is not implemented for MultimodalSolver." + ) + if not div_free == 0.0: + assert ( + self.source_distribution_p is not None + ), "Source distribution p must be specified in order to add a divergence-free term to the probability velocity for each discrete modality." + # Initialize the current state `x_t` with the initial state `X_0`. device = x_init[0].device + batch_size = x_init[0].shape[0] time_grid = time_grid.to(device) if step_size is None: + # If step_size is None then set the t discretization to time_grid. t_discretization = time_grid n_steps = len(time_grid) - 1 else: - t_init, t_final = time_grid[0].item(), time_grid[-1].item() - n_steps = int( - torch.ceil(torch.tensor((t_final - t_init) / step_size)).item() - ) - t_discretization = torch.linspace( - t_init, t_final, n_steps + 1, device=device + # If step_size is float then t discretization is uniform with step size set by step_size. + t_init = time_grid[0].item() + t_final = time_grid[-1].item() + assert ( + t_final - t_init + ) > step_size, f"Time interval [time_grid[0], time_grid[-1]] must be larger than step_size. Got a time interval [{t_init}, {t_final}] and step_size {step_size}." + + n_steps = ceil((t_final - t_init) / step_size) + t_discretization = torch.tensor( + [t_init + step_size * i for i in range(n_steps)] + [t_final], + device=x_init.device, ) - states: List[Tensor] = [x.clone() for x in x_init] - intermediates: List[List[Tensor]] = ( + if return_intermediates: + # Get order of intermediate steps + order = torch.argsort(time_grid) + # Compute intermediate steps to return via nearest points in t_discretization to time_grid. + time_grid = get_nearest_times( + time_grid=time_grid, t_discretization=t_discretization + ) + + states: Sequence[Tensor] = [x.clone() for x in x_init] + intermediates: Sequence[List[Tensor]] = ( [[x.clone()] for x in x_init] if return_intermediates else [] ) - if method != "euler": - raise NotImplementedError( - f"Method '{method}' is not implemented for MultimodalSolver." - ) + steps_counter = 0 + + if verbose: + if not TQDM_AVAILABLE: + raise ImportError( + "tqdm is required for verbose mode. Please install it." + ) + ctx = tqdm(total=t_final, desc=f"NFE: {steps_counter}") + else: + ctx = nullcontext() - with torch.set_grad_enabled(enable_grad): + with ctx, torch.set_grad_enabled(enable_grad): for i in range(n_steps): - t = t_discretization[i : i + 1] - h = t_discretization[i + 1] - t_discretization[i] + # NOTE: For now, all modalities share the same time + t = [t_discretization[i : i + 1].repeat(batch_size)] * len(states) + h = t_discretization[i + 1 : i + 2] - t_discretization[i : i + 1] outputs = self.model(states, t, **model_extras) @@ -157,38 +212,96 @@ def sample( "The model must return a sequence of tensors matching the number of modalities." ) - new_states = [] for idx, config in enumerate(self.modality_configs): - current_state = states[idx] model_output = outputs[idx] if config["type"] == "continuous": - new_state = current_state + h * model_output + # Sample x_{t+h} = x_t + h * v(x_t,t) + states[idx] = states[idx] + h * model_output + elif config["type"] == "discrete": - p_1t = model_output dtype = config.get("dtype_categorical", torch.float32) + # Sample x_1 ~ p_1|t( \cdot |x_t) + p_1t = model_output + x_1 = categorical(p_1t.to(dtype=dtype)) + + # Checks if final step if i == n_steps - 1: - new_state = categorical(p_1t.to(dtype)) + states[idx] = x_1 # x_t = x_1 at final step else: + vocabulary_size = p_1t.shape[1] + if self.source_distribution_p is not None: + assert self.source_distribution_p.shape == torch.Size( + [vocabulary_size] + ), f"Source distribution p dimension must match the vocabulary size {vocabulary_size}. Got {self.source_distribution_p.shape}." + + # Compute u_t(x|x_t,x_1) path: MixtureDiscreteProbPath = config["path"] - x_1 = categorical(p_1t.to(dtype)) - scheduler_output = path.scheduler(t=t) - p_th_t = path.conditional_probability( - x_1=x_1, - x_t=current_state, - t=t, - h=h, - alpha_t=scheduler_output["alpha_t"], - sigma_t=scheduler_output["sigma_t"], + scheduler_output = path.scheduler(t=t[idx]) + + k_t = scheduler_output.alpha_t + d_k_t = scheduler_output.d_alpha_t + + delta_1 = F.one_hot( + x_1, num_classes=self.vocabulary_size + ).to(k_t.dtype) + u = d_k_t / (1 - k_t) * delta_1 + + # Add divergence-free part + div_free_t = ( + div_free(t[idx]) if callable(div_free) else div_free ) - new_state = categorical(p_th_t.to(dtype)) - new_states.append(new_state) - states = new_states + if div_free_t > 0: + p_0 = self.source_distribution_p[ + (None,) * states[idx].dim() + ] + u = u + div_free_t * d_k_t / (k_t * (1 - k_t)) * ( + (1 - k_t) * p_0 + k_t * delta_1 + ) + + # Set u_t(x_t|x_t,x_1) = 0 + delta_t = F.one_hot( + states[idx], num_classes=self.vocabulary_size + ) + u = torch.where( + delta_t.to(dtype=torch.bool), torch.zeros_like(u), u + ) + + # Sample x_t ~ u_t( \cdot |x_t,x_1) + intensity = u.sum(dim=-1) # Assuming u_t(xt|xt,x1) := 0 + mask_jump = torch.rand( + size=states[idx].shape, device=states[idx].device + ) < 1 - torch.exp(-h * intensity) + + if mask_jump.sum() > 0: + states[idx][mask_jump] = categorical( + u[mask_jump].to(dtype=dtype) + ) + + # Increment time for each modality + t[idx] = t[idx] + h + + steps_counter += 1 if return_intermediates: for idx, s in enumerate(states): - intermediates[idx].append(s.clone()) + if t[idx] in time_grid: + intermediates[idx].append(s.clone()) - return intermediates if return_intermediates else states + if verbose: + ctx.n = torch.cat(t).mean().long().item() + ctx.refresh() + ctx.set_description(f"NFE: {steps_counter}") + + if return_intermediates: + if step_size is None: + return intermediates + else: + return [ + [intermediates[idx][i] for i in order] + for idx in range(len(intermediates)) + ] + else: + return states diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 531df4f..4e67e19 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -145,6 +145,7 @@ def sample( device: torch.device = torch.device("cpu"), steps: int = 1000, step_size: Optional[float] = None, + div_free: Union[float, Callable[[float], float]] = 0.0, method: MULTIMODAL_METHOD = "euler", return_intermediates: bool = False, enable_grad: bool = False, @@ -164,6 +165,10 @@ def sample( steps (int, optional): Number of integration steps for the ODE solver. step_size (Optional[float]): Fixed step size for uniform discretization. If ``None``, the step size is computed from ``steps``. + div_free (Union[float, Callable[[float], float]]): The coefficient + of the divergence-free term in the probability velocity + (for discrete modalities). Can be either a float or a time + dependent function. Defaults to 0.0. method (MULTIMODAL_METHOD): Numerical integration method. Currently only ``"euler"`` is supported, representing a single forward step. return_intermediates (bool): If ``True``, returns a list of tensors for @@ -217,6 +222,7 @@ def sample( samples = solver.sample( x_init=x_init, step_size=step_size, + div_free=div_free, method=method, time_grid=time_grid, return_intermediates=return_intermediates, From cf051878ff8ffc593b4e827905b9546e76dc1ac0 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 23 Sep 2025 17:51:55 -0700 Subject: [PATCH 11/44] Fix variable references --- flow_matching/solver/multimodal_solver.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index bd57546..44b8258 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -49,7 +49,9 @@ class MultimodalSolver(Solver): Each dictionary must have a ``'type'`` key, which is either ``'continuous'`` or ``'discrete'``. Discrete modality configs must also provide a ``'path'`` key with a `MixtureDiscreteProbPath` object. - source_distribution_p (Optional[Tensor], optional): Source distribution, must be of shape [vocabulary_size]. Required only when divergence-free term for the probability velocity is non-zero. Defaults to None. + source_distribution_p (Optional[Tensor], optional): Source distribution, + must be of shape [vocabulary_size]. Required only when divergence-free + term for the probability velocity is non-zero. Defaults to None. Raises: TypeError: If `model` is not callable. @@ -243,9 +245,9 @@ def sample( k_t = scheduler_output.alpha_t d_k_t = scheduler_output.d_alpha_t - delta_1 = F.one_hot( - x_1, num_classes=self.vocabulary_size - ).to(k_t.dtype) + delta_1 = F.one_hot(x_1, num_classes=vocabulary_size).to( + k_t.dtype + ) u = d_k_t / (1 - k_t) * delta_1 # Add divergence-free part @@ -263,7 +265,7 @@ def sample( # Set u_t(x_t|x_t,x_1) = 0 delta_t = F.one_hot( - states[idx], num_classes=self.vocabulary_size + states[idx], num_classes=vocabulary_size ) u = torch.where( delta_t.to(dtype=torch.bool), torch.zeros_like(u), u From 72f674531abd2156d6ab862734b395a7073b802a Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 23 Sep 2025 19:16:41 -0700 Subject: [PATCH 12/44] Finish updating example for multimodal model --- examples/2d_multimodal_flow_matching.ipynb | 400 ++++++++++-------- examples/2d_multimodal_flow_matching.py | 451 --------------------- flow_matching/solver/multimodal_solver.py | 10 +- flow_matching/utils/multimodal.py | 9 +- 4 files changed, 234 insertions(+), 636 deletions(-) delete mode 100644 examples/2d_multimodal_flow_matching.py diff --git a/examples/2d_multimodal_flow_matching.ipynb b/examples/2d_multimodal_flow_matching.ipynb index ddf31e4..2a31ec1 100644 --- a/examples/2d_multimodal_flow_matching.ipynb +++ b/examples/2d_multimodal_flow_matching.ipynb @@ -36,24 +36,26 @@ "outputs": [], "source": [ "import time\n", - "import torch\n", - "from torch import nn, Tensor\n", "\n", - "# flow_matching\n", - "from flow_matching.utils.multimodal import Flow\n", + "from typing import Any, Dict, List, Sequence\n", + "\n", + "# visualization\n", + "import matplotlib.pyplot as plt\n", + "import torch\n", + "from flow_matching.path import AffineProbPath, MixtureDiscreteProbPath\n", "from flow_matching.path.scheduler import (\n", - " PolynomialConvexScheduler, # discrete scheduler (training)\n", " CondOTScheduler, # continuous scheduler (training)\n", + " PolynomialConvexScheduler, # discrete scheduler (training)\n", ")\n", - "from flow_matching.path import MixtureDiscreteProbPath, AffineProbPath\n", "\n", - "# visualization\n", - "import matplotlib.pyplot as plt\n", + "# flow_matching\n", + "from flow_matching.utils.multimodal import Flow\n", + "from torch import nn, Tensor\n", "\n", - "# To avoide meshgrid warning\n", + "# To avoid meshgrid warning\n", "import warnings\n", "\n", - "warnings.filterwarnings(\"ignore\", category=UserWarning, module='torch')" + "warnings.filterwarnings(\"ignore\", category=UserWarning, module=\"torch\")" ] }, { @@ -91,7 +93,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 3, @@ -121,7 +123,13 @@ "class SharedTransformer(nn.Module):\n", " \"\"\"\n", " Shared Transformer trunk used by both modalities.\n", + "\n", + " Args:\n", + " hidden_dim (int): The hidden dimension of the model.\n", + " nhead (int): The number of attention heads.\n", + " num_layers (int): The number of TransformerEncoder layers.\n", " \"\"\"\n", + "\n", " def __init__(self, hidden_dim: int = 128, nhead: int = 4, num_layers: int = 2):\n", " super().__init__()\n", " encoder_layer = nn.TransformerEncoderLayer(d_model=hidden_dim, nhead=nhead)\n", @@ -129,8 +137,13 @@ "\n", " def forward(self, x: Tensor) -> Tensor:\n", " \"\"\"\n", - " x: (seq_len, batch, hidden_dim)\n", - " Returns transformed tensor of same shape.\n", + " Forward pass through the shared Transformer.\n", + "\n", + " Args:\n", + " x (Tensor): Input tensor of shape (sequence_length, batch_size, hidden_dim).\n", + "\n", + " Returns:\n", + " Tensor: Output tensor of the same shape as input.\n", " \"\"\"\n", " return self.transformer(x)" ] @@ -140,7 +153,7 @@ "id": "af22ef56", "metadata": {}, "source": [ - "## Discrete modality dataset and model" + "## Datasets" ] }, { @@ -158,6 +171,14 @@ " \"\"\"\n", " Generate a batch of discrete (categorical) samples.\n", " Returns a tensor of shape (batch, 2) with integer token IDs.\n", + "\n", + " Args:\n", + " n_grid_points (int): Number of grid points along one axis (should be divisible by 4).\n", + " batch_size (int): Number of samples to generate.\n", + " device (str): Device to place the tensor on.\n", + "\n", + " Returns:\n", + " Tensor: A tensor of shape (batch_size, 2) with integer token IDs.\n", " \"\"\"\n", " assert n_grid_points % 4 == 0, \"grid size must be divisible by 4\"\n", " n_grid_points //= 4\n", @@ -178,74 +199,26 @@ " return torch.stack([x1, x2], dim=1).long()\n", "\n", "\n", - "class Swish(nn.Module):\n", - " \"\"\"Swish activation (x * sigmoid(x)).\"\"\"\n", - "\n", - " def forward(self, x: Tensor) -> Tensor:\n", - " return torch.sigmoid(x) * x\n", - "\n", - "\n", - "class DiscreteTransformerModel(nn.Module):\n", - " \"\"\"\n", - " Model for the discrete modality with separate input and output heads,\n", - " sharing a common Transformer trunk.\n", + "def inf_train_gen_continuous(batch_size: int = 200, device: str = \"cpu\") -> Tensor:\n", " \"\"\"\n", - " def __init__(\n", - " self,\n", - " shared_transformer: SharedTransformer,\n", - " vocab_size: int = 128,\n", - " time_dim: int = 1,\n", - " hidden_dim: int = 128,\n", - " length: int = 2,\n", - " ):\n", - " super().__init__()\n", - " self.shared = shared_transformer\n", - " self.input_dim = vocab_size\n", - " self.time_dim = time_dim\n", - " self.hidden_dim = hidden_dim\n", - " self.length = length\n", - "\n", - " self.time_embedding = nn.Linear(1, time_dim)\n", - " self.token_embedding = nn.Embedding(vocab_size, hidden_dim)\n", - " self.input_proj = nn.Linear(hidden_dim + time_dim, hidden_dim)\n", - " self.output_head = nn.Linear(hidden_dim, vocab_size)\n", - " self.activation = Swish()\n", - "\n", - " def sample_shape(self, batch_size: int) -> torch.Size:\n", - " return torch.Size((batch_size, self.length))\n", - "\n", - " def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor:\n", - " return torch.randint(low=0, high=self.input_dim, size=shape, device=device)\n", - "\n", - " def forward(self, x: Tensor, t: Tensor) -> Tensor:\n", - " \"\"\"\n", - " x: (B, length) integer token IDs\n", - " t: (B,) time scalar\n", - " Returns logits of shape (B, length, vocab_size or input_dim)\n", - " \"\"\"\n", - " if t.ndim == 0:\n", - " t = t.unsqueeze(0).expand(x.shape[0])\n", - "\n", - " # Token embedding\n", - " x_emb = self.token_embedding(x) # (B, length, hidden_dim)\n", - "\n", - " # Time embedding\n", - " t_emb = self.time_embedding(t.unsqueeze(-1).float()) # (B, time_dim)\n", - " t_emb = t_emb.unsqueeze(1).expand(-1, self.length, -1) # (B, length, time_dim)\n", - "\n", - " # Concatenate and project\n", - " h = torch.cat([x_emb, t_emb], dim=-1) # (B, length, hidden_dim+time_dim)\n", - " h = self.input_proj(h) # (B, length, hidden_dim)\n", + " Generate a batch of 2-D continuous points from a checkerboard-like distribution.\n", + " Returns a tensor of shape (batch, 2).\n", "\n", - " # Transformer expects (seq_len, batch, hidden_dim)\n", - " h = h.permute(1, 0, 2) # (length, B, hidden_dim)\n", - " h = self.shared(h) # (length, B, hidden_dim)\n", - " h = h.permute(1, 0, 2) # (B, length, hidden_dim)\n", + " Args:\n", + " batch_size (int): Number of samples to generate.\n", + " device (str): Device to place the tensor on.\n", "\n", - " # Output logits\n", - " h = self.activation(h)\n", - " logits = self.output_head(h) # (B, length, vocab_size or input_dim)\n", - " return logits" + " Returns:\n", + " Tensor: A tensor of shape (batch_size, 2) with continuous values.\n", + " \"\"\"\n", + " x1 = torch.rand(batch_size, device=device) * 4 - 2\n", + " x2_ = (\n", + " torch.rand(batch_size, device=device)\n", + " - torch.randint(high=2, size=(batch_size,), device=device) * 2\n", + " )\n", + " x2 = x2_ + (torch.floor(x1) % 2)\n", + " data = torch.stack([x1, x2], dim=1) / 0.45\n", + " return data.float()" ] }, { @@ -253,7 +226,7 @@ "id": "e1faf8fd", "metadata": {}, "source": [ - "# Continuous modality dataset and model" + "## Unified multimodal model" ] }, { @@ -263,79 +236,132 @@ "metadata": {}, "outputs": [], "source": [ - "def inf_train_gen_continuous(batch_size: int = 200, device: str = \"cpu\") -> Tensor:\n", - " \"\"\"\n", - " Generate a batch of 2-D continuous points from a checkerboard-like distribution.\n", - " Returns a tensor of shape (batch, 2).\n", - " \"\"\"\n", - " x1 = torch.rand(batch_size, device=device) * 4 - 2\n", - " x2_ = (\n", - " torch.rand(batch_size, device=device)\n", - " - torch.randint(high=2, size=(batch_size,), device=device) * 2\n", - " )\n", - " x2 = x2_ + (torch.floor(x1) % 2)\n", - " data = torch.stack([x1, x2], dim=1) / 0.45\n", - " return data.float()\n", + "class Swish(nn.Module):\n", + " \"\"\"Swish activation (x * sigmoid(x)).\"\"\"\n", + "\n", + " def forward(self, x: Tensor) -> Tensor:\n", + " \"\"\"Forward pass through the Swish activation.\"\"\"\n", + " return torch.sigmoid(x) * x\n", "\n", "\n", - "class ContinuousTransformerModel(nn.Module):\n", + "class TransformerModel(nn.Module):\n", " \"\"\"\n", - " Model for the continuous modality with separate input and output heads,\n", - " sharing a common Transformer trunk.\n", + " A unified Transformer-based model for handling multiple modalities.\n", + "\n", + " This model processes a sequence of modalities, each with its own input\n", + " and output heads, while sharing a central Transformer trunk. It is designed\n", + " to be flexible for both discrete (categorical) and continuous data types.\n", + "\n", + " Args:\n", + " shared_transformer (SharedTransformer): The shared TransformerEncoder module.\n", + " modality_configs (List[Dict[str, Any]]): A list of dictionaries, each configuring a modality.\n", + " Required keys per config:\n", + " - 'type': 'discrete' or 'continuous'.\n", + " - 'length': The sequence length for this modality's tokens.\n", + " If 'type' is 'discrete':\n", + " - 'vocab_size': The size of the vocabulary.\n", + " If 'type' is 'continuous':\n", + " - 'input_dim': The feature dimension of the continuous data.\n", + " time_dim (int): The dimension of the time embedding.\n", + " hidden_dim (int): The hidden dimension of the model and transformer.\n", + "\n", + " Raises:\n", + " ValueError: If an unknown modality type is provided.\n", " \"\"\"\n", + "\n", " def __init__(\n", " self,\n", " shared_transformer: SharedTransformer,\n", - " input_dim: int = 2,\n", + " modality_configs: List[Dict[str, Any]],\n", " time_dim: int = 1,\n", " hidden_dim: int = 128,\n", " ):\n", " super().__init__()\n", " self.shared = shared_transformer\n", - " self.input_dim = input_dim\n", - " self.time_dim = time_dim\n", - " self.hidden_dim = hidden_dim\n", - "\n", - " self.time_embedding = nn.Linear(1, time_dim)\n", - " self.position_proj = nn.Linear(input_dim, hidden_dim)\n", - " self.input_proj = nn.Linear(hidden_dim + time_dim, hidden_dim)\n", - " self.output_head = nn.Linear(hidden_dim, input_dim)\n", - " self.activation = Swish()\n", - "\n", - " def sample_shape(self, batch_size: int) -> torch.Size:\n", - " return torch.Size((batch_size, self.input_dim))\n", - "\n", - " def sample_prior(self, shape: torch.Size, device: torch.device) -> Tensor:\n", - " return torch.randn(shape, device=device)\n", - "\n", - " def forward(self, x: Tensor, t: Tensor) -> Tensor:\n", - " \"\"\"\n", - " x: (B, input_dim) positions\n", - " t: (B,) time scalar\n", - " Returns velocity vectors of shape (B, input_dim)\n", + " self.modality_configs = modality_configs\n", + " self.seq_lengths = [config[\"length\"] for config in modality_configs]\n", + "\n", + " self.input_embedders = nn.ModuleList()\n", + " self.time_embedders = nn.ModuleList()\n", + " self.input_projectors = nn.ModuleList()\n", + " self.output_heads = nn.ModuleList()\n", + " self.activations = nn.ModuleList()\n", + "\n", + " for config in self.modality_configs:\n", + " self.time_embedders.append(nn.Linear(1, time_dim))\n", + " self.input_projectors.append(nn.Linear(hidden_dim + time_dim, hidden_dim))\n", + " self.activations.append(Swish())\n", + "\n", + " if config[\"type\"] == \"discrete\":\n", + " self.input_embedders.append(\n", + " nn.Embedding(config[\"vocab_size\"], hidden_dim)\n", + " )\n", + " self.output_heads.append(nn.Linear(hidden_dim, config[\"vocab_size\"]))\n", + " elif config[\"type\"] == \"continuous\":\n", + " self.input_embedders.append(nn.Linear(config[\"input_dim\"], hidden_dim))\n", + " self.output_heads.append(nn.Linear(hidden_dim, config[\"input_dim\"]))\n", + " else:\n", + " raise ValueError(f\"Unknown modality type: {config['type']}\")\n", + "\n", + " def forward(\n", + " self, x_modalities: Sequence[Tensor], t_modalities: Sequence[Tensor]\n", + " ) -> Sequence[Tensor]:\n", " \"\"\"\n", - " if t.ndim == 0:\n", - " t = t.unsqueeze(0).expand(x.shape[0])\n", - "\n", - " # Position projection\n", - " x_emb = self.position_proj(x) # (B, hidden_dim)\n", + " Forward pass for multiple modalities.\n", "\n", - " # Time embedding\n", - " t_emb = self.time_embedding(t.unsqueeze(-1).float()) # (B, time_dim)\n", + " Args:\n", + " x_modalities (Sequence[Tensor]): A sequence of input tensors, one for each modality.\n", + " Shape for discrete: (batch, length)\n", + " Shape for continuous: (batch, input_dim)\n", + " t_modalities (Sequence[Tensor]): A sequence of time tensors, one for each modality.\n", + " Shape for all: (batch, 1)\n", "\n", - " # Concatenate and project\n", - " h = torch.cat([x_emb, t_emb], dim=-1) # (B, hidden_dim+time_dim)\n", - " h = self.input_proj(h) # (B, hidden_dim)\n", - "\n", - " # Transformer expects (seq_len, batch, hidden_dim) with seq_len=1\n", - " h = h.unsqueeze(0) # (1, B, hidden_dim)\n", - " h = self.shared(h) # (1, B, hidden_dim)\n", - " h = h.squeeze(0) # (B, hidden_dim)\n", - "\n", - " # Output velocity\n", - " h = self.activation(h)\n", - " velocity = self.output_head(h) # (B, input_dim)\n", - " return velocity" + " Returns:\n", + " Sequence[Tensor]: A sequence of output tensors, one for each modality.\n", + " \"\"\"\n", + " embeddings = []\n", + "\n", + " # 1. Process each modality through its specific input head\n", + " for i, (x, t, config) in enumerate(\n", + " zip(x_modalities, t_modalities, self.modality_configs)\n", + " ):\n", + " # Embed time and expand to match sequence length\n", + " t_emb = self.time_embedders[i](t.unsqueeze(-1))\n", + " t_emb = t_emb.unsqueeze(1).expand(-1, config[\"length\"], -1)\n", + "\n", + " # Embed input based on modality type\n", + " if config[\"type\"] == \"discrete\":\n", + " x_emb = self.input_embedders[i](x) # (B, length, hidden_dim)\n", + " else: # continuous\n", + " x_emb = self.input_embedders[i](x) # (B, hidden_dim)\n", + " x_emb = x_emb.unsqueeze(1) # (B, 1, hidden_dim)\n", + "\n", + " # Combine, project, and activate\n", + " combined = torch.cat([x_emb, t_emb], dim=-1)\n", + " h = self.input_projectors[i](combined)\n", + " h = self.activations[i](h)\n", + "\n", + " # Prepare for transformer (seq_len, batch, hidden_dim)\n", + " embeddings.append(h.permute(1, 0, 2))\n", + "\n", + " # 2. Concatenate all modality embeddings and pass through shared transformer\n", + " full_sequence = torch.cat(embeddings, dim=0)\n", + " transformer_out = self.shared(full_sequence)\n", + "\n", + " # 3. Split the output and process through specific output heads\n", + " output_chunks = torch.split(transformer_out, self.seq_lengths, dim=0)\n", + " results = []\n", + " for i, chunk in enumerate(output_chunks):\n", + " # (length, B, hidden_dim) -> (B, length, hidden_dim)\n", + " chunk = chunk.permute(1, 0, 2)\n", + " output = self.output_heads[i](chunk)\n", + "\n", + " # Squeeze sequence dimension if it's 1 (for continuous case)\n", + " if output.size(1) == 1:\n", + " output = output.squeeze(1)\n", + " results.append(output)\n", + "\n", + " return results" ] }, { @@ -343,7 +369,7 @@ "id": "d5378557", "metadata": {}, "source": [ - "## Build multimodal model" + "## Instantiate modalities and model" ] }, { @@ -353,42 +379,51 @@ "metadata": {}, "outputs": [], "source": [ - "# ---- Discrete side -------------------------------------------------\n", + "# ---- General Hyperparameters -----------------------------------------\n", + "length = 2 # 2 tokens per sample\n", "vocab_size = 128\n", "added_token = 0 # uniform source distribution → no extra token\n", "vocab_size += added_token\n", - "length = 2 # 2 tokens per sample\n", + "hidden_dim = 128\n", "\n", - "# Shared transformer trunk\n", - "shared_transformer = SharedTransformer(hidden_dim=128, nhead=4, num_layers=2).to(device)\n", + "# ---- Shared transformer trunk ----------------------------------------\n", + "shared_transformer = SharedTransformer(hidden_dim=hidden_dim, nhead=4, num_layers=2).to(\n", + " device\n", + ")\n", "\n", - "discrete_model = DiscreteTransformerModel(\n", - " shared_transformer=shared_transformer,\n", - " vocab_size=vocab_size,\n", - " time_dim=1,\n", - " hidden_dim=128,\n", - " length=length,\n", - ").to(device)\n", - "discrete_path = MixtureDiscreteProbPath(scheduler=PolynomialConvexScheduler(n=2.0))\n", + "# ---- Model and Path Configuration ------------------------------------\n", + "modality_configs = [\n", + " {\n", + " \"type\": \"discrete\",\n", + " \"vocab_size\": vocab_size,\n", + " \"length\": length,\n", + " },\n", + " {\n", + " \"type\": \"continuous\",\n", + " \"input_dim\": length,\n", + " \"length\": 1, # This modality is treated as a single token in the sequence\n", + " },\n", + "]\n", "\n", - "# ---- Continuous side -----------------------------------------------\n", - "continuous_model = ContinuousTransformerModel(\n", + "# A unified model that handles all modalities\n", + "model = TransformerModel(\n", " shared_transformer=shared_transformer,\n", - " input_dim=length,\n", + " modality_configs=modality_configs,\n", " time_dim=1,\n", - " hidden_dim=128,\n", + " hidden_dim=hidden_dim,\n", ").to(device)\n", + "\n", + "# Path definitions remain distinct per modality\n", + "discrete_path = MixtureDiscreteProbPath(scheduler=PolynomialConvexScheduler(n=2.0))\n", "continuous_path = AffineProbPath(scheduler=CondOTScheduler())\n", "\n", - "# ---- Assemble modalities dict ---------------------------------------\n", + "# ---- Assemble modalities dict for Flow -------------------------------\n", "modalities = {\n", " \"discrete\": {\n", - " \"model\": discrete_model,\n", " \"path\": discrete_path,\n", " # loss omitted → Flow will use MixturePathGeneralizedKL automatically\n", " },\n", " \"continuous\": {\n", - " \"model\": continuous_model,\n", " \"path\": continuous_path,\n", " # loss omitted → Flow will use MSE loss automatically\n", " },\n", @@ -410,7 +445,7 @@ "metadata": {}, "outputs": [], "source": [ - "flow = Flow(modalities=modalities)\n", + "flow = Flow(model=model, modalities=modalities)\n", "\n", "# Optimizer (optimises both modality models)\n", "optimizer = torch.optim.Adam(flow.parameters(), lr=1e-3)" @@ -434,17 +469,23 @@ "name": "stdout", "output_type": "stream", "text": [ - "| iter 3000 | 9.83 ms/step | loss 9.204 \n", - "| iter 6000 | 10.00 ms/step | loss 9.772 \n", - "| iter 9000 | 10.02 ms/step | loss 9.799 \n", - "| iter 12000 | 10.02 ms/step | loss 8.503 \n" + "| iter 3000 | 14.35 ms/step | loss 9.040 \n", + "| iter 6000 | 16.78 ms/step | loss 9.292 \n", + "| iter 9000 | 18.14 ms/step | loss 9.037 \n", + "| iter 12000 | 18.66 ms/step | loss 9.878 \n", + "| iter 15000 | 18.56 ms/step | loss 9.466 \n", + "| iter 18000 | 18.55 ms/step | loss 9.251 \n", + "| iter 21000 | 18.27 ms/step | loss 9.220 \n", + "| iter 24000 | 18.40 ms/step | loss 9.489 \n", + "| iter 27000 | 18.48 ms/step | loss 9.835 \n", + "| iter 30000 | 18.33 ms/step | loss 9.114 \n" ] } ], "source": [ "lr = 1e-3\n", - "batch_size = 1024 # adjust as needed to fit in memory\n", - "iterations = 12001\n", + "batch_size = 2048\n", + "iterations = 30001\n", "print_every = 3000\n", "epsilon = 1e-3\n", "\n", @@ -476,14 +517,14 @@ " disc_path_sample = discrete_path.sample(t=t, x_0=x0_disc, x_1=x1_disc)\n", " cont_path_sample = continuous_path.sample(t=t, x_0=x0_cont, x_1=x1_cont)\n", "\n", - " # ---- Build the inputs dict expected by Flow.training_loss -----------\n", - " inputs = {\n", - " \"discrete\": (x1_disc, disc_path_sample.x_t, None), # dx_t is None for discrete\n", - " \"continuous\": (x1_cont, cont_path_sample.x_t, cont_path_sample.dx_t),\n", - " }\n", + " # ---- Build the inputs expected by Flow.training_loss -----------\n", + " x_1 = [x1_disc, x1_cont]\n", + " x_t = [disc_path_sample.x_t, cont_path_sample.x_t]\n", + " dx_t = [None, cont_path_sample.dx_t] # NOTE: dx_t is None for discrete\n", + " ts = [t] * 2 # NOTE: For now, both modalities share the same time\n", "\n", " # ---- Compute total loss and back‑propagate -------------------------\n", - " loss = flow.training_loss(inputs=inputs, t=t)\n", + " loss, _ = flow.training_loss(x_1=x_1, x_t=x_t, dx_t=dx_t, t=ts)\n", " loss.backward()\n", " optimizer.step()\n", "\n", @@ -511,8 +552,15 @@ "metadata": {}, "outputs": [], "source": [ + "x_init = [\n", + " torch.randint_like(\n", + " x1_disc, high=vocab_size\n", + " ), # discrete initial state (uniform categorical)\n", + " torch.randn_like(x1_cont), # continuous initial state (Gaussian noise)\n", + "]\n", + "\n", "flow.eval() # switch to eval mode for sampling\n", - "samples = flow.sample(batch_size=2_000, device=device, steps=1000)" + "samples = flow.sample(x_init=x_init, device=device, steps=1000)" ] }, { @@ -531,7 +579,7 @@ "outputs": [ { "data": { - "image/png": "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V2vVqlXTNm/eHPXZeOLECV8cPyUv0IUjeO6559SXYbFixVRq/+rVqzW/wodCvMe0adMi8+DD8I477tAqVKigvjR///vfqw+JoBSOgnD88+fP1xo3bqx+GNSvX1978cUXo15Havfw4cO1ypUrq3kuu+wybdu2bZpfHD58WL3nuO9LlCihnXvuudpf/vKXqC9Cv52Djz76KO69j4Ki2eP9z3/+owoDpUuX1sqWLav1799fFToy/fhRQDb6bMRyfjh+Sl4I/4tXo0REREQURIFtc0REREQUDwtHRERERDosHBERERHpsHBEREREpMPCEREREZEOC0dEREREOiwcEREREemwcERERESkw8IREUX861//klAopMaaIyIKKhaOiHwGhZtEj5EjR0q6eeedd+SKK65QA36ycEZEXsv2egeIyFl79+6N/P3mm2/Kww8/LNu2bYtMK126tKSbY8eOycUXX6xGRr/lllu83h0iCjjWHBH5TE5OTuRRrlw5VRMTfl6pUiUZN26cVKtWTYoXLy7NmzeXhQsXGq4rPz9fBgwYIPXr15ddu3apafPmzZOWLVtKiRIl5Nxzz5VRo0bJ6dOnI8tgey+//LL8/ve/l7POOkvq1q0r7777bsJ9vuGGG1QhrlOnTg6eCSIia1g4IgqQZ599Vp555hl5+umn5bPPPpPOnTvL1VdfLd98880Z8544cUKuu+46FeL6+OOPpUaNGurfvn37yt133y1bt26VqVOnyvTp02XMmDFRy6LAhFogbKNr167Sp08f+emnn1J4pERE1rFwRBQgKBQNHTpUrr/+eqlXr5488cQTqvZowoQJUfMdPXpUunXrJj/++KN89NFH8pvf/CZS6HnggQekX79+qtbo8ssvl9GjR6tCkt6NN94ovXv3lvPOO08ee+wxtb61a9em9FiJiKximyOigDh8+LDs2bNHLrrooqjpeP7pp59GTUPBBqG3JUuWSMmSJSPTMd8nn3wSVVOE0FteXp4cP35chdGgadOmkddLlSolZcuWlQMHDrh4dEREzmHhiIjOgFDYG2+8IatWrZJLL700Mh01QKg96tWr1xnLoA1SWNGiRaNeQzukgoICl/eaiMgZLBwRBQRqb3Jzc1XNT/v27SPT8fyCCy6Imvf222+Xxo0bq/ZI//jHPyLzoyE2Mt8QLiMi8isWjogC5L777pMRI0ZInTp1VFujadOmqQbXM2bMOGPeQYMGqZDZVVddJe+//75KtUdGGZ6jcfa1114rWVlZKtT2+eefy6OPPmp5v9BYG9lwCPtBuOuBcJYdEVEqsXBEFCB33XWXHDp0SO69917VBqhhw4YqzR7p9vEMHjxYhcMQZkPKP7LbFixYII888ohqzI3wGdL8b775Zlv7hX3o379/5DkajAMKcunYaSUR+VtI0zTN650gIiIiShdM5SciIiLSYeGIiIiISIeFIyIiIiIdFo6IiIiIdFg4IiIiItJh4YiIiIhIh4UjIiIiIh0WjoiIiIh0WDgiIiIi0mHhiIiIiEiHhSMiIiIiHRaOiIiIiORX/wegj9ixya38OgAAAABJRU5ErkJggg==", + "image/png": "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", "text/plain": [ "

" ] @@ -541,7 +589,7 @@ }, { "data": { - "image/png": "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2eY7Mz9apHbaaSdp2LChrFy5st79+rM22ArR+908PgipahjpWGenTp0kCfRDyx9E+ziv/uC8+oPzGp/z6mekKL9xpLcwNGnSRHr16iWzZs0yM8uyydf68/Dhwws+p3fv3ub3F198cd19M2fONPeHJVUNIwAA4J+RI0fKmWeeKQcddJAccsghZrq+zjo766yzzO/POOMM6dixo5mery666CLp27ev3HHHHXLcccfJ1KlTZd68eTJx4sTQXgMNIwAAYMXgwYNl1apVMnr0aJNArdPuZ8yYUZdgvWzZMjN6k9WnTx9Tu+jqq6+WK6+8Uvbcc095/PHHQ6thpGgYxYyO62rSW6HxXVSO8+oPzqs/OK/+4LzaocNmxYbOZs+evc19P/7xj80tKlJV4BEAAKAUCjwCAADUomEEAABQi4YRAABALRpGCbBp0yaT+a8FuN58882wDyfWPvzwQznnnHOka9eu0rx5c9ljjz1MMqbtgmlpcP/998tuu+1maqrowpKvvfZa2IcUazq9WRfn1AK1ukq51onRFcth1y233GL+lubW1UG60DBKgMsuu8z3iqppoev5aEGyCRMmyNtvvy133XWXWeFZp5HCuUceecTUM9FG5YIFC6Rnz55mYcjPPvss7EOLrRdffFGGDRsmr7zyiimAt2XLFjnmmGNMjRjY8frrr5vP/v777x/2oSBEzEqLuWeeecZ8AT322GOy7777yhtvvGGiR7Bn7NixMn78eHn//ffDPpTY0AiRRjfuu+8+87M2NnWphREjRsgVV1wR9uElgtaK0ciRNpiOOOKIsA8n9tavXy8HHnig/OpXv5KbbrrJ/B3V4oRIHyJGMabryQwdOlR+//vfS4sWLcI+nMRas2aNtGnTJuzDiA0ddpw/f77079+/7j4t6KY/v/zyy6EeW9KuS8W1aYdG47Tycu51i3SiwGNMaaBvyJAhcsEFF5jS65obA/uWLl0q9957r9x+++1hH0psfP7551JdXV1X6TZLf9ahSninETjNgTn88MNDrRCcFLoMhQ756lAaQMQoYnSYQRP/St30y0W/rHW15lGjRoV9yIk6r7k+/vhjOfbYY01FVo3MAVGKbixatMh8ocOb5cuXm/W6Hn744dAWX0W0kGMUwbyBf//73yUfs/vuu8vJJ58sTz75pPlCz9JeesOGDeW0006T3/72twEcbfLOq64OrT755BPp16+fHHbYYTJ58uR6a/ug/FCaDu3+6U9/qlthW+nCkqtXr5bp06eHenxxp0st6DmcM2eOmT0Jb3RdrpNOOsn87cz9W6p/W/Vzr7N+c3+H5KNhFFO6EN/atWvrftYvcp31o19GmvjaqVOnUI8vzjRSdOSRR0qvXr3koYce4o9iBfQa1JW1NbKZHfrp0qWL+VIn+boy+qdak9enTZtm1pvSxTbhnUbeP/roo3r36Urwe+21l1x++eUMVaYQOUYxpV8yubbbbjvzr9bdoVHkrVGkkaJdd93V5BVppCmrQ4cOoR5bnOhMSY0Qaf6bNpB0do9OK9cvHFQ+fKarkGu0SGsZ6crlqnXr1qbmFiqj5zK/8dOyZUvZcccdaRSlFA0jIIfWh9GEa73lNzAJrjo3ePBg06gcPXq0+QLXqc8zZszYJiEbzmnJCKUN91yTJk0yEzEA2MFQGgAAQC0ySgEAAGrRMAIAAKhFwwgAAKAWDSMAAIBaNIwAAABq0TACAACoRcMIAACgFg0jAACAWjSMAAAAatEwAgAAqEXDCAAAoBYNIwD16OKvHTp0kJtvvrnuvrlz50qTJk1k1qxZoR4bAPiNhhGAetq2bSsPPvigXHfddTJv3jxZt26dnH766TJ8+HA56qijZMSIEdKmTZttVnkHgCSoymQymbAPAkD0DBs2TJ577jk56KCDZOHChfL6669L06ZNZdGiRbJs2TK57bbbZPbs2WEfJgBYRcQIQEG33367bN26VR599FF5+OGHTaNI9ejRQ1q0aBH24QGAL2gYASjovffek08++URqamrkww8/DPtwACAQjYLZDYA42bx5s/zkJz+RwYMHS/fu3eXcc881w2nt2rUL+9AAwFdEjABs46qrrpI1a9bIPffcI5dffrl885vflLPPPjvswwIA35F8DaAeTag++uij5YUXXpBvf/vb5j4dSuvZs6fccsst5r8feugh+c9//iMdO3aU559/Xrp06RL2YQOAFTSMAAAAajGUBgAAUIuGEQAAQC0aRgAAALVoGAEAANSiYQQAAFCLhhEAAEAtGkYAAAC1aBgBAADUomEEAABQi4YRAABALRpGAAAAtWgYAQAAyNf+H9xjQZP3m/43AAAAAElFTkSuQmCC", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkYAAAHqCAYAAADh64FkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXvZJREFUeJzt3Qm8lFX5wPHngoDsiQvIIioaKgYKqIAlqChupdbfzEpcKQ1NslJxA5cEwwW3EDOkUtK0P5rmkoJAhibgEmpQuEGyaX9lU7Z75/95jnduc4dZ3uW8++/rZ7zcuTPve+Z935k55znPOacml8vlBAAAANIk6gIAAADEBRUjAACAelSMAAAA6lExAgAAqEfFCAAAoB4VIwAAgHpUjAAAAOpRMQIAAKhHxQgAAKAeFaOMGzJkiLkhHLNmzZKamhrz062xY8ea5xbafffd5cwzz7RYwnTR46XHLWwvv/yyNG/eXN5//33JwrVp03vvvWfKMXXqVGvbfOutt2S77baTN954w9o2kV5UjEL09ttvy/e//33Zc889Zfvtt5d27drJoYceKrfddpt89tlnge1XPxT0y0E/cJBunOt4uOKKK+S0006T7t27m9/r6urMF/3XvvY16datm7Ru3Vr2339/uf7662Xjxo2uKi75W4sWLaRjx46mYXPDDTfIhx9+KGn15JNP+qrg7rfffnL88cfL1VdfbbVcSKftoi5AVvzpT3+SU045xXyYDR8+3Hwobt68WV544QX56U9/Km+++abcc889gX1ZXnPNNeYDVCMMhf785z8Hsk+EY/HixdKkSRNH5xrheO211+S5556TuXPnNtz36aefyllnnSUDBgyQ8847T3bZZRd58cUXZcyYMTJjxgyZOXPmNtHAcn74wx/KQQcdJLW1taYypPvR7dxyyy3y+9//Xo444ghJMq1MakOxWbNmjSpGd911l6/KkR734447zjRQe/ToYam0SCMqRiF499135Vvf+pZ5w+sH4K677trwt5EjR8qSJUtMxSkKGu5HcmlFG/Fy3333yW677WYqQYXvs7/+9a8yaNCghvtGjBhhKq/5ytHQoUMdbf8rX/mK/M///E+j+15//XU5+uij5Rvf+IapHBd+xiSNVhA1om6bHt8ddthBfv3rX8u1115rfftID7rSQvDzn/9c1q9fL7/61a9KfmDttddectFFFzX8vnXrVrnuuutMq0a/+PTD8/LLL5dNmzY1ep7ef8IJJ5io08EHH2w+TLSb7je/+U3DYzR8r5EqdfjhhzeE4fN5BMU5RvlwvbY8f/azn0nXrl3Ndo888khTgXOS31Iqb2n16tVyzjnnmNC/bq9Pnz7mA8pJjkOpnIOVK1eaFriWT4+RHtcTTzyxaheSlrdNmzaydOlSc+z03126dDGtUbVw4ULT4tauDq3ITps2bZttvPPOO+aYdujQQVq1amW+AEtVbP/973/LSSedZLalEYIf/ehH25xD9Ze//MVsT79M9bVoV4s+1kn3auE5qHSuzzjjDNlpp51ky5Yt22xDv1B79uxZcT//+te/zJdup06dzPnT466V/TVr1jSqEOix09eqr0O7LyZNmlSyzHrstVz9+/eXli1bype+9KWG8/6///u/5nfdT79+/eTVV18teQ71PAwbNswc386dO5svu1wuV/WYffDBB3L22Weba1HL2atXL5kyZco2j7vjjjvM3/Qc6xeqlrXU9VDs0UcfNcehMAKkFaPCSlHeySefbH7+4x//ED/0/TRx4kT55JNP5M4776z6eKfXpvrb3/4mxxxzjLRv394ci8GDB5tKXqn8N/2M0PPzhS98wTxe36MaLSv07LPPype//GXzGD2Peu3p51u597tuL//+LOxK1HOt15K+74tp96TuX1MX8jQCpZ9Ljz32WNXjg2wjYhSCxx9/3FRYSn0wlnLuueeaSoO2Cn/84x+bD6Zx48aZD8/p06c3eqx+EOnjtNKhX376Aa8fJPqFoh/qhx12mAm933777ebDZ9999zXPy/8sZ/z48aaL5ic/+Yn58tPK3Xe+8x1TFrf0C14/kLSsF1xwgeyxxx7y8MMPm3LqB3lhpdAp/ZLW7scLL7zQfDhqxUs/cLXCU60LSbsgjj32WHNs9HU98MADplz6JaG5Ifo6v/71r8vdd99tuj0HDhxoyqxWrVplzqN+2Otx3XHHHc250tyRRx55pOGLTl+zVia1PPo4/eL+7W9/ayKGxfRY6PbOP/98sz1N3NUvZf3y0r85Velcn3766abC/Mwzz5hKSWEFU8ukUYtytMtXKyD6xanHWytHWrl44oknzPnTLyCllSC95vRYaKKrXvc/+MEPTH6NRkYL6bXw7W9/23xxffe735WbbrpJvvrVr5pjrmXX5ym97r/5zW9u02Wo51C/rLVSqufw6aefNq9BGxWVogF6/vQ5+sWq53znnXeWp556yrx/1q5dK6NGjTKP++Uvf2mOpb639PrUL9q///3v5vrXcpejx0XPed++fR2csc+Pv9JKq1/5zwHtHtdGTTlurk29T98r+nmix1fPQb4CrBV6bZAV0nOl7xU9b6+88orce++9puJ14403mr/re1avv969e5vzpBVTvRaKK1qF9BpZvny5eX9rOfP0HOq1o+f///7v/0xDJU+vPT2f+vdC+jq0YqR/0xxPoKQcArVmzRptwuZOPPFER49/7bXXzOPPPffcRvf/5Cc/MffPnDmz4b7u3bub++bMmdNw3+rVq3MtWrTI/fjHP2647+GHHzaPe/7557fZ3+DBg80tTx+jj913331zmzZtarj/tttuM/cvXLiw0f7POOOMqtucOHGiee7999/fcN/mzZtzAwcOzLVp0ya3du3aRvsuLue7775r7r/vvvvM7x9//LH5fcKECTm3tLz63BtuuKHhPt1ey5YtczU1NbkHH3yw4f5FixaZx44ZM6bhvlGjRpn7/vKXvzTct27dutwee+yR23333XO1tbWNXvPvf//7hsdt2LAht9dee23zGj/99NNtyjlu3DhTnvfff7/hPi1H8Vu2+ByUO9darq5du+ZOPfXURvffcsstZj/vvPNO2WP26quvmm3qtisp9TqGDRuW23PPPbcps25v7ty5Dfc988wz5j49D4WvefLkydu8nvw5vPDCCxvuq6uryx1//PG55s2b5z788MOG+4vP3znnnJPbddddcx999FGjMn3rW9/KtW/fvuE16Pu1V69eObeee+45s8/HH3/c0eOHDh2aa9eunbkGq8m/Pyqdhz59+uR22GGHittxem3qMd17773NOdR/5+kx0uv9qKOO2ubaPPvssxvt6+STT87tuOOODb/feuut5nGF56hY8ftdjRw5cptrXy1evNjcP2nSpEb3f+1rXzPvx8Jyq2nTppnH/+1vf6t4jJBtdKUFTFsmqm3bto4er0mG6uKLL250v0aOVHGXjXZXaM5BnraANTSt3Qx+aAi8MP8ovw8v29XXpFEGHaVTGNbW1qp2Mc6ePdvV9rTrRcumXS8ff/yxeKFRuTwN6esx04iRtnjz9D79W+Fr1teirWTtCsjT7oDvfe97pgtA8zvyj9PuvcJcEO2G0MeVej15GzZskI8++shEpfR7vbgbyStt6Wsk7I9//KOsW7eu4X6Nlum+8hGxUvIRIY02FXeLlHsdGmXU16HdLnr8Crvc8tetRuLyDjnkEPNTIxHapVh8f6nrTiM+efkIkEa3NPG5FD2ef/jDH0xkSv+t5cvfNCKmZdQoh9LzrhG7efPmiRv/+c9/zE/teqtGR5JpWTU6q/uzQa/FwvNbitNrU5PItQtVI2T6uvLHSq9RjTjNmTPHRAOLE5wL6eeGPjf/OZh/nRq1KX6uF1/84hfNNaLXcZ5GjzQKqNd7cUJ7/rzo6wDKoWIUsHy4ttqHVZ7Oe6JfYpp3VEgrFvqhUjwvSuGXSOGb32uFodx28x8oXrarZd57770bdYWofFeP27leNPyuoXn98NM8kXyXWL5bohrNXdEKZPGXv+bNFH+Q6v2Fr1nLWiofp/i16E89h8XbK/Vc7dLQbkXtCtAvNi2bVihUcYXCD+0W1G6UfHesdk8tWLDAdLNVopUmrahrt4h2+WglQnM+isum3SGa4KoVTL1W9XXkc0eKH1t8feUrX5pfVer+4utOryXtni7+klTl8sx0BJd2/enoTy1b4U0bAkq7ZNWll15qzoVWgvXa1a7ASt09xarlOj300ENy5ZVXmq4v7UItpNdx4c3NVB7a0KjWCHN6bWqlSGkXffHx0mtBu1arndfiz41TTz3VTFGiDRN972qemuYz+qkk6XWt5yb/3tPuZ82lK3Vd58+L0xGAyCYqRiFUjLQP3+3EYk7fuE2bNi15v5MkVL/bLVdGzf/wws32NBfkn//8p8ll0IrOVVddZSonTiIs5V5bUMeyEn1tRx11lIkE6pexJu5qLkU+8dRGq7owSqM5Fvfff7/5XX9q5K0wSlbOzTffbHJstKKjX9Qa7dN8Io2qKB0CrVEEbYnrsHF9Pfo6NKm31OuI4hzky6B5J1q2Ujf90lZ6LWnF8cEHHzTRQY006c9KuVhKc8SqNSB0P/plrvPqaE5VMY3mFN60EuWEVgb0PVHcqPJ7vCZMmFD2eGnl0c3506iiRpo0UqYVF72mtLKk7wGvnxtaudIIdD5qpNe1JsqXaoTkz4uNnC6kF8nXIdBkQ22l6rwlhd0HpehIKP1A0tZaYYK0Jo1qazc/YZwbQbWOtDWoZSqmLbfC1ryWWT8A9XUVRo0WLVrU8Pf89lTxNstFlHTUnnYx6k2P1wEHHGC+wPNf/EHQsuoXZrHi16I/tTKsXwiFx7/4uToKTr/MNIFbvyzz9EvHi2rnWveh0Z8VK1aYEVb65eyk20fpSDG9aaRD587RSoR+seskhZrsqhEE7aorjBo8//zzEgS9lrR7LR8lUnocVbnke410aDRFv4CdDI3XyJd+aetNu+g0IV+TmkePHl12OPk+++zTMEVHKZq8rQn6+sWtkRJNUi9WfO61AuqEJv9rpVUjepU4vTbzc/1o487pVAJO6GeAVqL1ppVo7VLUQQ96rZTbT6XrWiOteh1rxUi7zzR6pCP0StHzovsvvG6AYkSMQnDJJZeYD1kNH2sFp5i2tnX2a6UTkKniN7Z+gCj9AHBL961KVWL80A/Ol156yXxp5OlIpWXLljV6nL4m7RIobPnq6CEdeaUtzny3kX5ga4tTW5SFfvGLXzT6XfNcimcL1rLol165Ice26GvRUWNayc3TnAut+OoXskZl8o/TkTT6ZVVY7uJJPPMt7MKIiP47fz3YPtea56VfMjrSSisWxaN2StH8ED1fhbSCpF8w+eNd6nVoN4uOYApK4bB03a/+rpED/cItRcuooxk1+lMqgls4c3Q+VyhPI2t6bnU/paY8yNOpH7Q7cP78+dv8TUeV6vtXrxN9nxTmZBXSykHhzcmcRDqPkUZRtZJbPAKwmNNrU6OL+r7SEYPaRVfMy0zbmv9TTBs0qtJ7t9p1rdEnze/TyXL1PGsUqRTtOtaKZr6LFiiFiFEI9MNFW+fa8tQoUOHM19ryzg9dz89Hon36+iGlHwJaadAvYo0o6LwjOj+NW/rBox8WmpejX1aao5Ofb8YPrejph6sOm9buGK3gabSmeFZZTeqcPHmyeY36waRfDPq8fMsunxOhH1Y6D49WmPTLW7ejXyD5vI/CyIB++ek+9ctKW92aN6OVznIfiLZcdtll8rvf/c4MYdbuJG2t6rnRlqh+4eYjYjp5n35R67nW16xfbjrUWJNciyMM+jp1WgQd6q2tc92O1xyxaudaoyZ6vvSa0zwgJxVtHbKtic16brSlrZUkfS35ikZ+LiStPGhisw6v1i9SHfKu+9XolG0asdEh+vpe0eRbzTfT7jvt6ivOHyukic4amdDn6DnS60e/rDXpWrt38l/c+no0r0+jYpoLo5UaPZ96vKrl8Oi8Ono9FkZkNMdQIzl6XvXLu3gQhV4D1aLJeTpMXhsGGvnSCpy+jzRSp+8f3a+WuxKn16Zey5pLpNe6ViY0D0srfnqd6jHUa1UjhW7oEH1t+Ohx1IaQvre14aP5fYUDGoppJU3pe06PY3HlR7en3Zh6XWt5S322aYVWB3rkp4IAyop6WFyW/POf/8yNGDHCDCPVYcVt27bNHXroobk77rgjt3HjxobHbdmyJXfNNdeYIbHNmjXLdevWLTd69OhGj8kPe9YhytWGy6tf/vKXZth006ZNGw3JLTdcv3hIcKkhtOrmm2/OdenSxUwRoK9l/vz5Jfe/atWq3FlnnZXbaaedzGv/0pe+tM22lA7j/cY3vpFr1aqVGXb8/e9/P/fGG2802rcOtdbhu/vss0+udevWZpj1IYcc0mj4cTk61FufU+qYlRqeXeoYv/3227n/+Z//yX3hC1/Ibb/99rmDDz4498QTT2zzXB12rsOG9bXo677oootyTz/99DbDz9966y0zbFunLtDH6TXy+uuvb3O8nQzXr3Su8/Q46f3f+973ck7oUH4dht2jRw/zejt06JA7/PDDzdD0Qn/84x9zvXv3No/Ra/zGG2/MTZkyxexLr59Kx1Tp4/S8lrruCqdmyJ9DPQ9HH320Ob4dO3Y0xyc/XUK54fr5a1H3o+8rfX916tQpd+SRR+buueeeRtMEHHbYYWaouV7b+tp/+tOfmuk3qnnllVe2mdIh/zrK3UpNe1Es/97M37TsO++8synnz372MzNVh1NOr838dA1f//rXG46Fnr9vfvObuRkzZmxzbRYPw9frt/D863N0KoTOnTubzwH9edppp5nPxuJjVXjtb9261UzPoK9Xp5co9dX1gx/8wNyvQ/JLeeqpp8zf//Wvfzk+TsimGv1f+WoTgLTRodIafdSWe+FUD0mhkUeNOJbq3okLjWjmJ05EODTRX1cX0G774uiX0mteI3jFk+QCxcgxAjJGu7g0Ob5S1wX80YRizalzOxUFvNGuRe3G167dUpUi7QrVbnldagmohhwjICN06LmODtT8Fk3uZi6X4GgOU+GgBARDc5Q0N0wjiJpvVW55Ic3tLB5AAJRDxQjICB2RpqMAdVJBElCRBjoSTYfoa7K1rhGYH+EG+EFXGpARmk6oo6N0pFGp+XOSQie/jHN+EcKji1Prda0jUguXiEE8jB8/3kSm84szl6OjCXWEro421alA8ktjRYWKEQAAsGrevHlmmpbevXtXfJxOWaPRbI1k68oFmiSvN7erRdjEqDQAAGDN+vXrpW/fvmaOKp0ZX7s4y81GrvP76SS5mhyfN2DAAPOcUkvmhCG58XSPywjojK86QRuJpwCAMLuxdQqH4sW0gxihZzvxP1e0fIzSyWP1VorOvq6TburM7VoxqkRXEdBligrpJJ66bmRUMlUx0kpR8erdAACEQZdL0lm+g6wU7dG9jaxc7W1B3nLatGmzTV6fLqg8duzYkqNfdSZ57UpzQued0tnlC+nven9UMlUxyk/l/2U5TraTZlEXByH47Kv9zc+Wj2+7dhUAhGGrbJEX5Mmqy8n4pZEirRS9v2B3adfWTmRq7bo66d7vPVOp02Vg8kpFi/QxOmWCLoRcbqHlJMhUxSgfCtRK0XY1VIyyoO0Tr3/+D843gKjUZ/KGlcLRpm2NudlQJ59vRytFhRWjUnTtPZ1bSvOL8nRNP51lX9fn04WC8wtO5+nafsWLq+vv1db8C1KmKkYAAKRdba5OanP2tuVmKZyFCxc2uk8XH9ah+Jdeeuk2lSKliyfPmDGj0ZB+jTg5XVQ5CFSMACABPjv5EPOz5fS/RV0UoCTtKtx///0b3de6dWvZcccdG+4fPny4dOnSRcaNG2d+1663wYMHy80332wStjVHaf78+XLPPfdIVJjHCACAFKmTnNWbTUuXLpUVK1Y0/D5o0CCZNm2aqQj16dPHLO+iI9KKK1hhytQ8RmvXrpX27dvLEDmRHCPAZSQi/7jCxxLFiJ9S5ymofUR13ov3H3V5qtma2yKz5DFZs2ZN1TwdG99xKxfvZjX5ulPPpYGXPU7oSgMAIEXqzH/2tpU1RIyQCWG0ooPYVxAt4bi3rhEfYVwrSb0e3ZQ77IjRskVdrEaMuu3zQaYiRuQYAQAA1KMrDbDccrXZ8i2XQ+GnrF7LF2bUDfHAeU7msbGZNF1nOfk6CagYAQCQIlqZqaVi5BkVIyRaYRSjUiuu1P3FUZc4twD9RIq8RpeSmvuRdcXviVLCPKfVop3lylLpdbgZQenm8YWP4/rPLipGAACkCF1p/lAxQqLFqTVno4VZbRvF9zvJ+/FanuLnxelYo7y4nie3749KUV4vz3XyeC95dG9PHGB+9hj1kqt9Ir6oGAEAkCK1uZy52dpW1lAxAgAgRXRKRnsTPGYPEzwiEeIw0ZzbhM64KJf8mpeUZRVQmZPzZ/sc25jCIYr3dthLioQ9weOif3SUtpYmeFy3rk722XdVpiZ4JGIEAECK1Focrl9L8jUQT+WSjm0mOvtJCnW7L1u8JF8HcSwRHKfnx0kCse1IkZPt2by+vG6r2nFI27Vfm/v8ZmtbWcOSIAAAAPXIMUKiJC264TRvqdrrKR4SnLTjgHQqFZ3ye22m8doOO8fotbd2sZpjdMB+q8kxAgAAyVQnNVIrNda2lTVUjJAo1fJjwmxtOtmX07yQaoonjws7tyNO+4K9UWhOz1u5x5V6XhQjR6P4HOCaTy8qRgAApEhd7vObrW1lDTlGgEte5jPy2zL3+jjbzwWCvt7SeH2GnWM0/82O0sZSjtH6dXXSvxfzGAEAgISqtZhjVJvBHKPERozGjx8vo0ePlosuukgmTpzo6DlEjNIvitmp09LCTerM3rB/bablmnYqbTNfz31zV6sRo0G9VmQqYpTIeYzmzZsnkydPlt69e0ddFAAAkCKJ60pbv369fOc735Ff/vKXcv3110ddHMRMFC1cN/sMMoeoeK4jt7ISHUg7G3MLOX1e/przc91FKa2RsbpcjbnZ2lbWJC5iNHLkSDn++ONl6NChVR+7adMmE1osvAEAkIUcI1u3rElUxOjBBx+UV155xXSlOTFu3Di55pprAi8XkrWivO3WoZtITZDrsdlusae1NZ12NucWqva8zrNzrt57fspSah9+txm3+cAQD4mJGC1btswkWj/wwAOy/fbbO3qOJmdrwlj+ptsAACDNaqWJ1VvWJGZU2qOPPionn3yyNG3atOG+2tpaqampkSZNmphus8K/lcKotPRK+yzPxS3kPFqxyMp7IOzy2Nxm2KPSZr7RzeqotCP2X5apUWmJ6Uo78sgjZeHChY3uO+uss2SfffaRSy+9tGqlCACALMhZTL7OZTD5OjEVo7Zt28r+++/f6L7WrVvLjjvuuM39yJ64tFyTNPLNxvPjFjnIsjDPQRi5e0HkGLm5XpN8TTPBoz/Z6zwEAABIeo6RDeQYISmiWC0ciDKXJy+N13jYOUZP/X0PaW0px2jDujo5tve75BgBAIBkqpMaqbPUIVQnmYmdNKBihFRJSmSlWjmL77c590vcjw3C43VGbJv8bJO14BAEKkYAAKQIydf+UDFCqgTZAnSaC1FqziHbEaBKLd6gWsNRjUCCfbaukSDWSrMxcsxtRDZtanNNzM3OtnKSNYxKAwAAqEfECKkS5AiXKPMwbOyj+DlxyC+BfU4ie9VGO1a7NvJ/9xIlshHN8Vpum+Kcp/R58rWdLrC6DHalETECAACoxzxGQIAtwTi2KuNYJoQbHSWyUnm+sML7yj02zvMYPfz6PtKqrZ1lsj5dVyun9FmUqXmMiBgBAJDC5GtbN6cmTZokvXv3NhUovQ0cOFCeeuqpso+fOnWqWQi+8Lb99ttL1MgxQir5ad1FkZcUZuu72nPj3MLHtiqdp3LnMohrwOm+gry+nG7by3Hh/VBd165dZfz48bL33nuLdkb9+te/lhNPPFFeffVV6dWrV8nnaAVq8eLFDb9r5ShqVIwAAEgRnfU6ipmvv/rVrzb6/Wc/+5mJIr300ktlK0ZaEerUqZPECRUjJIrT0ShxzbdIwjxNSTgOCPY8BRlpdRrN8VMWv2WwSV/P1i0bRR5/LLR91uZqzM3Wtryora2Vhx9+WDZs2GC61MpZv369dO/eXerq6qRv375yww03lK1EhYWKEQAAqJrYXahFixbmVmzhwoWmIrRx40Zp06aNTJ8+Xfbbbz8ppWfPnjJlyhSTl6TJ3TfddJMMGjRI3nzzTdMtFxVGpQEBRlTyMwNXm+8lyFXLq81ZY+s1INujKcOIOJaLJtm61oMS9qi0qa/2sToq7cwDX9/m/jFjxsjYsWO3uX/z5s2ydOlS81ofeeQRuffee2X27NllK0eFtmzZIvvuu6+cdtppct1110lUiBgBAJAidbkm5mZnWznzc9myZY0qdaWiRap58+ay1157mX/369dP5s2bJ7fddptMnjy56r6aNWsmBx54oCxZskSiRMUIcMhL69NWpMhLC7jaiBunkSAiRfEWRHTEyxpkYaxw73Qm7yDLkFXt6ofgu6W5Q5s2bXKcl6Rdcccdd5xEiYoRAAApUitNzM3OtnKOHzt69Gg59thjZbfddpN169bJtGnTZNasWfLMM8+Yvw8fPly6dOki48aNM79fe+21MmDAABNh+uSTT2TChAny/vvvy7nnnitRomIEFAlznhWvI3kqldFp+YkEJZOfEZh+xXmOLy/bJFJk1+rVq03lZ8WKFSbXSZOqtVJ01FFHmb9r7lGTJv+tsH388ccyYsQIWblypeywww6m623u3LmO8pGCRMUIAIAUqfMxzL7Utpz61a9+VfHvGj0qdOutt5pb3FAxAkLIlbC9GriTGXrJn0gnL+czTqO2bOy72jbCmAspOxM8NpGsyd4rBgAAKIOIEVKt1CiVoNduKrVCd7XRNEHwMrIIyePkfDo912FEloLM3QtyH0l6H7ld/LXatrKGihEAAClSJzXmZmtbWcPM10BIeUFAVKK4Zr3uM4xZ4INQaR9hz3x9+4IB0rKNnbjHZ+u3yg/7vRR42eOEiBEAAClCV5o/VIyAMmzmBdlqPdtcr42IWHZ4PZd+roUgrqsgRnECxagYAQCQInZnvm4iWUOOEVIhSTkUtreBbHEzh0+5qM3ywTXWZz/3OzIs6jygIIWdY/TzeV+xmmN0yUF/yVSOUfaqggAAAGXQlYZUcLo6faXHum1NllvR2836ZV7nl7H1WCSPn/Oaf26P6cFdO16v6TCu7ay8J3S2altdYHUZjJ9QMQIAIEXqck3Mzda2soYcIyDGORBxj/7EvXzItriMvAw7x+iGlw+X7S3lGG1cv1UuP/j5TOUYETECACBFaqXG3GxtK2uoGAEOhdG69JqDFJW4lw/+RrYl/fyGsUZhHI8VXWn+ZO8VAwAAlEHECIhxizGOrVEkX7XrKeqcnKj26UUcy1drsQusVrKHiBEAAEA9IkZAjFuMcWiNJqXlnnZpido43WZSrrc4vj/IMfKHihEAAClSm2tibra2lTVUjIAEtJ6LvT1xgPW1rsqJU0s4y8LI+/EyG7VbcZ6jy8kM+cXiVH7YQcUIAIAUyUmN1FlKvs4xjxEAW2tAlXqOrRZ5PlJUal9pyUVBZUFGc8JgY1+2r7u0rEVIV5o/2XvFAAAAZRAxAiyr1JL028p0EnGKoiWb9hmU08breSmMUHp5vpsyOCmj1/L7iQwl4Vquy9WYm61tZQ0VIwAAUqRWmpibrW1lDRUjZE6Soxheyhzl603iMc4Cr/loSctBCmKbSf78gDNUjAAASBG60vyhYoRMyM/7ozrPzllpESal5ZiW15FVQZ6fKM953KM35bbJ+yT9qBgBAJAiddLE3GxtK2uoGCFVyrUg3cwQ7TXfwkvr1e3InEqP9zpiiBawpCLi5+TxNkaCuXlc0NuwPXeXl5mv46g2V2NutraVNdmrCgIAAJRRk8vlKidcpMjatWulffv2MkROlO1qmkVdHIQgjPyZOK2hlvR9I7mieK8VR8uqRZLKldFJJNZPhHhrbovMksdkzZo10q5dOwn6O+77c74hLdrY+Y7btH6LTD7sD4GXPU7oSgMAIEVyuSZSZ2kpj1wGlwShYoRUs5EDYWNbbnnJAbHVYidSlExRR/rC2K/bWaidPr5UBMrveoZILipGAACkSK3UmJutbWUNFSOgDJujuoKK5vgZRWNrhBLiISnnKU7XnZO1B91uA8lHxQgAgBSpy9mbsbouM8Oz/ouKEVItzBFjlfZlu3VsI9fI79+zKI7RgTiWqZJqI8TikMsXVHQpLHUWk6/rMph8nb1XDAAAUAbzGAEx5mdkXBS5HEmLXqRJlo590l5r2PMYnf78adK8TXMr29y8frP89vDfOSr7pEmTzO29994zv/fq1UuuvvpqOfbYY8s+5+GHH5arrrrKPGfvvfeWG2+8UY477jiJEhEjAABSJL8kiK2bU127dpXx48fLggULZP78+XLEEUfIiSeeKG+++WbJx8+dO1dOO+00Oeecc+TVV1+Vk046ydzeeOMNiRIRI0Qmbq2+uJUnDa8v7cc0aaIc8eX1caVmt7ZV/rcnDjA/O8/ObbNdm8cq7IjRt2d+22rEaNoR0zyXvUOHDjJhwgRT+Sl26qmnyoYNG+SJJ55ouG/AgAFywAEHyN133y1RIWIEAECK5JOvbd3yla7C26ZNm6SS2tpaefDBB03FZ+DAgSUf8+KLL8rQoUMb3Tds2DBzf5QYlYbIRB1FcDvKxG0L2MljvXJSljiMorGxzzhFncJcfT6Ibdsqj80Z2L2MjrR1fHuMeslzueKsTmrsDdeXz7fTrVu3RvePGTNGxo4du83jFy5caCpCGzdulDZt2sj06dNlv/32K7ntlStXSseOHRvdp7/r/VGiYgQAACpatmxZo660Fi1alHxcz5495bXXXjNdb4888oicccYZMnv27LKVoziiYhSCOLV4k9p6tnkMvW7L5kzYQawuHuV8MEGK0+uIwzUQ5HvAxhxYYZyv5YM/j2J0lnR+tvqV04iRpaU8cvXb0UqRkxyj5s2by1577WX+3a9fP5k3b57cdtttMnny5G0e26lTJ1m1alWj+/R3vT9K5BgBAIBA1NXVlc1H0i63GTNmNLrv2WefLZuTFBYiRgGIQ25HGBGYIFrP1WbDDbPcYfCbjxH3tZ2SFi31M29Ukl5/tVXm465SblClyGpSXp9fml9kb0mQGsePHT16tJmzaLfddpN169bJtGnTZNasWfLMM8+Yvw8fPly6dOki48aNM79fdNFFMnjwYLn55pvl+OOPN8naOsz/nnvukShRMQIAIEWiWhJk9erVpvKzYsUKM21A7969TaXoqKOOMn9funSpNGny3+0NGjTIVJ6uvPJKufzyy80Ej48++qjsv//+EiXmMfIpjBFIXgSxBpctSWm9xWEEUhSS9nqSVl6Ed67jcm2EPY/Ryc+eJc1a25nHaMuGzTL9qPsCL3ucEDECACBFoupKSwsqRgAApEidxVFpdZa2kyRUjEIUZljXa6jZzYSBbv/utmxRczqUOSmvJ67dDH7L6XXIOZLH7bnmGoAXVIwAAEgRutL8Ifk6BHFqwYaxVEFeHF5vlsXpugtDqcVGbU1pEIdjGYcypEXYk86GnXx97NMjrCZfP3XMLzOVfJ2YCR513oODDjpI2rZtK7vssoucdNJJsnjx4qiLBQAAUiQxXWm61srIkSNN5Wjr1q1mzoOjjz5a3nrrLWndunUsWxhxnEgv7ksV5NE69s/2sXMyNUWU17zNpTKKxeE6jEMZ0iLtx5KutIxUjJ5++ulGv0+dOtVEjhYsWCCHHXZYZOUCAADpkZiKUTHt71QdOnQo+xhdn6VwjRbtf/UqTlEfv8+p9LyoIzVR7z+LnF4LhX8PaoSem/PvNf+n0uO57uBUnPLPihExykiOUfGidKNGjZJDDz204tThmpekiWj5W7du3UItJwAAYcsVzGXk95aT7EnkqLTzzz9fnnrqKXnhhReka9euriJGWjkKe1Sa29FbQYzu8tqqLnxOUqfjR/yXS3FzrfidT8vJvqIYYcn7Jb3CHpU29Mnvy3atW1jZ5tYNm+S54yZnalRa4rrSLrjgAnniiSdkzpw5FStFqkWLFuYGAEBW0JWWkYiRFvPCCy+U6dOny6xZs8wqvGHMY/T2xAHmZ49RLzlu3SV58dE4lgnZFuU1yfsBSYwYDXnifKsRo1knTCJiFEc6VH/atGny2GOPmbmMVq5cae7Xi6Bly5ZRFw8AAKRAYipGkyZNMj+HDBnS6P777rtPzjzzzMD2WxwpyqvUgrS9xpabnIhy+UBxnFMJ8RTE+fOzzSivozDfP7xvYAtdaRmpGCWkxw8AACRYYipGceVkNmC/3MzpUu33MKIBQY5aYz224AV5TMvl7DnhdbSZjehpGNcZ1zJsIWLkDxUjAABSJJerMTdb28qaxIxKs8HLqLS05BKQY4Q4iFv+UrVt5nmZ/6vc8+I66zzSMyrt0McusDoq7a8n3smoNAAAkEz5WattbStrqBgFoFrLL4qZo72upeblubR4UY6fKInTXKEoc4q85AMW30/kCH6RY5TBtdIAAACCQMQoAF5bel5HdwXBz75p8cKp5YM/b432mF49Yul0HrBqj7NxfVaLVlUrY6Vt8b6BXyRf+0PFCACAFKErzR8qRj55ycXxOhN2GHMm2RDnssH/HERe5yGyNbO8X05GilWL/NiIQhFZBeKJihEAAClCV5o/VIx8CnPtp6TMQRTE7NRRrv0Gu1Eiv4I891637XY2bi/7ABAOKkYAAKSIRnls5QbliBihmnwLsGE0TYit6Lisy+Z2G05H8FTav9t8LMSbn6hPEOc+iLmQ3D6eKChs0eUsbK1pkZPsYR4jAACAekSMXMq35ornXQlz32mRttcD51GPuJ97pxFKm6PR4n5MkBy6jIf+Z2tbWUPFCACAFGFUmj9UjFwiD8A9P61tjneyOJ2LKy3n02aOVNqODZBUVIwAAEgRHZFWw8zXnlExcsnL2kdeVdqO033EuRVaapZh8i7SLW3n00+OUV7xGmlxfs8CWUDFCACAFNGh+taG6+ckc6gYWeS1pedlzhSn+whiZfFi1bZZ7e+0jNPD1nsgKWzOeJ3UY4D4IfnaH+YxAgAAqEfEyKJqMzuHOauz17lSvLRavbaA86u1x2UNLviXP9duz20Q+TVRrHhvI+cI8IuIkT9EjAAASBEdSWbz5tS4cePkoIMOkrZt28ouu+wiJ510kixevLjic6ZOnSo1NTWNbttvv71EiYiRT25GVIWZQ+B1H5VGirnNhapWBiJF6VXu3FaLJNl8bziNjhY+1u/+q0VPSz2m1Huu8HdyjpAUs2fPlpEjR5rK0datW+Xyyy+Xo48+Wt566y1p3bp12ee1a9euUQVKK0dRomIEAECKRDUq7emnn94mGqSRowULFshhhx1W9nlaEerUqZPEBRWjAHN2ip8ThzmHnOZ+uBlNAzi9hoOIEnp93/jJPXK7TyePYw4v2K0Y2coxEs/WrFljfnbo0KHi49avXy/du3eXuro66du3r9xwww3Sq1cviQo5RgAAoKK1a9c2um3atKni47WSM2rUKDn00ENl//33L/u4nj17ypQpU+Sxxx6T+++/3zxv0KBB8u9//1uiUpPLZWf6Jj2Z7du3lyFyomxX08zKNivlDqQNOQ/wOodVsSDm0XK67TBGvvnZB++z9Nma2yKz5DETQdF8mqC/4/b67Whp2spOAnPtpxtlyenjtrl/zJgxMnbs2LLPO//88+Wpp56SF154Qbp27ep4f1u2bJF9991XTjvtNLnuuuskCnSlAQCAipYtW9aoUteiRYuyj73gggvkiSeekDlz5riqFKlmzZrJgQceKEuWLJGoUDEq4id3oFrrsdRzbJYlaKzlBLeCvEbCyC3yui0nuYflHsv7Cn5pN5CtrqBc/U+tFFWLdmkH1IUXXijTp0+XWbNmyR577OF6f7W1tbJw4UI57rjjJCpUjAAASJGoJngcOXKkTJs2zeQL6VxGK1euNPdr917Lli3Nv4cPHy5dunQxcx6pa6+9VgYMGCB77bWXfPLJJzJhwgR5//335dxzz5WoUDEq4ieaU631mH9OlBEXp/v2s+J9mK+P6FU0yo1uLHd/EOcpzNmpg5i13W/5/W4HsG3SpEnm55AhQxrdf99998mZZ55p/r106VJp0uS/474+/vhjGTFihKlE7bDDDtKvXz+ZO3eu7LfffhIVKkYAAKRJEH1pDjgZy6VdbIVuvfVWc4sTRqUlKIrhZDbqoMtgU6kysn5aNketxeU6jVt54lomxHtU2p5Tr5Amlkal1X26Ud4582eBlz1OmMcIAACgHl1pAfDbsiuXQ+BmNuokjCArtUZUGLkp8M9rpLI4Iuhn9ukgeI28Blk2rn0kZUmQtKBiBABAikQ1Ki0tyDHyqFQL0W+rMcn5QkCW8J5EnHOMdp9ypdUco/fOvj5TOUZEjAAASBON8tiK9OSyFzGiYuRSubWf/OQheGl9RjGXCa1kFHM7ijAO11AYa6XZFIdjBmQJFSMAAFKE5Gt/yDGy1GK2OaIqKS3E4nImpdxIB6dR06RFiKLYF9KVY9T9l1dZzTF6f8R1mcoxYh4jAACAenSl+WzNVcqt8Lrump8Woq1WZqW10srtw2bLlmhUOtic+drr+8Tm+8nmiNFqz/WzbiOyjeH6/lAxAgAgbTKTJGMfFSOPol4l3FbUJozoTxDloZUcT27PS7VIUuFjbM0P5mYOsmr79HP92bp2eQ8AdlExAgAgRehK84eKUUIF3dqslGNku4VaaV/l0EqOJ7+5REFEAp1cv0FHJL1c4wCiQcUIAIC05RfZyjHKSeZQMcrYzMNxzDGKwyg82Ln+/M7IXunxNke4uS2H01GSft5PXMuwR7u/bHWB1UjWMI8RAABAPSJGIQqiReh3jao4jPbyk39B6zpa5a6/IM5LtW0uH/x5y7bH9MrbsXGN+517yHZ5gEboSvOFihEAAGlCxcgX1kqz2LoLaiRLEC1JWqlIcm5cUCPdeF8gDWuldfvFWGnS0tJaaZ9tlGU/GJuptdKIGAEAkCY695Ct+Ydy2Uu+pmLkUr4l+czy183PYZ37bPM3t/Kt1IYcifpWtJ8ROtVEkVsURMudFn6y5M9X59k533lnQc55FeX153dkH6D9QLb6gnKZ6VP6L0alAQAA1CPHyCUbLcSktwiJ0qCcKEc1sho94irsHKOud1xjNcfo3xeOyVSOEREjAACAeuQYuRTErLpJa9FWm/0X6Wd7Xiw/11I+N6+zlJ6durhs5f7upfxxys3j/YisJl/vueeeMm/ePNlxxx0b3f/JJ59I37595Z133nG1PSpGAACkSE3u85utbcXde++9J7W1tdvcv2nTJvnggw9cb4+KUQBsjxizwelaT06eG2Q5kQx+oxc2oxvVZt9umDOpaEbsuM/u7vYY8X5E1vzxj39s+Pczzzxj8qvytKI0Y8YM2X333V1vl4oRAABpkpGZr0866STzs6amRs4444xGf2vWrJmpFN18882ut0vFKABuW8lu73ei+Ll+oj1+W6LkPqSf3+iGm2vD7ajO4jmT3JTVb8TLy7VfKvcJcCUjOUZ1dXXm5x577GFyjHbaaScr26ViBAAAEuvdd9+1uj3mMYpwZXgn2yy33fxjelz6D/Nz+YC1je532tKtFq2qtC0gSdHDMNYezOM9g0jXSrvlOrtrpV18VeznMdJ8Ir2tXr26IZKUN2XKFFfbImIEAECaZCTHKO+aa66Ra6+9Vvr37y+77rqryTnyg4qRT0G0DIvXTCvV0s3/e3mZkTZOR5L5WYfKawvcyfPIS0ontyOy3DwnDteQjX01jKKr8P4H8F933323TJ06VU4//XSxgYoRAABpkrGI0ebNm2XQoEHWtkeOkUdxacWFWY64vGakX5Zy3HhfpV/oOUY3Wc4x+km8c4wuvfRSadOmjVx11VVWtkfECACANMnIcP28jRs3yj333CPPPfec9O7d28xhVOiWW24RN6gYRcDmauBhjoSjRZttlaI4tq7p4vyaqAUxX1G1OcYQvLRH6bK2JMjf//53OeCAA8y/33jjjUZ/85KITcUIAAAk1vPPP291e55yjObPny8/+tGPpGnTpnLJJZfIcccdZ+4/+eSTZfr0omFSKZ/HKKj1yZJcljhuO0s5K1FKe0scSEKO0W43Xm81x2jppVc6Kvu4cePkf//3f2XRokXSsmVLkxB94403Ss+ePSs+7+GHHzb5QboY7N57722ek69XRMFTxOj888+X6667zvz7sssukyeeeELuuOMO+eSTTyRod911l0yYMEFWrlwpffr0Mfs9+OCDA98vAAAob/bs2TJy5Eg56KCDZOvWrXL55ZfL0UcfLW+99Za0bt265HPmzp0rp512mqlUnXDCCTJt2jSzBtorr7wi+++/vzhx+OGHV+wymzlzpgReMdKa4DHHHGP+fcQRR8gPf/hDOfbYY+XTTz+VID300ENy8cUXmzkLDjnkEJk4caIMGzZMFi9eLLvsskug+wYAAOU9/fTTjX7XuYX0u3nBggVy2GGHlXzObbfdZuoTP/3pT83vGnR59tln5c477zTf9U7k84vytmzZIq+99prJNypeXDawilGTJk1MxKZTp07SvHlzU/jbb79dfvzjH0uQNLN8xIgRctZZZ5nfdb9/+tOfzHTfGrmKGz8LZdridKLHIPYVRLeK120lpWsnjK6oagnOdIeFI+3H2e/r85PQnnUaO7GWfC3eafeb6tChQ9nHvPjiiybgUUgDHo8++qjj/dx6660l7x87dqysX79eAq8YaZKTVka0QlRIo0br1q2TICdw0lrn6NGjG1XQhg4dag4sAAAILn+pUIsWLcytHF2vbNSoUXLooYdW7BLTIEvHjh0b3ae/6/1+ffe73zWpNjfddFOwFSMNeWkl6IYbbmi476OPPjJRnBdeeEGuuOIKCYLuo7a2tuQB1ESvUjZt2mRu5U5sFkS5HEKUrbcoWpCV9um0PGGUt9pQeD9D8ZPeYrd13TjZjtOFnpPKb/ndPD9OAz3SOo9Rt27dGt09ZswYE5EpR3ONtCtL6wVR0aDJ9ttvH07EaPjw4aYPUJOk3n33XTnnnHPki1/8ounTixNN5tLF5QAAyIwAlgRZtmxZo1FplaJFF1xwgRmUNWfOHOnatWvFzWtKzqpVqxrdp7/r/U59/etfb1zkXE5WrFhhRtB7mQ3b03B97bM777zz5JFHHjHhMk2W0mH7fle0rdaV1qpVK7NPzVjP08QqHQ332GOPOYoYaa3X5nB9hoEj7teEzQlFgSSL6toOe7h+93E/kyYeIiWl1G3cKO+PvsJR2bU6ceGFF5ppe2bNmmWG3ldz6qmnmoFbjz/+eMN9OsxfZ7B2mnydzzsuTLPZeeedzeAwHRUXSvL1P//5T1MT05rg8uXLzagwfWHlhuPZoDlN/fr1kxkzZjRUjLRSpr9r7bSUan2gAACkTkSLyI4cOdL0JGmgom3btg15QlpZ09HsSnucunTpYnp01EUXXSSDBw+Wm2++WY4//nh58MEHTf1Cl/hw6r777hObXFeMxo8fb/oWv/e975n5hJYsWSKnn366qd3df//9MnDgQAmKZq5rhKh///4moUqH62/YsGGb2mKY6NtOD7+TRzpZ4oFFf5FENq8lJ8sPed1mHHL4srwkyKRJk8zPIUOGbFNxOfPMM82/ly5daiI6hdEhrUxdeeWVZt4jjTLpiDSncxgV0gFa//jHP8y/e/XqJQceeKCEUjHSOQe00DpvkdLCv/zyy+YF6cEo7LqyTUNuH374oVx99dWmJqpzF+i8CcUJ2QAAIFw5B5k52sVW7JRTTjE3r1avXi3f+ta3zLa/8IUvmPs0xUYnftQIlHarBZpjpKPDdtppp7KzXmpILK7CXhIE7qUtymFjCRMvz/ezb2RX2q6ZuLyesHOMdr/ebo7Re1c6yzGKigZN3nnnHfnNb34j++67r7lPZ9vWHqa99tpLfve73wUbMSpXKVJxrhQBAJAJEeUYRUV7jp577rmGSpHab7/9zBJioSVfA0Eh96axamWsNALOb64UssfPHFZxFGb+EqKjA7GaNdu2F0jv07+59d8MKAAAkJrka1u3uNNh+Tq6TUfJ533wwQfyox/9SI488kjX2yNihFQr1dpzGoWxEXGJw9puxeUqXiutOJ8JyVYqiuj1mk5blMTLcUjbMUijO++8U772ta/J7rvv3jBDt05IqYPDdLS8W1SMAABIkwCWBIkzrQy98sorJs8ov0SY5hvpWqpeeJr5OqkYlYYs5BqUK6fNSBjC4+f4Fz/X9rUR1ZqDeUGW1+YxCXtU2h5jb7A6Ku3dsZfHclTazJkzzQTPL7300jZl0/LqHEk6e/ZXvvIVV9slxwgAACTOxIkTZcSIESUrbFpB/P73vy+33HKL6+3SlYbMq9YyTFoOzvLBn4e+e0x3FzVAPPk5T05HKsbpWqhUljAjXXE6JkmZ+Tpsr7/+utx4441l/65D9W+66SbX26ViBABAmmRkHqNVq1aVHKaft91225nVMtyiYoRU8NNyrBYp8jNrtd9WZ/EIMieKH+s0akBkKX38nsMwroEg3rth7BvR08Vo33jjDTO7dSl///vfZdddd3W9XXKMAABIE5tzGOUkto477ji56qqrZOPGjdv87bPPPjML3p9wwgmut8uoNKQSLcH/4lggCTl8ab4+wx6VtueVN0hTS6PSajdulHeuj+eoNO1K69u3rzRt2tSMTuvZs6e5X4fs63IgtbW1Zhi/24Xm6UoDAACJ07FjR5k7d66cf/75Mnr0aMnHeWpqamTYsGGmcuS2UqSoGCGV4tL6jOO8MVlooSM+10i1bUSRv1Tt/qDLFbiMJF+r7t27y5NPPikff/yxLFmyxFSO9t57b9lhhx3EKypGAAAg0XbYYQc56KCDrGyLihEyl68Q5uy55crldJ82yup0VFq5fSe65QxP5zSMc+5lxKXbEZRu521Ky7WelXmMgsKoNAAAgHpEjJAoNlq6ttcKcxNZcRsBsrE+lp+WOZKt2qztUUZPncpfv4XXsNcoKOAEFSMAANIkQ8nXQaBiBFThJ6rTsG6Zw2iN27yeUqNoip9bbd9uV2B3Uz6Er1RExm9Ojc2oY36f+bX8qrEZ6cxK3hw5Rv6QYwQAAFCPiBFSzU3rOQhuW7tRrFOWxBXYUZ6bXDenUcK456f5HW1XKe8psdd9BiM9thAxAgAAqEfECIhRPoLbfUZRtrD3C3v8zvcT5HvCz7b9lid11zPJ175QMQIAIEVIvvaHihFSzcY8QNXut7ktGy3XoFr1qWtVZ5DfObyCvAa4vhAXVIwAAEgTutJ8oWKEzKkWUSk3UsdPizaolreTeYyKMRM2ooji2JxB3va+04auNH8YlQYAAFCPiBEyx2mL1e2cQqUeF3QLtdL2y62D1Xl2LtOt6bQK8zza2Jfb91Uht+/FIMoZ6/cNXWm+UDECACBNqBj5QsUIqeJktXC/c7lEyU0rNQmvB/a4udajUO39ZvN69RLddbutOBxTBIOKEQAAKULytT9UjJAqXlqATqJMtvbtVhjzyEQxSgjh8JoX43bkppdtRnl9xaEMiC8qRgAApAk5Rr7U5HK5zLzstWvXSvv27WWInCjb1TSLujgIUZjrfAW5L1q4iFLSrj9b0WC/tua2yCx5TNasWSPt2rUL/Duu50U3SNMW21vZZu2mjbL4tssDL3ucMI8RAABAPbrSgAha015b3jbXfqv2uLi0tuGd2+vMaW5RGGWJa05REqJmJF/7Q8UIAIA0IcfIF3KMkCqlZsktbt0locUXpxE8SAY310jW1stz+n7ykx9Y6fiHnWO0z4V2c4wW3eE8x2jOnDkyYcIEWbBggaxYsUKmT58uJ510UtnHz5o1Sw4//PBt7tfndurUSaJAjhEAACmS70qzdXNjw4YN0qdPH7nrrrtcPW/x4sWmMpS/7bLLLhIVutKQKn5msi0WZY6N01mBSyG6lE1uZr7OR4qCuMajXFus2qz2bnKoyh2bcvfH6v0WYVfasccea25uaUXoC1/4gsQBESMAAFC1m25twW3Tpk1Wt3/AAQfIrrvuKkcddZT89a9/lSgRMULmOG25xqoF6EJSyw37wrzGgxzRFvTrLVX2RH8+BBAx6tatW6O7x4wZI2PHjvW9ea0M3X333dK/f39T2br33ntlyJAh8re//U369u0rUaBiBAAAKlq2bFmj5OsWLVpY2W7Pnj3NLW/QoEHy9ttvy6233iq//e1vJQpUjJA5tlp6XvIzwsz/qbYvW+tmIX28nHO/10eQcwz5iWYlcT6vmvqbrW0prRSFNfP1wQcfLC+88IJEhYoRAABpkvB5jF577TXTxRYVKkaAR+VGqTh5ThitZ1ujgpLQQkZpSTrnNvbpNiLk5PhEMbouydavXy9Llixp+P3dd981FZ0OHTrIbrvtJqNHj5YPPvhAfvOb35i/T5w4UfbYYw/p1auXbNy40eQYzZw5U/785z9H9hqoGAEAkCJRLgkyf/78RhM2XnzxxebnGWecIVOnTjVzFC1durTh75s3b5Yf//jHprLUqlUr6d27tzz33HMlJ30MCzNfI5Vo1SVj3hkkU6VrI065a25nvA6qLGHPfN3r+3Znvn5zsvOZr9OAeYwAAADq0ZWGVPHS8otzZMTNiBjb+SRxPB7wx++17iUnp9w2glBcPqczyKfyWs9MX5B9RIwAAADqETFCqpTLJai0anacW4thziOD9KkWQXHLTyQ2jMgs74Hok6/TgIoRAABpkvB5jKJGxQipUi5/IYoVvcN+nWHOkQRUk6TIbNwiX4gWFSMAAFKErjR/qBghVWyuV+Z0RJiXlqPfdcz8jE6jpZsdttcvc5O753Sbbh4XRbQmke8XutJ8YVQaAABAPSJGSAU3LUmnuQI2Worlok7VZt61uUZauX04RU5FdlSbYyh/Dbw9cYD52WPUS6GUq3j/QV27pSJgtkf2hYGuNH+oGAEAkCZ0pflCxQipVqnFaDuyUorTbYTRCvW6jyS0kCFWruVykczi3/ORIi85RmG+J/xsg0hpdlExAgAgTYgY+ULFCKngJk/IaW5REC3GqHIznKCFnD62Z7q2MTdRuby7IK8/v6NAkS1UjAAASBGSr/2hYoTMqTYiLMjWYxwjRXnLB9eYnz2mR10S2BaHiEiU82tV27afecFiia40X5jHCAAAoB4RI6SSl3mNyv3uZ9t+2dxXtW3FOZoFf8KYldpWGbzsI8j3ZLUIcxzV5HLmZmtbWUPFCACANKErzRcqRkilKEa2BMHNvqIcbYfo+Tmvfuc7slUOp/uI8trmfZN+VIwAAEgRRqX5Q8UIcCjuc6DEaZZthM/LnF2Vnut1/q0or6+0RIoRLSpGAACkCTlGvlAxQiqVm13Xyzb8traBMHlZv6waLyMWbUVa/LweG3OUJfH9TVeaP8xjBAAAkKSI0XvvvSfXXXedzJw5U1auXCmdO3eW7373u3LFFVdI8+bNoy4eYsxPay+JLcWgo2iIv8LzWi5CEqe1w7zMiO01mhuH1xsKutLSXzFatGiR1NXVyeTJk2WvvfaSN954Q0aMGCEbNmyQm266KeriAQAQG3Sl+VOTyyVzWssJEybIpEmT5J133nH8nLVr10r79u1liJwo29U0C7R8iFa53ILC+5IgyNl/U9dKRqrE5fq0UY6tuS0ySx6TNWvWSLt27SQo+e+4fqf+TJo2397KNms3b5QFD10ReNnjJBERo1L0JHXo0CHqYgAAEC90pWWvYrRkyRK54447qnajbdq0ydwKa9PIhkq5BXGIrNgYLVPM7fpWUbfEsySpEUubquU75bm5Pp3mzXnJr4tLxMqrLHaBpWJU2mWXXSY1NTUVb5pfVOiDDz6QY445Rk455RSTZ1TJuHHjTFgxf+vWrVvArwgAACRZpDlGH374ofznP/+p+Jg999yzYeTZ8uXLZciQITJgwACZOnWqNGnSxHXESCtH5BhlTxAt9ji0KIlExFscrpE0cjrazuuoPK+PjU2O0SnXy3bN7OQYbd2yURY8fCU5RmHZeeedzc0JjRQdfvjh0q9fP7nvvvuqVopUixYtzA0AACA1OUZaKdJIUffu3U1ekUaa8jp16hRp2QAAiBOG62egYvTss8+ahGu9de3atdHfEjrbAELmJgyepKVAgnhdiMf5Scv5CnKgQbkBBV4mjay2r0RhVFr6lwQ588wzTQWo1A0AACBTESPEU1patMWCeD1ej5XNY+x2OD/C5XT5ChvCXOA1iteVdTV1n99sbStrqBgBAJAmdKX5QsUInhFZcN4K93qswjzGacttSaswJgONk1JRILc5RUDqcowAAIC7UWm2bm7MmTNHvvrVr0rnzp3NJM2PPvpo1efMmjVL+vbta6bX0YXidZ7CKBExiiFaP+Ecs6COc9InXUxKmaudP6d/L/WYMN+Dbvdlo0y2Xl8Yx8fJPogcFdGBSbYGJ+XcbWfDhg3Sp08fOfvss+XrX/961ce/++67cvzxx8t5550nDzzwgMyYMUPOPfdc2XXXXWXYsGESBSpGAADAimOPPdbcnLr77rtljz32kJtvvtn8vu+++8oLL7wgt956KxUj/FfcWjtxaoWVK4uXstl+PXE4Pmk//4X8zjNV6e9eo1BehLmvavtMGhZGTv4Ejy+++KIMHTq00X1aIRo1apREhYoRAACoug5bEEturVy5Ujp27NjoPv1d9/fZZ59Jy5YtJWxUjFCV7fwLP8+nRRg+m8fcb15Q1KKcCyruxyYKHIvwhut369at0d1jxoyRsWPHShpRMQIAIEWC6EpbtmyZtGvXruF+Wwu063qnq1atanSf/q77iiJapKgYxVhcW59hjmShlRwdP8feay6Yk30Vz2sTh2sjjNyjOLxOZFe7du0aVYxsGThwoDz55JPbrI+q90eFeYwAAEjjcH1bNxfWr18vr732mrnlh+Prv5cuXWp+Hz16tAwfPrzh8TpM/5133pFLLrlEFi1aJL/4xS/k97//vfzoRz+SqBAxirFKLUS/LVSbeUOMnoHfeWUqPS4JI4+cvj4nj7UpjtE1pHtU2vz58+Xwww9v+P3iiy82P8844wwzceOKFSsaKklKh+r/6U9/MhWh2267Tbp27Sr33ntvZEP1FRUjAABgxZAhQyRXIcpUalZrfc6rr74qcUHFKKFsRXxsrrtUvC3yg5LNxnmzMZtznK71ctuodu1H9R7gvZdRLCLrCzlGAAAA9YgYxZjNiIvNGaPLYRRNOpVa3dztObaZ6+I1X8lG/ly5baTl2ifKmw5Jmvk6jqgYAQCQJnW5z2+2tpUxVIwCEMXK1Tbmh4nz6DREp9JIMT/bCOr6sjE7td98OT8RMqd5TEHgvQtQMQIAIF1IvvaFipFLTlptbuY0cfJ4N+VyWhYveRi2W7JEmOCGreskjOut3HvEyf6d5jHFbc4kxEeNxdygGskeRqUBAADUq8lVmokpZdauXSvt27eXIXKibFfTzMo249I6i2P0JY5lQvYEcR1ybcONrbktMksekzVr1gSy3ljxd9yhR46V7bbb3so2t27dKH+dMTbwsscJXWkAAKQIw/X9oWLkU5Cjt9y0SuPYcnWb34Tke3viAPOzx6iXAttHHK4fosNAelExAgAgTRiV5gsVowgUz41SfH+538NouQcx23aem3WjaBUnQ/F58hsp8jLqs9pzvFxvXuctsjFfUZzmhAKyiIoRAAApUpPLmZutbWUNo9IsCiJ3KGz5SFPn2f+9LGyVsziKFefjAG/8Rl6SJu2vD8kclfaVw8ZYHZX2lznXZGpUGvMYAQAA1KMrLYDcB7+5Q2G0Qsvto1SOiK3yFG+bVnb6OL3W4xZpcVueMEbfAV7RleYPFSMAANKEUWm+UDFyyE3OhN9WcBj5GdVa8k4e61XcogXwzu/IKhvXgo1tuH2ujUhREGsmAvCPihEAAGmi3V+2usBy2QsZUTEKoVVqe1X6IPfpZs4XN8/x83jEQ6l1AYOc5yeKbfrdRxCz1RNhhVssCeIPo9IAAADqMY9RALxGVOLUMiwVHQCCunYrPT/K90Wc3pNIrrDnMRo88Eqr8xjNfvF65jECAADIInKMAuC1dRmnVmmYLff8nDCKeWGSydZITC/bDnJ0V5AjRIlGISg1dZ/fbG0ra6gYAQCQJoxK84WKkc81n7zk4sRl7ha3gtpXkLNtI/1rqYVxjfidrd7L/GBZW3cOiAsqRgAApAkzX/vCqDSHGBkDJEO1SEvUEdsgR/AhnsIelXZ4/8utjkp7fv4NjEoDAADIIiJGASiVTxB0CzYMcZxpGMnG+eYYZEHoEaN+o+1GjBaMI2IEAACQRSRfB6Bay6/470ltKfqdP6ZUSzmpxwLezrmTUVxeI6t+1iisdm17LUup51Q7Bl7/jgzTfiBb8w/lJHOoGAEAkCI1uZy52dpW1pBj5HO2ZiczNbuNrEQ5aoZWKGxym0+Xlusvrmu/IRs5RkcceJls19RSjlHtRpn56vhM5RgRMQIAIHXzGNma+Voyh4qRR27W9HLbMrTRkowiUkRLGMXc5gfZmBcoyLXTnHKzzzBzp5ARLAniC6PSAAAA6hExigGnLT0v67L53acbfmfytV0exIefvLpq12qYozzdvldLPc5veZ0eB2SYjkirsbitjKFiBABAijAqzR8qRhaVy21w2uKtNoInyOhOFDkRXvaNZHB7jZTLl3GzjTBEsd4auUNImrvuuksmTJggK1eulD59+sgdd9whBx98cMnHTp06Vc4666xG97Vo0UI2btwoUSHHCACANCZf27q58NBDD8nFF18sY8aMkVdeecVUjIYNGyarV68u+xydBmDFihUNt/fff1+iRMTIYqvM7yiROMyIXarM5SJXQY40QjaUu4aCuGZszC1k85r3+rkAxNktt9wiI0aMaIgC3X333fKnP/1JpkyZIpdddlnJ59TU1EinTp0kLogYAQCQJgFEjNauXdvotmnTpm12u3nzZlmwYIEMHTq04b4mTZqY31988cWyxV2/fr10795dunXrJieeeKK8+eabEiUiRgG0yuKQE+B1rhMno2iK72deFcQpGuIlCkX0BqkSwDxG3bp1a3S3dpWNHTu20X0fffSR1NbWSseOHRvdr78vWrSo5OZ79uxpokm9e/c2s2vfdNNNMmjQIFM56tq1q0SBihEAAKho2bJljZYE0QRpGwYOHGhueVop2nfffWXy5Mly3XXXSRSoGMU4T8GPMEeAMdoMQY3sTOp15Oc9TqQVcZzHqF27dlXXSttpp52kadOmsmrVqkb36+9Oc4iaNWsmBx54oCxZskSiQo4RAAApnMfI1s2p5s2bS79+/WTGjBkN99XV1ZnfC6NClWhX3MKFC2XXXXeVqBAxcikueQpuVym32Qq1tS1axunjN2pa6XlO10CLw/Vkc3QrkCQXX3yxnHHGGdK/f38zd9HEiRNlw4YNDaPUhg8fLl26dJFx48aZ36+99loZMGCA7LXXXvLJJ5+Y+Y90uP65554b2WugYgQA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KIcJHiOmAbszzzxTzjvvPDOFuubFIBhLliyRO+64Q2666aaoi5I4H330kdTW1jbMXpunv2uXJezQKJzmvxx66KGRzvybJrrEhHb9alca4AQRo4Bo14Im+VW66ReKflHrqsujR4+OusipO7aFPvjgAznmmGPM7KoanQPiGtl44403zJc5/Fu2bJlZi+uBBx6IbGFVJA85RgHmCfznP/+p+Jg999xTvvnNb8rjjz9uvszztGXetGlT+c53viO//vWvQyhtOo+trvSsli9fLkOGDJEBAwbI1KlTG63TA+ddadrN+8gjjzSsmq10schPPvlEHnvssUjLlwa6hIIexzlz5piRlPBP19w6+eSTzedp4eerft7q54COBC78G6CoGEVMF9Rbu3Ztw+/6Ja4jffQLSJNdu3btGmn5kk4jRYcffrj069dP7r//fj4EfdDrUVfL1ihnvttnt912M1/oJF97px/BmsA+ffp0s46ULqIJOzQa//777ze6T1d532effeTSSy+luxIlkWMUMf1iKdSmTRvzU+fcoVLkv1KkkaLu3bubvCKNNOV16tQp0rIlkY6a1AiR5sJpBUlH9eiQcv2igb/uM11dXKNFOpeRrkiu2rdvb+bfgnd6PIsrP61bt5Ydd9yRShHKomKE1NI5YTThWm/FlUwCpe6deuqppnJ59dVXmy9vHfL89NNPb5OQDXd0+gillfhC9913nxmYASBcdKUBAADUIwsVAACgHhUjAACAelSMAAAA6lExAgAAqEfFCAAAoB4VIwAAgHpUjAAAAOpRMQIAAKhHxQgAAKAeFSMAAIB6VIwAAADqUTEC0IguFNupUye54YYbGu6bO3euNG/eXGbMmBFp2QAgaFSMADSy8847y5QpU2Ts2LEyf/58WbdunZx++ulywQUXyJFHHikXXnihdOjQYZvV4AEgDWpyuVwu6kIAiJ+RI0fKc889J/3795eFCxfKvHnzpEWLFvLGG2/I0qVL5ec//7nMmjUr6mICgFVEjACUdNNNN8nWrVvl4YcflgceeMBUitT+++8vrVq1irp4ABAIKkYASnr77bdl+fLlUldXJ++9917UxQGAUGwXzm4AJMnmzZvlu9/9rpx66qnSs2dPOffcc0132i677BJ10QAgUESMAGzjiiuukDVr1sjtt98ul156qXzxi1+Us88+O+piAUDgSL4G0IgmVB911FHy/PPPy5e//GVzn3al9enTR8aPH2/+ff/998v//d//SZcuXWTmzJmy2267RV1sALCCihEAAEA9utIAAADqUTECAACoR8UIAACgHhUjAACAelSMAAAA6lExAgAAqEfFCAAAoB4VIwAAgHpUjAAAAOpRMQIAAKhHxQgAAKAeFSMAAAD53P8D0K806+xF6vkAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -552,7 +600,7 @@ ], "source": [ "# ---- Discrete modality -------------------------------------------------\n", - "discrete_samples = samples[\"discrete\"].cpu().numpy() # shape (N, 2) integer tokens\n", + "discrete_samples = samples[0].cpu().numpy() # shape (N, 2) integer tokens\n", "vocab = vocab_size\n", "\n", "# Plot a 2‑D histogram of the discrete samples\n", @@ -571,7 +619,7 @@ "plt.show()\n", "\n", "# ---- Continuous modality -----------------------------------------------\n", - "continuous_samples = samples[\"continuous\"].cpu().numpy() # shape (N, 2)\n", + "continuous_samples = samples[1].cpu().numpy() # shape (N, 2)\n", "\n", "# Plot a 2‑D histogram of the continuous samples\n", "plt.figure(figsize=(6, 5))\n", diff --git a/examples/2d_multimodal_flow_matching.py b/examples/2d_multimodal_flow_matching.py deleted file mode 100644 index b02de18..0000000 --- a/examples/2d_multimodal_flow_matching.py +++ /dev/null @@ -1,451 +0,0 @@ -# %% [markdown] -# # A simple 2D Multimodal Flow Matching model - -# %% [markdown] -# This notebook trains and evaluates a multimodal FM model that jointly handles -# a discrete modality (categorical data) and a continuous modality (real‑valued 2‑D data). -# -# Dataset: 2D discrete/continuous checkerboard -# Model (probability denoiser/velocity): MLPs for each modality and a shared Transformer trunk - -# %% [markdown] -# ## Imports and init device - -# %% -import time - -# To avoide meshgrid warning -import warnings -from typing import Any, Dict, List, Sequence - -# visualization -import matplotlib.pyplot as plt -import torch -from flow_matching.path import AffineProbPath, MixtureDiscreteProbPath -from flow_matching.path.scheduler import ( - CondOTScheduler, # continuous scheduler (training) - PolynomialConvexScheduler, # discrete scheduler (training) -) - -# flow_matching -from flow_matching.utils.multimodal import Flow -from torch import nn, Tensor - -warnings.filterwarnings("ignore", category=UserWarning, module="torch") - -# %% -if torch.cuda.is_available(): - device = "cuda:0" - print("Using GPU") -elif torch.backends.mps.is_available(): - device = "mps" - print("Using MPS") -else: - device = "cpu" - print("Using CPU") - -# %% -torch.manual_seed(42) - -# %% [markdown] -# ## Shared model - -# %% - - -class SharedTransformer(nn.Module): - """ - Shared Transformer trunk used by both modalities. - - Args: - hidden_dim (int): The hidden dimension of the model. - nhead (int): The number of attention heads. - num_layers (int): The number of TransformerEncoder layers. - """ - - def __init__(self, hidden_dim: int = 128, nhead: int = 4, num_layers: int = 2): - super().__init__() - encoder_layer = nn.TransformerEncoderLayer(d_model=hidden_dim, nhead=nhead) - self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) - - def forward(self, x: Tensor) -> Tensor: - """ - Forward pass through the shared Transformer. - - Args: - x (Tensor): Input tensor of shape (sequence_length, batch_size, hidden_dim). - - Returns: - Tensor: Output tensor of the same shape as input. - """ - return self.transformer(x) - - -# %% [markdown] -# ## Datasets - -# %% -def inf_train_gen_discrete( - n_grid_points: int = 128, - batch_size: int = 200, - device: str = "cpu", -) -> Tensor: - """ - Generate a batch of discrete (categorical) samples. - Returns a tensor of shape (batch, 2) with integer token IDs. - - Args: - n_grid_points (int): Number of grid points along one axis (should be divisible by 4). - batch_size (int): Number of samples to generate. - device (str): Device to place the tensor on. - - Returns: - Tensor: A tensor of shape (batch_size, 2) with integer token IDs. - """ - assert n_grid_points % 4 == 0, "grid size must be divisible by 4" - n_grid_points //= 4 - - x1 = torch.randint(low=0, high=n_grid_points * 4, size=(batch_size,), device=device) - samples_x2 = torch.randint( - low=0, high=n_grid_points, size=(batch_size,), device=device - ) - - x2 = ( - samples_x2 - + 2 * n_grid_points - - torch.randint(low=0, high=2, size=(batch_size,), device=device) - * 2 - * n_grid_points - + (torch.floor(x1 / n_grid_points) % 2) * n_grid_points - ) - return torch.stack([x1, x2], dim=1).long() - - -def inf_train_gen_continuous(batch_size: int = 200, device: str = "cpu") -> Tensor: - """ - Generate a batch of 2-D continuous points from a checkerboard-like distribution. - Returns a tensor of shape (batch, 2). - - Args: - batch_size (int): Number of samples to generate. - device (str): Device to place the tensor on. - - Returns: - Tensor: A tensor of shape (batch_size, 2) with continuous values. - """ - x1 = torch.rand(batch_size, device=device) * 4 - 2 - x2_ = ( - torch.rand(batch_size, device=device) - - torch.randint(high=2, size=(batch_size,), device=device) * 2 - ) - x2 = x2_ + (torch.floor(x1) % 2) - data = torch.stack([x1, x2], dim=1) / 0.45 - return data.float() - - -# %% [markdown] -# ## Unified Multimodal Model - -# %% -class Swish(nn.Module): - """Swish activation (x * sigmoid(x)).""" - - def forward(self, x: Tensor) -> Tensor: - """Forward pass through the Swish activation.""" - return torch.sigmoid(x) * x - - -class TransformerModel(nn.Module): - """ - A unified Transformer-based model for handling multiple modalities. - - This model processes a sequence of modalities, each with its own input - and output heads, while sharing a central Transformer trunk. It is designed - to be flexible for both discrete (categorical) and continuous data types. - - Args: - shared_transformer (SharedTransformer): The shared TransformerEncoder module. - modality_configs (List[Dict[str, Any]]): A list of dictionaries, each configuring a modality. - Required keys per config: - - 'type': 'discrete' or 'continuous'. - - 'length': The sequence length for this modality's tokens. - If 'type' is 'discrete': - - 'vocab_size': The size of the vocabulary. - If 'type' is 'continuous': - - 'input_dim': The feature dimension of the continuous data. - time_dim (int): The dimension of the time embedding. - hidden_dim (int): The hidden dimension of the model and transformer. - - Raises: - ValueError: If an unknown modality type is provided. - """ - - def __init__( - self, - shared_transformer: SharedTransformer, - modality_configs: List[Dict[str, Any]], - time_dim: int = 1, - hidden_dim: int = 128, - ): - super().__init__() - self.shared = shared_transformer - self.modality_configs = modality_configs - self.seq_lengths = [config["length"] for config in modality_configs] - - self.input_embedders = nn.ModuleList() - self.time_embedders = nn.ModuleList() - self.input_projectors = nn.ModuleList() - self.output_heads = nn.ModuleList() - self.activations = nn.ModuleList() - - for config in self.modality_configs: - self.time_embedders.append(nn.Linear(1, time_dim)) - self.input_projectors.append(nn.Linear(hidden_dim + time_dim, hidden_dim)) - self.activations.append(Swish()) - - if config["type"] == "discrete": - self.input_embedders.append( - nn.Embedding(config["vocab_size"], hidden_dim) - ) - self.output_heads.append(nn.Linear(hidden_dim, config["vocab_size"])) - elif config["type"] == "continuous": - self.input_embedders.append(nn.Linear(config["input_dim"], hidden_dim)) - self.output_heads.append(nn.Linear(hidden_dim, config["input_dim"])) - else: - raise ValueError(f"Unknown modality type: {config['type']}") - - def forward( - self, x_modalities: Sequence[Tensor], t_modalities: Sequence[Tensor] - ) -> Sequence[Tensor]: - """ - Forward pass for multiple modalities. - - Args: - x_modalities (Sequence[Tensor]): A sequence of input tensors, one for each modality. - Shape for discrete: (batch, length) - Shape for continuous: (batch, input_dim) - t_modalities (Sequence[Tensor]): A sequence of time tensors, one for each modality. - Shape for all: (batch, 1) - - Returns: - Sequence[Tensor]: A sequence of output tensors, one for each modality. - """ - embeddings = [] - - # 1. Process each modality through its specific input head - for i, (x, t, config) in enumerate( - zip(x_modalities, t_modalities, self.modality_configs) - ): - # Embed time and expand to match sequence length - t_emb = self.time_embedders[i](t) - t_emb = t_emb.unsqueeze(1).expand(-1, config["length"], -1) - - # Embed input based on modality type - if config["type"] == "discrete": - x_emb = self.input_embedders[i](x) # (B, length, hidden_dim) - else: # continuous - x_emb = self.input_embedders[i](x) # (B, hidden_dim) - x_emb = x_emb.unsqueeze(1) # (B, 1, hidden_dim) - - # Combine, project, and activate - combined = torch.cat([x_emb, t_emb], dim=-1) - h = self.input_projectors[i](combined) - h = self.activations[i](h) - - # Prepare for transformer (seq_len, batch, hidden_dim) - embeddings.append(h.permute(1, 0, 2)) - - # 2. Concatenate all modality embeddings and pass through shared transformer - full_sequence = torch.cat(embeddings, dim=0) - transformer_out = self.shared(full_sequence) - - # 3. Split the output and process through specific output heads - output_chunks = torch.split(transformer_out, self.seq_lengths, dim=0) - results = [] - for i, chunk in enumerate(output_chunks): - # (length, B, hidden_dim) -> (B, length, hidden_dim) - chunk = chunk.permute(1, 0, 2) - output = self.output_heads[i](chunk) - - # Squeeze sequence dimension if it's 1 (for continuous case) - if output.size(1) == 1: - output = output.squeeze(1) - results.append(output) - - return results - - -# %% [markdown] -# ## Instantiate modalities and model - -# %% -# ---- General Hyperparameters ----------------------------------------- -length = 2 # 2 tokens per sample -vocab_size = 128 -added_token = 0 # uniform source distribution → no extra token -vocab_size += added_token -hidden_dim = 128 - -# ---- Shared transformer trunk ---------------------------------------- -shared_transformer = SharedTransformer(hidden_dim=hidden_dim, nhead=4, num_layers=2).to( - device -) - -# ---- Model and Path Configuration ------------------------------------ -modality_configs = [ - { - "type": "discrete", - "vocab_size": vocab_size, - "length": length, - }, - { - "type": "continuous", - "input_dim": length, - "length": 1, # This modality is treated as a single token in the sequence - }, -] - -# A unified model that handles all modalities -model = TransformerModel( - shared_transformer=shared_transformer, - modality_configs=modality_configs, - time_dim=1, - hidden_dim=hidden_dim, -).to(device) - -# Path definitions remain distinct per modality -discrete_path = MixtureDiscreteProbPath(scheduler=PolynomialConvexScheduler(n=2.0)) -continuous_path = AffineProbPath(scheduler=CondOTScheduler()) - -# ---- Assemble modalities dict for Flow ------------------------------- -modalities = { - "discrete": { - "path": discrete_path, - # loss omitted → Flow will use MixturePathGeneralizedKL automatically - }, - "continuous": { - "path": continuous_path, - # loss omitted → Flow will use MSE loss automatically - }, -} - -# %% [markdown] -# ## Instantiate the multimodal Flow model - -# %% -flow = Flow(model=model, modalities=modalities) - -# Optimizer (optimises both modality models) -optimizer = torch.optim.Adam(flow.parameters(), lr=1e-3) - -# %% [markdown] -# ## Training loop - -# %% -lr = 1e-3 -batch_size = 1024 # adjust as needed to fit in memory -iterations = 12001 -print_every = 3000 -epsilon = 1e-3 - -source_distribution = "uniform" # for the discrete modality - -start_time = time.time() -for i in range(iterations): - optimizer.zero_grad() - - # ---- Discrete data ------------------------------------------------- - x1_disc = inf_train_gen_discrete( - n_grid_points=vocab_size - added_token, - batch_size=batch_size, - device=device, - ) - if source_distribution == "uniform": - x0_disc = torch.randint_like(x1_disc, high=vocab_size) - else: # mask case (not used here) - raise NotImplementedError - - # ---- Continuous data ----------------------------------------------- - x1_cont = inf_train_gen_continuous(batch_size=batch_size, device=device) - x0_cont = torch.randn_like(x1_cont) # isotropic Gaussian prior - - # ---- Sample a common time tensor for both modalities --------------- - t = torch.rand(batch_size, device=device) * (1 - epsilon) - - # ---- Sample from each path to obtain x_t --------------------------- - disc_path_sample = discrete_path.sample(t=t, x_0=x0_disc, x_1=x1_disc) - cont_path_sample = continuous_path.sample(t=t, x_0=x0_cont, x_1=x1_cont) - - # ---- Build the inputs expected by Flow.training_loss ----------- - x_1 = [x1_disc, x1_cont] - x_t = [disc_path_sample.x_t, cont_path_sample.x_t] - dx_t = [None, cont_path_sample.dx_t] # NOTE: dx_t is None for discrete - ts = [t, t] # NOTE: For now, both modalities share the same time - - # ---- Compute total loss and back‑propagate ------------------------- - loss, _ = flow.training_loss(x_1=x_1, x_t=x_t, dx_t=dx_t, t=ts) - loss.backward() - optimizer.step() - - # ---- Logging ------------------------------------------------------- - if (i + 1) % print_every == 0: - elapsed = time.time() - start_time - print( - f"| iter {i+1:6d} | {elapsed*1000/print_every:5.2f} ms/step | loss {loss.item():8.3f} " - ) - start_time = time.time() - -# %% [markdown] -# ## Sampling from the trained multimodal model - -# %% -x_init = [ - torch.randint_like( - x1_disc, high=vocab_size - ), # discrete initial state (uniform categorical) - torch.randn_like(x1_cont), # continuous initial state (Gaussian noise) -] - -flow.eval() # switch to eval mode for sampling -samples = flow.sample(x_init=x_init, device=device, steps=1000) - -# %% [markdown] -# ## Visualization - -# %% -# ---- Discrete modality ------------------------------------------------- -discrete_samples = samples[0].cpu().numpy() # shape (N, 2) integer tokens -vocab = vocab_size - -# Plot a 2‑D histogram of the discrete samples -plt.figure(figsize=(6, 5)) -plt.hist2d( - discrete_samples[:, 0], - discrete_samples[:, 1], - bins=vocab, - cmap="viridis", -) -plt.title("Discrete modality samples (token histogram)") -plt.xlabel("Token 1") -plt.ylabel("Token 2") -plt.colorbar(label="Count") -plt.tight_layout() -plt.show() - -# ---- Continuous modality ----------------------------------------------- -continuous_samples = samples[1].cpu().numpy() # shape (N, 2) - -# Plot a 2‑D histogram of the continuous samples -plt.figure(figsize=(6, 5)) -plt.hist2d( - continuous_samples[:, 0], - continuous_samples[:, 1], - bins=200, - cmap="viridis", -) -plt.title("Continuous modality samples (2-D density)") -plt.xlabel("x₁") -plt.ylabel("x₂") -plt.colorbar(label="Count") -plt.tight_layout() -plt.show() diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 44b8258..d3dfad0 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -172,7 +172,7 @@ def sample( n_steps = ceil((t_final - t_init) / step_size) t_discretization = torch.tensor( [t_init + step_size * i for i in range(n_steps)] + [t_final], - device=x_init.device, + device=device, ) if return_intermediates: @@ -225,14 +225,14 @@ def sample( dtype = config.get("dtype_categorical", torch.float32) # Sample x_1 ~ p_1|t( \cdot |x_t) - p_1t = model_output + p_1t = torch.softmax(model_output, dim=-1) x_1 = categorical(p_1t.to(dtype=dtype)) # Checks if final step if i == n_steps - 1: states[idx] = x_1 # x_t = x_1 at final step else: - vocabulary_size = p_1t.shape[1] + vocabulary_size = p_1t.shape[-1] if self.source_distribution_p is not None: assert self.source_distribution_p.shape == torch.Size( [vocabulary_size] @@ -240,7 +240,7 @@ def sample( # Compute u_t(x|x_t,x_1) path: MixtureDiscreteProbPath = config["path"] - scheduler_output = path.scheduler(t=t[idx]) + scheduler_output = path.scheduler(t=t[idx][:, None, None]) k_t = scheduler_output.alpha_t d_k_t = scheduler_output.d_alpha_t @@ -293,7 +293,7 @@ def sample( intermediates[idx].append(s.clone()) if verbose: - ctx.n = torch.cat(t).mean().long().item() + ctx.n = (torch.cat(t) * n_steps).mean().long().item() ctx.refresh() ctx.set_description(f"NFE: {steps_counter}") diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 4e67e19..c5bbedc 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -199,8 +199,9 @@ def sample( x_init[i] = x_init[i].to(device) # Set up Euler solver for each modality. - modality_configs = { - name: { + modality_configs = [ + { + "name": name, "type": ( "discrete" if isinstance(path, MixtureDiscreteProbPath) @@ -209,7 +210,7 @@ def sample( "path": path, } for name, path in self.paths.items() - } + ] solver = MultimodalSolver( model=self.model, modality_configs=modality_configs, @@ -227,7 +228,7 @@ def sample( time_grid=time_grid, return_intermediates=return_intermediates, enable_grad=enable_grad, - model_extras=model_extras, + **model_extras, ) return samples From f5bcf0eeca30673cf623a26e900bcea8e6bdc73c Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 23 Sep 2025 19:24:55 -0700 Subject: [PATCH 13/44] Update multimodal example image --- docs/source/_images/multimodal.png | Bin 73099 -> 73826 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/source/_images/multimodal.png b/docs/source/_images/multimodal.png index 7260fd1c1b3b53f5ea34a706a9e16ec5ca24646e..a475d87416463a59420c60e0379acbc4c2478cef 100644 GIT binary patch literal 73826 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%0A6FKb0UKdv+`lGL&;{fzi8k-P@Y$o<2Qvb} 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Ξba%AkkJR`!7HYH*^eF>1KU7~&hyV_x8j!F8lf>~*|cY&Nr)m~ z9?*4`x58`ZKEI#<+D@K{Or<|{nRqSv!zY~PemIyp)8X{fM52xL3{Nq4bQa}<4qH@Z zTx30R*lNA@2C<(PNTkM}s|Oqb%5vXvG8Sjs(}vzNL8EO!zke*$XzmSbX>4c+ zIG1eKF4E6cyKg&ek{tLTB4Vu)%+!G<{e?}rITP<*|9OfJloe+jMy~uCmY=X`JvTqc zJaiB45^C^>D{eiaJMW#0UP_jI^+gfLF}01|{{%1YR^Q#YSGRXjxL=~g88K}iQb`i) z#tmW}=6nR1fHiw*W82@d&3Z%XnhwjF5?73zwj~zkm7fn#@@g3-(vvjgA2HGgU?fE&~=>!0ez|UXD8AZj|^yDDknhFPiq7Es0tWgL7lQkAlR2<9+ z%mD#gdaPurfP} zV4m9!@;o2HEA~GNV}))$ToUD8+>PArp|893s>%UGAH+;;P_xd4ft3h~9iZ=0e?>>6 z(}W6@vUR@PNbBd2l4U`lBQ4yKxL}zg3Ma?YfovpyP3-G1t~^v2W2-@52W<9-+%A8k z&Sxs7)|6FNadU{?;?Q`qtN3_h+{m`QrdR`-4Beal^A)w-K!e_B4>(y(jYiEP^~Xdn z>OOd0DLEY^7W#WVE>hz8Ndu@JN5l{3`l5LdjrK_cvhYS5H4gCR9PjfA^gL;lPn7jP zew`rGe|@S)F;KYNry^g`@-4%I$D#_(L;GLab=9dJz+f~zy^?oig0w)zw?r-*tQ@^O zX6M>!6=aTHMP=$;LAA-;S&iI{v;fEaR2h*TA93@?c>c#VZspZrVjH)p;&$w;aHH

3>`9>4^{RS$tX2zrRemv`o#xca|@9r`$W9+2h$ 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 Date: Tue, 23 Sep 2025 19:26:46 -0700 Subject: [PATCH 14/44] Update docs --- docs/source/flow_matching.solver.rst | 1 + flow_matching/solver/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/source/flow_matching.solver.rst b/docs/source/flow_matching.solver.rst index 99b00e8..dd8ebd1 100644 --- a/docs/source/flow_matching.solver.rst +++ b/docs/source/flow_matching.solver.rst @@ -14,5 +14,6 @@ Solvers Solver ODESolver MixtureDiscreteEulerSolver + MultimodalSolver RiemannianODESolver diff --git a/flow_matching/solver/__init__.py b/flow_matching/solver/__init__.py index 6bd7b01..3a3bedb 100644 --- a/flow_matching/solver/__init__.py +++ b/flow_matching/solver/__init__.py @@ -14,5 +14,6 @@ "Solver", "ModelWrapper", "MixtureDiscreteEulerSolver", + "MultimodalSolver", "RiemannianODESolver", ] From 8f04e2403accd0f1e2c12252692db78b005d87c8 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 12:13:38 -0700 Subject: [PATCH 15/44] Document Flow class --- ..._wrapper.rst => flow_matching.utils.multimodal.rst} | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) rename docs/source/{flow_matching.utils.model_wrapper.rst => flow_matching.utils.multimodal.rst} (55%) diff --git a/docs/source/flow_matching.utils.model_wrapper.rst b/docs/source/flow_matching.utils.multimodal.rst similarity index 55% rename from docs/source/flow_matching.utils.model_wrapper.rst rename to docs/source/flow_matching.utils.multimodal.rst index 4310c96..024e132 100644 --- a/docs/source/flow_matching.utils.model_wrapper.rst +++ b/docs/source/flow_matching.utils.multimodal.rst @@ -1,16 +1,18 @@ -``flow_matching.utils.model_wrapper`` +``flow_matching.utils.multimodal`` ============================= -.. currentmodule:: flow_matching.utils.model_wrapper +.. currentmodule:: flow_matching.utils.multimodal -ModelWrapper +Flow -------------------------------- +Generic multimodal flow class + .. autosummary:: :toctree: generated :nosignatures: :template: classtemplate.rst - ModelWrapper + Flow From cc80d1f936e6eeb5e01e3357d88fd85747265194 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 13:43:17 -0700 Subject: [PATCH 16/44] Add unit test for MultimodalSolver --- flow_matching/solver/multimodal_solver.py | 8 +- tests/solver/test_multimodal_solver.py | 244 ++++++++++++++++++++++ 2 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 tests/solver/test_multimodal_solver.py diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index d3dfad0..7c8b960 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -183,9 +183,11 @@ def sample( time_grid=time_grid, t_discretization=t_discretization ) - states: Sequence[Tensor] = [x.clone() for x in x_init] + states: Sequence[Tensor] = [(x if enable_grad else x.clone()) for x in x_init] intermediates: Sequence[List[Tensor]] = ( - [[x.clone()] for x in x_init] if return_intermediates else [] + [[x if enable_grad else x.clone()] for x in x_init] + if return_intermediates + else [] ) steps_counter = 0 @@ -290,7 +292,7 @@ def sample( if return_intermediates: for idx, s in enumerate(states): if t[idx] in time_grid: - intermediates[idx].append(s.clone()) + intermediates[idx].append(s if enable_grad else s.clone()) if verbose: ctx.n = (torch.cat(t) * n_steps).mean().long().item() diff --git a/tests/solver/test_multimodal_solver.py b/tests/solver/test_multimodal_solver.py new file mode 100644 index 0000000..776a198 --- /dev/null +++ b/tests/solver/test_multimodal_solver.py @@ -0,0 +1,244 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the CC-by-NC license found in the +# LICENSE file in the root directory of this source tree. +import unittest +from unittest.mock import MagicMock + +import torch +from flow_matching.path import MixtureDiscreteProbPath +from flow_matching.path.scheduler import PolynomialConvexScheduler + +from flow_matching.solver.multimodal_solver import MultimodalSolver +from flow_matching.utils import ModelWrapper +from torch import Tensor + + +# ---------------------------------------------------------------------- +# Helper models for continuous and discrete modalities +# ---------------------------------------------------------------------- +class ContinuousVelocityModel(ModelWrapper): + def __init__(self): + super().__init__(None) + + def forward(self, xs: list[Tensor], t: list[Tensor], **extras) -> list[Tensor]: + # xs is a list of modality states; we only have one continuous modality here. + # Return a list with the same length as xs. + return [2.0 * xs[0]] + + +class DiscreteLogitsModel(ModelWrapper): + def __init__(self, vocab_size: int): + super().__init__(None) + self.vocab_size = vocab_size + + def forward(self, xs: list[Tensor], t: list[Tensor], **extras) -> list[Tensor]: + """Produce logits that give probability 1.0 to the last class.""" + batch = xs[0].shape[0] + logits = torch.full((batch, self.vocab_size), -1e9, device=xs[0].device) + logits[:, -1] = 1e9 + return [logits] + + +# ---------------------------------------------------------------------- +# Test suite +# ---------------------------------------------------------------------- +class TestMultimodalSolver(unittest.TestCase): + def setUp(self): + # Continuous modality config (no extra args needed) + self.continuous_cfg = {"type": "continuous"} + + # Discrete modality config + self.vocab_size = 3 + self.discrete_path = MixtureDiscreteProbPath( + scheduler=PolynomialConvexScheduler(n=2.0) + ) + self.discrete_cfg = { + "type": "discrete", + "path": self.discrete_path, + } + + # Source distribution for divergence‑free term (uniform) + self.source_p = torch.tensor([1.0 / self.vocab_size] * self.vocab_size) + + # Dummy models + self.continuous_model = ContinuousVelocityModel() + self.discrete_model = DiscreteLogitsModel(vocab_size=self.vocab_size) + + # Combined model that forwards to the appropriate sub‑model + class CombinedModel(ModelWrapper): + def __init__(self, cont, disc): + super().__init__(None) + self.cont = cont + self.disc = disc + + def forward(self, xs, t, **extras): + # xs[0] -> continuous, xs[1] -> discrete + cont_out = self.cont.forward([xs[0]], t, **extras)[0] + disc_out = self.disc.forward([xs[1]], t, **extras)[0] + return [cont_out, disc_out] + + self.model = CombinedModel(self.continuous_model, self.discrete_model) + + # ------------------------------------------------------------------ + # Basic initialization test + # ------------------------------------------------------------------ + def test_init(self): + solver = MultimodalSolver( + model=self.model, + modality_configs=[self.continuous_cfg, self.discrete_cfg], + source_distribution_p=self.source_p, + ) + self.assertIs(solver.model, self.model) + self.assertEqual( + solver.modality_configs, [self.continuous_cfg, self.discrete_cfg] + ) + self.assertTrue(torch.allclose(solver.source_distribution_p, self.source_p)) + + # ------------------------------------------------------------------ + # Simple sampling test (continuous + discrete) + # ------------------------------------------------------------------ + def test_sample_basic(self): + solver = MultimodalSolver( + model=self.model, + modality_configs=[self.continuous_cfg, self.discrete_cfg], + source_distribution_p=self.source_p, + ) + # Initial states: continuous (batch=1, dim=1), discrete (batch=1, categorical) + x_cont = torch.tensor([[0.0]]) # shape (1, 1) + x_disc = torch.tensor([[0]]) # shape (1, 1) + result = solver.sample( + x_init=[x_cont, x_disc], + step_size=0.1, + time_grid=torch.tensor([0.0, 1.0]), + ) + # Continuous modality: v = 2*x, Euler step => x_final = x0 + h*2*x0 = 0 + # Discrete modality: logits always select last class => final state = vocab_size-1 + self.assertTrue(torch.allclose(result[0], torch.zeros_like(result[0]))) + self.assertTrue(torch.equal(result[1], torch.tensor([self.vocab_size - 1]))) + + # ------------------------------------------------------------------ + # Return intermediates test + # ------------------------------------------------------------------ + def test_return_intermediates(self): + solver = MultimodalSolver( + model=self.model, + modality_configs=[self.continuous_cfg, self.discrete_cfg], + source_distribution_p=self.source_p, + ) + x_cont = torch.tensor([[1.0]]) # start at 1.0 + x_disc = torch.tensor([[0]]) # start at class 0 + intermediates = solver.sample( + x_init=[x_cont, x_disc], + step_size=0.5, + time_grid=torch.tensor([0.0, 0.5, 1.0]), + return_intermediates=True, + ) + # Should return a list of two lists (one per modality) + self.assertEqual(len(intermediates), 2) + # Continuous trajectory should have three entries (including start & end) + self.assertEqual(len(intermediates[0]), 3) + # Discrete trajectory should also have three entries + self.assertEqual(len(intermediates[1]), 3) + # Verify the final discrete state is the last class + self.assertTrue( + torch.equal(intermediates[1][-1], torch.tensor([self.vocab_size - 1])) + ) + + # ------------------------------------------------------------------ + # Gradient tracking test + # ------------------------------------------------------------------ + def test_gradient_enabled(self): + solver = MultimodalSolver( + model=self.model, + modality_configs=[self.continuous_cfg, self.discrete_cfg], + source_distribution_p=self.source_p, + ) + x_cont = torch.tensor([[2.0]], requires_grad=True) + x_disc = torch.tensor([[0]], requires_grad=False) + result = solver.sample( + x_init=[x_cont, x_disc], + step_size=0.1, + time_grid=torch.tensor([0.0, 1.0]), + enable_grad=True, + ) + # Only the continuous modality should have a gradient + loss = result[0].sum() + loss.backward() + self.assertIsNotNone(x_cont.grad) + self.assertIsNone(x_disc.grad) + + # ------------------------------------------------------------------ + # Divergence‑free term test (non‑zero) + # ------------------------------------------------------------------ + def test_divergence_free(self): + # Use a mock model that returns zero logits for the discrete modality + mock_model = MagicMock() + mock_model.return_value = [ + torch.zeros(1, 1), + torch.zeros(1, 1, self.vocab_size), + ] + + solver = MultimodalSolver( + model=mock_model, + modality_configs=[self.continuous_cfg, self.discrete_cfg], + source_distribution_p=self.source_p, + ) + x_cont = torch.tensor([[0.0]]) + x_disc = torch.tensor([[0]]) + # Use a constant divergence‑free term + result = solver.sample( + x_init=[x_cont, x_disc], + step_size=0.1, + div_free=0.5, + time_grid=torch.tensor([0.0, 1.0]), + ) + # With a non‑zero div_free, the solver should not raise an assertion. + # The exact numeric value is not critical; we just ensure the call succeeds. + self.assertIsInstance(result, list) + self.assertEqual(len(result), 2) + + # ------------------------------------------------------------------ + # Error handling tests + # ------------------------------------------------------------------ + def test_mismatched_initial_states(self): + solver = MultimodalSolver( + model=self.model, + modality_configs=[self.continuous_cfg, self.discrete_cfg], + ) + # Provide only one initial state instead of two + with self.assertRaises(ValueError): + solver.sample( + x_init=[torch.tensor([[0.0]])], + step_size=0.1, + time_grid=torch.tensor([0.0, 1.0]), + ) + + def test_invalid_modality_type(self): + # Create a bad config list + bad_cfg = [{"type": "unknown"}] + with self.assertRaises(ValueError): + MultimodalSolver( + model=self.model, + modality_configs=bad_cfg, + ) + + def test_missing_path_for_discrete(self): + bad_cfg = [{"type": "discrete"}] # No 'path' key + with self.assertRaises(ValueError): + MultimodalSolver( + model=self.model, + modality_configs=bad_cfg, + ) + + def test_non_callable_model(self): + with self.assertRaises(TypeError): + MultimodalSolver( + model=123, # Not callable + modality_configs=[self.continuous_cfg], + ) + + +if __name__ == "__main__": + unittest.main() From 675f9e15ce35b4901dbaf55f213e1879378330b2 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 13:55:56 -0700 Subject: [PATCH 17/44] Add unit test for multimodal Flow class --- docs/source/modules.rst | 1 + tests/utils/test_multimodal.py | 160 +++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 tests/utils/test_multimodal.py diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 093361a..c3bc258 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -10,3 +10,4 @@ API Reference flow_matching.solver flow_matching.utils.model_wrapper flow_matching.utils.manifolds + flow_matching.utils.multimodal diff --git a/tests/utils/test_multimodal.py b/tests/utils/test_multimodal.py new file mode 100644 index 0000000..b39fd01 --- /dev/null +++ b/tests/utils/test_multimodal.py @@ -0,0 +1,160 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the CC-by-NC license found in the +# LICENSE file in the root directory of this source tree. +import unittest +from unittest.mock import patch + +import torch +from flow_matching.path.mixture import MixtureDiscreteProbPath +from flow_matching.path.scheduler import PolynomialConvexScheduler + +from flow_matching.utils.multimodal import _default_continuous_loss, Flow +from torch import nn + + +class DummyContinuousPath: + """Simple placeholder for a continuous path.""" + + pass + + +class DummyModel(nn.Module): + """Model that returns logits for discrete and scaled inputs for continuous.""" + + def __init__(self, num_classes: int = 5): + super().__init__() + self.num_classes = num_classes + + def forward(self, xs, t, **kwargs): + outputs = [] + for x in xs: + if x.dtype == torch.long: + batch = x.shape[0] + # Return random logits for discrete modality + outputs.append(torch.randn(batch, self.num_classes)) + else: + # Return a simple transformation for continuous modality + outputs.append(x * 2.0) + return outputs + + +class DummyMultimodalSolver: + """Mock solver that records arguments and returns predefined samples.""" + + def __init__(self, model, modality_configs): + self.model = model + self.modality_configs = modality_configs + self.called_with = {} + + def sample(self, **kwargs): + self.called_with = kwargs + # Return a list of tensors matching the number of modalities + return [torch.tensor([1.0]), torch.tensor([2.0])] + + +class TestFlow(unittest.TestCase): + def setUp(self): + self.num_classes = 5 + self.discrete_path = MixtureDiscreteProbPath( + scheduler=PolynomialConvexScheduler(n=2.0) + ) + self.continuous_path = DummyContinuousPath() + self.modalities = { + "disc": {"path": self.discrete_path}, + "cont": {"path": self.continuous_path}, + } + self.model = DummyModel(num_classes=self.num_classes) + self.flow = Flow(model=self.model, modalities=self.modalities) + + def test_init_paths_and_losses(self): + # Paths should be stored correctly + self.assertIn("disc", self.flow.paths) + self.assertIn("cont", self.flow.paths) + self.assertIs(self.flow.paths["disc"], self.discrete_path) + self.assertIs(self.flow.paths["cont"], self.continuous_path) + + # Loss functions: discrete should be MixturePathGeneralizedKL (callable) + self.assertTrue(callable(self.flow.loss_fns["disc"])) + # Continuous should use the default continuous loss + self.assertIs(self.flow.loss_fns["cont"], _default_continuous_loss) + + def test_training_loss_computation(self): + batch = 3 + # Discrete tensors (int64) + x1_disc = torch.randint(0, self.num_classes, (batch,)) + x_t_disc = torch.randint(0, self.num_classes, (batch,)) + # Continuous tensors (float32) + x1_cont = torch.randn(batch, 2) + x_t_cont = torch.randn(batch, 2) + dx_t_cont = torch.randn(batch, 2) + # Assemble inputs matching modality order (disc, cont) + x_1 = [x1_disc, x1_cont] + x_t = [x_t_disc, x_t_cont] + dx_t = [None, dx_t_cont] + t = [torch.rand(batch), torch.rand(batch)] + + total_loss, loss_dict = self.flow.training_loss(x_1, x_t, dx_t, t) + + # Total loss should be a scalar tensor + self.assertIsInstance(total_loss, torch.Tensor) + self.assertEqual(total_loss.dim(), 0) + + # Loss dict should contain both modalities + self.assertSetEqual(set(loss_dict.keys()), {"disc", "cont"}) + # Each entry should be a scalar tensor + for loss in loss_dict.values(): + self.assertIsInstance(loss, torch.Tensor) + self.assertEqual(loss.dim(), 0) + + # Total loss should equal sum of individual losses + summed = sum(loss for loss in loss_dict.values()) + self.assertTrue(torch.allclose(total_loss, summed)) + + def test_training_loss_mismatched_lengths(self): + batch = 2 + x1_disc = torch.randint(0, self.num_classes, (batch,)) + x_t_disc = torch.randint(0, self.num_classes, (batch,)) + # x1_cont = torch.randn(batch, 2) + # x_t_cont = torch.randn(batch, 2) + # dx_t_cont = torch.randn(batch, 2) + + # Omit the continuous modality to trigger assertion + x_1 = [x1_disc] + x_t = [x_t_disc] + dx_t = [None] + t = [torch.rand(batch)] + + with self.assertRaises(AssertionError): + self.flow.training_loss(x_1, x_t, dx_t, t) + + def test_sample_dtype_validation_and_output(self): + batch = 4 + # Correct dtypes + x_init_disc = torch.randint(0, self.num_classes, (batch,)) + x_init_cont = torch.randn(batch, 2) + + with patch( + "flow_matching.utils.multimodal.MultimodalSolver", + DummyMultimodalSolver, + ): + samples = self.flow.sample([x_init_disc, x_init_cont], steps=5) + + # Should receive the dummy solver's output + self.assertEqual(len(samples), 2) + self.assertTrue(torch.equal(samples[0], torch.tensor([1.0]))) + self.assertTrue(torch.equal(samples[1], torch.tensor([2.0]))) + + def test_sample_wrong_dtype_raises(self): + batch = 3 + # Wrong dtype for discrete modality (float instead of long) + x_init_disc = torch.randn(batch, dtype=torch.float32) + x_init_cont = torch.randn(batch, 2) + + with self.assertRaises(AssertionError): + self.flow.sample([x_init_disc, x_init_cont], steps=5) + + +if __name__ == "__main__": + unittest.main() From 05a2352e6c089962328e0616cea3f69e60009f44 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 13:58:18 -0700 Subject: [PATCH 18/44] Add back ModelWrapper docs --- .../source/flow_matching.utils.model_wrapper.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/source/flow_matching.utils.model_wrapper.rst diff --git a/docs/source/flow_matching.utils.model_wrapper.rst b/docs/source/flow_matching.utils.model_wrapper.rst new file mode 100644 index 0000000..4310c96 --- /dev/null +++ b/docs/source/flow_matching.utils.model_wrapper.rst @@ -0,0 +1,16 @@ +``flow_matching.utils.model_wrapper`` +============================= + +.. currentmodule:: flow_matching.utils.model_wrapper + + +ModelWrapper +-------------------------------- + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: classtemplate.rst + + ModelWrapper + From 714c3c87afed3656b7f52fc63daf917f48cbcee8 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 14:55:03 -0700 Subject: [PATCH 19/44] Add support for custom loss weights --- flow_matching/utils/multimodal.py | 10 +++++++--- tests/utils/test_multimodal.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index c5bbedc..9330ab4 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -49,10 +49,11 @@ class Flow(nn.Module): posterior probability `p_1t`. modalities (Dict[str, Dict[str, Any]]): Mapping from modality name to a dict with keys: - - "path": a probability path object (e.g., MixtureDiscreteProbPath for discrete data, + - "path": A probability path object (e.g., MixtureDiscreteProbPath for discrete data, or any continuous path implementation). - - "loss" (optional): a callable loss function. If omitted, a default loss is chosen + - "loss" (optional): A callable loss function. If omitted, a default loss is chosen based on the path type. + - "weight" (optional): A float weight for the modality's training loss. Defaults to 1.0. """ def __init__( @@ -64,6 +65,7 @@ def __init__( self.model = model self.paths: Dict[str, Any] = {} self.loss_fns: Dict[str, Callable] = {} + self.loss_weights: Dict[str, float] = {} for name, spec in modalities.items(): path = spec["path"] @@ -77,6 +79,7 @@ def __init__( else: loss_fn = _default_continuous_loss self.loss_fns[name] = loss_fn + self.loss_weights[name] = spec.get("weight", 1.0) def training_loss( self, @@ -134,7 +137,8 @@ def training_loss( loss = loss_fn(logits[i], dx_t[i]) loss_dict[name] = loss.detach() - total_loss = total_loss + loss + weight = self.loss_weights.get(name, 1.0) + total_loss = total_loss + loss * weight return total_loss, loss_dict diff --git a/tests/utils/test_multimodal.py b/tests/utils/test_multimodal.py index b39fd01..a0d3551 100644 --- a/tests/utils/test_multimodal.py +++ b/tests/utils/test_multimodal.py @@ -155,6 +155,36 @@ def test_sample_wrong_dtype_raises(self): with self.assertRaises(AssertionError): self.flow.sample([x_init_disc, x_init_cont], steps=5) + def test_custom_loss_weights(self): + # Define modalities with custom loss weights + modalities = { + "disc": {"path": self.discrete_path, "weight": 0.5}, + "cont": {"path": self.continuous_path, "weight": 2.0}, + } + flow = Flow(model=self.model, modalities=modalities) + + # Prepare inputs + batch = 3 + x1_disc = torch.randint(0, self.num_classes, (batch,)) + x_t_disc = torch.randint(0, self.num_classes, (batch,)) + x1_cont = torch.randn(batch, 2) + x_t_cont = torch.randn(batch, 2) + dx_t_cont = torch.randn(batch, 2) + x_1 = [x1_disc, x1_cont] + x_t = [x_t_disc, x_t_cont] + dx_t = [None, dx_t_cont] + t = [torch.rand(batch), torch.rand(batch)] + + total_loss, loss_dict = flow.training_loss(x_1, x_t, dx_t, t) + + # Compute expected weighted total loss + expected_total = loss_dict["disc"] * 0.5 + loss_dict["cont"] * 2.0 + self.assertTrue(torch.allclose(total_loss, expected_total)) + + # Verify that loss_weights are stored correctly + self.assertEqual(flow.loss_weights["disc"], 0.5) + self.assertEqual(flow.loss_weights["cont"], 2.0) + if __name__ == "__main__": unittest.main() From 34540e0ec58bbd93cb1d628f8b038e5c84ff0c68 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 16:26:54 -0700 Subject: [PATCH 20/44] Return unaggregated losses within loss_dict --- flow_matching/utils/multimodal.py | 31 +++++++++++++++++++++++++------ tests/utils/test_multimodal.py | 6 +++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 9330ab4..a15961e 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -18,18 +18,33 @@ MULTIMODAL_METHOD = Literal["euler"] -def _default_continuous_loss(pred: Tensor, target: Tensor) -> Tensor: +def _default_continuous_loss( + pred: Tensor, target: Tensor, reduction: str = "none" +) -> Tensor: """ Mean squared error loss for continuous modalities. Args: pred (Tensor): predicted velocity field. target (Tensor): target velocity field. + reduction (str): reduction method, one of 'mean', 'sum', or 'none'. + + Raises: + ValueError: if reduction is not one of 'none', 'mean', or 'sum'. Returns: - Tensor: mean squared error loss. + Tensor: computed loss. """ - return torch.mean((pred - target) ** 2) + loss = (pred - target) ** 2 + + if reduction == "mean": + return torch.mean(loss) + elif reduction == "sum": + return torch.sum(loss) + elif reduction == "none": + return loss + else: + raise ValueError("reduction must be one of 'none', 'mean', or 'sum'") class Flow(nn.Module): @@ -75,7 +90,7 @@ def __init__( loss_fn = spec.get("loss") if loss_fn is None: if isinstance(path, MixtureDiscreteProbPath): - loss_fn = MixturePathGeneralizedKL(path) + loss_fn = MixturePathGeneralizedKL(path, reduction="none") else: loss_fn = _default_continuous_loss self.loss_fns[name] = loss_fn @@ -87,6 +102,7 @@ def training_loss( x_t: Sequence[Tensor], dx_t: Sequence[Tensor], t: Sequence[Tensor], + detach_loss_dict: bool = True, **model_extras: dict, ) -> Tuple[Sequence[Tensor], Dict[str, Tensor]]: """ @@ -101,6 +117,9 @@ def training_loss( containing the velocity field at time t. t (Sequence[Tensor]): Sequence of tensors, one per modality, containing the time values. + detach_loss_dict (bool): If ``True``, detaches individual modality losses + from the computation graph when storing them in the loss dictionary. + Defaults to ``True``. **model_extras (dict): Additional keyword arguments to pass to the model. Returns: @@ -136,9 +155,9 @@ def training_loss( ) loss = loss_fn(logits[i], dx_t[i]) - loss_dict[name] = loss.detach() + loss_dict[name] = loss.detach() if detach_loss_dict else loss weight = self.loss_weights.get(name, 1.0) - total_loss = total_loss + loss * weight + total_loss = total_loss + loss.mean() * weight return total_loss, loss_dict diff --git a/tests/utils/test_multimodal.py b/tests/utils/test_multimodal.py index a0d3551..6fd95bd 100644 --- a/tests/utils/test_multimodal.py +++ b/tests/utils/test_multimodal.py @@ -106,10 +106,10 @@ def test_training_loss_computation(self): # Each entry should be a scalar tensor for loss in loss_dict.values(): self.assertIsInstance(loss, torch.Tensor) - self.assertEqual(loss.dim(), 0) + self.assertEqual(loss.mean().dim(), 0) # Total loss should equal sum of individual losses - summed = sum(loss for loss in loss_dict.values()) + summed = sum(loss.mean() for loss in loss_dict.values()) self.assertTrue(torch.allclose(total_loss, summed)) def test_training_loss_mismatched_lengths(self): @@ -178,7 +178,7 @@ def test_custom_loss_weights(self): total_loss, loss_dict = flow.training_loss(x_1, x_t, dx_t, t) # Compute expected weighted total loss - expected_total = loss_dict["disc"] * 0.5 + loss_dict["cont"] * 2.0 + expected_total = loss_dict["disc"].mean() * 0.5 + loss_dict["cont"].mean() * 2.0 self.assertTrue(torch.allclose(total_loss, expected_total)) # Verify that loss_weights are stored correctly From 095dc806b9e95c524d44b147bfc827d1defbf822 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 16:27:32 -0700 Subject: [PATCH 21/44] Update docstring --- flow_matching/utils/multimodal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index a15961e..3e92193 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -22,7 +22,7 @@ def _default_continuous_loss( pred: Tensor, target: Tensor, reduction: str = "none" ) -> Tensor: """ - Mean squared error loss for continuous modalities. + Squared error loss for continuous modalities. Args: pred (Tensor): predicted velocity field. From 5531d4261c8a92f782861c53e512b67120b0fab4 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 16:49:29 -0700 Subject: [PATCH 22/44] Weight losses in loss_dict --- flow_matching/utils/multimodal.py | 2 +- tests/utils/test_multimodal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 3e92193..878ddf9 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -155,8 +155,8 @@ def training_loss( ) loss = loss_fn(logits[i], dx_t[i]) - loss_dict[name] = loss.detach() if detach_loss_dict else loss weight = self.loss_weights.get(name, 1.0) + loss_dict[name] = (loss.detach() if detach_loss_dict else loss) * weight total_loss = total_loss + loss.mean() * weight return total_loss, loss_dict diff --git a/tests/utils/test_multimodal.py b/tests/utils/test_multimodal.py index 6fd95bd..10faadf 100644 --- a/tests/utils/test_multimodal.py +++ b/tests/utils/test_multimodal.py @@ -178,7 +178,7 @@ def test_custom_loss_weights(self): total_loss, loss_dict = flow.training_loss(x_1, x_t, dx_t, t) # Compute expected weighted total loss - expected_total = loss_dict["disc"].mean() * 0.5 + loss_dict["cont"].mean() * 2.0 + expected_total = loss_dict["disc"].mean() + loss_dict["cont"].mean() self.assertTrue(torch.allclose(total_loss, expected_total)) # Verify that loss_weights are stored correctly From 8243fba6a15de6a0c07777a59b02244b20b2e4b8 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 24 Sep 2025 18:03:14 -0700 Subject: [PATCH 23/44] Add logits argument to training_loss() --- flow_matching/utils/multimodal.py | 10 ++++++++- tests/utils/test_multimodal.py | 36 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 878ddf9..e3d280c 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -102,6 +102,7 @@ def training_loss( x_t: Sequence[Tensor], dx_t: Sequence[Tensor], t: Sequence[Tensor], + logits: Optional[Sequence[Tensor]] = None, detach_loss_dict: bool = True, **model_extras: dict, ) -> Tuple[Sequence[Tensor], Dict[str, Tensor]]: @@ -117,6 +118,8 @@ def training_loss( containing the velocity field at time t. t (Sequence[Tensor]): Sequence of tensors, one per modality, containing the time values. + logits (Optional[Sequence[Tensor]]): Optional precomputed model outputs. + If provided, these are used instead of calling the model. detach_loss_dict (bool): If ``True``, detaches individual modality losses from the computation graph when storing them in the loss dictionary. Defaults to ``True``. @@ -131,10 +134,15 @@ def training_loss( len(x_1) == len(x_t) == len(dx_t) == len(t) == len(self.paths) ), "Input sequences must match the number of modalities." + if logits is not None: + assert len(logits) == len( + self.paths + ), "If provided, logits must match the number of modalities." + loss_dict = {} total_loss = 0.0 - logits = self.model(x_t, t, **model_extras) + logits = logits or self.model(x_t, t, **model_extras) for i, name in enumerate(self.paths): path = self.paths[name] diff --git a/tests/utils/test_multimodal.py b/tests/utils/test_multimodal.py index 10faadf..fb1dc53 100644 --- a/tests/utils/test_multimodal.py +++ b/tests/utils/test_multimodal.py @@ -185,6 +185,42 @@ def test_custom_loss_weights(self): self.assertEqual(flow.loss_weights["disc"], 0.5) self.assertEqual(flow.loss_weights["cont"], 2.0) + def test_training_loss_with_logits_argument(self): + batch = 3 + # Discrete tensors (int64) + x1_disc = torch.randint(0, self.num_classes, (batch,)) + x_t_disc = torch.randint(0, self.num_classes, (batch,)) + # Continuous tensors (float32) + x1_cont = torch.randn(batch, 2) + x_t_cont = torch.randn(batch, 2) + dx_t_cont = torch.randn(batch, 2) + x_1 = [x1_disc, x1_cont] + x_t = [x_t_disc, x_t_cont] + dx_t = [None, dx_t_cont] + t = [torch.rand(batch), torch.rand(batch)] + + # Deterministic logits for discrete and continuous modalities + logits_disc = torch.full((batch, self.num_classes), 0.5) + logits_cont = torch.full_like(dx_t_cont, 0.1) + logits = [logits_disc, logits_cont] + + # Ensure model forward is not called when logits are provided + with patch.object( + self.flow.model, + "forward", + side_effect=AssertionError("Model forward should not be called"), + ): + total_loss, loss_dict = self.flow.training_loss( + x_1, x_t, dx_t, t, logits=logits + ) + + # Verify total loss is scalar and matches sum of individual losses + self.assertIsInstance(total_loss, torch.Tensor) + self.assertEqual(total_loss.dim(), 0) + self.assertSetEqual(set(loss_dict.keys()), {"disc", "cont"}) + summed = sum(loss.mean() for loss in loss_dict.values()) + self.assertTrue(torch.allclose(total_loss, summed)) + if __name__ == "__main__": unittest.main() From bed9ec62b35334e667afe43500fe4ef820bbc5fe Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Thu, 25 Sep 2025 18:32:08 -0700 Subject: [PATCH 24/44] Clarify types --- examples/2d_multimodal_flow_matching.ipynb | 18 +++++------ flow_matching/solver/multimodal_solver.py | 14 ++++----- flow_matching/utils/multimodal.py | 36 +++++++++++----------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/examples/2d_multimodal_flow_matching.ipynb b/examples/2d_multimodal_flow_matching.ipynb index 2a31ec1..e8088db 100644 --- a/examples/2d_multimodal_flow_matching.ipynb +++ b/examples/2d_multimodal_flow_matching.ipynb @@ -30,14 +30,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "e7758331", "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", - "from typing import Any, Dict, List, Sequence\n", + "from typing import Any, Dict, List\n", "\n", "# visualization\n", "import matplotlib.pyplot as plt\n", @@ -231,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "a3517fbc", "metadata": {}, "outputs": [], @@ -248,7 +248,7 @@ " \"\"\"\n", " A unified Transformer-based model for handling multiple modalities.\n", "\n", - " This model processes a sequence of modalities, each with its own input\n", + " This model processes a list of modalities, each with its own input\n", " and output heads, while sharing a central Transformer trunk. It is designed\n", " to be flexible for both discrete (categorical) and continuous data types.\n", "\n", @@ -304,20 +304,20 @@ " raise ValueError(f\"Unknown modality type: {config['type']}\")\n", "\n", " def forward(\n", - " self, x_modalities: Sequence[Tensor], t_modalities: Sequence[Tensor]\n", - " ) -> Sequence[Tensor]:\n", + " self, x_modalities: List[Tensor], t_modalities: List[Tensor]\n", + " ) -> List[Tensor]:\n", " \"\"\"\n", " Forward pass for multiple modalities.\n", "\n", " Args:\n", - " x_modalities (Sequence[Tensor]): A sequence of input tensors, one for each modality.\n", + " x_modalities (List[Tensor]): A list of input tensors, one for each modality.\n", " Shape for discrete: (batch, length)\n", " Shape for continuous: (batch, input_dim)\n", - " t_modalities (Sequence[Tensor]): A sequence of time tensors, one for each modality.\n", + " t_modalities (List[Tensor]): A list of time tensors, one for each modality.\n", " Shape for all: (batch, 1)\n", "\n", " Returns:\n", - " Sequence[Tensor]: A sequence of output tensors, one for each modality.\n", + " List[Tensor]: A list of output tensors, one for each modality.\n", " \"\"\"\n", " embeddings = []\n", "\n", diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 7c8b960..2ee1ce2 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -6,7 +6,7 @@ from contextlib import nullcontext from math import ceil -from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from typing import Any, Callable, Dict, List, Optional, Union import torch from torch import Tensor @@ -97,7 +97,7 @@ def _validate_configs(self): def sample( self, - x_init: Sequence[Tensor], + x_init: List[Tensor], step_size: Optional[float], div_free: Union[float, Callable[[float], float]] = 0.0, method: str = "euler", @@ -106,11 +106,11 @@ def sample( enable_grad: bool = False, verbose: bool = False, **model_extras: dict, - ) -> Union[Sequence[Tensor], Sequence[List[Tensor]]]: + ) -> Union[List[Tensor], List[List[Tensor]]]: """Sample all modalities simultaneously. Args: - x_init (Sequence[Tensor]): Initial states for each modality. + x_init (List[Tensor]): Initial states for each modality. step_size (Optional[float]): Fixed step size for uniform discretization. If ``None``, the discretization is taken from ``time_grid``. div_free (Union[float, Callable[[float], float]]): The coefficient @@ -134,7 +134,7 @@ def sample( TypeError: If the model's output does not match the expected format. Returns: - Union[Sequence[Tensor], Sequence[List[Tensor]]]: If ``return_intermediates`` is + Union[List[Tensor], List[List[Tensor]]]: If ``return_intermediates`` is ``False`` (default), returns a list of final state tensors, one per modality. If ``True``, returns a list where each element is another list of tensors representing the trajectory for a modality. @@ -183,8 +183,8 @@ def sample( time_grid=time_grid, t_discretization=t_discretization ) - states: Sequence[Tensor] = [(x if enable_grad else x.clone()) for x in x_init] - intermediates: Sequence[List[Tensor]] = ( + states: List[Tensor] = [(x if enable_grad else x.clone()) for x in x_init] + intermediates: List[List[Tensor]] = ( [[x if enable_grad else x.clone()] for x in x_init] if return_intermediates else [] diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index e3d280c..7ebd4a7 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -4,7 +4,7 @@ # This source code is licensed under the CC-by-NC license found in the # LICENSE file in the root directory of this source tree. -from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union import torch from torch import nn, Tensor @@ -98,27 +98,27 @@ def __init__( def training_loss( self, - x_1: Sequence[Tensor], - x_t: Sequence[Tensor], - dx_t: Sequence[Tensor], - t: Sequence[Tensor], - logits: Optional[Sequence[Tensor]] = None, + x_1: List[Tensor], + x_t: List[Tensor], + dx_t: List[Tensor], + t: List[Tensor], + logits: Optional[List[Tensor]] = None, detach_loss_dict: bool = True, **model_extras: dict, - ) -> Tuple[Sequence[Tensor], Dict[str, Tensor]]: + ) -> Tuple[List[Tensor], Dict[str, Tensor]]: """ Compute the total training loss across all modalities. Args: - x_1 (Sequence[Tensor]): Sequence of tensors, one per modality, + x_1 (List[Tensor]): List of tensors, one per modality, containing the data at time 1. - x_t (Sequence[Tensor]): Sequence of tensors, one per modality, + x_t (List[Tensor]): List of tensors, one per modality, containing the data at time t. - dx_t (Sequence[Tensor]): Sequence of tensors, one per modality, + dx_t (List[Tensor]): List of tensors, one per modality, containing the velocity field at time t. - t (Sequence[Tensor]): Sequence of tensors, one per modality, + t (List[Tensor]): List of tensors, one per modality, containing the time values. - logits (Optional[Sequence[Tensor]]): Optional precomputed model outputs. + logits (Optional[List[Tensor]]): Optional precomputed model outputs. If provided, these are used instead of calling the model. detach_loss_dict (bool): If ``True``, detaches individual modality losses from the computation graph when storing them in the loss dictionary. @@ -126,7 +126,7 @@ def training_loss( **model_extras (dict): Additional keyword arguments to pass to the model. Returns: - Tuple[Sequence[Tensor], Dict[str, Tensor]]: + Tuple[List[Tensor], Dict[str, Tensor]]: Scalar loss (sum of modality losses) and a dictionary of individual modality losses. """ @@ -171,7 +171,7 @@ def training_loss( def sample( self, - x_init: Sequence[Tensor], + x_init: List[Tensor], time_grid: Optional[Tensor] = None, device: torch.device = torch.device("cpu"), steps: int = 1000, @@ -181,13 +181,13 @@ def sample( return_intermediates: bool = False, enable_grad: bool = False, **model_extras: dict, - ) -> Union[Sequence[Tensor], Sequence[List[Tensor]]]: + ) -> Union[List[Tensor], List[List[Tensor]]]: """ Generate samples for each modality using the inference scheduler. Args: - x_init (Sequence[Tensor]): - Sequence of tensors, one per modality, containing the initial states at time 0. + x_init (List[Tensor]): + List of tensors, one per modality, containing the initial states at time 0. For continuous modalities, this is typically Gaussian noise. For discrete modalities, this is typically samples from a uniform categorical distribution. time_grid (Optional[Tensor]): Optional tensor of time points defining the interval. @@ -208,7 +208,7 @@ def sample( **model_extras (dict): Additional keyword arguments to pass to the model. Returns: - Union[Sequence[Tensor], Sequence[List[Tensor]]]: A list where each element corresponds to a modality. + Union[List[Tensor], List[List[Tensor]]]: A list where each element corresponds to a modality. Each element is either a tensor of shape ``(batch_size, ...)`` containing the samples, or a list of tensors (if `return_intermediates` is True in `MultimodalSolver.sample`). """ From 2e96a1b967419e469cb9cc6b9d6188bea4d7e1e4 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Thu, 25 Sep 2025 18:47:54 -0700 Subject: [PATCH 25/44] Revert "Clarify types" This reverts commit bed9ec62b35334e667afe43500fe4ef820bbc5fe. --- examples/2d_multimodal_flow_matching.ipynb | 18 +++++------ flow_matching/solver/multimodal_solver.py | 14 ++++----- flow_matching/utils/multimodal.py | 36 +++++++++++----------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/examples/2d_multimodal_flow_matching.ipynb b/examples/2d_multimodal_flow_matching.ipynb index e8088db..2a31ec1 100644 --- a/examples/2d_multimodal_flow_matching.ipynb +++ b/examples/2d_multimodal_flow_matching.ipynb @@ -30,14 +30,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "e7758331", "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", - "from typing import Any, Dict, List\n", + "from typing import Any, Dict, List, Sequence\n", "\n", "# visualization\n", "import matplotlib.pyplot as plt\n", @@ -231,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "a3517fbc", "metadata": {}, "outputs": [], @@ -248,7 +248,7 @@ " \"\"\"\n", " A unified Transformer-based model for handling multiple modalities.\n", "\n", - " This model processes a list of modalities, each with its own input\n", + " This model processes a sequence of modalities, each with its own input\n", " and output heads, while sharing a central Transformer trunk. It is designed\n", " to be flexible for both discrete (categorical) and continuous data types.\n", "\n", @@ -304,20 +304,20 @@ " raise ValueError(f\"Unknown modality type: {config['type']}\")\n", "\n", " def forward(\n", - " self, x_modalities: List[Tensor], t_modalities: List[Tensor]\n", - " ) -> List[Tensor]:\n", + " self, x_modalities: Sequence[Tensor], t_modalities: Sequence[Tensor]\n", + " ) -> Sequence[Tensor]:\n", " \"\"\"\n", " Forward pass for multiple modalities.\n", "\n", " Args:\n", - " x_modalities (List[Tensor]): A list of input tensors, one for each modality.\n", + " x_modalities (Sequence[Tensor]): A sequence of input tensors, one for each modality.\n", " Shape for discrete: (batch, length)\n", " Shape for continuous: (batch, input_dim)\n", - " t_modalities (List[Tensor]): A list of time tensors, one for each modality.\n", + " t_modalities (Sequence[Tensor]): A sequence of time tensors, one for each modality.\n", " Shape for all: (batch, 1)\n", "\n", " Returns:\n", - " List[Tensor]: A list of output tensors, one for each modality.\n", + " Sequence[Tensor]: A sequence of output tensors, one for each modality.\n", " \"\"\"\n", " embeddings = []\n", "\n", diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 2ee1ce2..7c8b960 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -6,7 +6,7 @@ from contextlib import nullcontext from math import ceil -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Union import torch from torch import Tensor @@ -97,7 +97,7 @@ def _validate_configs(self): def sample( self, - x_init: List[Tensor], + x_init: Sequence[Tensor], step_size: Optional[float], div_free: Union[float, Callable[[float], float]] = 0.0, method: str = "euler", @@ -106,11 +106,11 @@ def sample( enable_grad: bool = False, verbose: bool = False, **model_extras: dict, - ) -> Union[List[Tensor], List[List[Tensor]]]: + ) -> Union[Sequence[Tensor], Sequence[List[Tensor]]]: """Sample all modalities simultaneously. Args: - x_init (List[Tensor]): Initial states for each modality. + x_init (Sequence[Tensor]): Initial states for each modality. step_size (Optional[float]): Fixed step size for uniform discretization. If ``None``, the discretization is taken from ``time_grid``. div_free (Union[float, Callable[[float], float]]): The coefficient @@ -134,7 +134,7 @@ def sample( TypeError: If the model's output does not match the expected format. Returns: - Union[List[Tensor], List[List[Tensor]]]: If ``return_intermediates`` is + Union[Sequence[Tensor], Sequence[List[Tensor]]]: If ``return_intermediates`` is ``False`` (default), returns a list of final state tensors, one per modality. If ``True``, returns a list where each element is another list of tensors representing the trajectory for a modality. @@ -183,8 +183,8 @@ def sample( time_grid=time_grid, t_discretization=t_discretization ) - states: List[Tensor] = [(x if enable_grad else x.clone()) for x in x_init] - intermediates: List[List[Tensor]] = ( + states: Sequence[Tensor] = [(x if enable_grad else x.clone()) for x in x_init] + intermediates: Sequence[List[Tensor]] = ( [[x if enable_grad else x.clone()] for x in x_init] if return_intermediates else [] diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 7ebd4a7..e3d280c 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -4,7 +4,7 @@ # This source code is licensed under the CC-by-NC license found in the # LICENSE file in the root directory of this source tree. -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union import torch from torch import nn, Tensor @@ -98,27 +98,27 @@ def __init__( def training_loss( self, - x_1: List[Tensor], - x_t: List[Tensor], - dx_t: List[Tensor], - t: List[Tensor], - logits: Optional[List[Tensor]] = None, + x_1: Sequence[Tensor], + x_t: Sequence[Tensor], + dx_t: Sequence[Tensor], + t: Sequence[Tensor], + logits: Optional[Sequence[Tensor]] = None, detach_loss_dict: bool = True, **model_extras: dict, - ) -> Tuple[List[Tensor], Dict[str, Tensor]]: + ) -> Tuple[Sequence[Tensor], Dict[str, Tensor]]: """ Compute the total training loss across all modalities. Args: - x_1 (List[Tensor]): List of tensors, one per modality, + x_1 (Sequence[Tensor]): Sequence of tensors, one per modality, containing the data at time 1. - x_t (List[Tensor]): List of tensors, one per modality, + x_t (Sequence[Tensor]): Sequence of tensors, one per modality, containing the data at time t. - dx_t (List[Tensor]): List of tensors, one per modality, + dx_t (Sequence[Tensor]): Sequence of tensors, one per modality, containing the velocity field at time t. - t (List[Tensor]): List of tensors, one per modality, + t (Sequence[Tensor]): Sequence of tensors, one per modality, containing the time values. - logits (Optional[List[Tensor]]): Optional precomputed model outputs. + logits (Optional[Sequence[Tensor]]): Optional precomputed model outputs. If provided, these are used instead of calling the model. detach_loss_dict (bool): If ``True``, detaches individual modality losses from the computation graph when storing them in the loss dictionary. @@ -126,7 +126,7 @@ def training_loss( **model_extras (dict): Additional keyword arguments to pass to the model. Returns: - Tuple[List[Tensor], Dict[str, Tensor]]: + Tuple[Sequence[Tensor], Dict[str, Tensor]]: Scalar loss (sum of modality losses) and a dictionary of individual modality losses. """ @@ -171,7 +171,7 @@ def training_loss( def sample( self, - x_init: List[Tensor], + x_init: Sequence[Tensor], time_grid: Optional[Tensor] = None, device: torch.device = torch.device("cpu"), steps: int = 1000, @@ -181,13 +181,13 @@ def sample( return_intermediates: bool = False, enable_grad: bool = False, **model_extras: dict, - ) -> Union[List[Tensor], List[List[Tensor]]]: + ) -> Union[Sequence[Tensor], Sequence[List[Tensor]]]: """ Generate samples for each modality using the inference scheduler. Args: - x_init (List[Tensor]): - List of tensors, one per modality, containing the initial states at time 0. + x_init (Sequence[Tensor]): + Sequence of tensors, one per modality, containing the initial states at time 0. For continuous modalities, this is typically Gaussian noise. For discrete modalities, this is typically samples from a uniform categorical distribution. time_grid (Optional[Tensor]): Optional tensor of time points defining the interval. @@ -208,7 +208,7 @@ def sample( **model_extras (dict): Additional keyword arguments to pass to the model. Returns: - Union[List[Tensor], List[List[Tensor]]]: A list where each element corresponds to a modality. + Union[Sequence[Tensor], Sequence[List[Tensor]]]: A list where each element corresponds to a modality. Each element is either a tensor of shape ``(batch_size, ...)`` containing the samples, or a list of tensors (if `return_intermediates` is True in `MultimodalSolver.sample`). """ From d63e655fb8bd30c241feee3a377916452d92af5f Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Thu, 25 Sep 2025 18:52:33 -0700 Subject: [PATCH 26/44] Handle tuple x_init inputs for sample() --- flow_matching/utils/multimodal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index e3d280c..3734e90 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -213,6 +213,7 @@ def sample( or a list of tensors (if `return_intermediates` is True in `MultimodalSolver.sample`). """ # Validate samples for each modality. + x_init = x_init if isinstance(x_init, list) else list(x_init) for i, name in enumerate(self.paths): path = self.paths[name] From 3ce97890d629007c76417404253b5c87ac8d1adf Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Sat, 27 Sep 2025 15:41:57 -0700 Subject: [PATCH 27/44] Make variable name more informative; fix unit test; make t reshaping for discrete sampling more robust; initialize solver once; and assert for all continuous floating-point types --- flow_matching/solver/multimodal_solver.py | 6 ++- flow_matching/utils/multimodal.py | 60 +++++++++++------------ tests/utils/test_multimodal.py | 9 ++-- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 7c8b960..59cf82c 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -242,7 +242,11 @@ def sample( # Compute u_t(x|x_t,x_1) path: MixtureDiscreteProbPath = config["path"] - scheduler_output = path.scheduler(t=t[idx][:, None, None]) + + t_expanded = t[idx].reshape( + -1, *[1] * (model_output.dim() - 1) + ) + scheduler_output = path.scheduler(t=t_expanded) k_t = scheduler_output.alpha_t d_k_t = scheduler_output.d_alpha_t diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 3734e90..2575e5b 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -96,13 +96,31 @@ def __init__( self.loss_fns[name] = loss_fn self.loss_weights[name] = spec.get("weight", 1.0) + # Set up Euler solver for each modality. + modality_configs = [ + { + "name": name, + "type": ( + "discrete" + if isinstance(path, MixtureDiscreteProbPath) + else "continuous" + ), + "path": path, + } + for name, path in self.paths.items() + ] + self.solver = MultimodalSolver( + model=self.model, + modality_configs=modality_configs, + ) + def training_loss( self, x_1: Sequence[Tensor], x_t: Sequence[Tensor], dx_t: Sequence[Tensor], t: Sequence[Tensor], - logits: Optional[Sequence[Tensor]] = None, + model_output: Optional[Sequence[Tensor]] = None, detach_loss_dict: bool = True, **model_extras: dict, ) -> Tuple[Sequence[Tensor], Dict[str, Tensor]]: @@ -118,7 +136,7 @@ def training_loss( containing the velocity field at time t. t (Sequence[Tensor]): Sequence of tensors, one per modality, containing the time values. - logits (Optional[Sequence[Tensor]]): Optional precomputed model outputs. + model_output (Optional[Sequence[Tensor]]): Optional precomputed model outputs. If provided, these are used instead of calling the model. detach_loss_dict (bool): If ``True``, detaches individual modality losses from the computation graph when storing them in the loss dictionary. @@ -134,15 +152,15 @@ def training_loss( len(x_1) == len(x_t) == len(dx_t) == len(t) == len(self.paths) ), "Input sequences must match the number of modalities." - if logits is not None: - assert len(logits) == len( + if model_output is not None: + assert len(model_output) == len( self.paths - ), "If provided, logits must match the number of modalities." + ), "If provided, model outputs must match the number of modalities." loss_dict = {} total_loss = 0.0 - logits = logits or self.model(x_t, t, **model_extras) + model_output = model_output or self.model(x_t, t, **model_extras) for i, name in enumerate(self.paths): path = self.paths[name] @@ -154,16 +172,16 @@ def training_loss( f"Expected integer tensor for discrete modality '{name}', " f"got {x_t[i].dtype}", ) - loss = loss_fn(logits[i], x_1[i], x_t[i], t[i]) + loss = loss_fn(model_output[i], x_1[i], x_t[i], t[i]) else: # Continuous case: model returns velocity field. - assert x_t[i].dtype == torch.float32, ( + assert x_t[i].is_floating_point(), ( f"Expected float tensor for continuous modality '{name}', " f"got {x_t[i].dtype}", ) - loss = loss_fn(logits[i], dx_t[i]) + loss = loss_fn(model_output[i], dx_t[i]) - weight = self.loss_weights.get(name, 1.0) + weight = self.loss_weights[name] loss_dict[name] = (loss.detach() if detach_loss_dict else loss) * weight total_loss = total_loss + loss.mean() * weight @@ -223,36 +241,18 @@ def sample( f"got {x_init[i].dtype}", ) else: - assert x_init[i].dtype == torch.float32, ( + assert x_init[i].is_floating_point(), ( f"Expected float tensor for continuous modality '{name}', " f"got {x_init[i].dtype}", ) x_init[i] = x_init[i].to(device) - # Set up Euler solver for each modality. - modality_configs = [ - { - "name": name, - "type": ( - "discrete" - if isinstance(path, MixtureDiscreteProbPath) - else "continuous" - ), - "path": path, - } - for name, path in self.paths.items() - ] - solver = MultimodalSolver( - model=self.model, - modality_configs=modality_configs, - ) - # Solve to obtain multimodal samples at time 1. step_size = step_size or (1.0 / steps) time_grid = time_grid or torch.linspace(0.0, 1.0, steps, device=device) - samples = solver.sample( + samples = self.solver.sample( x_init=x_init, step_size=step_size, div_free=div_free, diff --git a/tests/utils/test_multimodal.py b/tests/utils/test_multimodal.py index fb1dc53..c00edd5 100644 --- a/tests/utils/test_multimodal.py +++ b/tests/utils/test_multimodal.py @@ -51,7 +51,7 @@ def __init__(self, model, modality_configs): def sample(self, **kwargs): self.called_with = kwargs # Return a list of tensors matching the number of modalities - return [torch.tensor([1.0]), torch.tensor([2.0])] + return [torch.tensor([1]), torch.tensor([2.0])] class TestFlow(unittest.TestCase): @@ -139,11 +139,14 @@ def test_sample_dtype_validation_and_output(self): "flow_matching.utils.multimodal.MultimodalSolver", DummyMultimodalSolver, ): + self.flow = Flow( + model=self.model, modalities=self.modalities + ) # Reinitialize to use dummy solver samples = self.flow.sample([x_init_disc, x_init_cont], steps=5) # Should receive the dummy solver's output self.assertEqual(len(samples), 2) - self.assertTrue(torch.equal(samples[0], torch.tensor([1.0]))) + self.assertTrue(torch.equal(samples[0], torch.tensor([1]))) self.assertTrue(torch.equal(samples[1], torch.tensor([2.0]))) def test_sample_wrong_dtype_raises(self): @@ -211,7 +214,7 @@ def test_training_loss_with_logits_argument(self): side_effect=AssertionError("Model forward should not be called"), ): total_loss, loss_dict = self.flow.training_loss( - x_1, x_t, dx_t, t, logits=logits + x_1, x_t, dx_t, t, model_output=logits ) # Verify total loss is scalar and matches sum of individual losses From b03325e527699057a56ef8e5ab5ceed7b82bf2dd Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Sat, 27 Sep 2025 19:08:15 -0700 Subject: [PATCH 28/44] Add x_1_prediction flag to support x_1 parametrization for continuous modalities --- flow_matching/solver/multimodal_solver.py | 41 ++++++++++++++++-- flow_matching/utils/multimodal.py | 15 +++++-- tests/solver/test_multimodal_solver.py | 10 +++-- tests/utils/test_multimodal.py | 51 +++++++++++++++++++---- 4 files changed, 98 insertions(+), 19 deletions(-) diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 59cf82c..e002056 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -47,14 +47,25 @@ class MultimodalSolver(Solver): modality_configs (List[Dict[str, Any]]): A list of configuration dictionaries, one for each modality. Each dictionary must have a ``'type'`` key, which is either - ``'continuous'`` or ``'discrete'``. Discrete modality configs must - also provide a ``'path'`` key with a `MixtureDiscreteProbPath` object. + ``'continuous'`` or ``'discrete'``. Discrete modality configs may + provide a ``'dtype_categorical'`` key with the desired data type + for categorical logit sampling (e.g., ``torch.float32``) and + must provide a ``'path'`` key with a `MixtureDiscreteProbPath` + instance. Continuous modality configs must provide a ``'path'`` + key with a `ProbPath` instance + (e.g., `AffineProbPath(scheduler=CondOTScheduler())`) as well as + an ``'x_1_prediction'`` key which is either ``True`` or ``False``. + If ``True``, the model is expected to predict the clean data `x_1` + for that modality, and such predictions will be reparameterized + as velocities during the sampling process. If ``False``, the model + is expected to predict the velocities directly. source_distribution_p (Optional[Tensor], optional): Source distribution, must be of shape [vocabulary_size]. Required only when divergence-free term for the probability velocity is non-zero. Defaults to None. Raises: - TypeError: If `model` is not callable. + TypeError: If ``model`` is not callable or if ``modality_configs`` + is not a list of dictionaries. """ def __init__( @@ -94,6 +105,19 @@ def _validate_configs(self): raise TypeError( f"'path' for discrete modality {i} must be a MixtureDiscreteProbPath instance." ) + if config["type"] == "continuous": + if "path" not in config: + raise ValueError( + f"Continuous modality {i} requires a 'path' in its config." + ) + if "x_1_prediction" not in config: + raise ValueError( + f"Continuous modality {i} requires an 'x_1_prediction' key in its config." + ) + if not isinstance(config["x_1_prediction"], bool): + raise TypeError( + f"'x_1_prediction' for continuous modality {i} must be a boolean." + ) def sample( self, @@ -221,7 +245,16 @@ def sample( if config["type"] == "continuous": # Sample x_{t+h} = x_t + h * v(x_t,t) - states[idx] = states[idx] + h * model_output + path = config["path"] + velocity_output = ( + path.target_to_velocity( + x_1=model_output, x_t=states[idx], t=t[idx] + ) + if config["x_1_prediction"] + else model_output + ) + + states[idx] = states[idx] + h * velocity_output elif config["type"] == "discrete": dtype = config.get("dtype_categorical", torch.float32) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 2575e5b..b9a7788 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -69,6 +69,10 @@ class Flow(nn.Module): - "loss" (optional): A callable loss function. If omitted, a default loss is chosen based on the path type. - "weight" (optional): A float weight for the modality's training loss. Defaults to 1.0. + - "x_1_prediction" (continuous paths only, optional): If True, the model is expected to predict + the clean data `x_1` for that modality, and such predictions will be reparameterized + as velocities during the sampling process. If False, the model is expected to predict + the velocities directly. Defaults to False. """ def __init__( @@ -97,7 +101,7 @@ def __init__( self.loss_weights[name] = spec.get("weight", 1.0) # Set up Euler solver for each modality. - modality_configs = [ + self.modality_configs = [ { "name": name, "type": ( @@ -106,12 +110,13 @@ def __init__( else "continuous" ), "path": path, + "x_1_prediction": modalities[name].get("x_1_prediction", False), } for name, path in self.paths.items() ] self.solver = MultimodalSolver( model=self.model, - modality_configs=modality_configs, + modality_configs=self.modality_configs, ) def training_loss( @@ -165,6 +170,7 @@ def training_loss( for i, name in enumerate(self.paths): path = self.paths[name] loss_fn = self.loss_fns[name] + modality_config = self.modality_configs[i] if isinstance(path, MixtureDiscreteProbPath): # Discrete case: model should output logits. @@ -179,7 +185,10 @@ def training_loss( f"Expected float tensor for continuous modality '{name}', " f"got {x_t[i].dtype}", ) - loss = loss_fn(model_output[i], dx_t[i]) + loss = loss_fn( + model_output[i], + x_1[i] if modality_config["x_1_prediction"] else dx_t[i], + ) weight = self.loss_weights[name] loss_dict[name] = (loss.detach() if detach_loss_dict else loss) * weight diff --git a/tests/solver/test_multimodal_solver.py b/tests/solver/test_multimodal_solver.py index 776a198..5e2015d 100644 --- a/tests/solver/test_multimodal_solver.py +++ b/tests/solver/test_multimodal_solver.py @@ -7,8 +7,8 @@ from unittest.mock import MagicMock import torch -from flow_matching.path import MixtureDiscreteProbPath -from flow_matching.path.scheduler import PolynomialConvexScheduler +from flow_matching.path import AffineProbPath, MixtureDiscreteProbPath +from flow_matching.path.scheduler import CondOTScheduler, PolynomialConvexScheduler from flow_matching.solver.multimodal_solver import MultimodalSolver from flow_matching.utils import ModelWrapper @@ -47,7 +47,11 @@ def forward(self, xs: list[Tensor], t: list[Tensor], **extras) -> list[Tensor]: class TestMultimodalSolver(unittest.TestCase): def setUp(self): # Continuous modality config (no extra args needed) - self.continuous_cfg = {"type": "continuous"} + self.continuous_cfg = { + "type": "continuous", + "path": AffineProbPath(scheduler=CondOTScheduler()), + "x_1_prediction": False, + } # Discrete modality config self.vocab_size = 3 diff --git a/tests/utils/test_multimodal.py b/tests/utils/test_multimodal.py index c00edd5..fe9f8fd 100644 --- a/tests/utils/test_multimodal.py +++ b/tests/utils/test_multimodal.py @@ -7,19 +7,13 @@ from unittest.mock import patch import torch -from flow_matching.path.mixture import MixtureDiscreteProbPath -from flow_matching.path.scheduler import PolynomialConvexScheduler +from flow_matching.path import AffineProbPath, MixtureDiscreteProbPath +from flow_matching.path.scheduler import CondOTScheduler, PolynomialConvexScheduler from flow_matching.utils.multimodal import _default_continuous_loss, Flow from torch import nn -class DummyContinuousPath: - """Simple placeholder for a continuous path.""" - - pass - - class DummyModel(nn.Module): """Model that returns logits for discrete and scaled inputs for continuous.""" @@ -60,7 +54,7 @@ def setUp(self): self.discrete_path = MixtureDiscreteProbPath( scheduler=PolynomialConvexScheduler(n=2.0) ) - self.continuous_path = DummyContinuousPath() + self.continuous_path = AffineProbPath(scheduler=CondOTScheduler()) self.modalities = { "disc": {"path": self.discrete_path}, "cont": {"path": self.continuous_path}, @@ -188,6 +182,45 @@ def test_custom_loss_weights(self): self.assertEqual(flow.loss_weights["disc"], 0.5) self.assertEqual(flow.loss_weights["cont"], 2.0) + def test_training_loss_x1_prediction_true(self): + # Define a custom continuous loss that returns the target tensor. + def custom_continuous_loss(pred, target, reduction="none"): + # Return the target directly to verify it's used. + return target + + # Set up modalities with x_1_prediction enabled for the continuous path. + modalities = { + "disc": {"path": self.discrete_path}, + "cont": { + "path": self.continuous_path, + "loss": custom_continuous_loss, + "x_1_prediction": True, + }, + } + flow = Flow(model=self.model, modalities=modalities) + + # Prepare inputs. + batch = 3 + x1_disc = torch.randint(0, self.num_classes, (batch,)) + x_t_disc = torch.randint(0, self.num_classes, (batch,)) + x1_cont = torch.randn(batch, 2) + x_t_cont = torch.randn(batch, 2) + dx_t_cont = torch.randn( + batch, 2 + ) # Should be ignored due to x_1_prediction=True + x_1 = [x1_disc, x1_cont] + x_t = [x_t_disc, x_t_cont] + dx_t = [None, dx_t_cont] + t = [torch.rand(batch), torch.rand(batch)] + + total_loss, loss_dict = flow.training_loss(x_1, x_t, dx_t, t) + + # The continuous loss should have used x1_cont as the target. + self.assertTrue(torch.allclose(loss_dict["cont"], x1_cont)) + # Total loss should be sum of discrete loss mean and x1_cont mean. + expected_total = loss_dict["disc"].mean() + loss_dict["cont"].mean() + self.assertTrue(torch.allclose(total_loss, expected_total)) + def test_training_loss_with_logits_argument(self): batch = 3 # Discrete tensors (int64) From 3ef3cbabac4c537589e831a8f99c7534c59938f4 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Sat, 27 Sep 2025 19:39:42 -0700 Subject: [PATCH 29/44] Use t_expanded for x_1 prediction --- flow_matching/solver/multimodal_solver.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index e002056..85c60b1 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -243,12 +243,16 @@ def sample( for idx, config in enumerate(self.modality_configs): model_output = outputs[idx] + t_expanded = t[idx].reshape( + -1, *[1] * (model_output.dim() - 1) + ) # Expand t to match model_output shape + if config["type"] == "continuous": # Sample x_{t+h} = x_t + h * v(x_t,t) path = config["path"] velocity_output = ( path.target_to_velocity( - x_1=model_output, x_t=states[idx], t=t[idx] + x_1=model_output, x_t=states[idx], t=t_expanded ) if config["x_1_prediction"] else model_output @@ -275,10 +279,6 @@ def sample( # Compute u_t(x|x_t,x_1) path: MixtureDiscreteProbPath = config["path"] - - t_expanded = t[idx].reshape( - -1, *[1] * (model_output.dim() - 1) - ) scheduler_output = path.scheduler(t=t_expanded) k_t = scheduler_output.alpha_t @@ -291,7 +291,7 @@ def sample( # Add divergence-free part div_free_t = ( - div_free(t[idx]) if callable(div_free) else div_free + div_free(t_expanded) if callable(div_free) else div_free ) if div_free_t > 0: From fa0dc3952a1590037b04cb5c40463fd0f534129a Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Sun, 28 Sep 2025 14:41:50 -0700 Subject: [PATCH 30/44] Add import --- flow_matching/utils/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flow_matching/utils/__init__.py b/flow_matching/utils/__init__.py index 0085c44..ff58b08 100644 --- a/flow_matching/utils/__init__.py +++ b/flow_matching/utils/__init__.py @@ -6,6 +6,7 @@ from .categorical_sampler import categorical from .model_wrapper import ModelWrapper +from .multimodal import Flow from .utils import expand_tensor_like, gradient, unsqueeze_to_match __all__ = [ @@ -13,5 +14,6 @@ "expand_tensor_like", "gradient", "categorical", + "Flow", "ModelWrapper", ] From 2bf97e06ad8e65498e2e0c55b31e1decd6887b79 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 1 Oct 2025 09:59:03 -0700 Subject: [PATCH 31/44] Resolve circular import --- flow_matching/utils/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flow_matching/utils/__init__.py b/flow_matching/utils/__init__.py index ff58b08..0085c44 100644 --- a/flow_matching/utils/__init__.py +++ b/flow_matching/utils/__init__.py @@ -6,7 +6,6 @@ from .categorical_sampler import categorical from .model_wrapper import ModelWrapper -from .multimodal import Flow from .utils import expand_tensor_like, gradient, unsqueeze_to_match __all__ = [ @@ -14,6 +13,5 @@ "expand_tensor_like", "gradient", "categorical", - "Flow", "ModelWrapper", ] From c5a5a9386b0fb9223fa03a369987d5b8221cbaec Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 1 Oct 2025 11:19:07 -0700 Subject: [PATCH 32/44] Allow custom time_grids --- flow_matching/utils/multimodal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index b9a7788..09c1d4e 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -259,7 +259,11 @@ def sample( # Solve to obtain multimodal samples at time 1. step_size = step_size or (1.0 / steps) - time_grid = time_grid or torch.linspace(0.0, 1.0, steps, device=device) + time_grid = ( + time_grid + if time_grid is not None + else torch.linspace(0.0, 1.0, steps, device=device) + ) samples = self.solver.sample( x_init=x_init, From 7f4e569939c8949a0742cde2a5fc4cb1aa0e36f9 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 1 Oct 2025 11:58:33 -0700 Subject: [PATCH 33/44] Add verbose flag --- flow_matching/utils/multimodal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 09c1d4e..63fab7a 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -207,6 +207,7 @@ def sample( method: MULTIMODAL_METHOD = "euler", return_intermediates: bool = False, enable_grad: bool = False, + verbose: bool = False, **model_extras: dict, ) -> Union[Sequence[Tensor], Sequence[List[Tensor]]]: """ @@ -232,6 +233,7 @@ def sample( return_intermediates (bool): If ``True``, returns a list of tensors for each modality containing the state at each intermediate time step. enable_grad (bool): Whether to enable gradient tracking during integration. + verbose (bool): If ``True``, prints progress during sampling. **model_extras (dict): Additional keyword arguments to pass to the model. Returns: @@ -273,6 +275,7 @@ def sample( time_grid=time_grid, return_intermediates=return_intermediates, enable_grad=enable_grad, + verbose=verbose, **model_extras, ) From 4a16e2b40e17ff98e25a31c6a486d3671e2cec23 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 1 Oct 2025 14:35:16 -0700 Subject: [PATCH 34/44] Allow one to customize model forward function for multimodal solver --- flow_matching/solver/multimodal_solver.py | 8 +++++++- flow_matching/utils/multimodal.py | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 85c60b1..44f524a 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -62,6 +62,9 @@ class MultimodalSolver(Solver): source_distribution_p (Optional[Tensor], optional): Source distribution, must be of shape [vocabulary_size]. Required only when divergence-free term for the probability velocity is non-zero. Defaults to None. + model_sampling_fn (str, optional): If ``model`` is a class instance + with multiple methods, this specifies the method to use for + forward passes during sampling. Defaults to ``"forward"``. Raises: TypeError: If ``model`` is not callable or if ``modality_configs`` @@ -73,6 +76,7 @@ def __init__( model: Union[ModelWrapper, Callable], modality_configs: List[Dict[str, Any]], source_distribution_p: Optional[Tensor] = None, + model_sampling_fn: str = "forward", ): super().__init__() if not callable(model): @@ -80,6 +84,7 @@ def __init__( self.model = model self.modality_configs = modality_configs self.source_distribution_p = source_distribution_p + self.model_sampling_fn = model_sampling_fn self._validate_configs() @@ -231,7 +236,8 @@ def sample( t = [t_discretization[i : i + 1].repeat(batch_size)] * len(states) h = t_discretization[i + 1 : i + 2] - t_discretization[i : i + 1] - outputs = self.model(states, t, **model_extras) + model_fn = getattr(self.model, self.model_sampling_fn, self.model) + outputs = model_fn(states, t, **model_extras) if not isinstance(outputs, (list, tuple)) or len(outputs) != len( states diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 63fab7a..3708b7d 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -73,12 +73,16 @@ class Flow(nn.Module): the clean data `x_1` for that modality, and such predictions will be reparameterized as velocities during the sampling process. If False, the model is expected to predict the velocities directly. Defaults to False. + model_sampling_fn (str, optional): If ``model`` is a class instance + with multiple methods, this specifies the method to use for + forward passes during sampling. Defaults to ``"forward"``. """ def __init__( self, model: nn.Module, modalities: Dict[str, Dict[str, Any]], + model_sampling_fn: str = "forward", ) -> None: super().__init__() self.model = model @@ -117,6 +121,7 @@ def __init__( self.solver = MultimodalSolver( model=self.model, modality_configs=self.modality_configs, + model_forward_fn=model_sampling_fn, ) def training_loss( From 81ac9d2c94386d13b74e5229854ac8fb366542a9 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 1 Oct 2025 14:40:47 -0700 Subject: [PATCH 35/44] Fix unit tests --- flow_matching/utils/multimodal.py | 2 +- tests/solver/test_multimodal_solver.py | 3 ++- tests/utils/test_multimodal.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flow_matching/utils/multimodal.py b/flow_matching/utils/multimodal.py index 3708b7d..f239aa8 100644 --- a/flow_matching/utils/multimodal.py +++ b/flow_matching/utils/multimodal.py @@ -121,7 +121,7 @@ def __init__( self.solver = MultimodalSolver( model=self.model, modality_configs=self.modality_configs, - model_forward_fn=model_sampling_fn, + model_sampling_fn=model_sampling_fn, ) def training_loss( diff --git a/tests/solver/test_multimodal_solver.py b/tests/solver/test_multimodal_solver.py index 5e2015d..9a844af 100644 --- a/tests/solver/test_multimodal_solver.py +++ b/tests/solver/test_multimodal_solver.py @@ -179,7 +179,8 @@ def test_gradient_enabled(self): def test_divergence_free(self): # Use a mock model that returns zero logits for the discrete modality mock_model = MagicMock() - mock_model.return_value = [ + mock_model.forward = MagicMock() + mock_model.forward.return_value = [ torch.zeros(1, 1), torch.zeros(1, 1, self.vocab_size), ] diff --git a/tests/utils/test_multimodal.py b/tests/utils/test_multimodal.py index fe9f8fd..d41770b 100644 --- a/tests/utils/test_multimodal.py +++ b/tests/utils/test_multimodal.py @@ -37,9 +37,10 @@ def forward(self, xs, t, **kwargs): class DummyMultimodalSolver: """Mock solver that records arguments and returns predefined samples.""" - def __init__(self, model, modality_configs): + def __init__(self, model, modality_configs, model_sampling_fn=None): self.model = model self.modality_configs = modality_configs + self.model_sampling_fn = model_sampling_fn self.called_with = {} def sample(self, **kwargs): From 6334c4c198d59498cba3bea452115bfaab9acf99 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 7 Oct 2025 17:05:46 -0700 Subject: [PATCH 36/44] Add optional jump_coefficient argument to MixturePathGeneralizedKL.forward() --- flow_matching/loss/generalized_loss.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/flow_matching/loss/generalized_loss.py b/flow_matching/loss/generalized_loss.py index cc1507e..c1e8ef2 100644 --- a/flow_matching/loss/generalized_loss.py +++ b/flow_matching/loss/generalized_loss.py @@ -4,6 +4,8 @@ # This source code is licensed under the CC-by-NC license found in the # LICENSE file in the root directory of this source tree. +from typing import Optional + import torch from torch import Tensor from torch.nn.modules.loss import _Loss @@ -31,7 +33,14 @@ def __init__(self, path: MixtureDiscreteProbPath, reduction: str = "mean") -> No super().__init__(None, None, reduction) self.path = path - def forward(self, logits: Tensor, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor: + def forward( + self, + logits: Tensor, + x_1: Tensor, + x_t: Tensor, + t: Tensor, + jump_coefficient: Optional[Tensor] = None, + ) -> Tensor: r"""Evaluates the generalized KL loss. Args: @@ -39,15 +48,21 @@ def forward(self, logits: Tensor, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor x_1 (Tensor): target data point :math:`x_1 \sim q`, shape (batch, d). x_t (Tensor): conditional sample at :math:`x_t \sim p_t(\cdot|x_1)`, shape (batch, d). t (Tensor): times in :math:`[0,1]`, shape (batch). + jump_coefficient (Optional[Tensor], optional): precomputed jump coefficient. If not provided, it will be computed inside this function. Shape (batch,). Defaults to None. Raises: - ValueError: reduction value must be one of ``'none'`` | ``'mean'`` | ``'sum'``. + ValueError: if ``jump_coefficient`` is not None and does not have shape (batch,). + Raises: + ValueError: if ``reduction`` is not one of 'none', 'mean', or 'sum'. Returns: Tensor: Generalized KL loss. """ x_1_shape = x_1.shape + if jump_coefficient is not None and jump_coefficient.shape != (x_1_shape[0],): + raise ValueError("jump_coefficient must be a vector of shape (batch,)") + # extract x_1 value of log(p_{1|t}(x|x_t)). log_p_1t = torch.log_softmax(logits, dim=-1) log_p_1t_x1 = torch.gather(log_p_1t, dim=-1, index=x_1.unsqueeze(-1)) @@ -61,7 +76,9 @@ def forward(self, logits: Tensor, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor scheduler_output = self.path.scheduler(t) jump_coefficient = ( - scheduler_output.d_alpha_t / (1 - scheduler_output.alpha_t) + jump_coefficient + if jump_coefficient is not None + else scheduler_output.d_alpha_t / (1 - scheduler_output.alpha_t) )[(...,) + (None,) * (x_1.dim() - 1)] jump_coefficient = jump_coefficient.repeat(1, *x_1_shape[1:]) delta_x1_xt = (x_t == x_1).to(log_p_1t.dtype) From 628c2bc7e1a82215222310bf6f6a435aff4bc75e Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 7 Oct 2025 17:06:27 -0700 Subject: [PATCH 37/44] Fix raises syntax --- flow_matching/loss/generalized_loss.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flow_matching/loss/generalized_loss.py b/flow_matching/loss/generalized_loss.py index c1e8ef2..11d6189 100644 --- a/flow_matching/loss/generalized_loss.py +++ b/flow_matching/loss/generalized_loss.py @@ -51,9 +51,8 @@ def forward( jump_coefficient (Optional[Tensor], optional): precomputed jump coefficient. If not provided, it will be computed inside this function. Shape (batch,). Defaults to None. Raises: - ValueError: if ``jump_coefficient`` is not None and does not have shape (batch,). - Raises: - ValueError: if ``reduction`` is not one of 'none', 'mean', or 'sum'. + ValueError: if ``jump_coefficient`` is not None and does not have shape (batch,) + or if ``reduction`` is not one of 'none', 'mean', or 'sum'. Returns: Tensor: Generalized KL loss. From 13ad9a704628db4b086a1de1f690848f1694a2a6 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Tue, 7 Oct 2025 17:20:43 -0700 Subject: [PATCH 38/44] Simplify syntax --- flow_matching/loss/generalized_loss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow_matching/loss/generalized_loss.py b/flow_matching/loss/generalized_loss.py index 11d6189..98cdfe9 100644 --- a/flow_matching/loss/generalized_loss.py +++ b/flow_matching/loss/generalized_loss.py @@ -59,7 +59,7 @@ def forward( """ x_1_shape = x_1.shape - if jump_coefficient is not None and jump_coefficient.shape != (x_1_shape[0],): + if jump_coefficient is not None and jump_coefficient.shape != x_1_shape[:1]: raise ValueError("jump_coefficient must be a vector of shape (batch,)") # extract x_1 value of log(p_{1|t}(x|x_t)). From 70761372f7f8a0d646c3676d786ad7ee11096838 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 8 Oct 2025 11:30:24 -0700 Subject: [PATCH 39/44] Update 2d_discrete_flow_matching.ipynb --- examples/2d_discrete_flow_matching.ipynb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/2d_discrete_flow_matching.ipynb b/examples/2d_discrete_flow_matching.ipynb index f5bbca0..d9d44d8 100644 --- a/examples/2d_discrete_flow_matching.ipynb +++ b/examples/2d_discrete_flow_matching.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "id": "rb5VSo4mNkVd" }, @@ -45,13 +45,14 @@ "from flow_matching.loss import MixturePathGeneralizedKL\n", "\n", "# visualization\n", + "import numpy as np\n", "import matplotlib.cm as cm\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -66,9 +67,6 @@ "if torch.cuda.is_available():\n", " device = 'cuda:0'\n", " print('Using gpu')\n", - "elif torch.backends.mps.is_available():\n", - " device = \"mps\"\n", - " print(\"Using MPS\")\n", "else:\n", " device = 'cpu'\n", " print('Using cpu.')" From 21d08aa715ee9d711eb6d256824a60d290c5b2a2 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 8 Oct 2025 11:31:03 -0700 Subject: [PATCH 40/44] Update 2d_flow_matching.ipynb --- examples/2d_flow_matching.ipynb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/2d_flow_matching.ipynb b/examples/2d_flow_matching.ipynb index e0c49ed..622f411 100644 --- a/examples/2d_flow_matching.ipynb +++ b/examples/2d_flow_matching.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "id": "rb5VSo4mNkVd" }, @@ -40,7 +40,7 @@ "# flow_matching\n", "from flow_matching.path.scheduler import CondOTScheduler\n", "from flow_matching.path import AffineProbPath\n", - "from flow_matching.solver import ODESolver\n", + "from flow_matching.solver import Solver, ODESolver\n", "from flow_matching.utils import ModelWrapper\n", "\n", "# visualization\n", @@ -57,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -72,9 +72,6 @@ "if torch.cuda.is_available():\n", " device = 'cuda:0'\n", " print('Using gpu')\n", - "elif torch.backends.mps.is_available():\n", - " device = \"mps\"\n", - " print(\"Using MPS\")\n", "else:\n", " device = 'cpu'\n", " print('Using cpu.')" @@ -134,7 +131,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ From ffb6ac2a44a3b7ba51381e3949c6b153f8ae0bd0 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 8 Oct 2025 11:32:00 -0700 Subject: [PATCH 41/44] Update 2d_riemannian_flow_matching_flat_torus.ipynb --- examples/2d_riemannian_flow_matching_flat_torus.ipynb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/2d_riemannian_flow_matching_flat_torus.ipynb b/examples/2d_riemannian_flow_matching_flat_torus.ipynb index 0f93e5d..8a80135 100644 --- a/examples/2d_riemannian_flow_matching_flat_torus.ipynb +++ b/examples/2d_riemannian_flow_matching_flat_torus.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "id": "rb5VSo4mNkVd" }, @@ -25,13 +25,14 @@ "import time\n", "import torch\n", "import math\n", + "import numpy as np\n", "\n", "from torch import nn, Tensor\n", "\n", "# flow_matching\n", "from flow_matching.path import GeodesicProbPath\n", "from flow_matching.path.scheduler import CondOTScheduler\n", - "from flow_matching.solver import RiemannianODESolver\n", + "from flow_matching.solver import ODESolver, RiemannianODESolver\n", "from flow_matching.utils import ModelWrapper\n", "from flow_matching.utils.manifolds import FlatTorus, Manifold\n", "\n", @@ -43,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -58,9 +59,6 @@ "if torch.cuda.is_available():\n", " device = 'cuda:0'\n", " print('Using gpu')\n", - "elif torch.backends.mps.is_available():\n", - " device = \"mps\"\n", - " print(\"Using MPS\")\n", "else:\n", " device = 'cpu'\n", " print('Using cpu.')" From 6f6d117358b0f4623b806aa39ccf813e182d1497 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 8 Oct 2025 11:32:29 -0700 Subject: [PATCH 42/44] Update 2d_riemannian_flow_matching_sphere.ipynb --- examples/2d_riemannian_flow_matching_sphere.ipynb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/2d_riemannian_flow_matching_sphere.ipynb b/examples/2d_riemannian_flow_matching_sphere.ipynb index 5b3cac2..c5d8a58 100644 --- a/examples/2d_riemannian_flow_matching_sphere.ipynb +++ b/examples/2d_riemannian_flow_matching_sphere.ipynb @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -59,9 +59,6 @@ "if torch.cuda.is_available():\n", " device = 'cuda:0'\n", " print('Using gpu')\n", - "elif torch.backends.mps.is_available():\n", - " device = \"mps\"\n", - " print(\"Using MPS\")\n", "else:\n", " device = 'cpu'\n", " print('Using cpu.')" From 4d15d8db10d1b46dc1e5e3ca3909f5bf0b29eb91 Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 8 Oct 2025 11:33:29 -0700 Subject: [PATCH 43/44] Update generalized_loss.py --- flow_matching/loss/generalized_loss.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/flow_matching/loss/generalized_loss.py b/flow_matching/loss/generalized_loss.py index 98cdfe9..cc1507e 100644 --- a/flow_matching/loss/generalized_loss.py +++ b/flow_matching/loss/generalized_loss.py @@ -4,8 +4,6 @@ # This source code is licensed under the CC-by-NC license found in the # LICENSE file in the root directory of this source tree. -from typing import Optional - import torch from torch import Tensor from torch.nn.modules.loss import _Loss @@ -33,14 +31,7 @@ def __init__(self, path: MixtureDiscreteProbPath, reduction: str = "mean") -> No super().__init__(None, None, reduction) self.path = path - def forward( - self, - logits: Tensor, - x_1: Tensor, - x_t: Tensor, - t: Tensor, - jump_coefficient: Optional[Tensor] = None, - ) -> Tensor: + def forward(self, logits: Tensor, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor: r"""Evaluates the generalized KL loss. Args: @@ -48,20 +39,15 @@ def forward( x_1 (Tensor): target data point :math:`x_1 \sim q`, shape (batch, d). x_t (Tensor): conditional sample at :math:`x_t \sim p_t(\cdot|x_1)`, shape (batch, d). t (Tensor): times in :math:`[0,1]`, shape (batch). - jump_coefficient (Optional[Tensor], optional): precomputed jump coefficient. If not provided, it will be computed inside this function. Shape (batch,). Defaults to None. Raises: - ValueError: if ``jump_coefficient`` is not None and does not have shape (batch,) - or if ``reduction`` is not one of 'none', 'mean', or 'sum'. + ValueError: reduction value must be one of ``'none'`` | ``'mean'`` | ``'sum'``. Returns: Tensor: Generalized KL loss. """ x_1_shape = x_1.shape - if jump_coefficient is not None and jump_coefficient.shape != x_1_shape[:1]: - raise ValueError("jump_coefficient must be a vector of shape (batch,)") - # extract x_1 value of log(p_{1|t}(x|x_t)). log_p_1t = torch.log_softmax(logits, dim=-1) log_p_1t_x1 = torch.gather(log_p_1t, dim=-1, index=x_1.unsqueeze(-1)) @@ -75,9 +61,7 @@ def forward( scheduler_output = self.path.scheduler(t) jump_coefficient = ( - jump_coefficient - if jump_coefficient is not None - else scheduler_output.d_alpha_t / (1 - scheduler_output.alpha_t) + scheduler_output.d_alpha_t / (1 - scheduler_output.alpha_t) )[(...,) + (None,) * (x_1.dim() - 1)] jump_coefficient = jump_coefficient.repeat(1, *x_1_shape[1:]) delta_x1_xt = (x_t == x_1).to(log_p_1t.dtype) From 45765502fd9fcc66ec7ea1b7de53e4f0a24b545f Mon Sep 17 00:00:00 2001 From: Alex Morehead Date: Wed, 8 Oct 2025 11:59:41 -0700 Subject: [PATCH 44/44] Use expand_tensor_like utility function --- flow_matching/solver/multimodal_solver.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/flow_matching/solver/multimodal_solver.py b/flow_matching/solver/multimodal_solver.py index 44f524a..029e1a6 100644 --- a/flow_matching/solver/multimodal_solver.py +++ b/flow_matching/solver/multimodal_solver.py @@ -16,7 +16,7 @@ from flow_matching.path import MixtureDiscreteProbPath from flow_matching.solver.solver import Solver from flow_matching.solver.utils import get_nearest_times -from flow_matching.utils import categorical, ModelWrapper +from flow_matching.utils import categorical, expand_tensor_like, ModelWrapper try: from tqdm import tqdm @@ -249,9 +249,10 @@ def sample( for idx, config in enumerate(self.modality_configs): model_output = outputs[idx] - t_expanded = t[idx].reshape( - -1, *[1] * (model_output.dim() - 1) - ) # Expand t to match model_output shape + t_expanded = expand_tensor_like( + input_tensor=t[idx], + expand_to=model_output, + ) if config["type"] == "continuous": # Sample x_{t+h} = x_t + h * v(x_t,t)