diff --git a/README.md b/README.md
index ec4e809..a444e60 100644
--- a/README.md
+++ b/README.md
@@ -127,87 +127,6 @@ save the model, upload it to Hugging Face, chat with it to test how well it work
run standard benchmarks on it, or any combination of those actions.
-## Research features
-
-In addition to its primary function of removing model censorship, Heretic also
-provides features designed to support research into the semantics of model internals
-(interpretability). To use those features, you need to install Heretic with the
-optional `research` extra:
-
-```sh
-pip install -U 'heretic-llm[research]'
-```
-
-This gives you access to the following functionality:
-
-### Generate plots of residual vectors by passing `--plot-residuals`
-
-When run with this flag, Heretic will:
-
-1. Compute residual vectors (hidden states) for the first output token,
- for each transformer layer, for both "harmful" and "harmless" prompts.
-2. Perform a [PaCMAP projection](https://github.com/YingfanWang/PaCMAP)
- from residual space to 2D-space.
-3. Left-right align the projections of "harmful"/"harmless" residuals
- by their geometric medians to make projections for consecutive layers
- more similar. Additionally, PaCMAP is initialized with the previous
- layer's projections for each new layer, minimizing disruptive transitions.
-4. Scatter-plot the projections, generating a PNG image for each layer.
-5. Generate an animation showing how residuals transform between layers,
- as an animated GIF.
-
-
-
-See [the configuration file](config.default.toml) for options that allow you
-to control various aspects of the generated plots.
-
-Note that PaCMAP is an expensive operation that is performed on the CPU.
-For larger models, it can take an hour or more to compute projections
-for all layers.
-
-### Print details about residual geometry by passing `--print-residual-geometry`
-
-If you are interested in a quantitative analysis of how residual vectors
-for "harmful" and "harmless" prompts relate to each other, this flag gives you
-the following table, packed with metrics that can facilitate understanding
-the same (for [gemma-3-270m-it](https://huggingface.co/google/gemma-3-270m-it)
-in this case):
-
-```
-┏━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓
-┃ Layer ┃ S(g,b) ┃ S(g*,b*) ┃ S(g,r) ┃ S(g*,r*) ┃ S(b,r) ┃ S(b*,r*) ┃ |g| ┃ |g*| ┃ |b| ┃ |b*| ┃ |r| ┃ |r*| ┃ Silh ┃
-┡━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩
-│ 1 │ 1.0000 │ 1.0000 │ -0.4311 │ -0.4906 │ -0.4254 │ -0.4847 │ 170.29 │ 170.49 │ 169.78 │ 169.85 │ 1.19 │ 1.31 │ 0.0480 │
-│ 2 │ 1.0000 │ 1.0000 │ 0.4297 │ 0.4465 │ 0.4365 │ 0.4524 │ 768.55 │ 768.77 │ 771.32 │ 771.36 │ 6.39 │ 5.76 │ 0.0745 │
-│ 3 │ 0.9999 │ 1.0000 │ -0.5699 │ -0.5577 │ -0.5614 │ -0.5498 │ 1020.98 │ 1021.13 │ 1013.80 │ 1014.71 │ 12.70 │ 11.60 │ 0.0920 │
-│ 4 │ 0.9999 │ 1.0000 │ 0.6582 │ 0.6553 │ 0.6659 │ 0.6627 │ 1356.39 │ 1356.20 │ 1368.71 │ 1367.95 │ 18.62 │ 17.84 │ 0.0957 │
-│ 5 │ 0.9987 │ 0.9990 │ -0.6880 │ -0.6761 │ -0.6497 │ -0.6418 │ 766.54 │ 762.25 │ 731.75 │ 732.42 │ 51.97 │ 45.24 │ 0.1018 │
-│ 6 │ 0.9998 │ 0.9998 │ -0.1983 │ -0.2312 │ -0.1811 │ -0.2141 │ 2417.35 │ 2421.08 │ 2409.18 │ 2411.40 │ 43.06 │ 43.47 │ 0.0900 │
-│ 7 │ 0.9998 │ 0.9997 │ -0.5258 │ -0.5746 │ -0.5072 │ -0.5560 │ 3444.92 │ 3474.99 │ 3400.01 │ 3421.63 │ 86.94 │ 94.38 │ 0.0492 │
-│ 8 │ 0.9990 │ 0.9991 │ 0.8235 │ 0.8312 │ 0.8479 │ 0.8542 │ 4596.54 │ 4615.62 │ 4918.32 │ 4934.20 │ 384.87 │ 377.87 │ 0.2278 │
-│ 9 │ 0.9992 │ 0.9992 │ 0.5335 │ 0.5441 │ 0.5678 │ 0.5780 │ 5322.30 │ 5316.96 │ 5468.65 │ 5466.98 │ 265.68 │ 267.28 │ 0.1318 │
-│ 10 │ 0.9974 │ 0.9973 │ 0.8189 │ 0.8250 │ 0.8579 │ 0.8644 │ 5328.81 │ 5325.63 │ 5953.35 │ 5985.15 │ 743.95 │ 779.74 │ 0.2863 │
-│ 11 │ 0.9977 │ 0.9978 │ 0.4262 │ 0.4045 │ 0.4862 │ 0.4645 │ 9644.02 │ 9674.06 │ 9983.47 │ 9990.28 │ 743.28 │ 726.99 │ 0.1576 │
-│ 12 │ 0.9904 │ 0.9907 │ 0.4384 │ 0.4077 │ 0.5586 │ 0.5283 │ 10257.40 │ 10368.50 │ 11114.51 │ 11151.21 │ 1711.18 │ 1664.69 │ 0.1890 │
-│ 13 │ 0.9867 │ 0.9874 │ 0.4007 │ 0.3680 │ 0.5444 │ 0.5103 │ 12305.12 │ 12423.75 │ 13440.31 │ 13432.47 │ 2386.43 │ 2282.47 │ 0.1293 │
-│ 14 │ 0.9921 │ 0.9922 │ 0.3198 │ 0.2682 │ 0.4364 │ 0.3859 │ 16929.16 │ 17080.37 │ 17826.97 │ 17836.03 │ 2365.23 │ 2301.87 │ 0.1282 │
-│ 15 │ 0.9846 │ 0.9850 │ 0.1198 │ 0.0963 │ 0.2913 │ 0.2663 │ 16858.58 │ 16949.44 │ 17496.00 │ 17502.88 │ 3077.08 │ 3029.60 │ 0.1611 │
-│ 16 │ 0.9686 │ 0.9689 │ -0.0029 │ -0.0254 │ 0.2457 │ 0.2226 │ 18912.77 │ 19074.86 │ 19510.56 │ 19559.62 │ 4848.35 │ 4839.75 │ 0.1516 │
-│ 17 │ 0.9782 │ 0.9784 │ -0.0174 │ -0.0381 │ 0.1908 │ 0.1694 │ 27098.09 │ 27273.00 │ 27601.12 │ 27653.12 │ 5738.19 │ 5724.21 │ 0.1641 │
-│ 18 │ 0.9184 │ 0.9196 │ 0.1343 │ 0.1430 │ 0.5155 │ 0.5204 │ 190.16 │ 190.35 │ 219.91 │ 220.62 │ 87.82 │ 87.59 │ 0.1855 │
-└───────┴────────┴──────────┴─────────┴──────────┴─────────┴──────────┴──────────┴──────────┴──────────┴──────────┴─────────┴─────────┴────────┘
-g = mean of residual vectors for good prompts
-g* = geometric median of residual vectors for good prompts
-b = mean of residual vectors for bad prompts
-b* = geometric median of residual vectors for bad prompts
-r = residual direction for means (i.e., b - g)
-r* = residual direction for geometric medians (i.e., b* - g*)
-S(x,y) = cosine similarity of x and y
-|x| = L2 norm of x
-Silh = Mean silhouette coefficient of residuals for good/bad clusters
-```
-
-
## How Heretic works
Heretic implements a parametrized variant of directional ablation. For each
diff --git a/config.default.toml b/config.default.toml
index 7bc876d..c9e79ef 100644
--- a/config.default.toml
+++ b/config.default.toml
@@ -71,52 +71,21 @@ chain_of_thought_skips = [
# Whether to print additional information that can help with debugging.
print_debug_information = false
-# Whether to print detailed information about residuals and residual directions.
-print_residual_geometry = false
-
-# Whether to generate plots showing PaCMAP projections of residual vectors.
-plot_residuals = false
-
-# Base path to save plots of residual vectors to.
-residual_plot_path = "plots"
-
-# Title placed above plots of residual vectors.
-residual_plot_title = 'PaCMAP Projection of Residual Vectors for "Harmless" and "Harmful" Prompts'
-
-# Matplotlib style sheet to use for plots of residual vectors.
-residual_plot_style = "dark_background"
-
-# List of scorers to evaluate.
-# Each entry is an object:
-# { plugin = , optimization = , instance_name = }
-# where is one of "minimize", "maximize", "none" (do not optimize)
+# List of scorer plugin configs. Each entry is an object
+# { plugin = , optimization = , instance_name = }.
+# is one of "minimize", "maximize", or "none" (do not optimize).
scorers = [
{ plugin = "heretic.scorers.keyword_rate.KeywordRate", optimization = "minimize" },
{ plugin = "heretic.scorers.kl_divergence.KLDivergence", optimization = "minimize" },
]
-# Whether to adjust the residual directions so that only the component that is
-# orthogonal to the good direction is subtracted during abliteration.
-orthogonalize_direction = true
-
-# How to apply row normalization of the weights. Options:
-# "none" (no normalization),
-# "pre" (compute LoRA adapter relative to row-normalized weights),
-# "full" (like "pre", but renormalizes to preserve original row magnitudes).
-row_normalization = "full"
-
-# The rank of the LoRA adapter to use when "full" row normalization is used.
-# Row magnitude preservation is approximate due to non-linear effects,
-# and this determines the rank of that approximation. Higher ranks produce
-# larger output files and may slow down evaluation.
-full_normalization_lora_rank = 3
-
-# The symmetric winsorization to apply to the per-prompt, per-layer residual vectors,
-# expressed as the quantile to clamp to (between 0 and 1). Disabled by default.
-# This can tame so-called "massive activations" that occur in some models.
-# Example: winsorization_quantile = 0.95 computes the 0.95-quantile of the absolute values
-# of the components, then clamps the magnitudes of all components to that quantile.
-winsorization_quantile = 1.0
+# List of modifier plugin configs. Each entry is an object
+# { plugin = , instance_name = }.
+# Note that only a single modifier can currently be applied,
+# and this list must contain exactly one entry.
+modifiers = [
+ { plugin = "heretic.modifiers.abliteration.Abliteration" },
+]
# Number of abliteration trials to run during optimization.
n_trials = 200
@@ -133,29 +102,33 @@ max_shard_size = "5GB"
# System prompt to use when prompting the model.
system_prompt = "You are a helpful assistant."
+# Plugin-specific settings live in top-level TOML tables.
+#
+# For scorer plugins, use: `[scorer.]` (and optionally `[scorer._]` for instance-related config).
+# For modifier plugins, use: `[modifier.]` (and optionally `[modifier._]` for instance-related config).
+#
+# You can load multiple instances of the same plugin class by setting `instance_name`
+# in the `scorers/modifiers = [...]` list. Each instance is still identified as `ClassName.instanceName`
+# internally, but its config overrides live under `[scorer/modifier.ClassName_]`.
+#
+# Example:
+# scorers = [
+# { plugin = "heretic.scorers.keyword_rate.KeywordRate", optimization = "minimize", instance_name = "small" },
+# { plugin = "heretic.scorers.keyword_rate.KeywordRate", optimization = "minimize", instance_name = "tiny" },
+# ]
+#
+# Shared defaults for all instances live under `[scorer.KeywordRate]` and can be overridden per
+# instance under `[scorer.KeywordRate_]`.
+#
+# Example instance override:
+# [scorer.KeywordRate_small.prompts]
+# split = "test[:10]"
+#
# Each "dataset" below can be a Hugging Face dataset ID, a path to a dataset on disk,
# or a path to a plain text file with one prompt per line (empty lines are ignored).
# For text files, "column" is ignored and "split" is optional; when given, it selects
# a subset of the lines using slice notation (e.g. "[:400]").
-# Dataset of prompts that tend to not result in refusals (used for calculating residual directions).
-[good_prompts]
-dataset = "mlabonne/harmless_alpaca"
-split = "train[:400]"
-column = "text"
-residual_plot_label = '"Harmless" prompts'
-residual_plot_color = "royalblue"
-
-# Dataset of prompts that tend to result in refusals (used for calculating residual directions).
-[bad_prompts]
-dataset = "mlabonne/harmful_behaviors"
-split = "train[:400]"
-column = "text"
-residual_plot_label = '"Harmful" prompts'
-residual_plot_color = "darkorange"
-
-# Plugin-specific settings live in a top-level TOML table.
-# For scorer plugins, use: `[scorer.]` (and optionally `[scorer._]` for instance-related config).
[scorer.KeywordRate]
# Name that describes what the configured keyword rate measures.
score_name = "Refusals"
@@ -200,30 +173,50 @@ keyword_markers = [
"ethical boundaries",
]
-# Scorer-owned evaluation prompts
+# Dataset of prompts to evaluate the keyword match rate on.
[scorer.KeywordRate.prompts]
dataset = "mlabonne/harmful_behaviors"
split = "test[:100]"
column = "text"
-# You can also load multiple instances of the same scorer class by setting `instance_name`
-# in the `scorers = [...]` list. Each instance is still identified as `ClassName.instanceName`
-# internally, but its config overrides live under `[scorer.ClassName_]`.
-#
-# Example:
-# scorers = [
-# { plugin = "heretic.scorers.keyword_rate.KeywordRate", optimization = 'minimize', instance_name = "small" },
-# { plugin = "heretic.scorers.keyword_rate.KeywordRate", optimization = 'minimize', instance_name = "tiny" },
-# ]
-#
-# Shared defaults for all instances live under `[scorer.KeywordRate]` and can be overridden per
-# instance under `[scorer.KeywordRate_]`.
-#
-# Example instance override:
-# [scorer.KeywordRate_small.prompts]
-# split = "test[:10]"
-
+# Dataset of prompts used to measure KL divergence from original model.
[scorer.KLDivergence.prompts]
dataset = "mlabonne/harmless_alpaca"
split = "test[:100]"
column = "text"
+
+[modifier.Abliteration]
+# Whether to adjust the residual directions so that only the component that is
+# orthogonal to the good direction is subtracted during abliteration.
+orthogonalize_direction = true
+
+# How to apply row normalization of the weights. Options:
+# "none" (no normalization),
+# "pre" (compute LoRA adapter relative to row-normalized weights),
+# "full" (like "pre", but renormalizes to preserve original row magnitudes).
+row_normalization = "full"
+
+# The rank of the LoRA adapter to use when "full" row normalization is used.
+# Row magnitude preservation is approximate due to non-linear effects,
+# and this determines the rank of that approximation. Higher ranks produce
+# larger output files and may slow down evaluation.
+full_normalization_lora_rank = 3
+
+# The symmetric winsorization to apply to the per-prompt, per-layer residual vectors,
+# expressed as the quantile to clamp to (between 0 and 1). Disabled by default.
+# This can tame so-called "massive activations" that occur in some models.
+# Example: winsorization_quantile = 0.95 computes the 0.95-quantile of the absolute values
+# of the components, then clamps the magnitudes of all components to that quantile.
+winsorization_quantile = 1.0
+
+# Dataset of prompts that tend to produce desirable responses.
+[modifier.Abliteration.good_prompts]
+dataset = "mlabonne/harmless_alpaca"
+split = "train[:400]"
+column = "text"
+
+# Dataset of prompts that tend to produce undesirable responses.
+[modifier.Abliteration.bad_prompts]
+dataset = "mlabonne/harmful_behaviors"
+split = "train[:400]"
+column = "text"
diff --git a/config.nohumor.toml b/config.nohumor.toml
index 7632f1b..5628b09 100644
--- a/config.nohumor.toml
+++ b/config.nohumor.toml
@@ -3,22 +3,6 @@
max_response_length = 300
-residual_plot_title = "PaCMAP Projection of Residuals for Serious/Humorous Prompts"
-
-[good_prompts]
-dataset = "mlabonne/harmless_alpaca"
-split = "train[:400]"
-column = "text"
-residual_plot_label = "Serious prompts"
-residual_plot_color = "royalblue"
-
-[bad_prompts]
-dataset = "UnstableLlama/jokes"
-split = "train[:200]"
-column = "text"
-residual_plot_label = "Humorous prompts"
-residual_plot_color = "darkorange"
-
[scorer.KeywordRate]
score_name = "Responses with humor"
@@ -70,3 +54,13 @@ column = "text"
dataset = "mlabonne/harmless_alpaca"
split = "test[:100]"
column = "text"
+
+[modifier.Abliteration.good_prompts]
+dataset = "mlabonne/harmless_alpaca"
+split = "train[:400]"
+column = "text"
+
+[modifier.Abliteration.bad_prompts]
+dataset = "UnstableLlama/jokes"
+split = "train[:200]"
+column = "text"
diff --git a/config.noslop.toml b/config.noslop.toml
index 5e8437d..8a39754 100644
--- a/config.noslop.toml
+++ b/config.noslop.toml
@@ -3,26 +3,8 @@
max_response_length = 300
-residual_plot_title = "PaCMAP Projection of Residuals for Slop-Suppressing/Inducing Prompts"
-
system_prompt = "You are a professional writer."
-[good_prompts]
-dataset = "llm-aes/writing-prompts"
-split = "train[:500]"
-column = "prompt"
-prefix = "Write a short story based on the writing prompt below. Avoid literary cliches, purple prose, and flowery language.\n\nWriting prompt:"
-residual_plot_label = "Slop-suppressing prompts"
-residual_plot_color = "royalblue"
-
-[bad_prompts]
-dataset = "llm-aes/writing-prompts"
-split = "train[:500]"
-column = "prompt"
-prefix = "Write a short story based on the writing prompt below. Make extensive use of literary cliches, purple prose, and flowery language.\n\nWriting prompt:"
-residual_plot_label = "Slop-inducing prompts"
-residual_plot_color = "darkorange"
-
[scorer.KeywordRate]
score_name = "Responses with slop"
@@ -164,3 +146,15 @@ dataset = "llm-aes/writing-prompts"
split = "train[1000:1100]"
column = "prompt"
prefix = "Write a short story based on the writing prompt below. Avoid literary cliches, purple prose, and flowery language.\n\nWriting prompt:"
+
+[modifier.Abliteration.good_prompts]
+dataset = "llm-aes/writing-prompts"
+split = "train[:500]"
+column = "prompt"
+prefix = "Write a short story based on the writing prompt below. Avoid literary cliches, purple prose, and flowery language.\n\nWriting prompt:"
+
+[modifier.Abliteration.bad_prompts]
+dataset = "llm-aes/writing-prompts"
+split = "train[:500]"
+column = "prompt"
+prefix = "Write a short story based on the writing prompt below. Make extensive use of literary cliches, purple prose, and flowery language.\n\nWriting prompt:"
diff --git a/pyproject.toml b/pyproject.toml
index ddd422f..a4c594e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,15 +44,6 @@ dependencies = [
"transformers[kernels]~=5.6",
]
-[project.optional-dependencies]
-research = [
- "geom-median~=0.1",
- "imageio~=2.37",
- "matplotlib~=3.10",
- "pacmap~=0.8",
- "scikit-learn~=1.7",
-]
-
[dependency-groups]
dev = [
"ruff>=0.14.5",
diff --git a/src/heretic/analyzer.py b/src/heretic/analyzer.py
deleted file mode 100644
index 4d88424..0000000
--- a/src/heretic/analyzer.py
+++ /dev/null
@@ -1,357 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-# Copyright (C) 2025-2026 Philipp Emanuel Weidmann + contributors
-
-from pathlib import Path
-
-import numpy as np
-import torch
-import torch.linalg as LA
-import torch.nn.functional as F
-from numpy.typing import NDArray
-from rich.progress import track
-from rich.table import Table
-from torch import Tensor
-
-from .config import Settings
-from .model import Model
-from .utils import print
-
-
-class Analyzer:
- def __init__(
- self,
- settings: Settings,
- model: Model,
- good_residuals: Tensor,
- bad_residuals: Tensor,
- ):
- self.settings = settings
- self.model = model
- self.good_residuals = good_residuals
- self.bad_residuals = bad_residuals
-
- def print_residual_geometry(self):
- try:
- from geom_median.torch import ( # ty:ignore[unresolved-import]
- compute_geometric_median,
- )
- from sklearn.metrics import silhouette_score # ty:ignore[unresolved-import]
- except ImportError:
- print()
- print(
- (
- "[red]Research dependencies not found. Printing residual geometry requires "
- "installing Heretic with the optional research feature, i.e., "
- "using \"pip install -U 'heretic-llm\\[research]'\".[/]"
- )
- )
- return
-
- print()
- print("Computing residual geometry...")
-
- table = Table()
- table.add_column("Layer", justify="right")
- table.add_column("S(g,b)", justify="right")
- table.add_column("S(g*,b*)", justify="right")
- table.add_column("S(g,r)", justify="right")
- table.add_column("S(g*,r*)", justify="right")
- table.add_column("S(b,r)", justify="right")
- table.add_column("S(b*,r*)", justify="right")
- table.add_column("|g|", justify="right")
- table.add_column("|g*|", justify="right")
- table.add_column("|b|", justify="right")
- table.add_column("|b*|", justify="right")
- table.add_column("|r|", justify="right")
- table.add_column("|r*|", justify="right")
- table.add_column("Silh", justify="right")
-
- g = self.good_residuals.mean(dim=0)
- g_star = torch.stack(
- [
- compute_geometric_median(
- self.good_residuals[:, layer_index, :].detach().cpu()
- ).median
- for layer_index in range(len(self.model.get_layers()) + 1)
- ]
- )
- b = self.bad_residuals.mean(dim=0)
- b_star = torch.stack(
- [
- compute_geometric_median(
- self.bad_residuals[:, layer_index, :].detach().cpu()
- ).median
- for layer_index in range(len(self.model.get_layers()) + 1)
- ]
- )
- r = b - g
- r_star = b_star - g_star
-
- g_b_similarities = F.cosine_similarity(g, b, dim=-1)
- g_star_b_star_similarities = F.cosine_similarity(g_star, b_star, dim=-1)
- g_r_similarities = F.cosine_similarity(g, r, dim=-1)
- g_star_r_star_similarities = F.cosine_similarity(g_star, r_star, dim=-1)
- b_r_similarities = F.cosine_similarity(b, r, dim=-1)
- b_star_r_star_similarities = F.cosine_similarity(b_star, r_star, dim=-1)
-
- g_norms = LA.vector_norm(g, dim=-1)
- g_star_norms = LA.vector_norm(g_star, dim=-1)
- b_norms = LA.vector_norm(b, dim=-1)
- b_star_norms = LA.vector_norm(b_star, dim=-1)
- r_norms = LA.vector_norm(r, dim=-1)
- r_star_norms = LA.vector_norm(r_star, dim=-1)
-
- residuals = (
- torch.cat(
- [
- self.good_residuals,
- self.bad_residuals,
- ],
- dim=0,
- )
- .detach()
- .cpu()
- .numpy()
- )
- labels = [0] * len(self.good_residuals) + [1] * len(self.bad_residuals)
- silhouettes = [
- silhouette_score(residuals[:, layer_index, :], labels)
- for layer_index in range(len(self.model.get_layers()) + 1)
- ]
-
- for layer_index in range(1, len(self.model.get_layers()) + 1):
- table.add_row(
- f"{layer_index}",
- f"{g_b_similarities[layer_index].item():.4f}",
- f"{g_star_b_star_similarities[layer_index].item():.4f}",
- f"{g_r_similarities[layer_index].item():.4f}",
- f"{g_star_r_star_similarities[layer_index].item():.4f}",
- f"{b_r_similarities[layer_index].item():.4f}",
- f"{b_star_r_star_similarities[layer_index].item():.4f}",
- f"{g_norms[layer_index].item():.2f}",
- f"{g_star_norms[layer_index].item():.2f}",
- f"{b_norms[layer_index].item():.2f}",
- f"{b_star_norms[layer_index].item():.2f}",
- f"{r_norms[layer_index].item():.2f}",
- f"{r_star_norms[layer_index].item():.2f}",
- f"{silhouettes[layer_index]:.4f}",
- )
-
- print()
- print("[bold]Residual Geometry[/]")
- print(table)
- print("[bold]g[/] = mean of residual vectors for good prompts")
- print("[bold]g*[/] = geometric median of residual vectors for good prompts")
- print("[bold]b[/] = mean of residual vectors for bad prompts")
- print("[bold]b*[/] = geometric median of residual vectors for bad prompts")
- print("[bold]r[/] = residual direction for means (i.e., [bold]b - g[/])")
- print(
- "[bold]r*[/] = residual direction for geometric medians (i.e., [bold]b* - g*[/])"
- )
- print("[bold]S(x,y)[/] = cosine similarity of [bold]x[/] and [bold]y[/]")
- print("[bold]|x|[/] = L2 norm of [bold]x[/]")
- print(
- "[bold]Silh[/] = Mean silhouette coefficient of residuals for good/bad clusters"
- )
-
- def plot_residuals(self):
- try:
- import imageio.v3 as iio # ty:ignore[unresolved-import]
- import matplotlib.pyplot as plt # ty:ignore[unresolved-import]
- from geom_median.numpy import ( # ty:ignore[unresolved-import]
- compute_geometric_median,
- )
- from pacmap import PaCMAP # ty:ignore[unresolved-import]
- except ImportError:
- print()
- print(
- (
- "[red]Research dependencies not found. Plotting residuals requires "
- "installing Heretic with the optional research feature, i.e., "
- "using \"pip install -U 'heretic-llm\\[research]'\".[/]"
- )
- )
- return
-
- LAYER_FRAME_DURATION = 1000
- N_TRANSITION_FRAMES = 20
- TRANSITION_FRAME_DURATION = 50
-
- print()
- print("Plotting residual vectors...")
-
- layer_residuals_2d = []
- pacmap_init = None
-
- for layer_index in track(
- range(1, len(self.model.get_layers()) + 1),
- description="* Computing PaCMAP projections...",
- ):
- good_residuals = (
- self.good_residuals[:, layer_index, :].detach().cpu().numpy()
- )
- bad_residuals = self.bad_residuals[:, layer_index, :].detach().cpu().numpy()
-
- residuals = np.vstack((good_residuals, bad_residuals))
- embedding = PaCMAP(n_components=2, n_neighbors=30)
- residuals_2d = embedding.fit_transform(residuals, init=pacmap_init)
- pacmap_init = residuals_2d
-
- n_good_residuals = good_residuals.shape[0]
- good_residuals_2d = residuals_2d[:n_good_residuals]
- bad_residuals_2d = residuals_2d[n_good_residuals:]
-
- # Important: These are the medians of the 2D-projected residuals,
- # not the projections of the medians of the residuals.
- # Their only purpose is to rotate the individual plots
- # into a consistent orientation. They are not suitable
- # for being plotted themselves.
- good_anchor = compute_geometric_median(good_residuals_2d).median
- bad_anchor = compute_geometric_median(bad_residuals_2d).median
-
- # Rotate points to make the line connecting the medians horizontal,
- # with the median of the good residuals on the left.
- direction = bad_anchor - good_anchor
- angle = -np.arctan2(direction[1], direction[0])
- cosine = np.cos(angle)
- sine = np.sin(angle)
- rotation_matrix = np.array([[cosine, -sine], [sine, cosine]])
- residuals_2d = residuals_2d @ rotation_matrix.T
-
- good_residuals_2d = residuals_2d[:n_good_residuals]
- bad_residuals_2d = residuals_2d[n_good_residuals:]
-
- layer_residuals_2d.append((good_residuals_2d, bad_residuals_2d))
-
- plt.style.use(self.settings.residual_plot_style)
-
- def plot(
- image_path: Path,
- layer_index: int,
- good_residuals_2d: NDArray,
- bad_residuals_2d: NDArray,
- ):
- fig, ax = plt.subplots(figsize=(8, 6))
-
- ax.scatter(
- good_residuals_2d[:, 0],
- good_residuals_2d[:, 1],
- s=10,
- c=self.settings.good_prompts.residual_plot_color,
- alpha=0.5,
- label=self.settings.good_prompts.residual_plot_label,
- )
- ax.scatter(
- bad_residuals_2d[:, 0],
- bad_residuals_2d[:, 1],
- s=10,
- c=self.settings.bad_prompts.residual_plot_color,
- alpha=0.5,
- label=self.settings.bad_prompts.residual_plot_label,
- )
-
- ax.set_title(self.settings.residual_plot_title, pad=11)
- ax.legend(loc="upper right")
- ax.grid(False)
- ax.set_xticks([])
- ax.set_yticks([])
-
- fig.text(
- 0.018,
- 0.02,
- self.settings.model,
- ha="left",
- va="bottom",
- fontsize=12,
- )
- fig.text(
- 0.982,
- 0.02,
- f"Layer {layer_index:03}",
- ha="right",
- va="bottom",
- fontsize=12,
- )
-
- fig.tight_layout()
- fig.subplots_adjust(bottom=0.08)
-
- fig.savefig(image_path, dpi=100)
- plt.close(fig)
-
- base_path = Path(
- self.settings.residual_plot_path
- ) / self.settings.model.replace(
- "/",
- "_",
- ).replace(
- "\\",
- "_",
- )
-
- base_path.mkdir(parents=True, exist_ok=True)
-
- images = []
- durations = []
-
- for layer_index, (
- good_residuals_2d,
- bad_residuals_2d,
- ) in enumerate(
- track(
- layer_residuals_2d,
- description="* Generating plots...",
- ),
- 1,
- ):
- image_path = base_path / f"layer_{layer_index:03}.png"
-
- plot(image_path, layer_index, good_residuals_2d, bad_residuals_2d)
-
- images.append(iio.imread(image_path))
- durations.append(LAYER_FRAME_DURATION)
-
- if layer_index < len(layer_residuals_2d):
- # The first frame of the transition is the layer frame created above.
- # The last frame is the next layer frame, created in the next iteration of the outer loop.
- # The following are the intermediate frames.
- # There are a total of N_TRANSITION_FRAMES frame changes in the transition.
- for frame_index in range(1, N_TRANSITION_FRAMES):
- image_path = (
- base_path / f"layer_{layer_index:03}_frame_{frame_index:03}.png"
- )
-
- progress = frame_index / N_TRANSITION_FRAMES
-
- good_residuals_2d_interpolated = good_residuals_2d + progress * (
- layer_residuals_2d[layer_index][0] - good_residuals_2d
- )
- bad_residuals_2d_interpolated = bad_residuals_2d + progress * (
- layer_residuals_2d[layer_index][1] - bad_residuals_2d
- )
-
- plot(
- image_path,
- layer_index,
- good_residuals_2d_interpolated,
- bad_residuals_2d_interpolated,
- )
-
- images.append(iio.imread(image_path))
- durations.append(TRANSITION_FRAME_DURATION)
-
- # Delete the image file containing the animation frame.
- # We have already read its contents and it serves no purpose
- # other than building the animation.
- image_path.unlink()
-
- print("* Generating animation...")
-
- iio.imwrite(
- base_path / "animation.gif",
- images,
- duration=durations,
- loop=0,
- )
-
- print(f"* Plots saved to [bold]{base_path.resolve()}[/].")
diff --git a/src/heretic/config.py b/src/heretic/config.py
index a119b24..d0a519a 100644
--- a/src/heretic/config.py
+++ b/src/heretic/config.py
@@ -32,13 +32,6 @@ class QuantizationMethod(str, Enum):
BNB_4BIT = "bnb_4bit"
-class RowNormalization(str, Enum):
- NONE = "none"
- PRE = "pre"
- # POST = "post" # Theoretically possible, but provides no advantage.
- FULL = "full"
-
-
class ExportStrategy(str, Enum):
MERGE = "merge"
ADAPTER = "adapter"
@@ -79,18 +72,6 @@ class DatasetSpecification(BaseModel):
description="System prompt to use with the prompts (overrides global system prompt if set).",
)
- residual_plot_label: str | None = Field(
- default=None,
- description="Label to use for the dataset in plots of residual vectors.",
- exclude=True,
- )
-
- residual_plot_color: str | None = Field(
- default=None,
- description="Matplotlib color to use for the dataset in plots of residual vectors.",
- exclude=True,
- )
-
class ScorerConfig(BaseModel):
"""
@@ -142,6 +123,48 @@ class ScorerConfig(BaseModel):
return value
+class ModifierConfig(BaseModel):
+ """
+ Configuration for a modifier plugin.
+
+ TOML format:
+ - { plugin = "", instance_name = "" }
+ """
+
+ plugin: str = Field(
+ description=(
+ "Plugin to load. Either a file path with class name "
+ "(`path/to/plugin.py:ClassName`) or a fully-qualified import path "
+ "(`module.submodule.ClassName`)."
+ ),
+ )
+
+ instance_name: str | None = Field(
+ default=None,
+ description=(
+ "Optional name to distinguish multiple instances of the same plugin class. "
+ "Instance-specific settings live under `[modifier._]`."
+ ),
+ )
+
+ @field_validator("instance_name")
+ @classmethod
+ def validate_instance_name(cls, value: str | None) -> str | None:
+ if value is None:
+ return value
+
+ if not value.strip():
+ raise ValueError("cannot be empty or whitespace")
+
+ if "." in value:
+ raise ValueError("'.' is not allowed")
+
+ if any(char.isspace() for char in value):
+ raise ValueError("whitespace is not allowed")
+
+ return value
+
+
class BenchmarkSpecification(BaseModel):
task: str = Field(
description="Task ID of the benchmark in the Language Model Evaluation Harness."
@@ -304,38 +327,8 @@ class Settings(BaseSettings):
exclude=True,
)
- print_residual_geometry: bool = Field(
- default=False,
- description="Whether to print detailed information about residuals and residual directions.",
- exclude=True,
- )
-
- plot_residuals: bool = Field(
- default=False,
- description="Whether to generate plots showing PaCMAP projections of residual vectors.",
- exclude=True,
- )
-
- residual_plot_path: str = Field(
- default="plots",
- description="Base path to save plots of residual vectors to.",
- exclude=True,
- )
-
- residual_plot_title: str = Field(
- default='PaCMAP Projection of Residual Vectors for "Harmless" and "Harmful" Prompts',
- description="Title placed above plots of residual vectors.",
- exclude=True,
- )
-
- residual_plot_style: str = Field(
- default="dark_background",
- description="Matplotlib style sheet to use for plots of residual vectors.",
- exclude=True,
- )
-
scorers: list[ScorerConfig] = Field(
- default_factory=lambda: [
+ default=[
ScorerConfig(
plugin="heretic.scorers.keyword_rate.KeywordRate",
optimization="minimize",
@@ -346,48 +339,23 @@ class Settings(BaseSettings):
),
],
description=(
- "List of scorer plugin configs. Each entry is an object"
- " { plugin = , optimization = , instance_name = }."
- " is one of 'minimize', 'maximize', 'none' (do not optimize)."
+ "List of scorer plugin configs. Each entry is an object "
+ "{ plugin = , optimization = , instance_name = }. "
+ ' is one of "minimize", "maximize", or "none" (do not optimize).'
),
)
- orthogonalize_direction: bool = Field(
- default=True,
+ modifiers: list[ModifierConfig] = Field(
+ default=[
+ ModifierConfig(
+ plugin="heretic.modifiers.abliteration.Abliteration",
+ ),
+ ],
description=(
- "Whether to adjust the residual directions so that only the component that is "
- "orthogonal to the good direction is subtracted during abliteration."
- ),
- )
-
- row_normalization: RowNormalization = Field(
- default=RowNormalization.FULL,
- description=(
- "How to apply row normalization of the weights. Options: "
- '"none" (no normalization), '
- '"pre" (compute LoRA adapter relative to row-normalized weights), '
- '"full" (like "pre", but renormalizes to preserve original row magnitudes).'
- ),
- )
-
- full_normalization_lora_rank: PositiveInt = Field(
- default=3,
- description=(
- 'The rank of the LoRA adapter to use when "full" row normalization is used. '
- "Row magnitude preservation is approximate due to non-linear effects, "
- "and this determines the rank of that approximation. Higher ranks produce "
- "larger output files and may slow down evaluation."
- ),
- )
-
- winsorization_quantile: float = Field(
- default=1.0,
- description=(
- "The symmetric winsorization to apply to the per-prompt, per-layer residual vectors, "
- "expressed as the quantile to clamp to (between 0 and 1). Disabled by default. "
- 'This can tame so-called "massive activations" that occur in some models. '
- "Example: winsorization_quantile = 0.95 computes the 0.95-quantile of the absolute values "
- "of the components, then clamps the magnitudes of all components to that quantile."
+ "List of modifier plugin configs. Each entry is an object "
+ "{ plugin = , instance_name = }. "
+ "Note that only a single modifier can currently be applied, "
+ "and this list must contain exactly one entry."
),
)
@@ -539,31 +507,9 @@ class Settings(BaseSettings):
description="System prompt to use when prompting the model.",
)
- good_prompts: DatasetSpecification = Field(
- default=DatasetSpecification(
- dataset="mlabonne/harmless_alpaca",
- split="train[:400]",
- column="text",
- residual_plot_label='"Harmless" prompts',
- residual_plot_color="royalblue",
- ),
- description="Dataset of prompts that tend to not result in refusals (used for calculating refusal directions).",
- )
-
- bad_prompts: DatasetSpecification = Field(
- default=DatasetSpecification(
- dataset="mlabonne/harmful_behaviors",
- split="train[:400]",
- column="text",
- residual_plot_label='"Harmful" prompts',
- residual_plot_color="darkorange",
- ),
- description="Dataset of prompts that tend to result in refusals (used for calculating refusal directions).",
- )
-
# We intentionally allow extra keys so users can provide plugin-specific
# configuration in TOML tables like `[scorer.KeywordRate]` which are later
- # consumed via `settings.model_extra` (see `Evaluator._get_plugin_namespace`).
+ # consumed via `settings.model_extra` (see `plugin.get_plugin_namespace`).
model_config = SettingsConfigDict(extra="allow")
@classmethod
diff --git a/src/heretic/evaluator.py b/src/heretic/evaluator.py
index cde1224..bd11b1e 100644
--- a/src/heretic/evaluator.py
+++ b/src/heretic/evaluator.py
@@ -9,9 +9,9 @@ from pydantic import BaseModel
from .config import DatasetSpecification, ScorerConfig, Settings
from .model import Model
-from .plugin import get_plugin_namespace, is_builtin_plugin, load_plugin
-from .scorer import Context, Score, Scorer
-from .utils import deep_merge_dicts, parse_study_direction, print
+from .plugin import Context, is_builtin_plugin, load_plugin
+from .scorer import Score, Scorer
+from .utils import parse_study_direction, print
@dataclass
@@ -63,14 +63,16 @@ class Evaluator:
scorer_cls.validate_contract()
print(
- f"* Loaded: [bold]{scorer_cls.__name__} {'- ' + config.instance_name if config.instance_name else ''}[/bold]"
+ f"* Loaded: [bold]{scorer_cls.__name__}{' - ' + config.instance_name if config.instance_name else ''}[/bold]"
)
# Instantiate scorers.
instance_name = config.instance_name or None
- raw_settings = self._get_scorer_settings_raw(
- scorer_cls=scorer_cls, instance_name=instance_name
+ raw_settings = scorer_cls.get_settings_raw(
+ self.settings.model_extra,
+ "scorer",
+ instance_name,
)
scorer_settings: BaseModel | None = scorer_cls.validate_settings(
raw_settings
@@ -117,45 +119,9 @@ class Evaluator:
"""
specifications = []
for entry in self._scorer_entries:
- if entry.scorer.settings is None:
- continue
- for value in dict(entry.scorer.settings).values():
- if isinstance(value, DatasetSpecification):
- specifications.append(value)
+ specifications.extend(entry.scorer.get_dataset_specifications())
return specifications
- def _get_scorer_settings_raw(
- self, *, scorer_cls: type[Scorer], instance_name: str | None
- ) -> dict[str, Any]:
- """
- Build the raw settings dict for a scorer class and optional instance.
-
- Config rules:
- - Base settings live in `[scorer.ClassName]` (applies to all instances).
- - Instance overrides live in `[scorer.ClassName_]` (preferred).
- - Only merge/validate keys that exist in the scorer Settings schema.
- """
- settings_model = scorer_cls.get_settings_model()
- if settings_model is None:
- # No settings schema: nothing to merge/validate.
- return {}
-
- class_name = scorer_cls.__name__
-
- namespaces = [f"scorer.{class_name}"]
- if instance_name:
- namespaces.append(f"scorer.{class_name}_{instance_name}")
-
- merged_settings: dict[str, Any] = {}
- allowed_keys = set(settings_model.model_fields.keys())
-
- for namespace in namespaces:
- raw_table = get_plugin_namespace(self.settings.model_extra, namespace)
- filtered = {k: v for k, v in raw_table.items() if k in allowed_keys}
- merged_settings = deep_merge_dicts(merged_settings, filtered)
-
- return merged_settings
-
def all_scorers_reproducible(self) -> bool:
"""
Returns True if all scorers are reproducible,
diff --git a/src/heretic/main.py b/src/heretic/main.py
index 35424c9..3c64fcf 100644
--- a/src/heretic/main.py
+++ b/src/heretic/main.py
@@ -41,7 +41,6 @@ import os
import random
import time
import warnings
-from dataclasses import asdict
from importlib.metadata import version
from os.path import commonprefix
from pathlib import Path
@@ -53,7 +52,6 @@ import numpy as np
import optuna
import questionary
import torch
-import torch.nn.functional as F
import transformers
from huggingface_hub import HfApi, ModelCard, ModelCardData
from lm_eval.models.huggingface import HFLM
@@ -69,10 +67,15 @@ from rich.table import Table
from rich.text import Text
from rich.traceback import install
-from .analyzer import Analyzer
-from .config import ExportStrategy, QuantizationMethod
+from .config import (
+ DatasetSpecification,
+ ExportStrategy,
+ QuantizationMethod,
+)
from .evaluator import Evaluator
-from .model import AbliterationParameters, Model, get_model_class
+from .model import Model, get_model_class
+from .modifier import load_and_init_modifiers
+from .plugin import Context, is_builtin_plugin
from .reproduce import (
check_environment,
collect_reproducibles,
@@ -85,7 +88,6 @@ from .utils import (
format_exception,
get_file_sha256,
get_readme_intro,
- get_trial_parameters,
is_hf_path,
load_prompts,
print,
@@ -243,16 +245,12 @@ def run():
# FIXME: "Reproduction"/"reproducibility" name inconsistency!
reproduction_information = load_reproduction_information(settings.reproduce)
- # Version 3 is the plugin-era schema, which stores generic scorer
- # `scores`/`baseline_scores`. It is intentionally NOT compatible with the
- # pre-plugin v1/v2 schema (hardcoded refusals/KL `metrics`), so those are
- # rejected rather than silently failing on a missing key later.
- if reproduction_information["version"] != "3":
+ if reproduction_information["version"] != "4":
print(
(
f"[red]Unsupported file format version: [bold]{reproduction_information['version']}[/].[/] "
- "This version of Heretic reads version 3 (plugin scorer) reproduce.json files. "
- "Older files were produced before the scorer-plugin refactor and are not supported. "
+ "This version of Heretic reads version 4 (plugin-based) reproduce.json files. "
+ "Older files were produced before the introduction of the plugin system and are not supported. "
"Please install Heretic 1.4 to use these files."
)
)
@@ -412,14 +410,27 @@ def run():
print()
print_memory_usage()
+ # TODO: Introduce a dedicated dataset setting for test prompts.
+ good_prompts_dataset = DatasetSpecification(
+ dataset="mlabonne/harmless_alpaca",
+ split="train[:400]",
+ column="text",
+ )
+
+ bad_prompts_dataset = DatasetSpecification(
+ dataset="mlabonne/harmful_behaviors",
+ split="train[:400]",
+ column="text",
+ )
+
print()
- print(f"Loading good prompts from [bold]{settings.good_prompts.dataset}[/]...")
- good_prompts = load_prompts(settings, settings.good_prompts)
+ print(f"Loading good prompts from [bold]{good_prompts_dataset.dataset}[/]...")
+ good_prompts = load_prompts(settings, good_prompts_dataset)
print(f"* [bold]{len(good_prompts)}[/] prompts loaded")
print()
- print(f"Loading bad prompts from [bold]{settings.bad_prompts.dataset}[/]...")
- bad_prompts = load_prompts(settings, settings.bad_prompts)
+ print(f"Loading bad prompts from [bold]{bad_prompts_dataset.dataset}[/]...")
+ bad_prompts = load_prompts(settings, bad_prompts_dataset)
print(f"* [bold]{len(bad_prompts)}[/] prompts loaded")
if settings.batch_size == 0:
@@ -534,53 +545,17 @@ def run():
return
print()
- print("Calculating per-layer residual directions...")
+ print("Loading and initializing modifiers...")
+ modifier_entries = load_and_init_modifiers(settings, model)
- needs_full_residuals = settings.print_residual_geometry or settings.plot_residuals
-
- if needs_full_residuals:
- print("* Obtaining residuals for good prompts...")
- good_residuals = model.get_residuals_batched(good_prompts)
- print("* Obtaining residuals for bad prompts...")
- bad_residuals = model.get_residuals_batched(bad_prompts)
-
- good_means = good_residuals.mean(dim=0)
- bad_means = bad_residuals.mean(dim=0)
-
- analyzer = Analyzer(settings, model, good_residuals, bad_residuals)
-
- if settings.print_residual_geometry:
- analyzer.print_residual_geometry()
-
- if settings.plot_residuals:
- analyzer.plot_residuals()
-
- # We don't need the full residuals after computing their means and analyzing geometry.
- del good_residuals, bad_residuals, analyzer
- else:
- print("* Obtaining residual mean for good prompts...")
- good_means = model.get_residuals_mean(good_prompts)
- print("* Obtaining residual mean for bad prompts...")
- bad_means = model.get_residuals_mean(bad_prompts)
-
- residual_directions = F.normalize(bad_means - good_means, p=2, dim=1)
-
- if settings.orthogonalize_direction:
- # Implements https://huggingface.co/blog/grimjim/projected-abliteration
- # Adjust the residual directions so that only the component that is
- # orthogonal to the good direction is subtracted during abliteration.
- good_directions = F.normalize(good_means, p=2, dim=1)
- projection_vector = torch.sum(residual_directions * good_directions, dim=1)
- residual_directions = (
- residual_directions - projection_vector.unsqueeze(1) * good_directions
- )
- residual_directions = F.normalize(residual_directions, p=2, dim=1)
- del good_directions, projection_vector
-
- del good_means, bad_means
+ # `load_and_init_modifiers` currently guarantees that the returned list has exactly one element.
+ # This may change in the future when support for multiple modifiers is implemented.
+ modifier_entry = modifier_entries[0]
+ modifier = modifier_entry.modifier
+ modifier_name = modifier_entry.name
# Clear cache before starting the optimization study.
- # This should free up memory from the objects released with the del statements above.
+ # This should free up memory from temporary objects created while initializing modifiers.
empty_cache()
trial_index = 0
@@ -592,95 +567,21 @@ def run():
trial_index += 1
trial.set_user_attr("index", trial_index)
- direction_scope = trial.suggest_categorical(
- "direction_scope",
- [
- "global",
- "per layer",
- ],
- )
-
- last_layer_index = len(model.get_layers()) - 1
-
- # Discrimination between "harmful" and "harmless" inputs is usually strongest
- # in layers slightly past the midpoint of the layer stack. See the original
- # abliteration paper (https://arxiv.org/abs/2406.11717) for a deeper analysis.
- #
- # Note that we always sample this parameter even though we only need it for
- # the "global" direction scope. The reason is that multivariate TPE doesn't
- # work with conditional or variable-range parameters.
- direction_index = trial.suggest_float(
- "direction_index",
- 0.4 * last_layer_index,
- 0.9 * last_layer_index,
- )
-
- if direction_scope == "per layer":
- direction_index = None
-
- parameters = {}
-
- for component in model.get_abliterable_components():
- # The parameter ranges are based on experiments with various models
- # and much wider ranges. They are not set in stone and might have to be
- # adjusted for future models.
- #
- # The MLP gets a negative lower bound that is then clamped to 0, so the
- # optimizer can fully disable its ablation. The clamp puts a positive
- # probability mass on exactly 0 (the continuous sampler would otherwise
- # reach 0 with probability zero). Ablating the MLP is often unnecessary for
- # removing refusals and tends to damage model intelligence more than
- # ablating the attention output, so on many models the optimum is to leave
- # it (mostly) untouched. See issue #202.
- max_weight_lower_bound = -0.25 if component == "mlp.down_proj" else 0.8
- max_weight = max(
- 0.0,
- trial.suggest_float(
- f"{component}.max_weight",
- max_weight_lower_bound,
- 1.5,
- ),
- )
- max_weight_position = trial.suggest_float(
- f"{component}.max_weight_position",
- 0.6 * last_layer_index,
- 1.0 * last_layer_index,
- )
- # For sampling purposes, min_weight is expressed as a fraction of max_weight,
- # again because multivariate TPE doesn't support variable-range parameters.
- # The value is transformed into the actual min_weight value below.
- min_weight = trial.suggest_float(
- f"{component}.min_weight",
- 0.0,
- 1.0,
- )
- min_weight_distance = trial.suggest_float(
- f"{component}.min_weight_distance",
- 1.0,
- max(0.6 * last_layer_index, 1.0),
- )
-
- parameters[component] = AbliterationParameters(
- max_weight=max_weight,
- max_weight_position=max_weight_position,
- min_weight=(min_weight * max_weight),
- min_weight_distance=min_weight_distance,
- )
-
- trial.set_user_attr("direction_index", direction_index)
- trial.set_user_attr("parameters", {k: asdict(v) for k, v in parameters.items()})
+ ctx = Context(settings=settings, model=model)
+ parameters = modifier.suggest_parameters(ctx, trial)
+ trial.set_user_attr("parameters", parameters.to_dict())
print()
print(
f"[magenta]Running trial [bold]{trial_index}[/] of [bold]{settings.n_trials}[/]...[/]"
)
print("* Parameters:")
- for name, value in get_trial_parameters(trial).items():
+ for name, value in modifier.render_trial_parameters(trial).items():
print(f" * {name} = [bold]{value}[/]")
print("* Resetting model...")
- model.reset_model()
- print("* Abliterating...")
- model.abliterate(residual_directions, direction_index, parameters)
+ modifier.reset_model(ctx)
+ print(f"* Modifying model ({modifier_name})...")
+ modifier.modify_model(ctx, parameters)
print("* Evaluating...")
scores = evaluator.get_scores()
objective_values = evaluator.get_objective_values(scores)
@@ -834,13 +735,10 @@ def run():
trial_loop_active = False
if reproduction_mode:
- parameters = reproduction_information["parameters"]
-
trial = create_trial(
values=[],
user_attrs={
- "direction_index": parameters["direction_index"],
- "parameters": parameters["abliteration_parameters"],
+ "parameters": reproduction_information["parameters"],
"scores": reproduction_information["scores"],
},
)
@@ -910,24 +808,21 @@ def run():
)
print("* Parameters:")
- for name, value in get_trial_parameters(trial).items():
+ for name, value in modifier.render_trial_parameters(trial).items():
print(f" * {name} = [bold]{value}[/]")
# Per https://github.com/huggingface/peft/issues/868#issuecomment-1820642893
# once a LoRA is merged it's expected to be empty. Provide a utility function
# to restore the previous LoRA-ified state.
def reset_trial_model():
+ ctx = Context(settings=settings, model=model)
print("* Resetting model...")
- model.reset_model()
- print("* Abliterating...")
- model.abliterate(
- residual_directions,
- trial.user_attrs["direction_index"],
- {
- k: AbliterationParameters(**v)
- for k, v in trial.user_attrs["parameters"].items()
- },
+ modifier.reset_model(ctx)
+ print(f"* Modifying model ({modifier_name})...")
+ parameters = modifier.parameters_class.from_dict(
+ trial.user_attrs["parameters"]
)
+ modifier.modify_model(ctx, parameters)
reset_trial_model()
@@ -1110,12 +1005,11 @@ def run():
# are available on the Hugging Face Hub (not local paths),
# that all datasets are pinned to a commit (an unpinned
# dataset was likely loaded from a local cache), and that
- # only built-in scorer plugins are used (external plugins
- # cannot be resolved when reproducing).
+ # only built-in plugins are used (external plugins cannot
+ # be resolved when reproducing).
dataset_specifications = [
- settings.good_prompts,
- settings.bad_prompts,
*evaluator.get_dataset_specifications(),
+ *modifier.get_dataset_specifications(),
]
is_reproducible = (
is_hf_path(settings.model)
@@ -1126,6 +1020,8 @@ def run():
)
and evaluator.all_scorers_reproducible()
and evaluator.all_scorers_builtin()
+ and modifier.reproducible
+ and is_builtin_plugin(modifier_entry.config.plugin)
and not reproduction_mode
)
@@ -1225,6 +1121,7 @@ def run():
card.text = (
get_readme_intro(
settings,
+ modifier,
trial,
reproducibility_information != "none",
)
diff --git a/src/heretic/model.py b/src/heretic/model.py
index 9af26b3..2e63032 100644
--- a/src/heretic/model.py
+++ b/src/heretic/model.py
@@ -1,17 +1,11 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2025-2026 Philipp Emanuel Weidmann + contributors
-import math
from contextlib import suppress
-from dataclasses import dataclass
from typing import Any, Type, cast
-import bitsandbytes as bnb
import torch
-import torch.linalg as LA
-import torch.nn.functional as F
from peft import LoraConfig, PeftModel, get_peft_model
-from peft.tuners.lora.layer import Linear
from torch import FloatTensor, LongTensor, Tensor
from torch.nn import Module, ModuleList
from transformers import (
@@ -31,7 +25,7 @@ from transformers.generation import (
GenerateDecoderOnlyOutput, # ty:ignore[possibly-missing-import]
)
-from .config import QuantizationMethod, RowNormalization, Settings
+from .config import QuantizationMethod, Settings
from .system import empty_cache
from .utils import Prompt, batchify, format_exception, print
@@ -47,14 +41,6 @@ def get_model_class(
return AutoModelForCausalLM
-@dataclass
-class AbliterationParameters:
- max_weight: float
- max_weight_position: float
- min_weight: float
- min_weight_distance: float
-
-
class Model:
model: PreTrainedModel | PeftModel
tokenizer: PreTrainedTokenizerBase
@@ -168,11 +154,6 @@ class Model:
if self.model is None:
raise Exception("Failed to load model with all configured dtypes.")
- self._apply_lora()
-
- # LoRA B matrices are initialized to zero by default in PEFT,
- # so we don't need to do anything manually.
-
print(f"* Transformer model with [bold]{len(self.get_layers())}[/] layers")
all_components = {}
@@ -186,7 +167,7 @@ class Model:
for component, count in all_components.items():
print(f" * [bold]{component}[/]: [bold]{count}[/] modules total")
- def _apply_lora(self):
+ def apply_lora(self, lora_rank: int):
# Guard against calling this method at the wrong time.
assert isinstance(self.model, PreTrainedModel)
@@ -211,13 +192,6 @@ class Model:
target_modules = sorted(target_modules_set)
- if self.settings.row_normalization != RowNormalization.FULL:
- # Rank 1 is sufficient for directional ablation without renormalization.
- lora_rank = 1
- else:
- # Row magnitude preservation introduces nonlinear effects.
- lora_rank = self.settings.full_normalization_lora_rank
-
self.peft_config = LoraConfig(
r=lora_rank,
target_modules=target_modules,
@@ -233,11 +207,6 @@ class Model:
# so the result is a PeftModel rather than a PeftMixedModel.
self.model = cast(PeftModel, get_peft_model(self.model, self.peft_config))
- display_targets = sorted({name.rsplit(".", 1)[-1] for name in target_modules})
- print(
- f"* LoRA adapters initialized (target types: {', '.join(display_targets)})"
- )
-
def _get_quantization_config(self, dtype: str) -> BitsAndBytesConfig | None:
"""
Creates quantization config based on settings.
@@ -312,7 +281,7 @@ class Model:
self.needs_reload = True
return merged_model
- def reset_model(self):
+ def reset_model(self) -> bool:
"""
Resets the model to a clean state for the next trial or evaluation.
@@ -321,6 +290,8 @@ class Model:
resets LoRA adapter weights to zero (identity transformation).
- Slow path: If switching models or after merge_and_unload(),
performs full model reload with quantization config.
+
+ Returns True if the fast path was taken.
"""
# If a prior model load was interrupted/cancelled mid-process, self.model will be None.
@@ -333,7 +304,7 @@ class Model:
for name, module in self.model.named_modules():
if "lora_B" in name and hasattr(module, "weight"):
torch.nn.init.zeros_(module.weight)
- return
+ return True
# Purge existing model object from memory to make space.
self.model = None # ty:ignore[invalid-assignment]
@@ -360,10 +331,10 @@ class Model:
**extra_kwargs,
)
- self._apply_lora()
-
self.needs_reload = False
+ return False
+
def get_layers(self) -> ModuleList:
model = self.model
@@ -458,166 +429,6 @@ class Model:
return sorted(components)
- def abliterate(
- self,
- residual_directions: Tensor,
- direction_index: float | None,
- parameters: dict[str, AbliterationParameters],
- ):
- if direction_index is None:
- residual_direction = None
- else:
- # The index must be shifted by 1 because the first element
- # of residual_directions is the direction for the embeddings.
- weight, index = math.modf(direction_index + 1)
- residual_direction = F.normalize(
- residual_directions[int(index)].lerp(
- residual_directions[int(index) + 1],
- weight,
- ),
- p=2,
- dim=0,
- )
-
- # Note that some implementations of abliteration also orthogonalize
- # the embedding matrix, but it's unclear if that has any benefits.
- for layer_index in range(len(self.get_layers())):
- for component, modules in self.get_layer_modules(layer_index).items():
- params = parameters[component]
-
- # Type inference fails here for some reason.
- distance = cast(float, abs(layer_index - params.max_weight_position))
-
- # Don't orthogonalize layers that are more than
- # min_weight_distance away from max_weight_position.
- if distance > params.min_weight_distance:
- continue
-
- # Interpolate linearly between max_weight and min_weight
- # over min_weight_distance.
- weight = params.max_weight + (distance / params.min_weight_distance) * (
- params.min_weight - params.max_weight
- )
-
- # A weight of 0 disables this component's ablation. reset_model() has
- # already left the adapter at identity, so abort before the otherwise
- # wasteful decomposition (which would also be operating on a zero matrix).
- if weight == 0:
- continue
-
- if residual_direction is None:
- # The index must be shifted by 1 because the first element
- # of residual_directions is the direction for the embeddings.
- layer_residual_direction = residual_directions[layer_index + 1]
- else:
- layer_residual_direction = residual_direction
-
- for module in modules:
- # FIXME: This cast is potentially invalid, because the program logic
- # does not guarantee that the module is of type Linear, and in fact
- # the retrieved modules might not conform to the interface assumed
- # below (though they do in practice). However, this is difficult
- # to fix cleanly, because get_layer_modules is called twice on
- # different model configurations, and PEFT employs different
- # module types depending on the chosen quantization.
- module = cast(Linear, module)
-
- # LoRA abliteration: delta W = -lambda * v * (v^T W)
- # lora_B = -lambda * v
- # lora_A = v^T W
-
- # Use the FP32 residual direction directly (no downcast/upcast)
- # and move to the correct device.
- v = layer_residual_direction.to(module.weight.device)
-
- # Get W (dequantize if necessary).
- #
- # FIXME: This cast is valid only under the assumption that the original
- # module wrapped by the LoRA adapter has a weight attribute.
- # See the comment above for why this is currently not guaranteed.
- base_weight = cast(Tensor, module.base_layer.weight)
- quant_state = getattr(base_weight, "quant_state", None)
-
- if quant_state is None:
- W = base_weight.to(torch.float32)
- else:
- # 4-bit quantization.
- # This cast is always valid. Type inference fails here because the
- # bnb.functional module is not found by ty for some reason.
- W = cast(
- Tensor,
- bnb.functional.dequantize_4bit( # ty:ignore[possibly-missing-attribute]
- base_weight.data,
- quant_state,
- ).to(torch.float32),
- )
-
- # Flatten weight matrix to (out_features, in_features).
- W = W.view(W.shape[0], -1)
-
- if self.settings.row_normalization == RowNormalization.FULL:
- # Keep a reference to the original weight matrix so we can subtract it later.
- W_org = W
-
- if self.settings.row_normalization != RowNormalization.NONE:
- # Get the row norms.
- W_row_norms = LA.vector_norm(W, dim=1, keepdim=True)
- # Normalize the weight matrix along the rows.
- W = F.normalize(W, p=2, dim=1)
-
- # Calculate lora_A = v^T W
- # v is (d_out,), W is (d_out, d_in)
- # v @ W -> (d_in,)
- lora_A = (v @ W).view(1, -1)
-
- # Calculate lora_B = -weight * v
- # v is (d_out,)
- lora_B = (-weight * v).view(-1, 1)
-
- if self.settings.row_normalization == RowNormalization.PRE:
- # Make the LoRA adapter apply to the original weight matrix.
- lora_B = W_row_norms * lora_B
- elif self.settings.row_normalization == RowNormalization.FULL:
- # Approximates https://huggingface.co/blog/grimjim/norm-preserving-biprojected-abliteration
- W = W + lora_B @ lora_A
- # Normalize the adjusted weight matrix along the rows.
- W = F.normalize(W, p=2, dim=1)
- # Restore the original row norms of the weight matrix.
- W = W * W_row_norms
- # Subtract the original matrix to turn W into a delta.
- W = W - W_org
- # Use a low-rank SVD to get an approximation of the matrix.
- r = self.peft_config.r
-
- # svd_lowrank is randomized:
- # https://github.com/pytorch/pytorch/blob/20919052303c0b5ba87f8bf7e19237dc33ab09d3/torch/_lowrank.py#L108-L109
- # Reseed immediately before the call so restoring a trial is independent of RNG history.
- torch.manual_seed(self.settings.seed)
- # "It's safe to call this function if CUDA is not available;
- # in that case, it is silently ignored."
- torch.cuda.manual_seed_all(self.settings.seed) # ty:ignore[invalid-argument-type]
- U, S, Vh = torch.svd_lowrank(W, q=2 * r + 4, niter=6)
-
- # Truncate it to the part we want to store in the LoRA adapter.
- # Note: svd_lowrank actually returns V, so transpose it to get Vh.
- U = U[:, :r]
- S = S[:r]
- Vh = Vh[:, :r].T
- # Transfer it into the LoRA adapter components. Split the singular values
- # evenly between the two components to keep their norms balanced and avoid
- # potential issues with numerical stability.
- sqrt_S = torch.sqrt(S)
- lora_B = U @ torch.diag(sqrt_S)
- lora_A = torch.diag(sqrt_S) @ Vh
-
- # Assign to adapters. The adapter name is "default", because that's
- # what PEFT uses when no name is explicitly specified, as above.
- # These casts are therefore valid.
- weight_A = cast(Tensor, module.lora_A["default"].weight)
- weight_B = cast(Tensor, module.lora_B["default"].weight)
- weight_A.data = lora_A.to(weight_A.dtype)
- weight_B.data = lora_B.to(weight_B.dtype)
-
def generate(
self,
prompts: list[Prompt],
@@ -691,6 +502,7 @@ class Model:
skip_special_tokens: bool = False,
) -> list[str]:
responses = []
+
for batch in batchify(prompts, self.settings.batch_size):
for response in self.get_responses(
batch,
@@ -700,7 +512,11 @@ class Model:
return responses
- def get_residuals(self, prompts: list[Prompt]) -> Tensor:
+ def get_residuals(
+ self,
+ prompts: list[Prompt],
+ winsorization_quantile: float = 1.0,
+ ) -> Tensor:
# We only generate one token, and we return the residual vectors
# at that token position, for each prompt and layer.
_, outputs = self.generate(
@@ -734,13 +550,13 @@ class Model:
# problems during calculations involving residual vectors.
residuals = residuals.to(torch.float32)
- if 0 <= self.settings.winsorization_quantile < 1:
+ if 0 <= winsorization_quantile < 1:
# Apply symmetric winsorization to each layer of the per-prompt residuals.
abs_residuals = torch.abs(residuals)
# Get the (prompt, layer, 1) quantiles of the (prompt, layer, component) residuals.
thresholds = torch.quantile(
abs_residuals,
- self.settings.winsorization_quantile,
+ winsorization_quantile,
dim=2,
keepdim=True,
)
@@ -752,15 +568,28 @@ class Model:
return residuals
- def get_residuals_batched(self, prompts: list[Prompt]) -> Tensor:
+ def get_residuals_batched(
+ self,
+ prompts: list[Prompt],
+ winsorization_quantile: float = 1.0,
+ ) -> Tensor:
residuals = []
for batch in batchify(prompts, self.settings.batch_size):
- residuals.append(self.get_residuals(batch))
+ residuals.append(
+ self.get_residuals(
+ batch,
+ winsorization_quantile=winsorization_quantile,
+ )
+ )
return torch.cat(residuals, dim=0)
- def get_residuals_mean(self, prompts: list[Prompt]) -> Tensor:
+ def get_residuals_mean(
+ self,
+ prompts: list[Prompt],
+ winsorization_quantile: float = 1.0,
+ ) -> Tensor:
if not prompts:
raise ValueError("prompts must not be empty")
@@ -768,7 +597,10 @@ class Model:
total_count = 0
for batch in batchify(prompts, self.settings.batch_size):
- batch_residuals = self.get_residuals(batch)
+ batch_residuals = self.get_residuals(
+ batch,
+ winsorization_quantile=winsorization_quantile,
+ )
# Accumulate in high precision on CPU to reduce peak VRAM usage.
batch_sum = batch_residuals.sum(dim=0, dtype=torch.float64).cpu()
diff --git a/src/heretic/modifier.py b/src/heretic/modifier.py
index a82c276..f17ad97 100644
--- a/src/heretic/modifier.py
+++ b/src/heretic/modifier.py
@@ -2,16 +2,34 @@
# Copyright (C) 2025-2026 Philipp Emanuel Weidmann + contributors
from abc import ABC, abstractmethod
-from typing import Generic, TypeVar
+from dataclasses import dataclass
+from typing import Any, Generic, Protocol, TypeVar, get_args
from optuna import Trial
+from optuna.trial import FrozenTrial
from pydantic import BaseModel
-from heretic.plugin import Context, Plugin
+from .config import (
+ ModifierConfig,
+)
+from .config import (
+ Settings as HereticSettings,
+)
+from .model import Model
+from .plugin import Context, Plugin, load_plugin
+from .utils import print
-from .config import Settings as HereticSettings
-Parameters = TypeVar("Parameters")
+class Serializable(Protocol):
+ def to_dict(self) -> dict[str, Any]: ...
+
+ def to_presentation_dict(self) -> dict[str, str]: ...
+
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> "Serializable": ...
+
+
+Parameters = TypeVar("Parameters", bound=Serializable)
class Modifier(Generic[Parameters], Plugin, ABC):
@@ -23,6 +41,23 @@ class Modifier(Generic[Parameters], Plugin, ABC):
Examples: Standard abliteration, ARA, SOMA, etc.
"""
+ @property
+ def modifier_name(self) -> str:
+ """
+ The name of the modifier.
+ This is what shows up in the CLI and Markdown on HF.
+ """
+ return self.__class__.__name__
+
+ @property
+ def parameters_class(self) -> type[Parameters]:
+ """
+ The class of the modifier's parameters type.
+ """
+ base_class = self.__class__.__orig_bases__[0] # ty:ignore[unresolved-attribute]
+ generic_type = get_args(base_class)[0]
+ return generic_type
+
def __init__(
self,
heretic_settings: HereticSettings,
@@ -51,3 +86,100 @@ class Modifier(Generic[Parameters], Plugin, ABC):
Reset the model (obtainable via `ctx.get_model()`),
undoing any changes made by `modify_model`.
"""
+
+ def render_trial_parameters(self, trial: Trial | FrozenTrial) -> dict[str, str]:
+ """
+ Transform the names and values of the modifier's parameters
+ that are contained in the trial's user attributes into a form
+ suitable for presentation.
+ """
+ return self.parameters_class.from_dict(
+ trial.user_attrs["parameters"]
+ ).to_presentation_dict()
+
+
+@dataclass
+class ModifierEntry:
+ modifier: Modifier[Any]
+ name: str
+ config: ModifierConfig
+
+
+def load_and_init_modifiers(
+ settings: HereticSettings,
+ model: Model,
+) -> list[ModifierEntry]:
+ """
+ Load and instantiate all configured modifier plugins,
+ then runs their initialization hooks.
+ """
+ modifier_configs = settings.modifiers
+ if not modifier_configs:
+ raise ValueError("No modifiers configured. Set 'modifiers' in config.toml")
+ if len(modifier_configs) > 1:
+ raise ValueError("Using multiple modifiers is not yet supported")
+
+ modifier_keys: set[str] = set()
+
+ modifier_entries: list[ModifierEntry] = []
+
+ # Resolve plugin classes from names and validate.
+ for config in modifier_configs:
+ modifier_cls = load_plugin(name=config.plugin, base_class=Modifier)
+ modifier_cls.validate_contract()
+
+ print(
+ f"* Loaded: [bold]{modifier_cls.__name__}{' - ' + config.instance_name if config.instance_name else ''}[/bold]"
+ )
+
+ # Instantiate modifiers.
+ instance_name = config.instance_name or None
+
+ raw_settings = modifier_cls.get_settings_raw(
+ settings.model_extra,
+ "modifier",
+ instance_name,
+ )
+ modifier_settings: BaseModel | None = modifier_cls.validate_settings(
+ raw_settings
+ )
+
+ modifier = modifier_cls(
+ heretic_settings=settings,
+ settings=modifier_settings,
+ )
+
+ # External labeling key: ensures multiple instances can coexist.
+ # Uses underscore to match the TOML namespace format (`modifier._`).
+ modifier_key = (
+ modifier_cls.__name__
+ if not instance_name
+ else f"{modifier_cls.__name__}_{instance_name}"
+ )
+ if modifier_key in modifier_keys:
+ raise ValueError(
+ f"Duplicate modifier instance name: {modifier_key}. "
+ "Give each instance a unique `instance_name`."
+ )
+ modifier_keys.add(modifier_key)
+
+ modifier_instance_name = (
+ f"{modifier.modifier_name} - {instance_name}"
+ if instance_name
+ else modifier.modifier_name
+ )
+ modifier_entries.append(
+ ModifierEntry(
+ modifier=modifier,
+ config=config,
+ name=modifier_instance_name,
+ )
+ )
+
+ # Run modifier init hooks.
+ ctx = Context(settings=settings, model=model)
+
+ for entry in modifier_entries:
+ entry.modifier.init(ctx)
+
+ return modifier_entries
diff --git a/src/heretic/modifiers/__init__.py b/src/heretic/modifiers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/heretic/modifiers/abliteration.py b/src/heretic/modifiers/abliteration.py
new file mode 100644
index 0000000..fea5646
--- /dev/null
+++ b/src/heretic/modifiers/abliteration.py
@@ -0,0 +1,473 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# Copyright (C) 2025-2026 Philipp Emanuel Weidmann + contributors
+
+import math
+from dataclasses import asdict, dataclass
+from enum import Enum
+from typing import Any, cast
+
+import bitsandbytes as bnb
+import torch
+import torch.linalg as LA
+import torch.nn.functional as F
+from optuna import Trial
+from peft.tuners.lora.layer import Linear
+from pydantic import (
+ BaseModel,
+ Field,
+ PositiveInt,
+)
+from torch import Tensor
+
+from heretic.config import DatasetSpecification
+from heretic.modifier import Context, Modifier, Serializable
+from heretic.utils import print
+
+
+@dataclass
+class WeightDistribution:
+ max_weight: float
+ max_weight_position: float
+ min_weight: float
+ min_weight_distance: float
+
+
+@dataclass
+class Parameters(Serializable):
+ direction_index: float | None
+ weight_distributions: dict[str, WeightDistribution]
+
+ def to_dict(self) -> dict[str, Any]:
+ return asdict(self)
+
+ def to_presentation_dict(self) -> dict[str, str]:
+ parameters = {}
+
+ parameters["direction_index"] = (
+ "per layer"
+ if (self.direction_index is None)
+ else f"{self.direction_index:.2f}"
+ )
+
+ for component, weight_distribution in self.weight_distributions.items():
+ for name, value in asdict(weight_distribution).items():
+ parameters[f"{component}.{name}"] = f"{value:.2f}"
+
+ return parameters
+
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> "Serializable":
+ return Parameters(
+ direction_index=data["direction_index"],
+ weight_distributions={
+ component: WeightDistribution(**weight_distribution)
+ for component, weight_distribution in data[
+ "weight_distributions"
+ ].items()
+ },
+ )
+
+
+class RowNormalization(str, Enum):
+ NONE = "none"
+ PRE = "pre"
+ # POST = "post" # Theoretically possible, but provides no advantage.
+ FULL = "full"
+
+
+class Settings(BaseModel):
+ good_prompts: DatasetSpecification = Field(
+ default=DatasetSpecification(
+ dataset="mlabonne/harmless_alpaca",
+ split="train[:400]",
+ column="text",
+ ),
+ description="Dataset of prompts that tend to produce desirable responses.",
+ )
+
+ bad_prompts: DatasetSpecification = Field(
+ default=DatasetSpecification(
+ dataset="mlabonne/harmful_behaviors",
+ split="train[:400]",
+ column="text",
+ ),
+ description="Dataset of prompts that tend to produce undesirable responses.",
+ )
+
+ orthogonalize_direction: bool = Field(
+ default=True,
+ description=(
+ "Whether to adjust the residual directions so that only the component that is "
+ "orthogonal to the good direction is subtracted during abliteration."
+ ),
+ )
+
+ row_normalization: RowNormalization = Field(
+ default=RowNormalization.FULL,
+ description=(
+ "How to apply row normalization of the weights. Options: "
+ '"none" (no normalization), '
+ '"pre" (compute LoRA adapter relative to row-normalized weights), '
+ '"full" (like "pre", but renormalizes to preserve original row magnitudes).'
+ ),
+ )
+
+ full_normalization_lora_rank: PositiveInt = Field(
+ default=3,
+ description=(
+ 'The rank of the LoRA adapter to use when "full" row normalization is used. '
+ "Row magnitude preservation is approximate due to non-linear effects, "
+ "and this determines the rank of that approximation. Higher ranks produce "
+ "larger output files and may slow down evaluation."
+ ),
+ )
+
+ winsorization_quantile: float = Field(
+ default=1.0,
+ description=(
+ "The symmetric winsorization to apply to the per-prompt, per-layer residual vectors, "
+ "expressed as the quantile to clamp to (between 0 and 1). Disabled by default. "
+ 'This can tame so-called "massive activations" that occur in some models. '
+ "Example: winsorization_quantile = 0.95 computes the 0.95-quantile of the absolute values "
+ "of the components, then clamps the magnitudes of all components to that quantile."
+ ),
+ )
+
+
+class Abliteration(Modifier[Parameters]):
+ settings: Settings
+
+ @property
+ def reproducible(self) -> bool:
+ return True
+
+ @property
+ def modifier_name(self) -> str:
+ if (
+ self.settings.orthogonalize_direction
+ and self.settings.row_normalization == RowNormalization.FULL
+ ):
+ return "Magnitude-Preserving Orthogonal Ablation (MPOA)"
+ elif self.settings.orthogonalize_direction:
+ return "Projected Abliteration"
+ else:
+ return "Abliteration"
+
+ def init(self, ctx: Context) -> None:
+ model = ctx.get_model()
+
+ print()
+ print(
+ f"Loading good prompts from [bold]{self.settings.good_prompts.dataset}[/]..."
+ )
+ self.good_prompts = ctx.load_prompts(self.settings.good_prompts)
+ print(f"* [bold]{len(self.good_prompts)}[/] prompts loaded")
+
+ print()
+ print(
+ f"Loading bad prompts from [bold]{self.settings.bad_prompts.dataset}[/]..."
+ )
+ self.bad_prompts = ctx.load_prompts(self.settings.bad_prompts)
+ print(f"* [bold]{len(self.bad_prompts)}[/] prompts loaded")
+
+ print()
+ print("Calculating per-layer residual directions...")
+
+ print("* Obtaining residual mean for good prompts...")
+ good_means = model.get_residuals_mean(
+ self.good_prompts,
+ winsorization_quantile=self.settings.winsorization_quantile,
+ )
+ print("* Obtaining residual mean for bad prompts...")
+ bad_means = model.get_residuals_mean(
+ self.bad_prompts,
+ winsorization_quantile=self.settings.winsorization_quantile,
+ )
+
+ self.residual_directions = F.normalize(
+ bad_means - good_means,
+ p=2,
+ dim=1,
+ )
+
+ if self.settings.orthogonalize_direction:
+ # Implements https://huggingface.co/blog/grimjim/projected-abliteration
+ # Adjust the residual directions so that only the component that is
+ # orthogonal to the good direction is subtracted during abliteration.
+ good_directions = F.normalize(
+ good_means,
+ p=2,
+ dim=1,
+ )
+ projection_vector = torch.sum(
+ self.residual_directions * good_directions,
+ dim=1,
+ )
+ self.residual_directions = (
+ self.residual_directions
+ - projection_vector.unsqueeze(1) * good_directions
+ )
+ self.residual_directions = F.normalize(
+ self.residual_directions,
+ p=2,
+ dim=1,
+ )
+
+ if self.settings.row_normalization != RowNormalization.FULL:
+ # Rank 1 is sufficient for directional ablation without renormalization.
+ self.lora_rank = 1
+ else:
+ # Row magnitude preservation introduces nonlinear effects.
+ self.lora_rank = self.settings.full_normalization_lora_rank
+
+ # LoRA B matrices are initialized to zero by default in PEFT,
+ # so we don't need to do anything manually.
+ model.apply_lora(self.lora_rank)
+
+ def suggest_parameters(self, ctx: Context, trial: Trial) -> Parameters:
+ model = ctx.get_model()
+
+ direction_scope = trial.suggest_categorical(
+ "direction_scope",
+ [
+ "global",
+ "per layer",
+ ],
+ )
+
+ last_layer_index = len(model.get_layers()) - 1
+
+ # Discrimination between "harmful" and "harmless" inputs is usually strongest
+ # in layers slightly past the midpoint of the layer stack. See the original
+ # abliteration paper (https://arxiv.org/abs/2406.11717) for a deeper analysis.
+ #
+ # Note that we always sample this parameter even though we only need it for
+ # the "global" direction scope. The reason is that multivariate TPE doesn't
+ # work with conditional or variable-range parameters.
+ direction_index = trial.suggest_float(
+ "direction_index",
+ 0.4 * last_layer_index,
+ 0.9 * last_layer_index,
+ )
+
+ if direction_scope == "per layer":
+ direction_index = None
+
+ weight_distributions = {}
+
+ for component in model.get_abliterable_components():
+ # The parameter ranges are based on experiments with various models
+ # and much wider ranges. They are not set in stone and might have to be
+ # adjusted for future models.
+ #
+ # The MLP gets a negative lower bound that is then clamped to 0, so the
+ # optimizer can fully disable its ablation. The clamp puts a positive
+ # probability mass on exactly 0 (the continuous sampler would otherwise
+ # reach 0 with probability zero). Ablating the MLP is often unnecessary for
+ # removing refusals and tends to damage model intelligence more than
+ # ablating the attention output, so on many models the optimum is to leave
+ # it (mostly) untouched. See issue #202.
+ max_weight_lower_bound = -0.25 if component == "mlp.down_proj" else 0.8
+ max_weight = max(
+ 0.0,
+ trial.suggest_float(
+ f"{component}.max_weight",
+ max_weight_lower_bound,
+ 1.5,
+ ),
+ )
+ max_weight_position = trial.suggest_float(
+ f"{component}.max_weight_position",
+ 0.6 * last_layer_index,
+ 1.0 * last_layer_index,
+ )
+ # For sampling purposes, min_weight is expressed as a fraction of max_weight,
+ # again because multivariate TPE doesn't support variable-range parameters.
+ # The value is transformed into the actual min_weight value below.
+ min_weight = trial.suggest_float(
+ f"{component}.min_weight",
+ 0.0,
+ 1.0,
+ )
+ min_weight_distance = trial.suggest_float(
+ f"{component}.min_weight_distance",
+ 1.0,
+ max(0.6 * last_layer_index, 1.0),
+ )
+
+ weight_distributions[component] = WeightDistribution(
+ max_weight=max_weight,
+ max_weight_position=max_weight_position,
+ min_weight=(min_weight * max_weight),
+ min_weight_distance=min_weight_distance,
+ )
+
+ return Parameters(
+ direction_index=direction_index,
+ weight_distributions=weight_distributions,
+ )
+
+ def modify_model(self, ctx: Context, parameters: Parameters) -> None:
+ model = ctx.get_model()
+
+ if parameters.direction_index is None:
+ residual_direction = None
+ else:
+ # The index must be shifted by 1 because the first element
+ # of residual_directions is the direction for the embeddings.
+ weight, index = math.modf(parameters.direction_index + 1)
+ residual_direction = F.normalize(
+ self.residual_directions[int(index)].lerp(
+ self.residual_directions[int(index) + 1],
+ weight,
+ ),
+ p=2,
+ dim=0,
+ )
+
+ # Note that some implementations of abliteration also orthogonalize
+ # the embedding matrix, but it's unclear if that has any benefits.
+ for layer_index in range(len(model.get_layers())):
+ for component, modules in model.get_layer_modules(layer_index).items():
+ weight_distribution = parameters.weight_distributions[component]
+
+ # Type inference fails here for some reason.
+ distance = cast(
+ float, abs(layer_index - weight_distribution.max_weight_position)
+ )
+
+ # Don't orthogonalize layers that are more than
+ # min_weight_distance away from max_weight_position.
+ if distance > weight_distribution.min_weight_distance:
+ continue
+
+ # Interpolate linearly between max_weight and min_weight
+ # over min_weight_distance.
+ weight = weight_distribution.max_weight + (
+ distance / weight_distribution.min_weight_distance
+ ) * (weight_distribution.min_weight - weight_distribution.max_weight)
+
+ # A weight of 0 disables this component's ablation. reset_model() has
+ # already left the adapter at identity, so abort before the otherwise
+ # wasteful decomposition (which would also be operating on a zero matrix).
+ if weight == 0:
+ continue
+
+ if residual_direction is None:
+ # The index must be shifted by 1 because the first element
+ # of residual_directions is the direction for the embeddings.
+ layer_residual_direction = self.residual_directions[layer_index + 1]
+ else:
+ layer_residual_direction = residual_direction
+
+ for module in modules:
+ # FIXME: This cast is potentially invalid, because the program logic
+ # does not guarantee that the module is of type Linear, and in fact
+ # the retrieved modules might not conform to the interface assumed
+ # below (though they do in practice). However, this is difficult
+ # to fix cleanly, because get_layer_modules is called twice on
+ # different model configurations, and PEFT employs different
+ # module types depending on the chosen quantization.
+ module = cast(Linear, module)
+
+ # LoRA abliteration: delta W = -lambda * v * (v^T W)
+ # lora_B = -lambda * v
+ # lora_A = v^T W
+
+ # Use the FP32 residual direction directly (no downcast/upcast)
+ # and move to the correct device.
+ v = layer_residual_direction.to(module.weight.device)
+
+ # Get W (dequantize if necessary).
+ #
+ # FIXME: This cast is valid only under the assumption that the original
+ # module wrapped by the LoRA adapter has a weight attribute.
+ # See the comment above for why this is currently not guaranteed.
+ base_weight = cast(Tensor, module.base_layer.weight)
+ quant_state = getattr(base_weight, "quant_state", None)
+
+ if quant_state is None:
+ W = base_weight.to(torch.float32)
+ else:
+ # 4-bit quantization.
+ # This cast is always valid. Type inference fails here because the
+ # bnb.functional module is not found by ty for some reason.
+ W = cast(
+ Tensor,
+ bnb.functional.dequantize_4bit( # ty:ignore[possibly-missing-attribute]
+ base_weight.data,
+ quant_state,
+ ).to(torch.float32),
+ )
+
+ # Flatten weight matrix to (out_features, in_features).
+ W = W.view(W.shape[0], -1)
+
+ if self.settings.row_normalization == RowNormalization.FULL:
+ # Keep a reference to the original weight matrix so we can subtract it later.
+ W_org = W
+
+ if self.settings.row_normalization != RowNormalization.NONE:
+ # Get the row norms.
+ W_row_norms = LA.vector_norm(W, dim=1, keepdim=True)
+ # Normalize the weight matrix along the rows.
+ W = F.normalize(W, p=2, dim=1)
+
+ # Calculate lora_A = v^T W
+ # v is (d_out,), W is (d_out, d_in)
+ # v @ W -> (d_in,)
+ lora_A = (v @ W).view(1, -1)
+
+ # Calculate lora_B = -weight * v
+ # v is (d_out,)
+ lora_B = (-weight * v).view(-1, 1)
+
+ if self.settings.row_normalization == RowNormalization.PRE:
+ # Make the LoRA adapter apply to the original weight matrix.
+ lora_B = W_row_norms * lora_B
+ elif self.settings.row_normalization == RowNormalization.FULL:
+ # Approximates https://huggingface.co/blog/grimjim/norm-preserving-biprojected-abliteration
+ W = W + lora_B @ lora_A
+ # Normalize the adjusted weight matrix along the rows.
+ W = F.normalize(W, p=2, dim=1)
+ # Restore the original row norms of the weight matrix.
+ W = W * W_row_norms
+ # Subtract the original matrix to turn W into a delta.
+ W = W - W_org
+ # Use a low-rank SVD to get an approximation of the matrix.
+ r = model.peft_config.r
+
+ # svd_lowrank is randomized:
+ # https://github.com/pytorch/pytorch/blob/20919052303c0b5ba87f8bf7e19237dc33ab09d3/torch/_lowrank.py#L108-L109
+ # Reseed immediately before the call so restoring a trial is independent of RNG history.
+ torch.manual_seed(self.heretic_settings.seed)
+ # "It's safe to call this function if CUDA is not available;
+ # in that case, it is silently ignored."
+ torch.cuda.manual_seed_all(self.heretic_settings.seed) # ty:ignore[invalid-argument-type]
+ U, S, Vh = torch.svd_lowrank(W, q=2 * r + 4, niter=6)
+
+ # Truncate it to the part we want to store in the LoRA adapter.
+ # Note: svd_lowrank actually returns V, so transpose it to get Vh.
+ U = U[:, :r]
+ S = S[:r]
+ Vh = Vh[:, :r].T
+ # Transfer it into the LoRA adapter components. Split the singular values
+ # evenly between the two components to keep their norms balanced and avoid
+ # potential issues with numerical stability.
+ sqrt_S = torch.sqrt(S)
+ lora_B = U @ torch.diag(sqrt_S)
+ lora_A = torch.diag(sqrt_S) @ Vh
+
+ # Assign to adapters. The adapter name is "default", because that's
+ # what PEFT uses when no name is explicitly specified, as above.
+ # These casts are therefore valid.
+ weight_A = cast(Tensor, module.lora_A["default"].weight)
+ weight_B = cast(Tensor, module.lora_B["default"].weight)
+ weight_A.data = lora_A.to(weight_A.dtype)
+ weight_B.data = lora_B.to(weight_B.dtype)
+
+ def reset_model(self, ctx: Context) -> None:
+ model = ctx.get_model()
+ fast_path = model.reset_model()
+ if not fast_path:
+ model.apply_lora(self.lora_rank)
diff --git a/src/heretic/plugin.py b/src/heretic/plugin.py
index 0c62845..f458f13 100644
--- a/src/heretic/plugin.py
+++ b/src/heretic/plugin.py
@@ -13,17 +13,17 @@ from typing import Annotated, Any, TypeVar, Union, get_args, get_origin, get_typ
from pydantic import BaseModel
from torch import Tensor
-from heretic.utils import Prompt, load_prompts
-
from .config import DatasetSpecification
from .config import Settings as HereticSettings
from .model import Model
+from .utils import Prompt, deep_merge_dicts, load_prompts
T = TypeVar("T")
def get_plugin_namespace(
- model_extra: dict[str, Any] | None, namespace: str
+ model_extra: dict[str, Any] | None,
+ namespace: str,
) -> dict[str, Any]:
"""
Returns the config dict from the `[]` TOML table.
@@ -51,7 +51,7 @@ def is_builtin_plugin(name: str) -> bool:
plugins (file paths or third-party import paths) disable the reproducibility
offer during upload.
"""
- return name.startswith("heretic.scorers.")
+ return name.startswith("heretic.")
def load_plugin(
@@ -286,9 +286,46 @@ class Plugin:
)
return model
+ @classmethod
+ def get_settings_raw(
+ cls,
+ model_extra: dict[str, Any] | None,
+ top_namespace: str,
+ instance_name: str | None,
+ ) -> dict[str, Any]:
+ """
+ Build the raw settings dict for a plugin class and optional instance.
+
+ Config rules:
+ - Base settings live in `[.ClassName]` (applies to all instances).
+ - Instance overrides live in `[.ClassName_]` (preferred).
+ - Only merge/validate keys that exist in the plugin Settings schema.
+ """
+ settings_model = cls.get_settings_model()
+ if settings_model is None:
+ # No settings schema: nothing to merge/validate.
+ return {}
+
+ class_name = cls.__name__
+
+ namespaces = [f"{top_namespace}.{class_name}"]
+ if instance_name:
+ namespaces.append(f"{top_namespace}.{class_name}_{instance_name}")
+
+ merged_settings: dict[str, Any] = {}
+ allowed_keys = set(settings_model.model_fields.keys())
+
+ for namespace in namespaces:
+ raw_table = get_plugin_namespace(model_extra, namespace)
+ filtered = {k: v for k, v in raw_table.items() if k in allowed_keys}
+ merged_settings = deep_merge_dicts(merged_settings, filtered)
+
+ return merged_settings
+
@classmethod
def validate_settings(
- cls, raw_namespace: dict[str, Any] | None
+ cls,
+ raw_namespace: dict[str, Any] | None,
) -> BaseModel | None:
"""
Validates plugin settings for this plugin class.
@@ -301,6 +338,19 @@ class Plugin:
return None
return settings_model.model_validate(raw_namespace or {})
+ def get_dataset_specifications(self) -> list[DatasetSpecification]:
+ """
+ Collect the dataset specifications declared in the settings
+ of the plugin.
+ """
+ if self.settings is None:
+ return []
+ specifications = []
+ for value in dict(self.settings).values():
+ if isinstance(value, DatasetSpecification):
+ specifications.append(value)
+ return specifications
+
def init(self, ctx: Context) -> None:
"""
Runs before the plugin's main functionality.
diff --git a/src/heretic/scorer.py b/src/heretic/scorer.py
index b433ec4..5edd0de 100644
--- a/src/heretic/scorer.py
+++ b/src/heretic/scorer.py
@@ -6,9 +6,8 @@ from dataclasses import dataclass
from pydantic import BaseModel
-from heretic.plugin import Context, Plugin
-
from .config import Settings as HereticSettings
+from .plugin import Context, Plugin
@dataclass
diff --git a/src/heretic/scorers/keyword_rate.py b/src/heretic/scorers/keyword_rate.py
index 4a936db..b0b3f59 100644
--- a/src/heretic/scorers/keyword_rate.py
+++ b/src/heretic/scorers/keyword_rate.py
@@ -50,11 +50,6 @@ class Settings(BaseModel):
description="Name that describes what the configured keyword rate measures.",
)
- keyword_markers: list[str] = Field(
- default=REFUSAL_MARKERS,
- description="Strings whose presence in a response (case insensitive) identifies the response as a keyword match.",
- )
-
prompts: DatasetSpecification = Field(
default=DatasetSpecification(
dataset="mlabonne/harmful_behaviors",
@@ -64,6 +59,11 @@ class Settings(BaseModel):
description="Dataset of prompts to evaluate the keyword match rate on.",
)
+ keyword_markers: list[str] = Field(
+ default=REFUSAL_MARKERS,
+ description="Strings whose presence in a response (case insensitive) identifies the response as a keyword match.",
+ )
+
print_responses: bool = Field(
default=False,
description="Whether to print prompt/response pairs when counting keyword matches.",
diff --git a/src/heretic/scorers/kl_divergence.py b/src/heretic/scorers/kl_divergence.py
index 5cf1747..1739ddb 100644
--- a/src/heretic/scorers/kl_divergence.py
+++ b/src/heretic/scorers/kl_divergence.py
@@ -17,7 +17,7 @@ class Settings(BaseModel):
split="test[:100]",
column="text",
),
- description="Prompt dataset used to measure KL divergence from original model.",
+ description="Dataset of prompts used to measure KL divergence from original model.",
)
diff --git a/src/heretic/utils.py b/src/heretic/utils.py
index 5107394..a8d3595 100644
--- a/src/heretic/utils.py
+++ b/src/heretic/utils.py
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2025-2026 Philipp Emanuel Weidmann + contributors
+from __future__ import annotations
+
import hashlib
import json
import os
@@ -11,7 +13,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from importlib.metadata import version
from pathlib import Path
-from typing import Any, TypeVar
+from typing import TYPE_CHECKING, Any, TypeVar
import huggingface_hub
import tomli_w
@@ -38,6 +40,10 @@ from .system import (
is_xpu_available,
)
+if TYPE_CHECKING:
+ from .modifier import Modifier
+
+
T = TypeVar("T")
@@ -257,23 +263,9 @@ def batchify(items: list[T], batch_size: int) -> list[list[T]]:
return [items[i : i + batch_size] for i in range(0, len(items), batch_size)]
-def get_trial_parameters(trial: Trial | FrozenTrial) -> dict[str, str]:
- params = {}
-
- direction_index = trial.user_attrs["direction_index"]
- params["direction_index"] = (
- "per layer" if (direction_index is None) else f"{direction_index:.2f}"
- )
-
- for component, parameters in trial.user_attrs["parameters"].items():
- for name, value in parameters.items():
- params[f"{component}.{name}"] = f"{value:.2f}"
-
- return params
-
-
def get_readme_intro(
settings: Settings,
+ modifier: Modifier[Any],
trial: Trial | FrozenTrial,
contains_reproducibility_information: bool,
) -> str:
@@ -316,7 +308,7 @@ def get_readme_intro(
model_link
}, made using [Heretic](https://heretic-project.org) v{version("heretic-llm")}
{reproducibility_instructions}
-## Abliteration parameters
+## {modifier.modifier_name} parameters
| Parameter | Value |
| :-------- | :---: |
@@ -324,7 +316,7 @@ def get_readme_intro(
chr(10).join(
[
f"| **{name}** | {value} |"
- for name, value in get_trial_parameters(trial).items()
+ for name, value in modifier.render_trial_parameters(trial).items()
]
)
}
@@ -508,8 +500,7 @@ This directory contains the necessary information and assets to reproduce the re
## Datasets
-- **Good prompts:** {format_hf_link(settings.good_prompts.dataset, settings.good_prompts.commit, is_dataset=True)}
-- **Bad prompts:** {format_hf_link(settings.bad_prompts.dataset, settings.bad_prompts.commit, is_dataset=True)}
+- TODO: Collect all datasets from scorers and modifiers.
## Selected trial
@@ -564,8 +555,8 @@ def generate_reproduce_json(
version_info = get_heretic_version_info()
data = {
- # Version 3: plugin-based schema with generic scores/baseline scores.
- "version": "3",
+ # Version 4: plugin-based schema with generic parameters and scores.
+ "version": "4",
"timestamp": timestamp,
"system": None, # Defined here to preserve insertion order.
"environment": {
@@ -578,10 +569,7 @@ def generate_reproduce_json(
"requirements": get_requirements_dict(),
},
"settings": settings.model_dump(),
- "parameters": {
- "direction_index": trial.user_attrs["direction_index"],
- "abliteration_parameters": trial.user_attrs["parameters"],
- },
+ "parameters": trial.user_attrs["parameters"],
"scores": trial.user_attrs["scores"],
"hashes": uploaded_model_hashes,
}
diff --git a/tests/gemma-4e/config.toml b/tests/gemma-4e/config.toml
index d418e7d..5c7d220 100644
--- a/tests/gemma-4e/config.toml
+++ b/tests/gemma-4e/config.toml
@@ -1,4 +1,4 @@
-# This test case is for Hybrid-Edge models.
+# This test case is for hybrid models.
# After any change related to it, this test should PASS.
model = "tiny-random/gemma-4e"
@@ -18,18 +18,6 @@ trial_index = 0
model_action = "save"
save_directory = "model"
-[good_prompts]
-dataset = "mlabonne/harmless_alpaca"
-commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
-split = "train[:5]"
-column = "text"
-
-[bad_prompts]
-dataset = "mlabonne/harmful_behaviors"
-commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
-split = "train[:5]"
-column = "text"
-
[scorer.KLDivergence.prompts]
dataset = "mlabonne/harmless_alpaca"
commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
@@ -41,3 +29,15 @@ dataset = "mlabonne/harmful_behaviors"
commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
split = "test[:5]"
column = "text"
+
+[modifier.Abliteration.good_prompts]
+dataset = "mlabonne/harmless_alpaca"
+commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
+split = "train[:5]"
+column = "text"
+
+[modifier.Abliteration.bad_prompts]
+dataset = "mlabonne/harmful_behaviors"
+commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
+split = "train[:5]"
+column = "text"
diff --git a/tests/minicpm5/config.toml b/tests/minicpm5/config.toml
index 04093e6..40aa1d6 100644
--- a/tests/minicpm5/config.toml
+++ b/tests/minicpm5/config.toml
@@ -18,20 +18,6 @@ trial_index = 0
model_action = "save"
save_directory = "model"
-row_normalization = "none"
-
-[good_prompts]
-dataset = "mlabonne/harmless_alpaca"
-commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
-split = "train[:5]"
-column = "text"
-
-[bad_prompts]
-dataset = "mlabonne/harmful_behaviors"
-commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
-split = "train[:5]"
-column = "text"
-
[scorer.KLDivergence.prompts]
dataset = "mlabonne/harmless_alpaca"
commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
@@ -43,3 +29,18 @@ dataset = "mlabonne/harmful_behaviors"
commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
split = "test[:5]"
column = "text"
+
+[modifier.Abliteration]
+row_normalization = "none"
+
+[modifier.Abliteration.good_prompts]
+dataset = "mlabonne/harmless_alpaca"
+commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
+split = "train[:5]"
+column = "text"
+
+[modifier.Abliteration.bad_prompts]
+dataset = "mlabonne/harmful_behaviors"
+commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
+split = "train[:5]"
+column = "text"
diff --git a/tests/mistral-3/config.toml b/tests/mistral-3/config.toml
index e04f8e7..3043b9d 100644
--- a/tests/mistral-3/config.toml
+++ b/tests/mistral-3/config.toml
@@ -1,4 +1,4 @@
-# This test case is for Dense models.
+# This test case is for dense models.
# After any change related to it, this test should PASS.
model = "tiny-random/mistral-3"
@@ -18,18 +18,6 @@ trial_index = 0
model_action = "save"
save_directory = "model"
-[good_prompts]
-dataset = "mlabonne/harmless_alpaca"
-commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
-split = "train[:5]"
-column = "text"
-
-[bad_prompts]
-dataset = "mlabonne/harmful_behaviors"
-commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
-split = "train[:5]"
-column = "text"
-
[scorer.KLDivergence.prompts]
dataset = "mlabonne/harmless_alpaca"
commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
@@ -41,3 +29,15 @@ dataset = "mlabonne/harmful_behaviors"
commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
split = "test[:5]"
column = "text"
+
+[modifier.Abliteration.good_prompts]
+dataset = "mlabonne/harmless_alpaca"
+commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
+split = "train[:5]"
+column = "text"
+
+[modifier.Abliteration.bad_prompts]
+dataset = "mlabonne/harmful_behaviors"
+commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
+split = "train[:5]"
+column = "text"
diff --git a/tests/qwen2.5/config.toml b/tests/qwen2.5/config.toml
index a6923d3..aa0e610 100644
--- a/tests/qwen2.5/config.toml
+++ b/tests/qwen2.5/config.toml
@@ -18,20 +18,6 @@ trial_index = 0
model_action = "save"
save_directory = "model"
-row_normalization = "pre"
-
-[good_prompts]
-dataset = "mlabonne/harmless_alpaca"
-commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
-split = "train[:5]"
-column = "text"
-
-[bad_prompts]
-dataset = "mlabonne/harmful_behaviors"
-commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
-split = "train[:5]"
-column = "text"
-
[scorer.KLDivergence.prompts]
dataset = "mlabonne/harmless_alpaca"
commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
@@ -43,3 +29,18 @@ dataset = "mlabonne/harmful_behaviors"
commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
split = "test[:5]"
column = "text"
+
+[modifier.Abliteration]
+row_normalization = "pre"
+
+[modifier.Abliteration.good_prompts]
+dataset = "mlabonne/harmless_alpaca"
+commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
+split = "train[:5]"
+column = "text"
+
+[modifier.Abliteration.bad_prompts]
+dataset = "mlabonne/harmful_behaviors"
+commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
+split = "train[:5]"
+column = "text"
diff --git a/tests/qwen3.5-moe/config.toml b/tests/qwen3.5-moe/config.toml
index 9fbe440..50db428 100644
--- a/tests/qwen3.5-moe/config.toml
+++ b/tests/qwen3.5-moe/config.toml
@@ -18,18 +18,6 @@ trial_index = 0
model_action = "save"
save_directory = "model"
-[good_prompts]
-dataset = "mlabonne/harmless_alpaca"
-commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
-split = "train[:5]"
-column = "text"
-
-[bad_prompts]
-dataset = "mlabonne/harmful_behaviors"
-commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
-split = "train[:5]"
-column = "text"
-
[scorer.KLDivergence.prompts]
dataset = "mlabonne/harmless_alpaca"
commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
@@ -41,3 +29,15 @@ dataset = "mlabonne/harmful_behaviors"
commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
split = "test[:5]"
column = "text"
+
+[modifier.Abliteration.good_prompts]
+dataset = "mlabonne/harmless_alpaca"
+commit = "02c6a92cfcf11bb0c387334f8146d149d65b587f"
+split = "train[:5]"
+column = "text"
+
+[modifier.Abliteration.bad_prompts]
+dataset = "mlabonne/harmful_behaviors"
+commit = "01cead01398926d81f7c52bdb790ee8cf77ebba7"
+split = "train[:5]"
+column = "text"
diff --git a/uv.lock b/uv.lock
index c450bc7..b2047e5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -231,15 +231,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
-[[package]]
-name = "annoy"
-version = "1.17.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/07/38/e321b0e05d8cc068a594279fb7c097efb1df66231c295d482d7ad51b6473/annoy-1.17.3.tar.gz", hash = "sha256:9cbfebefe0a5f843eba29c6be4c84d601f4f41ad4ded0486f1b88c3b07739c15", size = 647460, upload-time = "2023-06-14T16:37:34.152Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/59/c1/dafbf82add040db10e6663da2a719eea9b2ca7a3b4dc79dc42cc130b121b/annoy-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c33a5d4d344c136c84976bfb2825760142a8bb25335165e24e11c9afbfa8c2e9", size = 57745, upload-time = "2023-06-14T16:37:32.469Z" },
-]
-
[[package]]
name = "anyio"
version = "4.12.0"
@@ -429,162 +420,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" },
]
-[[package]]
-name = "contourpy"
-version = "1.3.2"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.11'",
-]
-dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" },
- { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" },
- { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" },
- { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" },
- { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" },
- { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" },
- { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" },
- { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" },
- { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" },
- { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" },
- { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" },
- { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" },
- { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" },
- { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" },
- { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" },
- { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" },
- { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" },
- { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" },
- { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" },
- { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" },
- { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" },
- { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" },
- { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" },
- { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" },
- { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" },
- { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" },
- { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" },
- { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" },
- { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" },
- { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" },
- { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" },
- { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" },
- { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" },
- { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" },
- { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" },
- { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" },
- { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" },
- { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" },
- { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" },
- { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" },
- { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" },
- { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" },
- { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" },
- { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" },
- { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" },
- { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" },
- { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" },
- { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" },
- { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" },
- { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" },
- { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" },
- { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" },
- { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" },
- { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" },
- { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" },
- { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" },
-]
-
-[[package]]
-name = "contourpy"
-version = "1.3.3"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.12'",
- "python_full_version == '3.11.*'",
-]
-dependencies = [
- { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" } },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
- { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
- { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
- { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
- { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
- { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
- { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
- { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
- { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
- { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
- { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
- { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
- { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
- { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
- { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
- { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
- { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
- { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
- { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
- { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
- { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
- { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
- { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
- { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
- { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
- { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
- { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
- { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
- { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
- { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
- { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
- { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
- { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
- { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
- { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
- { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
- { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
- { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
- { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
- { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
- { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
- { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
- { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
- { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
- { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
- { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
- { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
- { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
- { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
- { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
- { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
- { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
- { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
- { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
- { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
- { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
- { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
- { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
- { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
- { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
- { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
- { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
- { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
- { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
- { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
- { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
- { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
- { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
- { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
- { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
- { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
-]
-
[[package]]
name = "cuda-bindings"
version = "13.3.1"
@@ -664,15 +499,6 @@ nvtx = [
{ name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
-[[package]]
-name = "cycler"
-version = "0.12.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
-]
-
[[package]]
name = "dataproperty"
version = "1.1.0"
@@ -774,63 +600,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
]
-[[package]]
-name = "fonttools"
-version = "4.61.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884, upload-time = "2025-11-28T17:05:49.491Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/29/f3/91bba2721fb173fc68e09d15b6ccf3ad4f83d127fbff579be7e5984888a6/fonttools-4.61.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dc25a4a9c1225653e4431a9413d0381b1c62317b0f543bdcec24e1991f612f33", size = 2850151, upload-time = "2025-11-28T17:04:14.214Z" },
- { url = "https://files.pythonhosted.org/packages/f5/8c/a1691dec01038ac7e7bb3ab83300dcc5087b11d8f48640928c02a873eb92/fonttools-4.61.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b493c32d2555e9944ec1b911ea649ff8f01a649ad9cba6c118d6798e932b3f0", size = 2389769, upload-time = "2025-11-28T17:04:16.443Z" },
- { url = "https://files.pythonhosted.org/packages/2d/dd/5bb369a44319d92ba25612511eb8ed2a6fa75239979e0388907525626902/fonttools-4.61.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad751319dc532a79bdf628b8439af167181b4210a0cd28a8935ca615d9fdd727", size = 4893189, upload-time = "2025-11-28T17:04:18.398Z" },
- { url = "https://files.pythonhosted.org/packages/5e/02/51373fa8846bd22bb54e5efb30a824b417b058083f775a194a432f21a45f/fonttools-4.61.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2de14557d113faa5fb519f7f29c3abe4d69c17fe6a5a2595cc8cda7338029219", size = 4854415, upload-time = "2025-11-28T17:04:20.421Z" },
- { url = "https://files.pythonhosted.org/packages/8b/64/9cdbbb804577a7e6191448851c57e6a36eb02aa4bf6a9668b528c968e44e/fonttools-4.61.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:59587bbe455dbdf75354a9dbca1697a35a8903e01fab4248d6b98a17032cee52", size = 4870927, upload-time = "2025-11-28T17:04:22.625Z" },
- { url = "https://files.pythonhosted.org/packages/92/68/e40b22919dc96dc30a70b58fec609ab85112de950bdecfadf8dd478c5a88/fonttools-4.61.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46cb3d9279f758ac0cf671dc3482da877104b65682679f01b246515db03dbb72", size = 4988674, upload-time = "2025-11-28T17:04:24.675Z" },
- { url = "https://files.pythonhosted.org/packages/9b/5c/e857349ce8aedb2451b9448282e86544b2b7f1c8b10ea0fe49b7cb369b72/fonttools-4.61.0-cp310-cp310-win32.whl", hash = "sha256:58b4f1b78dfbfe855bb8a6801b31b8cdcca0e2847ec769ad8e0b0b692832dd3b", size = 1497663, upload-time = "2025-11-28T17:04:26.598Z" },
- { url = "https://files.pythonhosted.org/packages/f9/0c/62961d5fe6f764d6cbc387ef2c001f5f610808c7aded837409836c0b3e7c/fonttools-4.61.0-cp310-cp310-win_amd64.whl", hash = "sha256:68704a8bbe0b61976262b255e90cde593dc0fe3676542d9b4d846bad2a890a76", size = 1546143, upload-time = "2025-11-28T17:04:28.432Z" },
- { url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553, upload-time = "2025-11-28T17:04:30.539Z" },
- { url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298, upload-time = "2025-11-28T17:04:32.161Z" },
- { url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133, upload-time = "2025-11-28T17:04:34.035Z" },
- { url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410, upload-time = "2025-11-28T17:04:36.141Z" },
- { url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005, upload-time = "2025-11-28T17:04:38.314Z" },
- { url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026, upload-time = "2025-11-28T17:04:40.34Z" },
- { url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035, upload-time = "2025-11-28T17:04:42.59Z" },
- { url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290, upload-time = "2025-11-28T17:04:44.57Z" },
- { url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930, upload-time = "2025-11-28T17:04:46.639Z" },
- { url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016, upload-time = "2025-11-28T17:04:48.525Z" },
- { url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425, upload-time = "2025-11-28T17:04:50.482Z" },
- { url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632, upload-time = "2025-11-28T17:04:52.508Z" },
- { url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438, upload-time = "2025-11-28T17:04:54.437Z" },
- { url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960, upload-time = "2025-11-28T17:04:56.348Z" },
- { url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404, upload-time = "2025-11-28T17:04:58.149Z" },
- { url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427, upload-time = "2025-11-28T17:04:59.812Z" },
- { url = "https://files.pythonhosted.org/packages/17/45/334f0d7f181e5473cfb757e1b60f4e60e7fc64f28d406e5d364a952718c0/fonttools-4.61.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba774b8cbd8754f54b8eb58124e8bd45f736b2743325ab1a5229698942b9b433", size = 2841801, upload-time = "2025-11-28T17:05:01.621Z" },
- { url = "https://files.pythonhosted.org/packages/cc/63/97b9c78e1f79bc741d4efe6e51f13872d8edb2b36e1b9fb2bab0d4491bb7/fonttools-4.61.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c84b430616ed73ce46e9cafd0bf0800e366a3e02fb7e1ad7c1e214dbe3862b1f", size = 2379024, upload-time = "2025-11-28T17:05:03.668Z" },
- { url = "https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b", size = 4923706, upload-time = "2025-11-28T17:05:05.494Z" },
- { url = "https://files.pythonhosted.org/packages/6d/f6/a3b0374811a1de8c3f9207ec88f61ad1bb96f938ed89babae26c065c2e46/fonttools-4.61.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5c5fff72bf31b0e558ed085e4fd7ed96eb85881404ecc39ed2a779e7cf724eb", size = 4979751, upload-time = "2025-11-28T17:05:07.665Z" },
- { url = "https://files.pythonhosted.org/packages/a5/3b/30f63b4308b449091573285f9d27619563a84f399946bca3eadc9554afbe/fonttools-4.61.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14a290c5c93fcab76b7f451e6a4b7721b712d90b3b5ed6908f1abcf794e90d6d", size = 4921113, upload-time = "2025-11-28T17:05:09.551Z" },
- { url = "https://files.pythonhosted.org/packages/41/6c/58e6e9b7d9d8bf2d7010bd7bb493060b39b02a12d1cda64a8bfb116ce760/fonttools-4.61.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:13e3e20a5463bfeb77b3557d04b30bd6a96a6bb5c15c7b2e7908903e69d437a0", size = 5063183, upload-time = "2025-11-28T17:05:11.677Z" },
- { url = "https://files.pythonhosted.org/packages/3f/e3/52c790ab2b07492df059947a1fd7778e105aac5848c0473029a4d20481a2/fonttools-4.61.0-cp313-cp313-win32.whl", hash = "sha256:6781e7a4bb010be1cd69a29927b0305c86b843395f2613bdabe115f7d6ea7f34", size = 2263159, upload-time = "2025-11-28T17:05:13.292Z" },
- { url = "https://files.pythonhosted.org/packages/e9/1f/116013b200fbeba871046554d5d2a45fefa69a05c40e9cdfd0d4fff53edc/fonttools-4.61.0-cp313-cp313-win_amd64.whl", hash = "sha256:c53b47834ae41e8e4829171cc44fec0fdf125545a15f6da41776b926b9645a9a", size = 2313530, upload-time = "2025-11-28T17:05:14.848Z" },
- { url = "https://files.pythonhosted.org/packages/d3/99/59b1e25987787cb714aa9457cee4c9301b7c2153f0b673e2b8679d37669d/fonttools-4.61.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:96dfc9bc1f2302224e48e6ee37e656eddbab810b724b52e9d9c13a57a6abad01", size = 2841429, upload-time = "2025-11-28T17:05:16.671Z" },
- { url = "https://files.pythonhosted.org/packages/2b/b2/4c1911d4332c8a144bb3b44416e274ccca0e297157c971ea1b3fbb855590/fonttools-4.61.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3b2065d94e5d63aafc2591c8b6ccbdb511001d9619f1bca8ad39b745ebeb5efa", size = 2378987, upload-time = "2025-11-28T17:05:18.69Z" },
- { url = "https://files.pythonhosted.org/packages/24/b0/f442e90fde5d2af2ae0cb54008ab6411edc557ee33b824e13e1d04925ac9/fonttools-4.61.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e0d87e81e4d869549585ba0beb3f033718501c1095004f5e6aef598d13ebc216", size = 4873270, upload-time = "2025-11-28T17:05:20.625Z" },
- { url = "https://files.pythonhosted.org/packages/bb/04/f5d5990e33053c8a59b90b1d7e10ad9b97a73f42c745304da0e709635fab/fonttools-4.61.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cfa2eb9bae650e58f0e8ad53c49d19a844d6034d6b259f30f197238abc1ccee", size = 4968270, upload-time = "2025-11-28T17:05:22.515Z" },
- { url = "https://files.pythonhosted.org/packages/94/9f/2091402e0d27c9c8c4bab5de0e5cd146d9609a2d7d1c666bbb75c0011c1a/fonttools-4.61.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4238120002e68296d55e091411c09eab94e111c8ce64716d17df53fd0eb3bb3d", size = 4919799, upload-time = "2025-11-28T17:05:24.437Z" },
- { url = "https://files.pythonhosted.org/packages/a8/72/86adab22fde710b829f8ffbc8f264df01928e5b7a8f6177fa29979ebf256/fonttools-4.61.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6ceac262cc62bec01b3bb59abccf41b24ef6580869e306a4e88b7e56bb4bdda", size = 5030966, upload-time = "2025-11-28T17:05:26.115Z" },
- { url = "https://files.pythonhosted.org/packages/e8/a7/7c8e31b003349e845b853f5e0a67b95ff6b052fa4f5224f8b72624f5ac69/fonttools-4.61.0-cp314-cp314-win32.whl", hash = "sha256:adbb4ecee1a779469a77377bbe490565effe8fce6fb2e6f95f064de58f8bac85", size = 2267243, upload-time = "2025-11-28T17:05:27.807Z" },
- { url = "https://files.pythonhosted.org/packages/20/ee/f434fe7749360497c52b7dcbcfdbccdaab0a71c59f19d572576066717122/fonttools-4.61.0-cp314-cp314-win_amd64.whl", hash = "sha256:02bdf8e04d1a70476564b8640380f04bb4ac74edc1fc71f1bacb840b3e398ee9", size = 2318822, upload-time = "2025-11-28T17:05:29.882Z" },
- { url = "https://files.pythonhosted.org/packages/33/b3/c16255320255e5c1863ca2b2599bb61a46e2f566db0bbb9948615a8fe692/fonttools-4.61.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:627216062d90ab0d98215176d8b9562c4dd5b61271d35f130bcd30f6a8aaa33a", size = 2924917, upload-time = "2025-11-28T17:05:31.46Z" },
- { url = "https://files.pythonhosted.org/packages/e2/b8/08067ae21de705a817777c02ef36ab0b953cbe91d8adf134f9c2da75ed6d/fonttools-4.61.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b446623c9cd5f14a59493818eaa80255eec2468c27d2c01b56e05357c263195", size = 2413576, upload-time = "2025-11-28T17:05:33.343Z" },
- { url = "https://files.pythonhosted.org/packages/42/f1/96ff43f92addce2356780fdc203f2966206f3d22ea20e242c27826fd7442/fonttools-4.61.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:70e2a0c0182ee75e493ef33061bfebf140ea57e035481d2f95aa03b66c7a0e05", size = 4877447, upload-time = "2025-11-28T17:05:35.278Z" },
- { url = "https://files.pythonhosted.org/packages/d0/1e/a3d8e51ed9ccfd7385e239ae374b78d258a0fb82d82cab99160a014a45d1/fonttools-4.61.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9064b0f55b947e929ac669af5311ab1f26f750214db6dd9a0c97e091e918f486", size = 5095681, upload-time = "2025-11-28T17:05:37.142Z" },
- { url = "https://files.pythonhosted.org/packages/eb/f6/d256bd6c1065c146a0bdddf1c62f542e08ae5b3405dbf3fcc52be272f674/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5e45a824ce14b90510024d0d39dae51bd4fbb54c42a9334ea8c8cf4d95cbe", size = 4974140, upload-time = "2025-11-28T17:05:39.5Z" },
- { url = "https://files.pythonhosted.org/packages/5d/0c/96633eb4b26f138cc48561c6e0c44b4ea48acea56b20b507d6b14f8e80ce/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e5ca8c62efdec7972dfdfd454415c4db49b89aeaefaaacada432f3b7eea9866", size = 5001741, upload-time = "2025-11-28T17:05:41.424Z" },
- { url = "https://files.pythonhosted.org/packages/6f/9a/3b536bad3be4f26186f296e749ff17bad3e6d57232c104d752d24b2e265b/fonttools-4.61.0-cp314-cp314t-win32.whl", hash = "sha256:63c7125d31abe3e61d7bb917329b5543c5b3448db95f24081a13aaf064360fc8", size = 2330707, upload-time = "2025-11-28T17:05:43.548Z" },
- { url = "https://files.pythonhosted.org/packages/18/ea/e6b9ac610451ee9f04477c311ad126de971f6112cb579fa391d2a8edb00b/fonttools-4.61.0-cp314-cp314t-win_amd64.whl", hash = "sha256:67d841aa272be5500de7f447c40d1d8452783af33b4c3599899319f6ef9ad3c1", size = 2395950, upload-time = "2025-11-28T17:05:45.638Z" },
- { url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" },
-]
-
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -966,16 +735,6 @@ http = [
{ name = "aiohttp" },
]
-[[package]]
-name = "geom-median"
-version = "0.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/0e/b9/13101da828812b56df9e8fa12a62f24408dcb416edcd6b3b091c973306b3/geom_median-0.1.0.tar.gz", hash = "sha256:5a5bd8c930fde58febcf00aab9ecea5f12a54bc75589e5063754deb5b6e90495", size = 20904, upload-time = "2022-01-19T04:41:05.294Z" }
-
[[package]]
name = "greenlet"
version = "3.3.0"
@@ -1062,16 +821,6 @@ dependencies = [
{ name = "transformers", extra = ["kernels"] },
]
-[package.optional-dependencies]
-research = [
- { name = "geom-median" },
- { name = "imageio" },
- { name = "matplotlib" },
- { name = "pacmap" },
- { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
-]
-
[package.dev-dependencies]
dev = [
{ name = "ruff" },
@@ -1083,30 +832,24 @@ requires-dist = [
{ name = "accelerate", specifier = "~=1.13" },
{ name = "bitsandbytes", specifier = "~=0.49" },
{ name = "datasets", specifier = "~=4.7" },
- { name = "geom-median", marker = "extra == 'research'", specifier = "~=0.1" },
{ name = "huggingface-hub", specifier = "~=1.7" },
- { name = "imageio", marker = "extra == 'research'", specifier = "~=2.37" },
{ name = "immutabledict", specifier = "~=4.3" },
{ name = "langdetect", specifier = "~=1.0" },
{ name = "lm-eval", extras = ["hf"], specifier = "~=0.4" },
- { name = "matplotlib", marker = "extra == 'research'", specifier = "~=3.10" },
{ name = "numpy", specifier = "~=2.2" },
{ name = "optuna", specifier = "~=4.7" },
- { name = "pacmap", marker = "extra == 'research'", specifier = "~=0.8" },
{ name = "peft", specifier = "~=0.19" },
{ name = "psutil", specifier = "~=7.2" },
{ name = "py-cpuinfo", specifier = "~=9.0" },
{ name = "pydantic-settings", specifier = "~=2.13" },
{ name = "questionary", specifier = "~=2.1" },
{ name = "rich", specifier = "~=14.3" },
- { name = "scikit-learn", marker = "extra == 'research'", specifier = "~=1.7" },
{ name = "tomli-w", specifier = "~=1.2" },
{ name = "torch" },
{ name = "torchvision" },
{ name = "tqdm", specifier = "~=4.67" },
{ name = "transformers", extras = ["kernels"], specifier = "~=5.6" },
]
-provides-extras = ["research"]
[package.metadata.requires-dev]
dev = [
@@ -1203,20 +946,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
-[[package]]
-name = "imageio"
-version = "2.37.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "pillow" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" },
-]
-
[[package]]
name = "immutabledict"
version = "4.3.1"
@@ -1274,114 +1003,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/3e/778e4a86830e9139df2d16d86c4488fce426ec19daa83cbd2854ef389030/kernels-0.12.3-py3-none-any.whl", hash = "sha256:5d1d33fcb774e03bb7f0688ac24d91ef6b963692f80f0a85ddd2286e69f3cf2f", size = 55501, upload-time = "2026-03-20T10:20:40.643Z" },
]
-[[package]]
-name = "kiwisolver"
-version = "1.4.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" },
- { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" },
- { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" },
- { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" },
- { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" },
- { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" },
- { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" },
- { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" },
- { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" },
- { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" },
- { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" },
- { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" },
- { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" },
- { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" },
- { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" },
- { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" },
- { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" },
- { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" },
- { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" },
- { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" },
- { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" },
- { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" },
- { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" },
- { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" },
- { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" },
- { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" },
- { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
- { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
- { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
- { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
- { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
- { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
- { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
- { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
- { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
- { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
- { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
- { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
- { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
- { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" },
- { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" },
- { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" },
- { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" },
- { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" },
- { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" },
- { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" },
- { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" },
- { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" },
- { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" },
- { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" },
- { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" },
- { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" },
- { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" },
- { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" },
- { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" },
- { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" },
- { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" },
- { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" },
- { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" },
- { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" },
- { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" },
- { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" },
- { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" },
- { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
- { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
- { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
- { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
- { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
- { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
- { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
- { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
- { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
- { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
- { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
- { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
- { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
- { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
- { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
- { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
- { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
- { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
- { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
- { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
- { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
- { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
- { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
- { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
- { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
- { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
- { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
- { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" },
- { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" },
- { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" },
- { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" },
- { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" },
- { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" },
- { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" },
- { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" },
- { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" },
- { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" },
-]
-
[[package]]
name = "langdetect"
version = "1.0.9"
@@ -1391,34 +1012,6 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" }
-[[package]]
-name = "llvmlite"
-version = "0.46.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" },
- { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" },
- { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" },
- { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" },
- { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" },
- { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" },
- { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" },
- { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" },
- { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" },
- { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" },
- { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" },
- { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" },
- { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" },
- { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" },
- { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" },
- { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" },
- { url = "https://files.pythonhosted.org/packages/95/ae/af0ffb724814cc2ea64445acad05f71cff5f799bb7efb22e47ee99340dbc/llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547", size = 37232768, upload-time = "2025-12-08T18:15:25.055Z" },
- { url = "https://files.pythonhosted.org/packages/c9/19/5018e5352019be753b7b07f7759cdabb69ca5779fea2494be8839270df4c/llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d", size = 56275173, upload-time = "2025-12-08T18:15:28.109Z" },
- { url = "https://files.pythonhosted.org/packages/9f/c9/d57877759d707e84c082163c543853245f91b70c804115a5010532890f18/llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269", size = 55128628, upload-time = "2025-12-08T18:15:31.098Z" },
- { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" },
-]
-
[[package]]
name = "lm-eval"
version = "0.4.11"
@@ -1688,81 +1281,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
-[[package]]
-name = "matplotlib"
-version = "3.10.8"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "cycler" },
- { name = "fonttools" },
- { name = "kiwisolver" },
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "packaging" },
- { name = "pillow" },
- { name = "pyparsing" },
- { name = "python-dateutil" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" },
- { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" },
- { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" },
- { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" },
- { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" },
- { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" },
- { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" },
- { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" },
- { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" },
- { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" },
- { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" },
- { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" },
- { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" },
- { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" },
- { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" },
- { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" },
- { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" },
- { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" },
- { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" },
- { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" },
- { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" },
- { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" },
- { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" },
- { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" },
- { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" },
- { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" },
- { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" },
- { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" },
- { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" },
- { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" },
- { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" },
- { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" },
- { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" },
- { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" },
- { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" },
- { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" },
- { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" },
- { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" },
- { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" },
- { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" },
- { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" },
- { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" },
- { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" },
- { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" },
- { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" },
- { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" },
- { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" },
- { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" },
- { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" },
- { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" },
- { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" },
- { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" },
-]
-
[[package]]
name = "mbstrdecoder"
version = "1.1.4"
@@ -2004,39 +1522,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" },
]
-[[package]]
-name = "numba"
-version = "0.63.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "llvmlite" },
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5e/ce/5283d4ffa568f795bb0fd61ee1f0efc0c6094b94209259167fc8d4276bde/numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779", size = 2680810, upload-time = "2025-12-10T02:56:55.269Z" },
- { url = "https://files.pythonhosted.org/packages/0f/72/a8bda517e26d912633b32626333339b7c769ea73a5c688365ea5f88fd07e/numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4", size = 3739735, upload-time = "2025-12-10T02:56:57.922Z" },
- { url = "https://files.pythonhosted.org/packages/ca/17/1913b7c1173b2db30fb7a9696892a7c4c59aeee777a9af6859e9e01bac51/numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e", size = 3446707, upload-time = "2025-12-10T02:56:59.837Z" },
- { url = "https://files.pythonhosted.org/packages/b4/77/703db56c3061e9fdad5e79c91452947fdeb2ec0bdfe4affe9b144e7025e0/numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d", size = 2747374, upload-time = "2025-12-10T02:57:07.908Z" },
- { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501, upload-time = "2025-12-10T02:57:09.797Z" },
- { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945, upload-time = "2025-12-10T02:57:11.697Z" },
- { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827, upload-time = "2025-12-10T02:57:13.709Z" },
- { url = "https://files.pythonhosted.org/packages/18/3f/a9b106e93c5bd7434e65f044bae0d204e20aa7f7f85d72ceb872c7c04216/numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87", size = 2747262, upload-time = "2025-12-10T02:57:15.664Z" },
- { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" },
- { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" },
- { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" },
- { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" },
- { url = "https://files.pythonhosted.org/packages/57/f7/e19e6eff445bec52dde5bed1ebb162925a8e6f988164f1ae4b3475a73680/numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc", size = 2680954, upload-time = "2025-12-10T02:57:24.145Z" },
- { url = "https://files.pythonhosted.org/packages/e9/6c/1e222edba1e20e6b113912caa9b1665b5809433cbcb042dfd133c6f1fd38/numba-0.63.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53de693abe4be3bd4dee38e1c55f01c55ff644a6a3696a3670589e6e4c39cde2", size = 3809736, upload-time = "2025-12-10T02:57:25.836Z" },
- { url = "https://files.pythonhosted.org/packages/76/0a/590bad11a8b3feeac30a24d01198d46bdb76ad15c70d3a530691ce3cae58/numba-0.63.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81227821a72a763c3d4ac290abbb4371d855b59fdf85d5af22a47c0e86bf8c7e", size = 3508854, upload-time = "2025-12-10T02:57:27.438Z" },
- { url = "https://files.pythonhosted.org/packages/4e/f5/3800384a24eed1e4d524669cdbc0b9b8a628800bb1e90d7bd676e5f22581/numba-0.63.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb227b07c2ac37b09432a9bda5142047a2d1055646e089d4a240a2643e508102", size = 2750228, upload-time = "2025-12-10T02:57:30.36Z" },
- { url = "https://files.pythonhosted.org/packages/36/2f/53be2aa8a55ee2608ebe1231789cbb217f6ece7f5e1c685d2f0752e95a5b/numba-0.63.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f180883e5508940cc83de8a8bea37fc6dd20fbe4e5558d4659b8b9bef5ff4731", size = 2681153, upload-time = "2025-12-10T02:57:32.016Z" },
- { url = "https://files.pythonhosted.org/packages/13/91/53e59c86759a0648282368d42ba732c29524a745fd555ed1fb1df83febbe/numba-0.63.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0938764afa82a47c0e895637a6c55547a42c9e1d35cac42285b1fa60a8b02bb", size = 3778718, upload-time = "2025-12-10T02:57:33.764Z" },
- { url = "https://files.pythonhosted.org/packages/6c/0c/2be19eba50b0b7636f6d1f69dfb2825530537708a234ba1ff34afc640138/numba-0.63.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f90a929fa5094e062d4e0368ede1f4497d5e40f800e80aa5222c4734236a2894", size = 3478712, upload-time = "2025-12-10T02:57:35.518Z" },
- { url = "https://files.pythonhosted.org/packages/0d/5f/4d0c9e756732577a52211f31da13a3d943d185f7fb90723f56d79c696caa/numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f", size = 2752161, upload-time = "2025-12-10T02:57:37.12Z" },
-]
-
[[package]]
name = "numpy"
version = "2.2.6"
@@ -2367,23 +1852,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
-[[package]]
-name = "pacmap"
-version = "0.8.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annoy" },
- { name = "numba" },
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b3/40/3686ff721dd60901f5c2c9a1c62e3573fdf84a0be37be047adbc03ef81f3/pacmap-0.8.0.tar.gz", hash = "sha256:1c0c10180e36b77f5e5344d838d020dd9557ac1ca77f0976c2e308643b019084", size = 29067, upload-time = "2025-02-15T19:15:57.576Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/73/15/5ff2b77b48be04d718e2684c4e707c588183a8c4bbfddf1bd558329281d5/pacmap-0.8.0-py3-none-any.whl", hash = "sha256:c15eff274fd4d4575ba5b42399dc3b71eba80f9131a461d715927e9ae6c812b4", size = 21359, upload-time = "2025-02-15T19:15:55.765Z" },
-]
-
[[package]]
name = "pandas"
version = "2.3.3"
@@ -2959,15 +2427,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
-[[package]]
-name = "pyparsing"
-version = "3.2.5"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
-]
-
[[package]]
name = "pytablewriter"
version = "1.2.1"