Browse Source

feat: a baby is born?

master
CALVO GONZALEZ Ramon 9 months ago
parent
commit
c994967729
  1. 2
      pyproject.toml
  2. 142
      src/ddpm/generate_circle_dataset.py
  3. 231
      src/ddpm/sample.py
  4. 87
      src/ddpm/train.py
  5. 108
      uv.lock

2
pyproject.toml

@ -11,5 +11,7 @@ dependencies = [
"tqdm>=4.66.5",
"numpy>=2.1.2",
"pyqt6>=6.7.1",
"scipy>=1.15.2",
"seaborn>=0.13.2",
]

142
src/ddpm/generate_circle_dataset.py

@ -1,14 +1,51 @@
import numpy as np
from scipy.ndimage import label, center_of_mass
from PIL import Image, ImageDraw
from tqdm import tqdm
import os
from concurrent.futures import ProcessPoolExecutor
from itertools import repeat
RED = np.array((0xCC, 0x24, 0x1D))
GREEN = np.array((0x98, 0x97, 0x1A))
BLUE = np.array((0x45, 0x85, 0x88))
BACKGROUND = np.array((0x50, 0x49, 0x45))
import matplotlib.pyplot as plt
RED = (0xCC, 0x24, 0x1D)
GREEN = (0x98, 0x97, 0x1A)
BLUE = (0x45, 0x85, 0x88)
BACKGROUND = (0x50, 0x49, 0x45)
def create_sample_antialiased(
id: int, image_size: int, distance: int, radius: int, delta: int, scale=4
):
# Scale up the image dimensions
high_res_size = image_size * scale
high_res_radius = radius * scale
# Create a blank high-res image
im = Image.new("RGB", (high_res_size, high_res_size), BACKGROUND)
draw = ImageDraw.Draw(im)
# Random centers for the two circles at high resolution
dist = float("inf")
while (dist < (distance - delta) * scale) or (dist > (distance + delta) * scale):
x0, y0 = np.random.randint(
low=high_res_radius, high=high_res_size - high_res_radius, size=2
)
x1, y1 = np.random.randint(
low=high_res_radius, high=high_res_size - high_res_radius, size=2
)
dist = np.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2)
# Draw anti-aliased circles using PIL's ellipse method
draw.circle((x0, y0), high_res_radius, fill=GREEN)
draw.circle((x1, y1), high_res_radius, fill=BLUE)
# Downsample the image back to the target resolution with anti-aliasing
im = im.resize((image_size, image_size), resample=Image.LANCZOS)
return id, np.array(im)
def create_sample(id: int, image_size: int, distance: int, radius: int, delta: int):
@ -30,26 +67,52 @@ def create_sample(id: int, image_size: int, distance: int, radius: int, delta: i
dist = np.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2)
# Draw the circles
xx, yy = np.mgrid[:image_size, :image_size]
circle0 = (xx - x0) ** 2 + (yy - y0) ** 2
circle1 = (xx - x1) ** 2 + (yy - y1) ** 2
img = (
img
+ circle0[:, :, None] * GREEN[None, None, :]
+ circle1[:, :, None] * BLUE[None, None, :]
)
# Create boolean masks for the circles based on the radius
mask0 = (xx - x0) ** 2 + (yy - y0) ** 2 <= radius**2
mask1 = (xx - x1) ** 2 + (yy - y1) ** 2 <= radius**2
# Apply the colors to the pixels where the mask is True
img[mask0] = GREEN
img[mask1] = BLUE
return id, img
def generate_circle_dataset(
num_samples=1_000_000,
image_size=64,
radius=5,
distance=20,
delta=5,
):
def detect_circle_centers(image, background=BACKGROUND, threshold=30):
"""
Detects centers of circles in an image by finding connected components
that differ from the background color.
Args:
image (np.ndarray): The image array with shape (H, W, 3).
background (np.ndarray): The background color to ignore.
threshold (int): The minimum per-channel difference to consider a pixel
as part of a circle.
Returns:
centers (list of tuples): List of (row, col) coordinates for each detected circle.
"""
# Compute the absolute difference from the background for each pixel.
diff = np.abs(image.astype(np.int16) - np.array(background).astype(np.int16))
# Create a mask where any channel difference exceeds the threshold.
mask = np.any(diff > threshold, axis=-1)
# Label connected regions in the mask.
labeled, num_features = label(mask)
centers = []
# Compute the center of mass for each labeled region.
for i in range(1, num_features + 1):
center = center_of_mass(mask, labeled, i)
centers.append(center)
return centers
def generate_circle_dataset(num_samples, image_size, radius, distance, delta):
"""
Generate a dataset of images with two circles (red and blue) and save as numpy tensors.
@ -63,7 +126,7 @@ def generate_circle_dataset(
with ProcessPoolExecutor(max_workers=32) as executor:
for i, sample in executor.map(
create_sample,
create_sample_antialiased,
range(num_samples),
repeat(image_size),
repeat(distance),
@ -74,17 +137,52 @@ def generate_circle_dataset(
yield i, sample
def visualize_samples(dataset: np.array, output_dir: str):
# Define the grid size (e.g., 5x5)
grid_size = 5
fig, axes = plt.subplots(grid_size, grid_size, figsize=(10, 10))
for i in range(grid_size):
for j in range(grid_size):
idx = i * grid_size + j
if idx < len(dataset):
img = dataset[idx]
axes[i, j].imshow(img)
centers = detect_circle_centers(img)
# Plot each detected center in red. Note that center_of_mass returns (row, col)
for center in centers:
axes[i, j].scatter(center[1], center[0], c="red", s=20)
axes[i, j].axis("off")
plt.tight_layout()
plt.savefig(os.path.join(output_dir, "sample_grid.png"))
plt.close()
if __name__ == "__main__":
# Create output directory if it doesn't exist
total_samples = 1_000_000
image_size = 64
image_size = 32
distance = 11
delta = 4
radius = 3
output_dir = "data/circle_dataset"
os.makedirs(output_dir, exist_ok=True)
dataset = np.empty((total_samples, image_size, image_size, 3), dtype=np.uint8)
iterator = generate_circle_dataset(num_samples=total_samples)
iterator = generate_circle_dataset(
image_size=image_size,
num_samples=total_samples,
distance=distance,
delta=delta,
radius=radius,
)
for i, sample in tqdm(iterator, total=total_samples):
dataset[i] = sample
np.save(os.path.join(output_dir, "data_map.npy"), dataset)
visualize_samples(dataset, output_dir)
# Save the dataset
np.save(os.path.join(output_dir, "data32.npy"), dataset)
# np.savez_compressed(os.path.join(output_dir, "data.npy.npz"), dataset)

231
src/ddpm/sample.py

@ -1,16 +1,26 @@
from typing import Dict
from concurrent.futures import ProcessPoolExecutor
import torch
from tqdm import tqdm
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from train import extract, get_diffusion_params
from train import TIMESTEPS, IMAGE_SIZE, CHANNELS, DDIM_TIMESTEPS, NORM_MEAN, NORM_STD
from train import TIMESTEPS, IMAGE_SIZE, DDIM_TIMESTEPS
from model import UNet
from generate_circle_dataset import detect_circle_centers
matplotlib.use("Agg")
torch.manual_seed(1)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
GENERATE_SYNTHETIC_DATA = False
@torch.compile
@torch.no_grad()
def ddpm_sample(
model: torch.nn.Module,
@ -83,11 +93,11 @@ def ddpm_sample_images(
"""Generate new images using the trained model"""
x = torch.randn(batch_size, channels, image_size, image_size).to(device)
for t in tqdm(reversed(range(TIMESTEPS)), desc="DDPM Sampling", total=TIMESTEPS):
for t in tqdm(
reversed(range(TIMESTEPS)), desc="DDPM Sampling", total=TIMESTEPS, leave=False
):
t_batch = torch.full((batch_size,), t, device=device, dtype=torch.long)
x = ddpm_sample(model, x, t_batch, params)
if t % 100 == 0:
show_images(x)
if x.isnan().any():
raise ValueError(f"NaN detected in image at timestep {t}")
@ -121,53 +131,230 @@ def ddim_sample_images(
for i in tqdm(range(len(timesteps) - 1), desc="DDIM Sampling"):
t = torch.full((batch_size,), timesteps[i], device=device, dtype=torch.long)
x_before = x.clone()
x = ddim_sample(model, x, t, params)
if x.isnan().any():
raise ValueError(f"NaN detected at timestep {timesteps[i]}")
if i % 10 == 0:
show_images(x)
return x
def show_images(images: torch.Tensor, title=""):
"""Display a batch of images in a grid"""
mean = torch.tensor(NORM_MEAN).to(device).view(1, 3, 1, 1).expand_as(images)
std = torch.tensor(NORM_STD).to(device).view(1, 3, 1, 1).expand_as(images)
images = (images * std + mean).clip(0, 1)
images = images.detach().cpu().numpy()
for idx in range(min(16, len(images))):
plt.subplot(4, 4, idx + 1)
plt.imshow(np.transpose(images[idx], (1, 2, 0)))
plt.imshow(images[idx])
# plt.imshow(np.transpose(images[idx], (1, 2, 0)))
plt.axis("off")
plt.suptitle(title)
plt.draw()
plt.pause(0.001)
plt.savefig("media/circles-predicted.png")
plt.close()
def compute_statistics(data: np.array, generated: np.array):
data_centers = []
num_bad_samples = 0
# for centers in map(detect_circle_centers, data):
# if len(centers) == 2:
# data_centers.append(np.array(centers))
# else:
# num_bad_samples += 1
with ProcessPoolExecutor(max_workers=8) as executor:
for centers in executor.map(detect_circle_centers, data, chunksize=8):
if len(centers) == 2:
data_centers.append(np.array(centers))
else:
num_bad_samples += 1
if num_bad_samples > 0:
print("num bad samples in data: ", num_bad_samples)
data_centers = np.stack(data_centers, axis=0) # (num_samples, 2, 2)
num_bad_samples = 0
generated_centers = []
with ProcessPoolExecutor(max_workers=16) as executor:
for centers in executor.map(detect_circle_centers, generated, chunksize=8):
if len(centers) == 2:
generated_centers.append(np.array(centers))
else:
num_bad_samples += 1
if num_bad_samples > 0:
print("num bad samples in generated: ", num_bad_samples)
generated_centers = np.stack(generated_centers, axis=0) # (num_samples, 2, 2)
# Calculate distances from the center of the image
# image_center = IMAGE_SIZE / 2
# data_distances = np.sqrt(
# (data_centers[:, 0] - image_center) ** 2
# + (data_centers[:, 1] - image_center) ** 2
# )
# generated_distances = np.sqrt(
# (generated_centers[:, 0] - image_center) ** 2
# + (generated_centers[:, 1] - image_center) ** 2
# )
# Create a figure with subplots
plt.figure(figsize=(15, 10))
# Plot histogram of x positions
plt.subplot(2, 2, 1)
sns.histplot(
data_centers[:, :, 0].reshape(-1),
color="blue",
label="Data",
kde=True,
stat="density",
)
sns.histplot(
generated_centers[:, :, 0].reshape(-1),
color="orange",
label="Generated",
kde=True,
stat="density",
)
plt.title("X Position Distribution")
plt.xlabel("X Position")
plt.legend()
# Plot histogram of y positions
plt.subplot(2, 2, 2)
sns.histplot(
data_centers[:, :, 1].reshape(-1),
color="blue",
label="Data",
kde=True,
stat="density",
)
sns.histplot(
generated_centers[:, :, 1].reshape(-1),
color="orange",
label="Generated",
kde=True,
stat="density",
)
plt.title("Y Position Distribution")
plt.xlabel("Y Position")
plt.legend()
# Plot histogram of distances
plt.subplot(2, 2, 3)
distances = np.sqrt(
np.square(data_centers[:, ::2, 0] - data_centers[:, 1::2, 0])
+ np.square(data_centers[:, ::2, 1] - data_centers[:, 1::2, 1])
).squeeze()
generated_distances = np.sqrt(
np.square(generated_centers[:, ::2, 0] - generated_centers[:, 1::2, 0])
+ np.square(generated_centers[:, ::2, 1] - generated_centers[:, 1::2, 1])
).squeeze()
sns.histplot(distances, color="blue", label="Data", kde=True, stat="density")
sns.histplot(
generated_distances, color="orange", label="Generated", kde=True, stat="density"
)
plt.title("Distance between circles distribution")
plt.xlabel("Distance")
plt.legend()
# Plot 2D heatmap of center positions
plt.subplot(2, 2, 4)
sns.kdeplot(
x=data_centers[:, :, 0].reshape(-1),
y=data_centers[:, :, 1].reshape(-1),
cmap="Blues",
label="Data",
)
sns.kdeplot(
x=generated_centers[:, :, 0].reshape(-1),
y=generated_centers[:, :, 1].reshape(-1),
cmap="Oranges",
label="Generated",
)
plt.title("2D Heatmap of Center Positions")
plt.xlabel("X Position")
plt.ylabel("Y Position")
plt.xlim(0, IMAGE_SIZE)
plt.ylim(0, IMAGE_SIZE)
plt.legend()
plt.tight_layout()
plt.savefig("media/center_statistics.png")
print("Saved histograms at media/center_statistics.png")
plt.close()
def load_dataset(data_path: str):
print("Loading dataset...", end="", flush=True)
data = np.load(data_path)
print("Done.")
return data
def plot_bad_centers(generated: np.array):
generated_centers = []
num_bad_samples = 0
with ProcessPoolExecutor(max_workers=16) as executor:
for centers in executor.map(detect_circle_centers, generated, chunksize=8):
if len(centers) == 2:
generated_centers.append(np.array(centers))
else:
num_bad_samples += 1
generated_centers.append(np.zeros((2, 2)))
if num_bad_samples > 0:
print("num bad samples in generated: ", num_bad_samples)
generated_centers = np.stack(generated_centers, axis=0) # (num_samples, 2, 2)
generated_distances = np.sqrt(
np.square(generated_centers[:, ::2, 0] - generated_centers[:, 1::2, 0])
+ np.square(generated_centers[:, ::2, 1] - generated_centers[:, 1::2, 1])
).squeeze()
mask = generated_distances > 18.0
generated = generated[mask]
show_images(generated)
if __name__ == "__main__":
torch.set_float32_matmul_precision("high")
plt.figure(figsize=(10, 10))
data = load_dataset("./data/circle_dataset/data32.npy")
if GENERATE_SYNTHETIC_DATA:
nb_synthetic_samples = 10_000
params = get_diffusion_params(TIMESTEPS, device, eta=0.0)
model = UNet(32, TIMESTEPS).to(device)
model.load_state_dict(torch.load("model.pkl", weights_only=True))
model.eval()
generated_images = (
ddim_sample_images( # change to ddim_sample_images here to enable DDIM
generated = np.empty_like(data)
chunk = 500
samples = min(nb_synthetic_samples, data.shape[0])
for i in tqdm(range(samples // chunk), desc="Generating synthetic data."):
generated_images = ddpm_sample_images(
model=model,
image_size=IMAGE_SIZE,
batch_size=16,
channels=CHANNELS,
batch_size=chunk,
channels=3,
device=device,
params=params,
)
generated_images = torch.permute(generated_images, (0, 2, 3, 1))
generated[i * chunk : (i + 1) * chunk] = (
(generated_images * 255.0)
.clip(min=0.0, max=255.0)
.cpu()
.numpy()
.astype(np.uint8)
)
show_images(generated_images, title="Generated Images")
np.save("./data/circle_dataset/generated32.npy", generated)
else:
generated = np.load("./data/circle_dataset/generated32.npy")
# Keep the plot open after generation is finished
plt.show()
compute_statistics(data, generated)
plot_bad_centers(generated)

87
src/ddpm/train.py

@ -1,46 +1,40 @@
from typing import Dict, Callable
from itertools import islice
import numpy as np
import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from tqdm import tqdm
from model import UNet
# Set random seed for reproducibility
torch.manual_seed(42)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Hyperparameters
NUM_EPOCHS = 256
BATCH_SIZE = 128
NUM_EPOCHS = 1
BATCH_SIZE = 512
IMAGE_SIZE = 32
CHANNELS = 3
TIMESTEPS = 1000
DDIM_TIMESTEPS = 100
DDIM_TIMESTEPS = 500
NORM_MEAN = (0.4914, 0.4822, 0.4465)
NORM_STD = (0.2470, 0.2435, 0.2616)
def load_dataset(data_path: str):
print("Loading dataset... ", end="", flush=True)
data = np.load(data_path).astype(np.float32)
data = data / 255.0 # normalize between [0-1]
data = np.permute_dims(data, (0, 3, 1, 2)) # (b, h, w, c) -> (b, c, h, w)
print("Done.")
return data
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
def create_dataset_loader(data: np.array):
nb_batches = data.shape[0] // BATCH_SIZE
ids = np.arange(data.shape[0])
transform = transforms.Compose(
[
transforms.Resize(IMAGE_SIZE),
transforms.RandomHorizontalFlip(0.5),
transforms.ToTensor(),
transforms.Normalize(NORM_MEAN, NORM_STD),
]
)
for i in range(nb_batches):
batch_ids = ids[i * BATCH_SIZE : (i + 1) * BATCH_SIZE]
yield data[batch_ids]
train_dataset = datasets.CIFAR10(
root="./data", train=True, download=True, transform=transform
)
train_loader = DataLoader(
train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=8, pin_memory=True
)
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
def get_diffusion_params(
@ -91,7 +85,28 @@ def extract(a: torch.Tensor, t: torch.Tensor, x_shape: torch.Tensor.shape):
return out.reshape(batch_size, *((1,) * (len(x_shape) - 1))).to(t.device)
def get_lr(
it: int,
warmup_iters: int = 80,
lr_decay_iters: int = 900,
min_lr: float = 3e-5,
learning_rate: float = 1e-4,
):
# 1) linear warmup for warmup_iters steps
if it < warmup_iters:
return learning_rate * (it + 1) / (warmup_iters + 1)
# 2) if it > lr_decay_iters, return min learning rate
if it > lr_decay_iters:
return min_lr
# 3) in between, use cosine decay down to min learning rate
decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
assert 0 <= decay_ratio <= 1
coeff = 0.5 * (1.0 + np.cos(np.pi * decay_ratio)) # coeff ranges 0..1
return min_lr + coeff * (learning_rate - min_lr)
def get_loss_fn(model: torch.nn.Module, params: Dict[str, torch.Tensor]) -> Callable:
@torch.compile
def loss_fn(x_0):
batch_size = x_0.shape[0]
t = torch.randint(0, TIMESTEPS, (batch_size,), device=device)
@ -116,9 +131,13 @@ def train_epoch(
model.train()
total_loss = 0
with tqdm(train_loader, leave=False) as pbar:
with tqdm(islice(train_loader, 200), leave=False) as pbar:
steps = 0
for batch in pbar:
images = batch[0].to(device)
# lr = get_lr(steps)
# for param_group in optimizer.param_groups:
# param_group["lr"] = lr
images = torch.tensor(batch, device=device)
optimizer.zero_grad()
loss = loss_fn(images)
@ -126,12 +145,19 @@ def train_epoch(
optimizer.step()
total_loss += loss.item()
steps += 1
pbar.set_description(f"Loss: {loss.item():.4f}")
return total_loss / len(train_loader)
return total_loss / steps
if __name__ == "__main__":
torch.set_float32_matmul_precision("high")
# Set random seed for reproducibility
torch.manual_seed(42)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = UNet(32, TIMESTEPS).to(device)
nb_params = count_parameters(model)
print(f"Total number of parameters: {nb_params}")
@ -140,8 +166,11 @@ if __name__ == "__main__":
params = get_diffusion_params(TIMESTEPS, device)
loss_fn = get_loss_fn(model, params)
data = load_dataset("./data/circle_dataset/data32.npy")
# Main training loop
for e in tqdm(range(NUM_EPOCHS)):
train_loader = create_dataset_loader(data)
train_epoch(model, optimizer, train_loader, loss_fn)
# Save model after training

108
uv.lock

@ -68,6 +68,8 @@ dependencies = [
{ name = "matplotlib" },
{ name = "numpy" },
{ name = "pyqt6" },
{ name = "scipy" },
{ name = "seaborn" },
{ name = "torch" },
{ name = "torchvision" },
{ name = "tqdm" },
@ -78,6 +80,8 @@ requires-dist = [
{ name = "matplotlib", specifier = ">=3.9.2" },
{ name = "numpy", specifier = ">=2.1.2" },
{ name = "pyqt6", specifier = ">=6.7.1" },
{ name = "scipy", specifier = ">=1.15.2" },
{ name = "seaborn", specifier = ">=0.13.2" },
{ name = "torch", specifier = ">=2.5.0" },
{ name = "torchvision", specifier = ">=0.20.0" },
{ name = "tqdm", specifier = ">=4.66.5" },
@ -447,6 +451,40 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "pandas"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 },
{ url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 },
{ url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 },
{ url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 },
{ url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 },
{ url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 },
{ url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 },
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 },
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 },
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 },
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 },
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 },
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 },
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 },
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 },
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 },
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 },
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 },
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 },
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
]
[[package]]
name = "pillow"
version = "11.1.0"
@ -554,6 +592,67 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
]
[[package]]
name = "pytz"
version = "2025.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
]
[[package]]
name = "scipy"
version = "1.15.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/5d/3c78815cbab499610f26b5bae6aed33e227225a9fa5290008a733a64f6fc/scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd", size = 38756184 },
{ url = "https://files.pythonhosted.org/packages/37/20/3d04eb066b471b6e171827548b9ddb3c21c6bbea72a4d84fc5989933910b/scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301", size = 30163558 },
{ url = "https://files.pythonhosted.org/packages/a4/98/e5c964526c929ef1f795d4c343b2ff98634ad2051bd2bbadfef9e772e413/scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93", size = 22437211 },
{ url = "https://files.pythonhosted.org/packages/1d/cd/1dc7371e29195ecbf5222f9afeedb210e0a75057d8afbd942aa6cf8c8eca/scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20", size = 25232260 },
{ url = "https://files.pythonhosted.org/packages/f0/24/1a181a9e5050090e0b5138c5f496fee33293c342b788d02586bc410c6477/scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e", size = 35198095 },
{ url = "https://files.pythonhosted.org/packages/c0/53/eaada1a414c026673eb983f8b4a55fe5eb172725d33d62c1b21f63ff6ca4/scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8", size = 37297371 },
{ url = "https://files.pythonhosted.org/packages/e9/06/0449b744892ed22b7e7b9a1994a866e64895363572677a316a9042af1fe5/scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11", size = 36872390 },
{ url = "https://files.pythonhosted.org/packages/6a/6f/a8ac3cfd9505ec695c1bc35edc034d13afbd2fc1882a7c6b473e280397bb/scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53", size = 39700276 },
{ url = "https://files.pythonhosted.org/packages/f5/6f/e6e5aff77ea2a48dd96808bb51d7450875af154ee7cbe72188afb0b37929/scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded", size = 40942317 },
{ url = "https://files.pythonhosted.org/packages/53/40/09319f6e0f276ea2754196185f95cd191cb852288440ce035d5c3a931ea2/scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf", size = 38717587 },
{ url = "https://files.pythonhosted.org/packages/fe/c3/2854f40ecd19585d65afaef601e5e1f8dbf6758b2f95b5ea93d38655a2c6/scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37", size = 30100266 },
{ url = "https://files.pythonhosted.org/packages/dd/b1/f9fe6e3c828cb5930b5fe74cb479de5f3d66d682fa8adb77249acaf545b8/scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d", size = 22373768 },
{ url = "https://files.pythonhosted.org/packages/15/9d/a60db8c795700414c3f681908a2b911e031e024d93214f2d23c6dae174ab/scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb", size = 25154719 },
{ url = "https://files.pythonhosted.org/packages/37/3b/9bda92a85cd93f19f9ed90ade84aa1e51657e29988317fabdd44544f1dd4/scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27", size = 35163195 },
{ url = "https://files.pythonhosted.org/packages/03/5a/fc34bf1aa14dc7c0e701691fa8685f3faec80e57d816615e3625f28feb43/scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0", size = 37255404 },
{ url = "https://files.pythonhosted.org/packages/4a/71/472eac45440cee134c8a180dbe4c01b3ec247e0338b7c759e6cd71f199a7/scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32", size = 36860011 },
{ url = "https://files.pythonhosted.org/packages/01/b3/21f890f4f42daf20e4d3aaa18182dddb9192771cd47445aaae2e318f6738/scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d", size = 39657406 },
{ url = "https://files.pythonhosted.org/packages/0d/76/77cf2ac1f2a9cc00c073d49e1e16244e389dd88e2490c91d84e1e3e4d126/scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f", size = 40961243 },
{ url = "https://files.pythonhosted.org/packages/4c/4b/a57f8ddcf48e129e6054fa9899a2a86d1fc6b07a0e15c7eebff7ca94533f/scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9", size = 38870286 },
{ url = "https://files.pythonhosted.org/packages/0c/43/c304d69a56c91ad5f188c0714f6a97b9c1fed93128c691148621274a3a68/scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f", size = 30141634 },
{ url = "https://files.pythonhosted.org/packages/44/1a/6c21b45d2548eb73be9b9bff421aaaa7e85e22c1f9b3bc44b23485dfce0a/scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6", size = 22415179 },
{ url = "https://files.pythonhosted.org/packages/74/4b/aefac4bba80ef815b64f55da06f62f92be5d03b467f2ce3668071799429a/scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af", size = 25126412 },
{ url = "https://files.pythonhosted.org/packages/b1/53/1cbb148e6e8f1660aacd9f0a9dfa2b05e9ff1cb54b4386fe868477972ac2/scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274", size = 34952867 },
{ url = "https://files.pythonhosted.org/packages/2c/23/e0eb7f31a9c13cf2dca083828b97992dd22f8184c6ce4fec5deec0c81fcf/scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776", size = 36890009 },
{ url = "https://files.pythonhosted.org/packages/03/f3/e699e19cabe96bbac5189c04aaa970718f0105cff03d458dc5e2b6bd1e8c/scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828", size = 36545159 },
{ url = "https://files.pythonhosted.org/packages/af/f5/ab3838e56fe5cc22383d6fcf2336e48c8fe33e944b9037fbf6cbdf5a11f8/scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28", size = 39136566 },
{ url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 },
]
[[package]]
name = "seaborn"
version = "0.13.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "matplotlib" },
{ name = "numpy" },
{ name = "pandas" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914 },
]
[[package]]
name = "setuptools"
version = "77.0.3"
@ -673,3 +772,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec3
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
]

Loading…
Cancel
Save