//  [python]
def __call__(
        self,
        rgb: Float[Tensor, "B H W C"],
        camera,
        rgb_as_latents=False,
        guidance_eval=False,
        **kwargs,
    ):
        batch_size = rgb.shape[0]

        rgb_BCHW = rgb.permute(0, 3, 1, 2)
        latents: Float[Tensor, "B 4 64 64"]
        if rgb_as_latents:
            latents = (
                F.interpolate(rgb_BCHW, (32, 32), mode="bilinear", align_corners=False)
                * 2
                - 1
            )
        else:
            rgb_BCHW_512 = F.interpolate(
                rgb_BCHW, (256, 256), mode="bilinear", align_corners=False
            )
            # encode image into latents with vae
            latents = self.encode_images(rgb_BCHW_512)

        cond, aux = self.get_cond(camera)

        if self.cfg.use_anisotropic_schedule:
            azimuth_deg = camera["azimuth"] % 360
            degrees_from_canonical = torch.min(azimuth_deg, 360.0 - azimuth_deg)
            anisotropic_frac = degrees_from_canonical / 180.0

            # at 180 degrees from the canonical view, the offset is maximized
            anisotropic_steps = torch.clamp(
                self.global_step - self.cfg.anisotropic_offset * anisotropic_frac,
                min=0,
                max=None,
            ).to(torch.int32)

            ts = []
            for anisotropic_step in anisotropic_steps.tolist():
                min_step_percent = C(self.cfg.min_step_percent, 0, anisotropic_step)
                max_step_percent = C(self.cfg.max_step_percent, 0, anisotropic_step)

                min_step = int(self.num_train_timesteps * min_step_percent)
                max_step = int(self.num_train_timesteps * max_step_percent)
                t = torch.randint(
                    min_step,
                    max_step + 1,
                    [1],
                    dtype=torch.long,
                    device=self.device,
                )
                ts.append(t)
            t = torch.cat(ts)

            # print(azimuth_deg)
            # print(anisotropic_steps)
            # print()
            # import pdb
            # pdb.set_trace()

        else:
            min_step = self.min_step
            max_step = self.max_step

            # timestep ~ U(0.02, 0.98) to avoid very high/low noise level
            t = torch.randint(
                min_step,
                max_step + 1,
                [batch_size],
                dtype=torch.long,
                device=self.device,
            )

        # predict the noise residual with unet, NO grad!
        with torch.no_grad():
            # add noise
            noise = torch.randn_like(latents)  # TODO: use torch generator
            latents_noisy = self.scheduler.add_noise(latents, noise, t)
            # pred noise
            x_in = torch.cat([latents_noisy] * 2)
            t_in = torch.cat([t] * 2)
            noise_pred = self.model.apply_model(x_in, t_in, cond)

        # perform guidance
        noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)

        # import pdb
        # pdb.set_trace()

        if aux['nearest_idxs'] is not None:
            is_main_camera = (torch.stack(aux['nearest_idxs']) == 0)[:, None, None, None]
            is_aux_camera = ~is_main_camera 

            v_guidance_scale = self.cfg.guidance_scale * is_main_camera + self.cfg.guidance_scale_aux * is_aux_camera
        else:
            v_guidance_scale = self.cfg.guidance_scale
            is_main_camera = True
        # print(self.cfg.guidance_scale, self.cfg.guidance_scale_aux)

        noise_pred = noise_pred_uncond + v_guidance_scale * (
            noise_pred_cond - noise_pred_uncond
        )

        w = (1 - self.alphas[t]).reshape(-1, 1, 1, 1)
        grad = w * (noise_pred - noise)
        grad = torch.nan_to_num(grad)
        # clip grad for stable training?
        if self.grad_clip_val is not None:
            grad = grad.clamp(-self.grad_clip_val, self.grad_clip_val)

        # loss = SpecifyGradient.apply(latents, grad)
        # SpecifyGradient is not straghtforward, use a reparameterization trick instead
        target = (latents - grad).detach()
        # d(loss)/d(latents) = latents - target = latents - (latents - grad) = grad
        loss_sds = 0.5 * F.mse_loss(latents, target, reduction="sum") / batch_size

        if aux['c_crossattn_nearest'] is not None:
            rgb_for_clip = rgb.permute((0, 3, 1,2))  # NHWC -> NCHW
            rgb_for_clip = rgb_for_clip *2 - 1
            rgb_for_clip = F.interpolate(rgb_for_clip, (256, 256), mode='bilinear')

            clip_emb = self.model.get_learned_conditioning(rgb_for_clip.to(self.weights_dtype))

            loss_clip = F.mse_loss(aux['c_crossattn_nearest'], clip_emb, reduction='sum') / batch_size
            # import pdb
            # pdb.set_trace()
        else:
            loss_clip = None

        guidance_out = {
            "loss_sds": loss_sds,
            "loss_clip": loss_clip,
            "grad_norm": grad.norm(),
            "min_step": min_step,
            "max_step": max_step,
            "is_main_camera": is_main_camera,
        }



        # import pdb
        # pdb.set_trace()

        if guidance_eval:
            guidance_eval_utils = {
                "cond": cond,
                "t_orig": t,
                "latents_noisy": latents_noisy,
                "noise_pred": noise_pred,
            }
            guidance_eval_out = self.guidance_eval(**guidance_eval_utils)
            texts = []
            for n, e, a, c in zip(
                guidance_eval_out["noise_levels"], elevation, azimuth, camera_distances
            ):
                texts.append(
                    f"n{n:.02f}\ne{e.item():.01f}\na{a.item():.01f}\nc{c.item():.02f}"
                )
            guidance_eval_out.update({"texts": texts})
            guidance_out.update({"eval": guidance_eval_out})

        return guidance_out