Updated data generation code

This commit is contained in:
2025-09-01 14:46:34 -04:00
parent d998f6de4c
commit e018238935
14 changed files with 709 additions and 123 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.venv .venv
.env .env
__pycaches__ __pycache__
datasets/ datasets/
temp.* temp.*
test.* test.*

Binary file not shown.

Binary file not shown.

View File

@@ -9,7 +9,7 @@ class NoiseType(Enum):
class ModelConfig: class ModelConfig:
num_agents: int num_agents: int
embedding_dim: int = 16 embedding_dim: int = 64
input_dim: int = 1 input_dim: int = 1
output_dim: int = 1 output_dim: int = 1
@@ -19,15 +19,12 @@ class TrainConfig:
"""Configuration for the training process.""" """Configuration for the training process."""
learning_rate: float = 1e-3 learning_rate: float = 1e-3
epochs: int = 100 epochs: int = 100
batch_size: int = 32 batch_size: int = 4096 * 4
verbose: bool = False # Set to True to see epoch loss during training verbose: bool = False # See loss during training
log: bool = True log: bool = True # Logs the attention every log interval
log_epoch_interval: int = 10 log_epoch_interval: int = 10 # How often you want to log intervals
noise_type: NoiseType = NoiseType.NONE noise_type: NoiseType = NoiseType.NONE # What kind of noise you want
noise_level: float = 0.01 noise_level: float = 0.00 # Stddev for Normal, half-width for Uniform
# --- New parameters for this script ---
data_directory:str data_directory:str
# Threshold for converting attention scores to a binary graph f1_threshold: float = -0.5 # Threshold for binary classification
f1_threshold: float = -0.5

View File

@@ -8,7 +8,9 @@ import networkx as nx
import networkx.algorithms.community as nx_comm import networkx.algorithms.community as nx_comm
from tqdm import tqdm from tqdm import tqdm
import sims import sims
from dotenv import load_dotenv
load_dotenv()
def generate_connected_graph(rng: np.random.Generator, num_agents: int, graph_type: str) -> tuple[nx.Graph, str, np.ndarray]: def generate_connected_graph(rng: np.random.Generator, num_agents: int, graph_type: str) -> tuple[nx.Graph, str, np.ndarray]:
@@ -22,34 +24,33 @@ def generate_connected_graph(rng: np.random.Generator, num_agents: int, graph_ty
G = None G = None
if graph_type == "erdos_renyi": if graph_type == "erdos_renyi":
# p = 2.0 * np.log(num_agents) / num_agents
p = 0.5 p = 0.5
G = nx.erdos_renyi_graph(num_agents, p, seed=rng) G = nx.erdos_renyi_graph(num_agents, p, seed=rng)
elif graph_type == "watts_strogatz": elif graph_type == "watts_strogatz":
k = 2 k = 4
p = 0.1 p = 0.5
G = nx.watts_strogatz_graph(num_agents, k, p, seed=rng) G = nx.watts_strogatz_graph(num_agents, k, p, seed=rng)
elif graph_type == "barabasi_albert": elif graph_type == "barabasi_albert":
m = 2 G = nx.barabasi_albert_graph(num_agents, 2, seed=rng)
if m >= num_agents: m = max(1, num_agents - 1)
G = nx.barabasi_albert_graph(num_agents, m, seed=rng)
elif graph_type == "powerlaw_cluster": elif graph_type == "powerlaw_cluster":
m = 3 # Number of random edges to add for each new node m = 2
p = 0.1 # Probability of adding a triangle after adding a random edge p = 0.5
if m >= num_agents: m = max(1, num_agents - 1)
G = nx.powerlaw_cluster_graph(num_agents, m, p, seed=rng) G = nx.powerlaw_cluster_graph(num_agents, m, p, seed=rng)
# Add self-loops, as they are often assumed in consensus algorithms
G.add_edges_from([(i, i) for i in range(num_agents)])
adj_matrix = nx.to_numpy_array(G, dtype=np.float32)
return G, graph_type, adj_matrix graph_matrix = nx.adjacency_matrix(G).todense()
for i in range(num_agents):
graph_matrix[i,i]=1
G = nx.Graph(graph_matrix)
return G, graph_type, graph_matrix
def calculate_graph_metrics(G: nx.Graph) -> dict: def calculate_graph_metrics(G: nx.Graph) -> dict:
""" """
@@ -91,30 +92,30 @@ def calculate_graph_metrics(G: nx.Graph) -> dict:
# --- Spectral & Community Metrics (Potentially Slow) --- # --- Spectral & Community Metrics (Potentially Slow) ---
# These are also limited to smaller graphs. # These are also limited to smaller graphs.
if 1 < num_nodes < 500: # if 1 < num_nodes < 500:
# Eigenvalues of the Laplacian matrix # # Eigenvalues of the Laplacian matrix
try: # try:
laplacian_eigenvalues = sorted(nx.laplacian_spectrum(G)) # laplacian_eigenvalues = sorted(nx.laplacian_spectrum(G))
metrics["laplacian_eigenvalues"] = laplacian_eigenvalues # metrics["laplacian_eigenvalues"] = laplacian_eigenvalues
# The second-smallest eigenvalue of the Laplacian matrix # # The second-smallest eigenvalue of the Laplacian matrix
metrics["algebraic_connectivity"] = laplacian_eigenvalues[1] # metrics["algebraic_connectivity"] = laplacian_eigenvalues[1]
except Exception: # except Exception:
metrics["laplacian_eigenvalues"] = None # metrics["laplacian_eigenvalues"] = None
metrics["algebraic_connectivity"] = None # metrics["algebraic_connectivity"] = None
# Modularity using the Louvain community detection algorithm # # Modularity using the Louvain community detection algorithm
if num_edges > 0: # if num_edges > 0:
try: # try:
communities = nx_comm.louvain_communities(G, seed=123) # communities = nx_comm.louvain_communities(G, seed=123)
metrics["modularity"] = nx_comm.modularity(G, communities) # metrics["modularity"] = nx_comm.modularity(G, communities)
except Exception: # except Exception:
metrics["modularity"] = None # Algorithm may fail on some graphs # metrics["modularity"] = None # Algorithm may fail on some graphs
else: # else:
metrics["modularity"] = None # metrics["modularity"] = None
else: # else:
metrics["laplacian_eigenvalues"] = None # metrics["laplacian_eigenvalues"] = None
metrics["algebraic_connectivity"] = None # metrics["algebraic_connectivity"] = None
metrics["modularity"] = None # metrics["modularity"] = None
return metrics return metrics
@@ -132,9 +133,9 @@ def main():
# --- Configuration --- # --- Configuration ---
GRAPH_GEN_ALGOS = ["erdos_renyi", "barabasi_albert", "powerlaw_cluster", "watts_strogatz"] GRAPH_GEN_ALGOS = ["erdos_renyi", "barabasi_albert", "powerlaw_cluster", "watts_strogatz"]
AGENT_COUNTS = range(5, 51, 5)# 5, 10, ..., 50 agents AGENT_COUNTS = range(5, 51, 5)# 5, 10, ..., 50 agents
GRAPHS_PER_AGENT_COUNT = 100 GRAPHS_PER_AGENT_COUNT = 80
GRAPHS_PER_GRAPH_ALGO = GRAPHS_PER_AGENT_COUNT // len(GRAPH_GEN_ALGOS) GRAPHS_PER_GRAPH_ALGO = GRAPHS_PER_AGENT_COUNT // len(GRAPH_GEN_ALGOS)
SIMS_PER_GRAPH = 100 SIMS_PER_GRAPH = 400
OUTPUT_DIR = "datasets/consensus_dataset" OUTPUT_DIR = "datasets/consensus_dataset"
# --- Setup --- # --- Setup ---

View File

@@ -82,9 +82,9 @@ def main():
GRAPH_GEN_ALGOS = ["erdos_renyi", "barabasi_albert", "powerlaw_cluster", "watts_strogatz"] GRAPH_GEN_ALGOS = ["erdos_renyi", "barabasi_albert", "powerlaw_cluster", "watts_strogatz"]
AGENT_COUNTS = range(5, 51, 5) # 5, 10, ..., 50 agents AGENT_COUNTS = range(5, 51, 5) # 5, 10, ..., 50 agents
# AGENT_COUNTS = [50] # AGENT_COUNTS = [50]
GRAPHS_PER_AGENT_COUNT = 100 GRAPHS_PER_AGENT_COUNT = 80
GRAPHS_PER_GRAPH_ALGO = GRAPHS_PER_AGENT_COUNT // len(GRAPH_GEN_ALGOS) GRAPHS_PER_GRAPH_ALGO = GRAPHS_PER_AGENT_COUNT // len(GRAPH_GEN_ALGOS)
SIMS_PER_GRAPH = 100 SIMS_PER_GRAPH = 400
OUTPUT_DIR = "datasets/kuramoto_dataset" OUTPUT_DIR = "datasets/kuramoto_dataset"
# --- Setup --- # --- Setup ---

View File

@@ -46,7 +46,7 @@ def init_fn(key: jax.Array, config: ModelConfig):
def forward(params: dict, input_timesteps: jax.Array, config: ModelConfig): def forward(params: dict, input_timesteps: jax.Array, config: ModelConfig):
""" """
Model's forward function. Takes in the parameters and inptu timesteps, returns predictions Model's forward function. Takes in the parameters and input timesteps, returns predictions
""" """
batch_size, num_agents, _ = input_timesteps.shape batch_size, num_agents, _ = input_timesteps.shape
@@ -119,6 +119,7 @@ def train_model(config: ModelConfig, inputs: jax.Array, targets: jax.Array,
params, opt_state, loss_val = update_step(params, opt_state, x, y, config) params, opt_state, loss_val = update_step(params, opt_state, x, y, config)
running_loss += loss_val running_loss += loss_val
epoch_loss = running_loss / num_batches epoch_loss = running_loss / num_batches
loss_history[f"epoch_{epoch}"].append(epoch_loss) loss_history[f"epoch_{epoch}"].append(epoch_loss)

View File

@@ -10,7 +10,7 @@ from train_and_eval import calculate_f1_score
from sklearn.metrics import f1_score from sklearn.metrics import f1_score
if len(sys.argv) < 2: if len(sys.argv) < 2:
data_dir = "datasets/consensus_dataset" data_dir = "datasets/kuramoto_dataset"
else: else:
data_dir = "datasets/" + sys.argv[1] data_dir = "datasets/" + sys.argv[1]
@@ -22,53 +22,59 @@ for folder in tqdm(os.listdir(data_dir)):
folder_path = os.path.join(data_dir, folder) folder_path = os.path.join(data_dir, folder)
# Load model config from summary json for noise_level in os.listdir(os.path.join(folder_path, "results/NoiseType.NONE")):
with open(os.path.join(folder_path, "results/NoiseType.NONE", "summary_results.json"), "r") as f:
summary_results = json.load(f)
for i, graph in enumerate(os.listdir(folder_path)): # Load model config from summary json
with open(os.path.join(folder_path, "results/NoiseType.NONE", noise_level, "summary_results.json"), "r") as f:
summary_results = json.load(f)
# train_summary_results
summ_results = summary_results[i-1]
if graph == "results": # ignore the result folder for i, graph in enumerate(os.listdir(folder_path)):
# train_summary_results
summ_results = summary_results[i-1]
if graph == "results": # ignore the result folder
continue
graph_path = os.path.join(folder_path, graph)
# Load run data
with open(os.path.join(folder_path, graph), "r") as f:
run_data = json.load(f)
true_graph = np.array(run_data["adjacency_matrix"])
learned_graph = np.array(summ_results["raw_attention"])
predicted_graph = (learned_graph > THRESHOLD).astype(int)
true_flat = true_graph.flatten()
pred_flat = predicted_graph.flatten()
calc_f1_score = f1_score(true_flat, pred_flat)
datapoints[num_agents] = datapoints.get(num_agents, [])
datapoints[num_agents].append(calc_f1_score)
for key in datapoints.keys():
try:
datapoints[key] = sum(datapoints[key])/len(datapoints[key])
except:
continue continue
graph_path = os.path.join(folder_path, graph)
# Load run data x = []
with open(os.path.join(folder_path, graph), "r") as f: y = []
run_data = json.load(f)
true_graph = np.array(run_data["adjacency_matrix"]) for item in datapoints.items():
x.append(item[0])
y.append(item[1])
learned_graph = np.array(summ_results["raw_attention"]) plt.plot(x, y)
plt.show()
predicted_graph = (learned_graph > THRESHOLD).astype(int)
true_flat = true_graph.flatten()
pred_flat = predicted_graph.flatten()
calc_f1_score = f1_score(true_flat, pred_flat)
datapoints[num_agents] = datapoints.get(num_agents, [])
datapoints[num_agents].append(calc_f1_score)
for key in datapoints.keys():
datapoints[key] = sum(datapoints[key])/len(datapoints[key])
x = []
y = []
for item in datapoints.items():
x.append(item[0])
y.append(item[1])
plt.plot(x, y)
plt.show()

View File

@@ -12,6 +12,8 @@ dependencies = [
"matplotlib>=3.10.3", "matplotlib>=3.10.3",
"networkx>=3.5", "networkx>=3.5",
"optax>=0.2.5", "optax>=0.2.5",
"pyqt6>=6.9.1",
"python-dotenv>=1.1.1",
"scikit-learn>=1.7.1", "scikit-learn>=1.7.1",
"seaborn>=0.13.2", "seaborn>=0.13.2",
"tqdm>=4.67.1", "tqdm>=4.67.1",

File diff suppressed because one or more lines are too long

View File

@@ -51,7 +51,7 @@ def prepare_data_for_model(key: jax.Array, trajectories: np.ndarray, train_confi
all_targets = all_targets[all_indices] all_targets = all_targets[all_indices]
if train_config.noise_type != NoiseType.NONE and train_config.noise_level > 0: if train_config.noise_type != NoiseType.NONE and train_config.noise_level > 0:
noise_shape = full_dataset_inputs.shape noise_shape = all_inputs.shape
if train_config.noise_type == NoiseType.NORMAL: if train_config.noise_type == NoiseType.NORMAL:
noise = jax.random.normal(key, noise_shape) * train_config.noise_level noise = jax.random.normal(key, noise_shape) * train_config.noise_level
elif train_config.noise_type == NoiseType.UNIFORM: elif train_config.noise_type == NoiseType.UNIFORM:
@@ -60,18 +60,18 @@ def prepare_data_for_model(key: jax.Array, trajectories: np.ndarray, train_confi
minval=-train_config.noise_level, minval=-train_config.noise_level,
maxval=train_config.noise_level maxval=train_config.noise_level
) )
full_dataset_inputs += np.array(noise) # Add noise to inputs all_inputs += np.array(noise) # Add noise to inputs
full_dataset_inputs = np.expand_dims(all_inputs, axis=-1) all_inputs = np.expand_dims(all_inputs, axis=-1)
full_dataset_targets = np.expand_dims(all_targets, axis=-1) full_dataset_targets = np.expand_dims(all_targets, axis=-1)
# Create batches # Create batches
num_samples = full_dataset_inputs.shape[0] num_samples = all_inputs.shape[0]
num_batches = num_samples // batch_size num_batches = num_samples // batch_size
# Truncate to full batches # Truncate to full batches
truncated_inputs = full_dataset_inputs[:num_batches * batch_size] truncated_inputs = all_inputs[:num_batches * batch_size]
truncated_targets = full_dataset_targets[:num_batches * batch_size] truncated_targets = full_dataset_targets[:num_batches * batch_size]
# Reshape into batches # Reshape into batches
@@ -81,6 +81,40 @@ def prepare_data_for_model(key: jax.Array, trajectories: np.ndarray, train_confi
return batched_inputs, batched_targets return batched_inputs, batched_targets
def f1_score_np(y_true: np.ndarray, y_pred: np.ndarray) -> float:
"""
Compute the F1 score between two numpy arrays.
Parameters
----------
y_true : np.ndarray
Ground truth (correct) labels.
y_pred : np.ndarray
Predicted labels.
Returns
-------
float
The F1 score.
"""
# Ensure binary arrays (0 or 1)
y_true = np.asarray(y_true).astype(int)
y_pred = np.asarray(y_pred).astype(int)
# Compute True Positives, False Positives, and False Negatives
tp = np.sum((y_true == 1) & (y_pred == 1))
fp = np.sum((y_true == 0) & (y_pred == 1))
fn = np.sum((y_true == 1) & (y_pred == 0))
# Precision and Recall
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
# F1 Score
if precision + recall == 0:
return 0.0
return 2 * (precision * recall) / (precision + recall)
def calculate_f1_score( def calculate_f1_score(
params: dict, params: dict,
model_config: ModelConfig, model_config: ModelConfig,
@@ -111,7 +145,7 @@ def calculate_f1_score(
true_flat = true_graph.flatten() true_flat = true_graph.flatten()
pred_flat = predicted_graph.flatten() pred_flat = predicted_graph.flatten()
return f1_score(true_flat, pred_flat) return f1_score_np(true_flat, pred_flat)
def main(): def main():
"""Main script to run the training and evaluation pipeline.""" """Main script to run the training and evaluation pipeline."""
@@ -125,7 +159,7 @@ def main():
print(f"Please run the data generation script for '{train_config.data_directory}' first.") print(f"Please run the data generation script for '{train_config.data_directory}' first.")
return return
print(f"🚀 Starting training pipeline for '{train_config.data_directory}' data.") print(f"Starting training pipeline for '{train_config.data_directory}' data.")
# Get sorted list of agent directories # Get sorted list of agent directories
agent_dirs = sorted( agent_dirs = sorted(
@@ -146,7 +180,8 @@ def main():
os.makedirs(results_dir, exist_ok=True) os.makedirs(results_dir, exist_ok=True)
subdir = str(train_config.noise_type) subdir = str(train_config.noise_type)
sub_results_dir = os.path.join(results_dir, subdir) subsubdir = str(train_config.noise_level)
sub_results_dir = os.path.join(results_dir, subdir, subsubdir)
os.makedirs(sub_results_dir, exist_ok=True) os.makedirs(sub_results_dir, exist_ok=True)
print(f"\nProcessing {len(graph_files)} graphs for {agent_dir_name}...") print(f"\nProcessing {len(graph_files)} graphs for {agent_dir_name}...")

61
uv.lock generated
View File

@@ -415,6 +415,8 @@ dependencies = [
{ name = "matplotlib" }, { name = "matplotlib" },
{ name = "networkx" }, { name = "networkx" },
{ name = "optax" }, { name = "optax" },
{ name = "pyqt6" },
{ name = "python-dotenv" },
{ name = "scikit-learn" }, { name = "scikit-learn" },
{ name = "seaborn" }, { name = "seaborn" },
{ name = "tqdm" }, { name = "tqdm" },
@@ -429,6 +431,8 @@ requires-dist = [
{ name = "matplotlib", specifier = ">=3.10.3" }, { name = "matplotlib", specifier = ">=3.10.3" },
{ name = "networkx", specifier = ">=3.5" }, { name = "networkx", specifier = ">=3.5" },
{ name = "optax", specifier = ">=0.2.5" }, { name = "optax", specifier = ">=0.2.5" },
{ name = "pyqt6", specifier = ">=6.9.1" },
{ name = "python-dotenv", specifier = ">=1.1.1" },
{ name = "scikit-learn", specifier = ">=1.7.1" }, { name = "scikit-learn", specifier = ">=1.7.1" },
{ name = "seaborn", specifier = ">=0.13.2" }, { name = "seaborn", specifier = ">=0.13.2" },
{ name = "tqdm", specifier = ">=4.67.1" }, { name = "tqdm", specifier = ">=4.67.1" },
@@ -1678,6 +1682,54 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
] ]
[[package]]
name = "pyqt6"
version = "6.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyqt6-qt6" },
{ name = "pyqt6-sip" },
]
sdist = { url = "https://files.pythonhosted.org/packages/32/1b/567f46eb43ca961efd38d7a0b73efb70d7342854f075fd919179fdb2a571/pyqt6-6.9.1.tar.gz", hash = "sha256:50642be03fb40f1c2111a09a1f5a0f79813e039c15e78267e6faaf8a96c1c3a6", size = 1067230, upload-time = "2025-06-06T08:49:30.307Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/c4/fc2a69cf3df09b213185ef5a677c3940cd20e7855d29e40061a685b9c6ee/pyqt6-6.9.1-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:33c23d28f6608747ecc8bfd04c8795f61631af9db4fb1e6c2a7523ec4cc916d9", size = 59770566, upload-time = "2025-06-06T08:48:20.331Z" },
{ url = "https://files.pythonhosted.org/packages/d5/78/92f3c46440a83ebe22ae614bd6792e7b052bcb58ff128f677f5662015184/pyqt6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:37884df27f774e2e1c0c96fa41e817a222329b80ffc6241725b0dc8c110acb35", size = 37804959, upload-time = "2025-06-06T08:48:39.587Z" },
{ url = "https://files.pythonhosted.org/packages/5a/5e/e77fa2761d809cd08d724f44af01a4b6ceb0ff9648e43173187b0e4fac4e/pyqt6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:055870b703c1a49ca621f8a89e2ec4d848e6c739d39367eb9687af3b056d9aa3", size = 40414608, upload-time = "2025-06-06T08:49:00.26Z" },
{ url = "https://files.pythonhosted.org/packages/c4/09/69cf80456b6a985e06dd24ed0c2d3451e43567bf2807a5f3a86ef7a74a2e/pyqt6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:15b95bd273bb6288b070ed7a9503d5ff377aa4882dd6d175f07cad28cdb21da0", size = 25717996, upload-time = "2025-06-06T08:49:13.208Z" },
{ url = "https://files.pythonhosted.org/packages/52/b3/0839d8fd18b86362a4de384740f2f6b6885b5d06fda7720f8a335425e316/pyqt6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:08792c72d130a02e3248a120f0b9bbb4bf4319095f92865bc5b365b00518f53d", size = 25212132, upload-time = "2025-06-06T08:49:27.41Z" },
]
[[package]]
name = "pyqt6-qt6"
version = "6.9.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/40/04f652e714f85ba6b0c24f4ead860f2c5769f9e64737f415524d792d5914/pyqt6_qt6-6.9.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:3854c7f83ee4e8c2d91e23ab88b77f90e2ca7ace34fe72f634a446959f2b4d4a", size = 66236777, upload-time = "2025-06-03T14:53:17.684Z" },
{ url = "https://files.pythonhosted.org/packages/57/31/e4fa40568a59953ce5cf9a5adfbd1be4a806dafd94e39072d3cc0bed5468/pyqt6_qt6-6.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:123e4aeb037c099bb4696a3ea8edcb1d9d62cedd0b2b950556b26024c97f3293", size = 60551574, upload-time = "2025-06-03T14:53:48.42Z" },
{ url = "https://files.pythonhosted.org/packages/aa/8d/7c8073cbbefe9c103ec8add70f29ffee1db95a3755b429b9f47cd6afa41b/pyqt6_qt6-6.9.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cc5bd193ebd2d1a3ec66e1eee65bf532d762c239459bce1ecebf56177243e89b", size = 82000130, upload-time = "2025-06-03T14:54:26.585Z" },
{ url = "https://files.pythonhosted.org/packages/1e/60/a4ab932028b0c15c0501cb52eb1e7f24f4ce2e4c78d46c7cce58a375a88c/pyqt6_qt6-6.9.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:b065af7243d1d450a49470a8185301196a18b1d41085d3ef476eb55bbb225083", size = 80463127, upload-time = "2025-06-03T14:55:03.272Z" },
{ url = "https://files.pythonhosted.org/packages/e7/85/552710819019a96d39d924071324a474aec54b31c410d7de8ebb398adcc1/pyqt6_qt6-6.9.1-py3-none-win_amd64.whl", hash = "sha256:f9e54c424bc921ecb76792a75d123e4ecfc26b00b0c57dae526f41f1d57951d3", size = 73778423, upload-time = "2025-06-03T14:55:39.756Z" },
{ url = "https://files.pythonhosted.org/packages/16/b4/70f6b18a4913f2326dcf7acb15c12cc0b91cb3932c2ba3b5728811f22acd/pyqt6_qt6-6.9.1-py3-none-win_arm64.whl", hash = "sha256:432caaedf5570bc8a9b7c75bc6af6a26bf88589536472eca73417ac019f59d41", size = 49617924, upload-time = "2025-06-03T14:57:13.038Z" },
]
[[package]]
name = "pyqt6-sip"
version = "13.10.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2f/4a/96daf6c2e4f689faae9bd8cebb52754e76522c58a6af9b5ec86a2e8ec8b4/pyqt6_sip-13.10.2.tar.gz", hash = "sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe", size = 92548, upload-time = "2025-05-23T12:26:49.901Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/5b/1240017e0d59575289ba52b58fd7f95e7ddf0ed2ede95f3f7e2dc845d337/pyqt6_sip-13.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c", size = 112199, upload-time = "2025-05-23T12:26:32.503Z" },
{ url = "https://files.pythonhosted.org/packages/51/11/1fc3bae02a12a3ac8354aa579b56206286e8b5ca9586677b1058c81c2f74/pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6", size = 322757, upload-time = "2025-05-23T12:26:33.752Z" },
{ url = "https://files.pythonhosted.org/packages/21/40/de9491213f480a27199690616959a17a0f234962b86aa1dd4ca2584e922d/pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7", size = 304251, upload-time = "2025-05-23T12:26:35.66Z" },
{ url = "https://files.pythonhosted.org/packages/02/21/cc80e03f1052408c62c341e9fe9b81454c94184f4bd8a95d29d2ec86df92/pyqt6_sip-13.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e", size = 53519, upload-time = "2025-05-23T12:26:36.797Z" },
{ url = "https://files.pythonhosted.org/packages/77/cf/53bd0863252b260a502659cb3124d9c9fe38047df9360e529b437b4ac890/pyqt6_sip-13.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92", size = 45349, upload-time = "2025-05-23T12:26:37.729Z" },
{ url = "https://files.pythonhosted.org/packages/a1/1e/979ea64c98ca26979d8ce11e9a36579e17d22a71f51d7366d6eec3c82c13/pyqt6_sip-13.10.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1", size = 112227, upload-time = "2025-05-23T12:26:38.758Z" },
{ url = "https://files.pythonhosted.org/packages/d9/21/84c230048e3bfef4a9209d16e56dcd2ae10590d03a31556ae8b5f1dcc724/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca", size = 322920, upload-time = "2025-05-23T12:26:39.856Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/c6a28a142f14e735088534cc92951c3f48cccd77cdd4f3b10d7996be420f/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b", size = 303833, upload-time = "2025-05-23T12:26:41.075Z" },
{ url = "https://files.pythonhosted.org/packages/89/63/e5adf350c1c3123d4865c013f164c5265512fa79f09ad464fb2fdf9f9e61/pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277", size = 53527, upload-time = "2025-05-23T12:26:42.625Z" },
{ url = "https://files.pythonhosted.org/packages/58/74/2df4195306d050fbf4963fb5636108a66e5afa6dc05fd9e81e51ec96c384/pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244", size = 45373, upload-time = "2025-05-23T12:26:43.536Z" },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -1690,6 +1742,15 @@ 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, upload-time = "2024-03-01T18:36:18.57Z" }, { 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, upload-time = "2024-03-01T18:36:18.57Z" },
] ]
[[package]]
name = "python-dotenv"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
]
[[package]] [[package]]
name = "python-json-logger" name = "python-json-logger"
version = "3.3.0" version = "3.3.0"