diff --git a/CHANGELOG.md b/CHANGELOG.md index 24889a0..fafe601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,16 @@ brute-force reference or a closed-form value and has a regression test. ### New +- `sofic.generators.capacity`: capacity bounds for finite ε-transducers + (unifilar finite-state channels). `EpsilonTransducer.feedback_capacity()` + brackets the feedback capacity with Q-graph bounds (convex upper bound, + certified graph-based-encoder lower bound), the Q-graph duality bound, or + belief-state value iteration; `EpsilonTransducer.capacity()` brackets the + capacity without feedback with an optimized Markov input and the Q-graph + duality dynamic program. Results are `CapacityBounds(lower, upper, ...)`. + The optional `capacity` extra installs cvxpy for the convex step. +- `trapdoor()` / `trapdoor_et()` and `ising_channel()` / `ising_channel_et()` + example channels, each with a uniform initial state. - `sofic.inference.Trials`: `learn_epsilon_machine_cssr`, `learn_epsilon_transducer_cssr`, and `suggest_max_history` learn from independent trials, pooling counts without any history spanning two trials diff --git a/docs/examples.rst b/docs/examples.rst index 99cdb27..ba4c923 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -230,7 +230,14 @@ parametrized tests and sweeps: A parallel set of transducer factories (``bit_flip``, ``parity``, ``delay``, ``binary_channel``, …) lives alongside the processes and produces -:class:`~sofic.automata.transducers.MealyMachine` instances. +:class:`~sofic.automata.transducers.MealyMachine` instances. Two of them are +classic channels with memory whose feedback capacities are known: +``trapdoor`` (Blackwell's trapdoor channel, :math:`C_{\mathrm{fb}} = \log_2\varphi` +:cite:`Permuter2008`) and ``ising_channel`` (:math:`C_{\mathrm{fb}} \approx 0.575522` +:cite:`Elishco2014`), both with a uniform initial state. ``trapdoor_et`` and +``ising_channel_et`` return their two-state +:class:`~sofic.generators.epsilon_transducer.EpsilonTransducer` presentations, which +feed the :doc:`channel-capacity bounds `. Symbolic-shift examples (:mod:`sofic.examples.shifts`) provide the sofic-Dyck factories listed above; access them via diff --git a/docs/generators/channel_capacity.rst b/docs/generators/channel_capacity.rst new file mode 100644 index 0000000..7142d34 --- /dev/null +++ b/docs/generators/channel_capacity.rst @@ -0,0 +1,144 @@ +.. channel_capacity.rst +.. py:module:: sofic.generators.capacity + +**************** +Channel Capacity +**************** + +A finite :doc:`ε-transducer ` is a *unifilar finite-state +channel*: the pair ``(x, y)`` observed in causal state ``s`` determines the next +causal state. :mod:`sofic.generators.capacity` brackets the capacity of such a +channel, with feedback (the encoder sees past outputs) and without. + +Every method returns a :class:`CapacityBounds` ``lower <= C <= upper`` in bits +per channel use. Exact answers are not available in general: the feedback +capacity of finite-state channels is not Turing-computable +:cite:`Grigorescu2024`, and the capacity without feedback is open even for +the trapdoor channel. ``CapacityBounds.value`` returns the midpoint once the +bracket is narrower than ``tol``. + +The methods assume a strongly connected channel (checked), so the capacity +does not depend on the initial state. Inputs undefined at a state act as input +constraints for the feedback bounds; the no-feedback bounds require every +input to be admissible everywhere. + +Feedback capacity +================= + +With feedback, the encoder knows the causal state, and the feedback capacity +is the limit of normalized directed information :cite:`Permuter2008,Yang2005`. + +Q-graphs +-------- + +A :class:`QGraph` maps output histories onto a finite set of contexts by +walking a deterministic graph with one edge per output symbol +:cite:`Sabag2017`. For any Q-graph, + +.. math:: + + C_{\mathrm{fb}} \le \max_{P_{X \mid S, Q}} I(X, S; Y \mid Q), + +with ``(S, Q)`` stationary :cite:`Sabag2017`. This is a convex program in the +joint ``P(s, q, x)`` :cite:`Sabag2020`, solved with cvxpy when the optional +``capacity`` extra is installed (``pip install sofic[capacity]``) and with +scipy otherwise. On the lower side, any graph-based encoder +:math:`P(x \mid s, q)` is a valid feedback input; its directed-information rate +:math:`\bar h(Y) - \mathbb{E}_\pi H(Y \mid X, S)` is bracketed with block +entropies :cite:`Cover2006` and optimized. When the encoder is also +BCJR-invariant the single-letter rate :math:`I(X, S; Y \mid Q)` is achievable +:cite:`Sabag2017,Sabag2020`. :func:`qgraph_bounds` tries Markov Q-graphs of the +last ``k`` outputs plus any graphs you pass. + +Markov Q-graphs are not always enough. The trapdoor channel +:cite:`Permuter2008` is closed by a three-node graph: + +.. ipython:: + + In [1]: import numpy as np + + In [2]: from sofic.examples.processes import trapdoor_et + + In [3]: from sofic.generators.capacity import QGraph, qgraph_bounds + + In [4]: trapdoor = trapdoor_et() + + In [5]: graph = QGraph(("0", "1"), np.array([[0, 1], [0, 2], [1, 2]]), ("a", "b", "c")) + + In [6]: bounds = qgraph_bounds(trapdoor, qgraphs=[graph], max_order=1) + + @doctest + In [7]: round(bounds.upper, 6), round(np.log2((1 + np.sqrt(5)) / 2), 6) + Out[7]: (0.694242, 0.694242) + + @doctest + In [8]: bounds.gap < 1e-4 + Out[8]: True + +For the Ising channel :cite:`Elishco2014` (``ising_channel_et``), the four-node +graph with table ``[[1, 2], [0, 2], [0, 3], [0, 2]]`` closes the bracket at +:math:`C_{\mathrm{fb}} \approx 0.575522`. + +Duality bound +------------- + +:func:`duality_upper_bound` fixes a test distribution :math:`R(y \mid q)` on a +Q-graph. For a unifilar channel the resulting bound is the optimal average +reward of a *finite* Markov decision process with state ``(s, q)``, action +``x`` and reward :math:`D(P_{Y \mid x, s} \| R_{Y \mid q})` +:cite:`Huleihel2025`. Every test distribution gives a valid bound; the +starting one is the output law of the Q-graph upper-bound optimizer, refined +by Nelder--Mead. + +Value iteration +--------------- + +:func:`value_iteration_estimate` solves the belief-state dynamic program of +:cite:`Permuter2008,Yang2005` (state :math:`P(S \mid y^t)`, action +:math:`P(x \mid s)`, reward :math:`I(X, S; Y)`) on a gridded simplex. Its +bracket bounds the gain of the discretized problem only, so it is returned +with ``certified=False``; ``details["policy"]`` holds the greedy input law at +every grid belief. + +:func:`feedback_capacity` dispatches on ``method`` (``"qgraph"``, +``"duality"``, ``"value_iteration"``, or ``"all"``). + +Capacity without feedback +========================= + +* :func:`markov_input_lower_bound` optimizes an order-``k`` Markov input and + reports a block-entropy lower bound on its information rate. That rate is + the objective of the generalized Blahut--Arimoto algorithm + :cite:`Vontobel2008,Arnold2006`, maximized here directly. +* :func:`duality_upper_bound_no_feedback` evaluates the dual bound with a + Q-graph test distribution, a deterministic dynamic program over + :math:`P(S, Q \mid x^t)` :cite:`Huleihel2021`. Its value function is convex, + so interpolating on a simplex grid over-estimates it and the gridded bound + stays valid at any resolution. + +For the trapdoor channel, an order-3 Markov input certifies ``C >= 0.5727`` +and the order-1 Markov Q-graph at ``belief_resolution=24`` gives +``C <= 0.5936``. The best published bounds are ``0.572`` and ``0.584`` +:cite:`Huleihel2021`. :func:`capacity` dispatches on ``method`` +(``"markov_input"``, ``"duality"``, or ``"all"``). + +API +=== + +.. autoclass:: CapacityBounds + :members: gap, value + +.. autoclass:: QGraph + :members: markov, from_dfa, aligned + +.. autofunction:: feedback_capacity +.. autofunction:: qgraph_bounds +.. autofunction:: qgraph_upper_bound +.. autofunction:: qgraph_lower_bound +.. autofunction:: encoder_rate_bounds +.. autofunction:: duality_upper_bound +.. autofunction:: value_iteration_estimate +.. autofunction:: capacity +.. autofunction:: markov_input_lower_bound +.. autofunction:: duality_upper_bound_no_feedback +.. autofunction:: channel_tensors diff --git a/docs/generators/epsilon_transducer.rst b/docs/generators/epsilon_transducer.rst index d53ab53..7294e4b 100644 --- a/docs/generators/epsilon_transducer.rst +++ b/docs/generators/epsilon_transducer.rst @@ -57,11 +57,16 @@ quantity, reusing the directional-flow estimators of :doc:`directional_flow`. * :func:`directed_information` / :func:`transfer_entropy` — input-to-output information flow of the driven joint process. +These measures fix the input process. Optimizing over inputs gives the channel +capacity; :meth:`~EpsilonTransducer.feedback_capacity` and +:meth:`~EpsilonTransducer.capacity` bracket it with and without feedback +(:doc:`channel_capacity`). + API === .. autoclass:: EpsilonTransducer - :members: from_channel, from_iohmm, from_joint_generator, from_paired_sequences, is_unifilar, causal_states, statistical_complexity, driven_entropy_rate, directed_information, transfer_entropy, from_wfst + :members: from_channel, from_iohmm, from_joint_generator, from_paired_sequences, is_unifilar, causal_states, statistical_complexity, driven_entropy_rate, directed_information, transfer_entropy, feedback_capacity, capacity, from_wfst .. currentmodule:: sofic.generators.channel_measures diff --git a/docs/generators/generators.rst b/docs/generators/generators.rst index 49c24ae..13bbdfb 100644 --- a/docs/generators/generators.rst +++ b/docs/generators/generators.rst @@ -29,6 +29,7 @@ Computational mechanics epsilon_machine epsilon_transducer + channel_capacity block_convergence bidirectional_epsilon_machine mixed_state_presentation diff --git a/docs/references.bib b/docs/references.bib index 74ae9f6..0f31ad3 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -1311,3 +1311,113 @@ @incollection{blackwell1957entropy address = {Prague}, year = {1957}, } + +@article{Permuter2008, + author = {Permuter, Haim and Cuff, Paul and Van Roy, Benjamin and Weissman, Tsachy}, + title = {Capacity of the Trapdoor Channel With Feedback}, + journal = {IEEE Transactions on Information Theory}, + volume = {54}, + number = {7}, + pages = {3150--3165}, + year = {2008}, + doi = {10.1109/TIT.2008.924681}, +} + +@article{Elishco2014, + author = {Elishco, Ohad and Permuter, Haim}, + title = {Capacity and Coding for the {Ising} Channel With Feedback}, + journal = {IEEE Transactions on Information Theory}, + volume = {60}, + number = {9}, + pages = {5138--5149}, + year = {2014}, + doi = {10.1109/TIT.2014.2331951}, +} + +@article{Yang2005, + author = {Yang, Shaohua and Kav{\v{c}}i{\'c}, Aleksandar and Tatikonda, Sekhar}, + title = {Feedback Capacity of Finite-State Machine Channels}, + journal = {IEEE Transactions on Information Theory}, + volume = {51}, + number = {3}, + pages = {799--810}, + year = {2005}, + doi = {10.1109/TIT.2004.842626}, +} + +@article{Sabag2017, + author = {Sabag, Oron and Permuter, Haim H. and Pfister, Henry D.}, + title = {A Single-Letter Upper Bound on the Feedback Capacity of Unifilar Finite-State Channels}, + journal = {IEEE Transactions on Information Theory}, + volume = {63}, + number = {3}, + pages = {1392--1409}, + year = {2017}, + doi = {10.1109/TIT.2016.2636851}, +} + +@article{Sabag2020, + author = {Sabag, Oron and Huleihel, Bashar and Permuter, Haim H.}, + title = {Graph-Based Encoders and Their Performance for Finite-State Channels With Feedback}, + journal = {IEEE Transactions on Communications}, + volume = {68}, + number = {4}, + pages = {2106--2117}, + year = {2020}, + doi = {10.1109/TCOMM.2020.2965454}, +} + +@article{Huleihel2021, + author = {Huleihel, Bashar and Sabag, Oron and Permuter, Haim H. and Kashyap, Navin and Shamai (Shitz), Shlomo}, + title = {Computable Upper Bounds on the Capacity of Finite-State Channels}, + journal = {IEEE Transactions on Information Theory}, + volume = {67}, + number = {9}, + pages = {5674--5692}, + year = {2021}, + doi = {10.1109/TIT.2021.3091691}, +} + +@article{Huleihel2025, + author = {Huleihel, Bashar and Sabag, Oron and Aharoni, Ziv and Permuter, Haim H.}, + title = {The Duality Upper Bound for Finite-State Channels With Feedback}, + journal = {IEEE Transactions on Information Theory}, + volume = {71}, + number = {5}, + pages = {3255--3270}, + year = {2025}, + doi = {10.1109/TIT.2025.3545688}, +} + +@article{Vontobel2008, + author = {Vontobel, Pascal O. and Kav{\v{c}}i{\'c}, Aleksandar and Arnold, Dieter M. and Loeliger, Hans-Andrea}, + title = {A Generalization of the {Blahut}--{Arimoto} Algorithm to Finite-State Channels}, + journal = {IEEE Transactions on Information Theory}, + volume = {54}, + number = {5}, + pages = {1887--1918}, + year = {2008}, + doi = {10.1109/TIT.2008.920243}, +} + +@article{Arnold2006, + author = {Arnold, Dieter M. and Loeliger, Hans-Andrea and Vontobel, Pascal O. and Kav{\v{c}}i{\'c}, Aleksandar and Zeng, Wei}, + title = {Simulation-Based Computation of Information Rates for Channels With Memory}, + journal = {IEEE Transactions on Information Theory}, + volume = {52}, + number = {8}, + pages = {3498--3508}, + year = {2006}, + doi = {10.1109/TIT.2006.878110}, +} + +@article{Grigorescu2024, + author = {Grigorescu, Andrea and Boche, Holger and Schaefer, Rafael F. and Poor, H. Vincent}, + title = {Capacity of Finite State Channels With Feedback: Algorithmic and Optimization Theoretic Properties}, + journal = {IEEE Transactions on Information Theory}, + volume = {70}, + number = {8}, + pages = {5413--5426}, + year = {2024}, + doi = {10.1109/TIT.2024.3411919}, +} diff --git a/pyproject.toml b/pyproject.toml index 6f8b5c0..e2ffa1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ docs = [ "sofic[symbolic]", ] dev = [ - "sofic[test,viz]", + "sofic[test,viz,capacity]", "sofic[docs]", "ruff", "ty", @@ -75,6 +75,9 @@ bayes = [ sat = [ "python-sat>=1.8", ] +capacity = [ + "cvxpy>=1.4", +] symbolic = [ "sympy>=1.12", "dit[symbolic]>=2.3", diff --git a/sofic/examples/processes.py b/sofic/examples/processes.py index 53e2dc3..aa4fa0c 100644 --- a/sofic/examples/processes.py +++ b/sofic/examples/processes.py @@ -1153,17 +1153,22 @@ def _transducer( edges: Iterable[tuple[Hashable, Hashable, Any, Any, float]], *, initial: Hashable | None = None, + initials: Iterable[Hashable] | None = None, name: str | None = None, ) -> MealyMachine: edge_list = list(edges) states = list(dict.fromkeys([source for source, *_ in edge_list] + [target for _source, target, *_ in edge_list])) inputs = frozenset(input_symbol for _source, _target, input_symbol, _output_symbol, _prob in edge_list) outputs = frozenset(output_symbol for _source, _target, _input_symbol, output_symbol, _prob in edge_list) - start = initial if initial is not None else (states[0] if states else None) + if initials is not None: + starts = frozenset(initials) + else: + start = initial if initial is not None else (states[0] if states else None) + starts = frozenset({start} if start is not None else set()) machine = MealyMachine( input_alphabet=inputs, output_alphabet=outputs, - initial_states=frozenset({start} if start is not None else set()), + initial_states=starts, ) if name is not None: machine.name = name @@ -1264,6 +1269,53 @@ def gme(create_using: Any = None) -> MealyMachine: ) +def trapdoor(create_using: Any = None) -> MealyMachine: + """Blackwell's trapdoor channel as a unifilar finite-state channel. + + The state is the ball left in the channel. A new ball ``x`` is inserted and + one of the two balls, chosen uniformly, is emitted as ``y``; the other stays + behind, so the state update is ``s' = s XOR x XOR y``. The initial ball is + ``0`` or ``1`` with probability 1/2 each. Its feedback capacity is + ``log2(golden ratio)`` :cite:`Permuter2008`. + """ + del create_using + return _transducer( + [ + ("0", "0", "0", "0", 1), + ("0", "1", "1", "0", 0.5), + ("0", "0", "1", "1", 0.5), + ("1", "1", "1", "1", 1), + ("1", "1", "0", "0", 0.5), + ("1", "0", "0", "1", 0.5), + ], + initials=("0", "1"), + name="Trapdoor", + ) + + +def ising_channel(create_using: Any = None) -> MealyMachine: + """The Ising channel of Berger & Bonomi as a unifilar finite-state channel. + + The state is the previous input. If the new input ``x`` equals it the output + is ``x``; otherwise the output is uniform on ``{0, 1}``. The next state is + ``x``, and the initial state is uniform. Its feedback capacity is about + ``0.575522`` bits :cite:`Elishco2014`. + """ + del create_using + return _transducer( + [ + ("0", "0", "0", "0", 1), + ("0", "1", "1", "0", 0.5), + ("0", "1", "1", "1", 0.5), + ("1", "1", "1", "1", 1), + ("1", "0", "0", "0", 0.5), + ("1", "0", "0", "1", 0.5), + ], + initials=("0", "1"), + name="Ising Channel", + ) + + def binary_channel_et(p: float = 0.1, q: float = 0.2) -> Any: """Memoryless binary channel as its minimal (single-state) ε-transducer.""" from sofic.generators.epsilon_transducer import EpsilonTransducer @@ -1278,6 +1330,20 @@ def gm_to_even_et() -> Any: return EpsilonTransducer.from_channel(gm_to_even()) +def trapdoor_et() -> Any: + """Trapdoor channel as its two-state ε-transducer (uniform initial ball).""" + from sofic.generators.epsilon_transducer import EpsilonTransducer + + return EpsilonTransducer.from_channel(trapdoor()) + + +def ising_channel_et() -> Any: + """Ising channel as its two-state ε-transducer (uniform initial state).""" + from sofic.generators.epsilon_transducer import EpsilonTransducer + + return EpsilonTransducer.from_channel(ising_channel()) + + processes = [ "band_merging", "beads_on_necklace", @@ -1338,10 +1404,14 @@ def gm_to_even_et() -> Any: "sliding_nor", "parity", "gme", + "trapdoor", + "ising_channel", ] epsilon_transducers = [ "binary_channel_et", "gm_to_even_et", + "trapdoor_et", + "ising_channel_et", ] process_list = [globals()[name] for name in processes] diff --git a/sofic/generators/capacity/__init__.py b/sofic/generators/capacity/__init__.py new file mode 100644 index 0000000..7032c86 --- /dev/null +++ b/sofic/generators/capacity/__init__.py @@ -0,0 +1,36 @@ +"""Capacity bounds for finite unifilar channels (ε-transducers), with and without feedback.""" + +from sofic.generators.capacity._channel import ChannelTensors, channel_tensors +from sofic.generators.capacity.feedback import ( + duality_upper_bound, + encoder_rate_bounds, + feedback_capacity, + qgraph_bounds, + qgraph_lower_bound, + qgraph_upper_bound, + value_iteration_estimate, +) +from sofic.generators.capacity.no_feedback import ( + capacity, + duality_upper_bound_no_feedback, + markov_input_lower_bound, +) +from sofic.generators.capacity.qgraph import QGraph +from sofic.generators.capacity.results import CapacityBounds + +__all__ = [ + "CapacityBounds", + "ChannelTensors", + "QGraph", + "capacity", + "channel_tensors", + "duality_upper_bound", + "duality_upper_bound_no_feedback", + "encoder_rate_bounds", + "feedback_capacity", + "markov_input_lower_bound", + "qgraph_bounds", + "qgraph_lower_bound", + "qgraph_upper_bound", + "value_iteration_estimate", +] diff --git a/sofic/generators/capacity/_belief.py b/sofic/generators/capacity/_belief.py new file mode 100644 index 0000000..ff83165 --- /dev/null +++ b/sofic/generators/capacity/_belief.py @@ -0,0 +1,157 @@ +"""Probability-simplex grids and average-reward value iteration. + +Grid points are ``k / n`` for nonnegative integer vectors ``k`` summing to the +resolution ``n``. In cumulative coordinates ``y_i = n * sum_{j >= i} p_j`` +(``i = 1, ..., d``) the grid is the set of integer points with +``n >= y_1 >= ... >= y_d >= 0``, a union of cells of the Freudenthal (Kuhn) +triangulation of the cube, so piecewise-linear interpolation needs only a sort +per query point. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from itertools import combinations_with_replacement +from math import comb + +import numpy as np +from scipy.sparse import coo_matrix, csr_matrix + + +@dataclass(frozen=True, eq=False) +class SimplexGrid: + """Resolution-``n`` grid on the probability simplex with ``n_vertices`` corners.""" + + n_vertices: int + resolution: int + points: np.ndarray + _codes: np.ndarray + _order: np.ndarray + + @classmethod + def build(cls, n_vertices: int, resolution: int, *, max_points: int = 100_000) -> SimplexGrid: + if n_vertices < 1 or resolution < 1: + raise ValueError("need at least one vertex and resolution >= 1") + size = comb(resolution + n_vertices - 1, n_vertices - 1) + if size > max_points: + raise ValueError(f"belief grid would have {size} points (> max_points={max_points}); lower the resolution") + d = n_vertices - 1 + if d == 0: + cum = np.zeros((1, 0), dtype=int) + else: + cum = np.array( + [sorted(c, reverse=True) for c in combinations_with_replacement(range(resolution + 1), d)], + dtype=int, + ) + points = _cumulative_to_counts(cum, resolution) / resolution + codes = _encode(cum, resolution) + order = np.argsort(codes) + return cls(n_vertices, resolution, points, codes[order], order) + + @property + def size(self) -> int: + return self.points.shape[0] + + def interpolation(self, queries: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Return ``(indices, weights)``, each ``(m, d + 1)``, interpolating at ``queries``.""" + queries = np.asarray(queries, dtype=float) + m, d, n = queries.shape[0], self.n_vertices - 1, self.resolution + if d == 0: + return np.zeros((m, 1), dtype=int), np.ones((m, 1)) + tail = np.cumsum(queries[:, ::-1], axis=1)[:, ::-1][:, 1:] + y = np.clip(n * tail, 0.0, n) + y = np.minimum.accumulate(y, axis=1) + base = np.floor(y).astype(int) + frac = y - base + order = np.argsort(-frac, axis=1, kind="stable") + f_sorted = np.take_along_axis(frac, order, axis=1) + + vertices = np.empty((m, d + 1, d), dtype=int) + vertices[:, 0] = base + current = base.copy() + rows = np.arange(m) + for k in range(d): + current[rows, order[:, k]] += 1 + vertices[:, k + 1] = current + weights = np.empty((m, d + 1)) + weights[:, 0] = 1.0 - f_sorted[:, 0] + weights[:, 1:d] = f_sorted[:, :-1] - f_sorted[:, 1:] + weights[:, d] = f_sorted[:, -1] + + codes = _encode(vertices.reshape(-1, d), n) + position = np.searchsorted(self._codes, codes) + position = np.minimum(position, len(self._codes) - 1) + found = self._codes[position] == codes + weights = weights.reshape(-1) + if not np.all(found | (weights <= 1e-12)): + raise AssertionError("interpolation vertex outside the simplex grid") + indices = np.where(found, self._order[position], 0).reshape(m, d + 1) + return indices, np.where(found, weights, 0.0).reshape(m, d + 1) + + +def _cumulative_to_counts(cum: np.ndarray, n: int) -> np.ndarray: + padded = np.hstack([np.full((cum.shape[0], 1), n), cum, np.zeros((cum.shape[0], 1), dtype=int)]) + return padded[:, :-1] - padded[:, 1:] + + +def _encode(cum: np.ndarray, n: int) -> np.ndarray: + radix = (n + 1) ** np.arange(cum.shape[1], dtype=np.int64) + return cum.astype(np.int64) @ radix + + +def transition_operator( + n_rows: int, n_cols: int, rows: np.ndarray, indices: np.ndarray, weights: np.ndarray, scale: np.ndarray +) -> csr_matrix: + """Assemble ``W[row, index] += scale * weight`` from per-query interpolation data.""" + data = (weights * scale[:, None]).ravel() + row_ids = np.repeat(rows, indices.shape[1]) + matrix = coo_matrix((data, (row_ids, indices.ravel())), shape=(n_rows, n_cols)) + return matrix.tocsr() + + +@dataclass(frozen=True) +class ValueIterationResult: + """Span bounds ``gain_lower <= rho* <= gain_upper`` and the greedy policy. + + For any bias vector ``h`` and a monotone, translation-invariant operator + ``T``, every state's optimal average reward lies in + ``[min(Th - h), max(Th - h)]``. + """ + + gain_lower: float + gain_upper: float + bias: np.ndarray + policy: np.ndarray + iterations: int + + +def relative_value_iteration( + rewards: np.ndarray, + operator: csr_matrix, + *, + tol: float = 1e-9, + max_iter: int = 20_000, + damping: float = 0.5, + bias: np.ndarray | None = None, +) -> ValueIterationResult: + """Maximize average reward ``rewards[i, a] + (operator h)[a * n + i]``. + + ``operator`` stacks one ``(n, n)`` transition block per action; disallowed + actions carry reward ``-inf``. The damped update ``h <- (1 - t) h + t T h`` + removes periodicity without moving the fixed point. + """ + n, n_actions = rewards.shape + h = np.zeros(n) if bias is None else np.array(bias, dtype=float) + iterations = 0 + while True: + iterations += 1 + q = rewards + (operator @ h).reshape(n_actions, n).T + th = q.max(axis=1) + diff = th - h + lower, upper = float(diff.min()), float(diff.max()) + if upper - lower < tol or iterations >= max_iter: + break + h = h + damping * diff + h -= h[0] + policy = q.argmax(axis=1) + return ValueIterationResult(lower, upper, h, policy, iterations) diff --git a/sofic/generators/capacity/_channel.py b/sofic/generators/capacity/_channel.py new file mode 100644 index 0000000..4dd4e4c --- /dev/null +++ b/sofic/generators/capacity/_channel.py @@ -0,0 +1,127 @@ +"""Array form of a finite unifilar channel ``T(y, s' | s, x)`` with ``s' = f(s, x, y)``.""" + +from __future__ import annotations + +from collections.abc import Hashable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +import numpy as np +from scipy.sparse import csr_matrix +from scipy.sparse.csgraph import connected_components + +from sofic.exceptions import StochasticValidationError, UnifilarityError +from sofic.graph import ATTR_OUTPUT, ATTR_PROB, ATTR_SYMBOL + +if TYPE_CHECKING: + from sofic.automata.transducers import MealyMachine + + +@dataclass(frozen=True, eq=False) +class ChannelTensors: + """Dense arrays for a unifilar finite-state channel. + + ``P[s, x, y]`` is the output law, ``F[s, x, y]`` the successor state index + (``-1`` where ``P`` is zero), and ``allowed[s, x]`` marks inputs defined at + ``s``; undefined inputs act as input constraints. ``initial`` is the + initial state law. + """ + + states: tuple[Hashable, ...] + inputs: tuple[Any, ...] + outputs: tuple[Any, ...] + P: np.ndarray + F: np.ndarray + allowed: np.ndarray + initial: np.ndarray + + @property + def n_states(self) -> int: + return len(self.states) + + @property + def n_inputs(self) -> int: + return len(self.inputs) + + @property + def n_outputs(self) -> int: + return len(self.outputs) + + def trivial_upper_bound(self) -> float: + """Return ``log2 min(|X|, |Y|)``, which bounds every capacity.""" + return float(np.log2(min(self.n_inputs, self.n_outputs))) + + def conditional_entropy(self) -> np.ndarray: + """Return ``H[Y | X = x, S = s]`` in bits as an ``(S, X)`` array.""" + P = self.P + with np.errstate(divide="ignore", invalid="ignore"): + terms = np.where(P > 0, -P * np.log2(np.where(P > 0, P, 1.0)), 0.0) + return terms.sum(axis=2) + + +def _sorted(values: set[Any]) -> tuple[Any, ...]: + return tuple(sorted(values, key=repr)) + + +def channel_tensors(channel: MealyMachine) -> ChannelTensors: + """Return the :class:`ChannelTensors` of a unifilar stochastic Mealy machine.""" + transitions = list(channel.transitions()) + states = _sorted(set(channel.states())) + inputs = _sorted({t.data[ATTR_SYMBOL] for t in transitions}) + outputs = _sorted({t.data[ATTR_OUTPUT] for t in transitions}) + s_index = {state: i for i, state in enumerate(states)} + x_index = {symbol: i for i, symbol in enumerate(inputs)} + y_index = {symbol: i for i, symbol in enumerate(outputs)} + + shape = (len(states), len(inputs), len(outputs)) + P = np.zeros(shape) + F = np.full(shape, -1, dtype=int) + for transition in transitions: + s = s_index[transition.source] + x = x_index[transition.data[ATTR_SYMBOL]] + y = y_index[transition.data[ATTR_OUTPUT]] + prob = float(transition.data.get(ATTR_PROB, 1.0)) + if prob <= 0.0: + continue + target = s_index[transition.target] + if F[s, x, y] not in (-1, target): + raise UnifilarityError( + f"(state, input, output) = {(transition.source, inputs[x], outputs[y])} is ambiguous" + ) + P[s, x, y] += prob + F[s, x, y] = target + + totals = P.sum(axis=2) + allowed = totals > 0.0 + if not np.allclose(totals[allowed], 1.0): + raise StochasticValidationError("channel rows T(., . | s, x) must sum to one") + if not allowed.any(axis=1).all(): + raise StochasticValidationError("every state needs at least one admissible input") + + distribution = getattr(channel, "initial_distribution", None) or {} + if distribution: + initial = np.array([float(distribution.get(state, 0.0)) for state in states]) + else: + initial = np.array([1.0 if state in channel.initial_states else 0.0 for state in states]) + if initial.sum() == 0.0: + initial = np.ones(len(states)) + initial = initial / initial.sum() + return ChannelTensors(states, inputs, outputs, P, F, allowed, initial) + + +def require_strongly_connected(tensors: ChannelTensors) -> None: + """Raise unless some input policy can drive every state to every other state. + + The feedback-capacity theorems used here assume a strongly connected + unifilar channel :cite:`Permuter2008,Sabag2017`, which makes the capacity + independent of the initial state. + """ + n = tensors.n_states + reachable = np.zeros((n, n), dtype=bool) + sources, _xs, _ys = np.nonzero(tensors.P > 0) + reachable[sources, tensors.F[tensors.P > 0]] = True + n_components, _labels = connected_components(csr_matrix(reachable), directed=True, connection="strong") + if n_components != 1: + raise StochasticValidationError( + f"capacity bounds need a strongly connected channel; this one has {n_components} strong components" + ) diff --git a/sofic/generators/capacity/_rates.py b/sofic/generators/capacity/_rates.py new file mode 100644 index 0000000..30b0192 --- /dev/null +++ b/sofic/generators/capacity/_rates.py @@ -0,0 +1,51 @@ +"""Information rates of finite-state inputs driving a unifilar channel.""" + +from __future__ import annotations + +import networkx as nx +import numpy as np + + +def unique_closed_class(matrix: np.ndarray) -> np.ndarray | None: + """Return the states of the single closed class of ``matrix``, or ``None`` if there are several.""" + support = nx.DiGraph() + support.add_nodes_from(range(matrix.shape[0])) + support.add_edges_from(zip(*np.nonzero(matrix > 1e-12), strict=True)) + condensation = nx.condensation(support) + closed = [c for c in condensation.nodes if condensation.out_degree(c) == 0] + if len(closed) != 1: + return None + return np.array(sorted(condensation.nodes[closed[0]]["members"])) + + +def driven_rate_bounds(symbol_tensors: np.ndarray, noise: np.ndarray, block: int) -> tuple[float, float] | None: + r"""Bracket :math:`\bar h(Y) - \mathbb{E}_\pi[\text{noise}]` for an ergodic edge-emitting HMM. + + ``symbol_tensors[y, z, z']`` is the hidden chain of a finite-state input + driving a unifilar channel, and ``noise[z]`` is + :math:`\mathbb{E}[H(Y \mid X, S) \mid Z = z]`. Because ``(s, x, y)`` + determines the next channel state, :math:`H(Y^n \| X^n) / n \to + \mathbb{E}_\pi H(Y \mid X, S)`, so the result brackets the (directed) + information rate; the entropy-rate bracket is + :math:`H[Y_n \mid Y_{0:n}, Z_0] \le \bar h \le H[Y_n \mid Y_{0:n}]` + :cite:`Cover2006`. Returns ``None`` unless the chain has one closed class. + """ + from sofic.generators.measures import _iter_entropy_rate_bounds + from sofic.generators.stationary import stationary_distribution_from_transition + + matrix = symbol_tensors.sum(axis=0) + if unique_closed_class(matrix) is None: + return None + pi = stationary_distribution_from_transition(matrix) + for index, (lower, upper, _words) in enumerate(_iter_entropy_rate_bounds(pi, symbol_tensors)): + if index == block: + conditional = float(pi @ noise) + return lower - conditional, upper - conditional + raise AssertionError("unreachable") + + +def default_block(n_outputs: int, bits: int) -> int: + """Return the largest block length ``n >= 1`` with ``n_outputs ** n <= 2 ** bits``.""" + if n_outputs <= 1: + return 1 + return max(1, int(bits / np.log2(n_outputs) + 1e-9)) diff --git a/sofic/generators/capacity/feedback.py b/sofic/generators/capacity/feedback.py new file mode 100644 index 0000000..ddedc87 --- /dev/null +++ b/sofic/generators/capacity/feedback.py @@ -0,0 +1,654 @@ +r"""Feedback capacity of finite unifilar channels (ε-transducers). + +An ε-transducer is a unifilar finite-state channel: ``(s, x, y)`` determines +the next causal state. With feedback the encoder sees past outputs, hence knows +the causal state, and the feedback capacity is the limit of normalized directed +information :cite:`Permuter2008,Yang2005`. Feedback capacity of finite-state +channels is not computable in general :cite:`Grigorescu2024`, so every method +here returns a bracket: + +* :func:`qgraph_bounds` -- for a Q-graph, the single-letter upper bound + :math:`\max I(X, S; Y \mid Q)` over stationary joints :cite:`Sabag2017` + (a convex program :cite:`Sabag2020`) and the same objective restricted to + BCJR-invariant inputs as an achievable lower bound. +* :func:`value_iteration_estimate` -- the belief-state dynamic program of + :cite:`Permuter2008,Yang2005` on a gridded simplex (an uncertified estimate). +* :func:`duality_upper_bound` -- the duality bound with a Q-graph test + distribution, which for unifilar channels is a finite Markov decision + process :cite:`Huleihel2025`. +""" + +from __future__ import annotations + +import warnings +from collections.abc import Iterable, Sequence +from dataclasses import dataclass +from itertools import product +from math import log +from typing import Any, Literal + +import networkx as nx +import numpy as np +from scipy.optimize import minimize + +from sofic.generators.capacity._belief import ( + SimplexGrid, + relative_value_iteration, + transition_operator, +) +from sofic.generators.capacity._channel import ChannelTensors, channel_tensors, require_strongly_connected +from sofic.generators.capacity.qgraph import QGraph, coupled_kernel +from sofic.generators.capacity.results import CapacityBounds + +_LN2 = log(2.0) +_TINY = 1e-300 + +FeedbackMethod = Literal["qgraph", "value_iteration", "duality", "all"] + + +def as_tensors(channel: Any) -> ChannelTensors: + """Return ``channel`` as :class:`ChannelTensors`, checking strong connectivity.""" + tensors = channel if isinstance(channel, ChannelTensors) else channel_tensors(channel) + require_strongly_connected(tensors) + return tensors + + +# -- Q-graph bounds ----------------------------------------------------------- + + +@dataclass(frozen=True, eq=False) +class _QGraphProgram: + """Linear maps of the joint ``d[s, q, x]`` used by both Q-graph bounds.""" + + n_s: int + n_q: int + n_x: int + n_y: int + table: np.ndarray + allowed: np.ndarray + stationarity: np.ndarray + occupancy: np.ndarray + outputs: np.ndarray + context: np.ndarray + context_rep: np.ndarray + noise: np.ndarray + successor: np.ndarray + + @classmethod + def build(cls, tensors: ChannelTensors, qgraph: QGraph) -> _QGraphProgram: + table = qgraph.aligned(tensors.outputs) + n_s, n_x, n_y = tensors.P.shape + n_q = table.shape[0] + nv = n_s * n_q * n_x + kernel = coupled_kernel(tensors, table).reshape(nv, n_s * n_q) + occupancy = np.zeros((n_s * n_q, nv)) + for s, q, x in product(range(n_s), range(n_q), range(n_x)): + occupancy[s * n_q + q, (s * n_q + q) * n_x + x] = 1.0 + outputs = np.zeros((n_q * n_y, nv)) + successor = np.zeros((n_s * n_q * n_y, nv)) + for s, q, x, y in product(range(n_s), range(n_q), range(n_x), range(n_y)): + v = (s * n_q + q) * n_x + x + outputs[q * n_y + y, v] = tensors.P[s, x, y] + if tensors.P[s, x, y] > 0: + successor[(tensors.F[s, x, y] * n_q + q) * n_y + y, v] = tensors.P[s, x, y] + context = occupancy.reshape(n_s, n_q, nv).sum(axis=0) + noise = np.repeat(tensors.conditional_entropy()[:, None, :], n_q, axis=1).ravel() + allowed = np.repeat(tensors.allowed[:, None, :], n_q, axis=1).ravel() + return cls( + n_s, + n_q, + n_x, + n_y, + table, + allowed, + kernel.T - occupancy, + occupancy, + outputs, + context, + np.repeat(context, n_y, axis=0), + noise, + successor, + ) + + @property + def n_vars(self) -> int: + return self.n_s * self.n_q * self.n_x + + def information(self, d: np.ndarray) -> float: + """Return ``I(X, S; Y | Q) = H(Y | Q) - H(Y | X, S)`` in bits.""" + n = self.outputs @ d + m = self.context_rep @ d + with np.errstate(divide="ignore", invalid="ignore"): + terms = np.where(n > 0, n * np.log2(np.maximum(n, _TINY) / np.maximum(m, _TINY)), 0.0) + return float(-terms.sum() - self.noise @ d) + + def gradient(self, d: np.ndarray) -> np.ndarray: + n = np.maximum(self.outputs @ d, 1e-15) + m = np.maximum(self.context_rep @ d, 1e-15) + return -(self.outputs.T @ np.log2(n / m)) - self.noise + + def bcjr_residual(self, d: np.ndarray) -> np.ndarray: + """Return ``P(s+, q, y) P(q+) - P(q, y) P(s+, q+)`` with ``q+ = g(q, y)``.""" + b = (self.successor @ d).reshape(self.n_s, self.n_q, self.n_y) + n = (self.outputs @ d).reshape(self.n_q, self.n_y) + pi = (self.occupancy @ d).reshape(self.n_s, self.n_q) + pi_q = pi.sum(axis=0) + return (b * pi_q[self.table][None] - n[None] * pi[:, self.table]).ravel() + + def policy(self, d: np.ndarray) -> np.ndarray: + """Return ``P(x | s, q)``; unvisited ``(s, q)`` get the uniform admissible input.""" + joint = d.reshape(self.n_s, self.n_q, self.n_x) + mask = self.allowed.reshape(self.n_s, self.n_q, self.n_x).astype(float) + mass = joint.sum(axis=2, keepdims=True) + uniform = mask / mask.sum(axis=2, keepdims=True) + return np.where(mass > 1e-12, joint / np.maximum(mass, _TINY), uniform) + + def random_start(self, rng: np.random.Generator, tensors: ChannelTensors) -> np.ndarray: + mask = self.allowed.reshape(self.n_s, self.n_q, self.n_x) + policy = rng.dirichlet(np.ones(self.n_x), size=(self.n_s, self.n_q)) * mask + policy /= policy.sum(axis=2, keepdims=True) + pi = _stationary(self._chain(policy, tensors)) + return (pi.reshape(self.n_s, self.n_q, 1) * policy).ravel() + + def _chain(self, policy: np.ndarray, tensors: ChannelTensors) -> np.ndarray: + kernel = coupled_kernel(tensors, self.table) + n = self.n_s * self.n_q + return np.einsum("sqx,sqxtr->sqtr", policy, kernel).reshape(n, n) + + +def _stationary(matrix: np.ndarray) -> np.ndarray: + from sofic.generators.stationary import stationary_distribution_from_transition + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + return stationary_distribution_from_transition(matrix) + + +def _has_cvxpy() -> bool: + try: + import cvxpy # noqa: F401 + except ImportError: + return False + return True + + +def qgraph_upper_bound( + tensors: ChannelTensors, + qgraph: QGraph, + *, + solver: Literal["auto", "cvxpy", "scipy"] = "auto", +) -> tuple[float, np.ndarray]: + r"""Return :math:`\max I(X, S; Y \mid Q)` over stationary joints and the optimal joint. + + For any Q-graph this upper-bounds the feedback capacity of a strongly + connected unifilar channel :cite:`Sabag2017`. Optimizing over the joint + ``d[s, q, x]`` with linear stationarity constraints makes the problem + convex :cite:`Sabag2020`. ``solver="auto"`` uses cvxpy when installed; + its solution, plus random stationary starts, is polished with scipy's + SLSQP and the best feasible point is kept. The value is accurate to the + optimizer's tolerance. + """ + program = _QGraphProgram.build(tensors, qgraph) + use_cvxpy = solver == "cvxpy" or (solver == "auto" and _has_cvxpy()) + starts = [_solve_upper_cvxpy(program)] if use_cvxpy else [] + d = _solve_upper_scipy(program, tensors, starts) + d = np.where(program.allowed, np.maximum(d, 0.0), 0.0) + d /= d.sum() + return program.information(d), d + + +def _solve_upper_cvxpy(program: _QGraphProgram) -> np.ndarray: + import cvxpy as cp + + d = cp.Variable(program.n_vars, nonneg=True) + constraints = [program.stationarity @ d == 0, cp.sum(d) == 1] + if not program.allowed.all(): + constraints.append(d[np.flatnonzero(~program.allowed)] == 0) + conditional_output = -cp.sum(cp.rel_entr(program.outputs @ d, program.context_rep @ d)) / _LN2 + problem = cp.Problem(cp.Maximize(conditional_output - program.noise @ d), constraints) + problem.solve() + if d.value is None: + raise RuntimeError(f"cvxpy failed to solve the Q-graph upper bound ({problem.status})") + return np.asarray(d.value, dtype=float) + + +def _independent_rows(matrix: np.ndarray, target: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Return an equivalent full-row-rank system for ``matrix @ d == target``.""" + u, sigma, vt = np.linalg.svd(matrix, full_matrices=False) + rank = int((sigma > 1e-10 * sigma[0]).sum()) + return vt[:rank], (u[:, :rank].T @ target) / sigma[:rank] + + +def _solve_upper_scipy(program: _QGraphProgram, tensors: ChannelTensors, starts: list[np.ndarray]) -> np.ndarray: + nv = program.n_vars + equality = np.vstack([program.stationarity, np.ones((1, nv))]) + target = np.zeros(equality.shape[0]) + target[-1] = 1.0 + equality, target = _independent_rows(equality, target) + bounds = list(zip(np.zeros(nv), np.where(program.allowed, 1.0, 0.0), strict=True)) + rng = np.random.default_rng(0) + starts = [np.clip(start, 0.0, 1.0) for start in starts] + [program.random_start(rng, tensors) for _ in range(3)] + best, best_value = starts[0], -np.inf + for start in starts: + result = minimize( + lambda d: -program.information(d), + start, + jac=lambda d: -program.gradient(d), + method="SLSQP", + bounds=bounds, + constraints=[{"type": "eq", "fun": lambda d: equality @ d - target, "jac": lambda d: equality}], + options={"ftol": 1e-15, "maxiter": 3000}, + ) + value = -float(result.fun) + if value > best_value: + best, best_value = np.asarray(result.x, dtype=float), value + return best + + +def _encoder_kernel(tensors: ChannelTensors, table: np.ndarray) -> np.ndarray: + """Return ``K[s, q, x, y, s', q']`` for an encoder that sees ``(s, q)``.""" + n_s, n_x, n_y = tensors.P.shape + n_q = table.shape[0] + kernel = np.zeros((n_s, n_q, n_x, n_y, n_s, n_q)) + for s, x, y in zip(*np.nonzero(tensors.P > 0), strict=True): + kernel[s, np.arange(n_q), x, y, tensors.F[s, x, y], table[:, y]] = tensors.P[s, x, y] + return kernel + + +def encoder_rate_bounds( + tensors: ChannelTensors, qgraph: QGraph, policy: np.ndarray, *, block: int = 16 +) -> tuple[float, float] | None: + r"""Bracket the directed-information rate of the graph-based encoder ``policy[s, q, x]``. + + With feedback the encoder knows the causal state ``s`` and the context + ``q``, so any ``P(x | s, q)`` is a valid causal input and its rate + :math:`\bar h(Y) - \mathbb{E}_\pi H(Y \mid X, S)` is achievable + :cite:`Permuter2008,Sabag2020`. The lower end uses + :math:`H[Y_n \mid Y_{0:n}, Z_0]` with ``n = block``, so it is a certified + lower bound on the feedback capacity. Returns ``None`` when the encoder's + ``(S, Q)`` chain has more than one closed class. + """ + from sofic.generators.capacity._rates import driven_rate_bounds + + table = qgraph.aligned(tensors.outputs) + n_z = tensors.n_states * qgraph.n_nodes + kernel = _encoder_kernel(tensors, table) + symbol_tensors = np.einsum("sqx,sqxytr->ysqtr", policy, kernel).reshape(tensors.n_outputs, n_z, n_z) + noise = np.einsum("sqx,sx->sq", policy, tensors.conditional_entropy()).ravel() + return driven_rate_bounds(symbol_tensors, noise, block) + + +def _masked_softmax(logits: np.ndarray, mask: np.ndarray) -> np.ndarray: + shifted = np.where(mask, logits, -np.inf) + shifted = shifted - shifted.max(axis=-1, keepdims=True) + weights = np.where(mask, np.exp(shifted), 0.0) + return weights / weights.sum(axis=-1, keepdims=True) + + +def qgraph_lower_bound( + tensors: ChannelTensors, + qgraph: QGraph, + *, + start_joint: np.ndarray | None = None, + restarts: int = 4, + seed: int | None = 0, + block: int | None = None, + final_block: int | None = None, + bcjr_tol: float = 1e-7, +) -> tuple[float, np.ndarray | None]: + r"""Return a certified achievable feedback rate for an encoder on ``qgraph``. + + Optimizes the graph-based encoder :math:`P(x \mid s, q)` (L-BFGS-B on + softmax logits, started from ``start_joint`` -- typically the upper-bound + optimizer -- and ``restarts`` random policies) for the lower end of + :func:`encoder_rate_bounds` at ``block``, then re-evaluates the winner at + ``final_block`` (by default the largest ``n`` with :math:`|Y|^n \le 2^8` + and :math:`2^{16}`, respectively). The upper-bound optimizer's own policy + is evaluated too. If the winning input is also aperiodic and + BCJR-invariant, :math:`S^+ - Q^+ - (Q, Y)`, the single-letter rate + :math:`I(X, S; Y \mid Q)` is achievable as well :cite:`Sabag2017,Sabag2020` + and the larger value is returned. Returns ``(rate, policy)``. + """ + from sofic.generators.capacity._rates import default_block + + block = default_block(tensors.n_outputs, 8) if block is None else block + final_block = default_block(tensors.n_outputs, 16) if final_block is None else final_block + program = _QGraphProgram.build(tensors, qgraph) + mask = program.allowed.reshape(program.n_s, program.n_q, program.n_x) + rng = np.random.default_rng(seed) + + def objective(z: np.ndarray, n: int) -> float: + bounds = encoder_rate_bounds(tensors, qgraph, _masked_softmax(z.reshape(mask.shape), mask), block=n) + return 1.0 if bounds is None else -bounds[0] + + starts = [] + if start_joint is not None: + starts.append(np.log(np.maximum(program.policy(np.maximum(start_joint, 0.0)), 1e-9)).ravel()) + starts += [rng.normal(size=mask.size) for _ in range(restarts)] + best_z, best_value = None, np.inf + for z0 in starts: + result = minimize(objective, z0, args=(block,), method="L-BFGS-B", options={"maxiter": 500}) + if result.fun < best_value: + best_z, best_value = result.x, float(result.fun) + if best_z is None: + return 0.0, None + candidates = [_masked_softmax(best_z.reshape(mask.shape), mask)] + if start_joint is not None: + candidates.append(_clean_policy(program.policy(np.maximum(start_joint, 0.0)))) + best_rate, best_policy = 0.0, None + for policy in candidates: + final = encoder_rate_bounds(tensors, qgraph, policy, block=final_block) + rate = 0.0 if final is None else final[0] + single_letter = _bcjr_rate(program, tensors, policy, bcjr_tol) + if single_letter is not None: + rate = max(rate, single_letter) + if best_policy is None or rate > best_rate: + best_rate, best_policy = rate, policy + return max(best_rate, 0.0), best_policy + + +def _clean_policy(policy: np.ndarray, threshold: float = 1e-7) -> np.ndarray: + """Zero solver noise below ``threshold`` so near-deterministic inputs become deterministic.""" + cleaned = np.where(policy < threshold, 0.0, policy) + return cleaned / cleaned.sum(axis=-1, keepdims=True) + + +def _bcjr_rate(program: _QGraphProgram, tensors: ChannelTensors, policy: np.ndarray, tol: float) -> float | None: + """Return ``I(X, S; Y | Q)`` if ``policy`` is aperiodic and BCJR-invariant, else ``None``.""" + from sofic.generators.capacity._rates import unique_closed_class + + chain = program._chain(policy, tensors) + members = unique_closed_class(chain) + if members is None: + return None + support = nx.DiGraph() + support.add_edges_from(zip(*np.nonzero(chain[np.ix_(members, members)] > 1e-12), strict=True)) + if not nx.is_aperiodic(support): + return None + joint = (_stationary(chain).reshape(program.n_s, program.n_q, 1) * policy).ravel() + if np.max(np.abs(program.bcjr_residual(joint))) > tol: + return None + return program.information(joint) + + +@dataclass(frozen=True, eq=False) +class _QGraphOutcome: + qgraph: QGraph + upper: float + upper_joint: np.ndarray + lower: float + lower_policy: np.ndarray | None + + +def qgraph_bounds( + channel: Any, + *, + qgraphs: Iterable[QGraph] | None = None, + max_order: int = 2, + solver: Literal["auto", "cvxpy", "scipy"] = "auto", + restarts: int = 4, + seed: int | None = 0, + block: int | None = None, + final_block: int | None = None, +) -> CapacityBounds: + """Bracket the feedback capacity with Q-graph bounds. + + Tries the Markov Q-graphs of orders ``0, ..., max_order`` plus any + ``qgraphs`` and keeps the smallest upper and largest lower bound + :cite:`Sabag2017,Sabag2020`. + """ + tensors = as_tensors(channel) + candidates = [QGraph.markov(tensors.outputs, k) for k in range(max_order + 1)] + candidates += list(qgraphs or []) + outcomes = [] + for qgraph in candidates: + upper, upper_joint = qgraph_upper_bound(tensors, qgraph, solver=solver) + lower, policy = qgraph_lower_bound( + tensors, + qgraph, + start_joint=upper_joint, + restarts=restarts, + seed=seed, + block=block, + final_block=final_block, + ) + outcomes.append(_QGraphOutcome(qgraph, upper, upper_joint, lower, policy)) + best_upper = min(outcomes, key=lambda o: o.upper) + best_lower = max(outcomes, key=lambda o: o.lower) + upper = min(best_upper.upper, tensors.trivial_upper_bound()) + return CapacityBounds( + lower=min(max(best_lower.lower, 0.0), upper), + upper=upper, + method="qgraph", + feedback=True, + details={ + "upper_qgraph": best_upper.qgraph, + "upper_joint": best_upper.upper_joint, + "lower_qgraph": best_lower.qgraph, + "lower_policy": best_lower.lower_policy, + "per_qgraph": [(o.qgraph, o.lower, o.upper) for o in outcomes], + }, + ) + + +# -- belief-state dynamic program -------------------------------------------- + + +def _input_grid(n_inputs: int, allowed: np.ndarray, resolution: int) -> np.ndarray: + """Return all input laws on the admissible inputs with masses in ``1/resolution`` steps.""" + support = np.flatnonzero(allowed) + grid = SimplexGrid.build(len(support), resolution).points + laws = np.zeros((grid.shape[0], n_inputs)) + laws[:, support] = grid + return laws + + +def value_iteration_estimate( + channel: Any, + *, + belief_resolution: int = 100, + action_resolution: int = 10, + tol: float = 1e-9, + max_iter: int = 20_000, + max_grid_points: int = 100_000, +) -> CapacityBounds: + r"""Estimate the feedback capacity by value iteration on the belief simplex. + + The dynamic program of :cite:`Permuter2008,Yang2005` has state + :math:`\beta = P(S \mid y^t)`, action :math:`u(x \mid s)` and reward + :math:`I(X, S; Y)` under :math:`\beta u P`. The belief simplex and the + action set are gridded, so the returned bracket bounds the gain of the + *discretized* problem only and is marked ``certified=False``; it converges + to the capacity as both resolutions grow. ``details["policy"]`` holds the + greedy input law at every grid belief. + """ + tensors = as_tensors(channel) + n_s, n_x, n_y = tensors.P.shape + grid = SimplexGrid.build(n_s, belief_resolution, max_points=max_grid_points) + per_state = [_input_grid(n_x, tensors.allowed[s], action_resolution) for s in range(n_s)] + actions = np.array([np.stack(choice) for choice in product(*per_state)]) + n_b, n_a = grid.size, actions.shape[0] + if n_b * n_a * n_s * n_x * n_y > 5e7: + raise ValueError("belief/action grid too large; lower belief_resolution or action_resolution") + + noise = tensors.conditional_entropy() + joint = np.einsum("bs,asx,sxy->basxy", grid.points, actions, tensors.P) + p_y = joint.sum(axis=(2, 3)) + with np.errstate(divide="ignore", invalid="ignore"): + h_y = -np.where(p_y > 0, p_y * np.log2(np.maximum(p_y, _TINY)), 0.0).sum(axis=2) + rewards = h_y - np.einsum("bs,asx,sx->ba", grid.points, actions, noise) + + onehot = np.zeros((n_s, n_x, n_y, n_s)) + s_idx, x_idx, y_idx = np.nonzero(tensors.P > 0) + onehot[s_idx, x_idx, y_idx, tensors.F[s_idx, x_idx, y_idx]] = 1.0 + unnormalized = np.einsum("basxy,sxyt->bayt", joint, onehot) + queries = unnormalized / np.maximum(p_y[..., None], _TINY) + queries[p_y <= 0] = 1.0 / n_s + indices, weights = grid.interpolation(queries.reshape(-1, n_s)) + rows = (np.arange(n_a)[None, :, None] * n_b + np.arange(n_b)[:, None, None]) * np.ones((1, 1, n_y), dtype=int) + operator = transition_operator(n_a * n_b, n_b, rows.ravel(), indices, weights, p_y.ravel()) + + result = relative_value_iteration(rewards, operator, tol=tol, max_iter=max_iter) + return CapacityBounds( + lower=max(result.gain_lower, 0.0), + upper=min(result.gain_upper, tensors.trivial_upper_bound()), + method="value_iteration", + feedback=True, + certified=False, + details={ + "beliefs": grid.points, + "policy": actions[result.policy], + "bias": result.bias, + "iterations": result.iterations, + }, + ) + + +# -- duality upper bound ------------------------------------------------------ + + +def _divergence_rewards(tensors: ChannelTensors, test: np.ndarray) -> np.ndarray: + """Return ``D(P(. | x, s) || R(. | q))`` in bits as an ``(S, Q, X)`` array.""" + P = tensors.P[:, None, :, :] + R = test[None, :, None, :] + with np.errstate(divide="ignore", invalid="ignore"): + terms = np.where(P > 0, P * np.log2(np.where(P > 0, P, 1.0) / np.maximum(R, _TINY)), 0.0) + rewards = terms.sum(axis=3) + return np.where(tensors.allowed[:, None, :], rewards, -np.inf) + + +def _duality_operator(tensors: ChannelTensors, table: np.ndarray): + n_s, n_x, _n_y = tensors.P.shape + n_q = table.shape[0] + n = n_s * n_q + kernel = coupled_kernel(tensors, table).reshape(n, n_x, n) + from scipy.sparse import csr_matrix + + return csr_matrix(kernel.transpose(1, 0, 2).reshape(n_x * n, n)) + + +def _softmax_rows(logits: np.ndarray) -> np.ndarray: + shifted = logits - logits.max(axis=1, keepdims=True) + weights = np.exp(shifted) + return weights / weights.sum(axis=1, keepdims=True) + + +def _test_from_joint(program_table: np.ndarray, tensors: ChannelTensors, joint: np.ndarray | None) -> np.ndarray: + n_q, n_y = program_table.shape[0], tensors.n_outputs + if joint is None: + return np.full((n_q, n_y), 1.0 / n_y) + d = joint.reshape(tensors.n_states, n_q, tensors.n_inputs) + outputs = np.einsum("sqx,sxy->qy", d, tensors.P) + mass = outputs.sum(axis=1, keepdims=True) + test = np.where(mass > 1e-12, outputs / np.maximum(mass, _TINY), 1.0 / n_y) + return 0.999 * test + 0.001 / n_y + + +def duality_gain(tensors: ChannelTensors, qgraph: QGraph, test: np.ndarray, *, tol: float = 1e-10) -> float: + r"""Return the duality upper bound for the test distribution ``test[q, y]``. + + The bound is the optimal average reward of the finite MDP with state + ``(s, q)``, action ``x``, reward :math:`D(P_{Y \mid x, s} \| R_{Y \mid q})` + and transitions ``s' = f(s, x, y)``, ``q' = g(q, y)`` :cite:`Huleihel2025`; + the span bound ``max(Th - h)`` keeps it a valid upper bound at any accuracy. + """ + table = qgraph.aligned(tensors.outputs) + rewards = _divergence_rewards(tensors, test).reshape(-1, tensors.n_inputs) + result = relative_value_iteration(rewards, _duality_operator(tensors, table), tol=tol) + return result.gain_upper + + +def duality_upper_bound( + channel: Any, + *, + qgraphs: Iterable[QGraph] | None = None, + max_order: int = 2, + tests: Sequence[np.ndarray] | None = None, + optimize: bool = True, + solver: Literal["auto", "cvxpy", "scipy"] = "auto", +) -> CapacityBounds: + """Upper-bound the feedback capacity by the Q-graph duality bound. + + For each Q-graph the test distribution starts at the output law of the + Q-graph upper-bound optimizer (or ``tests``, aligned with ``qgraphs``) and, + with ``optimize=True``, is refined by Nelder--Mead on softmax logits. + Every test distribution yields a valid bound :cite:`Huleihel2025`. + """ + tensors = as_tensors(channel) + candidates = [QGraph.markov(tensors.outputs, k) for k in range(max_order + 1)] + user = list(qgraphs or []) + candidates += user + user_tests = list(tests or []) + best_value, best_graph, best_test = np.inf, None, None + for index, qgraph in enumerate(candidates): + table = qgraph.aligned(tensors.outputs) + user_index = index - (len(candidates) - len(user)) + if 0 <= user_index < len(user_tests): + start = np.asarray(user_tests[user_index], dtype=float) + else: + _value, joint = qgraph_upper_bound(tensors, qgraph, solver=solver) + start = _test_from_joint(table, tensors, joint) + value, test = duality_gain(tensors, qgraph, start), start + if optimize and tensors.n_outputs > 1: + value, test = _minimize_test(lambda r, g=qgraph: duality_gain(tensors, g, r, tol=1e-9), start, value) + value = duality_gain(tensors, qgraph, test) + if value < best_value: + best_value, best_graph, best_test = value, qgraph, test + return CapacityBounds( + lower=0.0, + upper=min(float(best_value), tensors.trivial_upper_bound()), + method="duality", + feedback=True, + details={"qgraph": best_graph, "test_distribution": best_test}, + ) + + +def _minimize_test(objective, start: np.ndarray, start_value: float) -> tuple[float, np.ndarray]: + logits0 = np.log(np.maximum(start, 1e-12)) + shape = start.shape + result = minimize( + lambda z: objective(_softmax_rows(z.reshape(shape))), + logits0.ravel(), + method="Nelder-Mead", + options={"xatol": 1e-7, "fatol": 1e-10, "maxiter": 400 * logits0.size}, + ) + test = _softmax_rows(result.x.reshape(shape)) + if result.fun < start_value: + return float(result.fun), test + return start_value, start + + +# -- entry point -------------------------------------------------------------- + + +def feedback_capacity(channel: Any, method: FeedbackMethod = "qgraph", **kwargs: Any) -> CapacityBounds: + """Bracket the feedback capacity of a finite unifilar channel. + + ``method`` selects :func:`qgraph_bounds`, :func:`value_iteration_estimate`, + :func:`duality_upper_bound`, or ``"all"``, which intersects the certified + brackets of the Q-graph and duality bounds and reports the value-iteration + estimate in ``details``. Keyword arguments go to the selected method(s). + """ + if method == "qgraph": + return qgraph_bounds(channel, **kwargs) + if method == "value_iteration": + return value_iteration_estimate(channel, **kwargs) + if method == "duality": + return duality_upper_bound(channel, **kwargs) + if method == "all": + tensors = as_tensors(channel) + qgraph_keys = ("qgraphs", "max_order", "solver", "restarts", "seed", "block", "final_block") + qgraph_kwargs = {k: kwargs[k] for k in qgraph_keys if k in kwargs} + duality_kwargs = {k: kwargs[k] for k in ("qgraphs", "max_order", "solver", "tests", "optimize") if k in kwargs} + vi_kwargs = {k: kwargs[k] for k in ("belief_resolution", "action_resolution", "max_grid_points") if k in kwargs} + q = qgraph_bounds(tensors, **qgraph_kwargs) + dual = duality_upper_bound(tensors, **duality_kwargs) + vi = value_iteration_estimate(tensors, **vi_kwargs) + return CapacityBounds( + lower=q.lower, + upper=min(q.upper, dual.upper), + method="all", + feedback=True, + details={"qgraph": q, "duality": dual, "value_iteration": vi}, + ) + raise ValueError(f"unknown feedback-capacity method {method!r}") diff --git a/sofic/generators/capacity/no_feedback.py b/sofic/generators/capacity/no_feedback.py new file mode 100644 index 0000000..8553e69 --- /dev/null +++ b/sofic/generators/capacity/no_feedback.py @@ -0,0 +1,237 @@ +r"""Capacity bounds for finite unifilar channels (ε-transducers) without feedback. + +The capacity of a finite-state channel without feedback has no known +computable characterization, even for the trapdoor channel. This module +brackets it: + +* :func:`markov_input_lower_bound` -- the information rate of an optimized + order-``k`` Markov input, the quantity maximized by the generalized + Blahut--Arimoto algorithm :cite:`Vontobel2008`, certified from below by + block entropy bounds :cite:`Cover2006`. +* :func:`duality_upper_bound_no_feedback` -- the dual capacity bound with a + Q-graph test distribution, which for unifilar channels is a deterministic + average-reward dynamic program over :math:`P(S_t, Q_t \mid x^t)` + :cite:`Huleihel2021`. + +Both assume an indecomposable channel (the initial state is forgotten), so +that the capacity does not depend on it. Inputs must be admissible in every +state, since an encoder without feedback cannot see the state. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from itertools import product +from typing import Any, Literal + +import numpy as np +from scipy.optimize import minimize + +from sofic.exceptions import StochasticValidationError +from sofic.generators.capacity._belief import SimplexGrid, relative_value_iteration, transition_operator +from sofic.generators.capacity._channel import ChannelTensors +from sofic.generators.capacity._rates import default_block, driven_rate_bounds +from sofic.generators.capacity.feedback import ( + _divergence_rewards, + _minimize_test, + _softmax_rows, + as_tensors, +) +from sofic.generators.capacity.qgraph import QGraph, coupled_kernel +from sofic.generators.capacity.results import CapacityBounds + +NoFeedbackMethod = Literal["markov_input", "duality", "all"] + + +def _require_unconstrained(tensors: ChannelTensors) -> None: + if not tensors.allowed.all(): + raise StochasticValidationError("no-feedback bounds need every input admissible in every state") + + +def _markov_input_tensors(tensors: ChannelTensors, policy: np.ndarray, order: int) -> tuple[np.ndarray, np.ndarray]: + """Return the ``(context, state)`` chain's symbol tensors and per-state noise for ``policy[c, x]``.""" + n_s, n_x, n_y = tensors.P.shape + contexts = list(product(range(n_x), repeat=order)) + index = {c: i for i, c in enumerate(contexts)} + n_c = len(contexts) + symbol = np.zeros((n_y, n_c, n_s, n_c, n_s)) + for c, context in enumerate(contexts): + for x in range(n_x): + target = index[(*context, x)[1:]] if order else 0 + for s, y in zip(*np.nonzero(tensors.P[:, x, :] > 0), strict=True): + symbol[y, c, s, target, tensors.F[s, x, y]] += policy[c, x] * tensors.P[s, x, y] + n_z = n_c * n_s + noise = (policy @ tensors.conditional_entropy().T).ravel() + return symbol.reshape(n_y, n_z, n_z), noise + + +def markov_input_lower_bound( + channel: Any, + *, + order: int = 1, + restarts: int = 4, + seed: int | None = 0, + block: int | None = None, + final_block: int | None = None, +) -> CapacityBounds: + r"""Lower-bound the no-feedback capacity with an optimized Markov input. + + An order-``k`` Markov input with transition law :math:`\theta(x \mid c)` + drives the channel; the pair (input context, channel state) is a finite + chain, and because ``(s, x, y)`` determines the next state the + information rate is :math:`\bar h(Y) - \mathbb{E}_\pi H(Y \mid X, S)`. + The entropy rate is bounded below by :math:`H[Y_n \mid Y_{0:n}, Z_0]` + :cite:`Cover2006`, so the result is a certified lower bound. ``theta`` is + optimized with L-BFGS-B on softmax logits at ``block`` and re-evaluated at + ``final_block`` (defaults: largest ``n`` with :math:`|Y|^n \le 2^8`, + :math:`2^{16}`). This is the objective of the generalized Blahut--Arimoto + algorithm :cite:`Vontobel2008,Arnold2006`; the optimizer here is direct. + """ + tensors = as_tensors(channel) + _require_unconstrained(tensors) + block = default_block(tensors.n_outputs, 8) if block is None else block + final_block = default_block(tensors.n_outputs, 16) if final_block is None else final_block + n_x = tensors.n_inputs + shape = (n_x**order, n_x) + rng = np.random.default_rng(seed) + + def rate(z: np.ndarray, n: int) -> tuple[float, float] | None: + symbol, noise = _markov_input_tensors(tensors, _softmax_rows(z.reshape(shape)), order) + return driven_rate_bounds(symbol, noise, n) + + def objective(z: np.ndarray) -> float: + bounds = rate(z, block) + return 1.0 if bounds is None else -bounds[0] + + starts = [np.zeros(int(np.prod(shape)))] + [rng.normal(size=int(np.prod(shape))) for _ in range(restarts)] + best_z, best_value = starts[0], np.inf + for z0 in starts: + result = minimize(objective, z0, method="L-BFGS-B", options={"maxiter": 500}) + if result.fun < best_value: + best_z, best_value = result.x, float(result.fun) + final = rate(best_z, final_block) + lower = 0.0 if final is None else max(final[0], 0.0) + return CapacityBounds( + lower=lower, + upper=tensors.trivial_upper_bound(), + method="markov_input", + feedback=False, + details={ + "order": order, + "input_policy": _softmax_rows(best_z.reshape(shape)), + "rate_estimate_upper": None if final is None else final[1], + }, + ) + + +def _no_feedback_dp(tensors: ChannelTensors, table: np.ndarray, resolution: int, max_grid_points: int): + """Grid and interpolated transition operator for the belief ``z = P(S, Q | x^t)``.""" + n_s, n_x, _n_y = tensors.P.shape + n_q = table.shape[0] + n = n_s * n_q + grid = SimplexGrid.build(n, resolution, max_points=max_grid_points) + kernel = coupled_kernel(tensors, table).reshape(n, n_x, n) + rows, indices, weights = [], [], [] + for x in range(n_x): + queries = grid.points @ kernel[:, x, :] + idx, w = grid.interpolation(queries) + rows.append(x * grid.size + np.arange(grid.size)) + indices.append(idx) + weights.append(w) + operator = transition_operator( + n_x * grid.size, + grid.size, + np.concatenate(rows), + np.vstack(indices), + np.vstack(weights), + np.ones(n_x * grid.size), + ) + return grid, operator + + +def _no_feedback_gain(tensors: ChannelTensors, grid: SimplexGrid, operator, test: np.ndarray, tol: float) -> float: + rewards = grid.points @ _divergence_rewards(tensors, test).reshape(-1, tensors.n_inputs) + return relative_value_iteration(rewards, operator, tol=tol).gain_upper + + +def duality_upper_bound_no_feedback( + channel: Any, + *, + qgraphs: Iterable[QGraph] | None = None, + max_order: int = 1, + tests: Sequence[np.ndarray] | None = None, + belief_resolution: int = 20, + max_grid_points: int = 100_000, + optimize: bool = True, +) -> CapacityBounds: + r"""Upper-bound the no-feedback capacity by the Q-graph duality bound. + + For a test distribution :math:`R(y \mid q)` on a Q-graph, + :math:`C \le \lim \frac1n \max_{x^n, s_0, q_0} D(P_{Y^n \mid x^n, s_0} \| R_{Y^n})`, + which for a unifilar channel is the deterministic dynamic program with + state :math:`z = P(S, Q \mid x^t)`, action ``x``, reward + :math:`\sum_{s,q} z(s,q) D(P_{Y \mid x,s} \| R_{Y \mid q})` and linear + dynamics :cite:`Huleihel2021`. Its value function is convex in ``z``, so + linear interpolation on a simplex grid over-estimates it and the span + bound ``max(Th - h)`` of the gridded problem is still a valid upper bound + at any resolution. The test distribution starts uniform (or ``tests``, + aligned with ``qgraphs``) and is refined by Nelder--Mead when + ``optimize`` is true. + """ + tensors = as_tensors(channel) + _require_unconstrained(tensors) + candidates = [QGraph.markov(tensors.outputs, k) for k in range(max_order + 1)] + user = list(qgraphs or []) + candidates += user + user_tests = list(tests or []) + best_value, best_graph, best_test = np.inf, None, None + for index, qgraph in enumerate(candidates): + table = qgraph.aligned(tensors.outputs) + grid, operator = _no_feedback_dp(tensors, table, belief_resolution, max_grid_points) + user_index = index - (len(candidates) - len(user)) + if 0 <= user_index < len(user_tests): + start = np.asarray(user_tests[user_index], dtype=float) + else: + start = np.full((qgraph.n_nodes, tensors.n_outputs), 1.0 / tensors.n_outputs) + value, test = _no_feedback_gain(tensors, grid, operator, start, 1e-10), start + if optimize and tensors.n_outputs > 1: + value, test = _minimize_test( + lambda r, g=grid, op=operator: _no_feedback_gain(tensors, g, op, r, 1e-8), start, value + ) + value = _no_feedback_gain(tensors, grid, operator, test, 1e-10) + if value < best_value: + best_value, best_graph, best_test = value, qgraph, test + return CapacityBounds( + lower=0.0, + upper=min(float(best_value), tensors.trivial_upper_bound()), + method="duality", + feedback=False, + details={"qgraph": best_graph, "test_distribution": best_test, "belief_resolution": belief_resolution}, + ) + + +def capacity(channel: Any, method: NoFeedbackMethod = "all", **kwargs: Any) -> CapacityBounds: + """Bracket the capacity without feedback of a finite unifilar channel. + + ``method`` selects :func:`markov_input_lower_bound`, + :func:`duality_upper_bound_no_feedback`, or ``"all"`` for both. Keyword + arguments go to the selected method(s). + """ + if method == "markov_input": + return markov_input_lower_bound(channel, **kwargs) + if method == "duality": + return duality_upper_bound_no_feedback(channel, **kwargs) + if method == "all": + tensors = as_tensors(channel) + lower_keys = ("order", "restarts", "seed", "block", "final_block") + upper_keys = ("qgraphs", "max_order", "tests", "belief_resolution", "max_grid_points", "optimize") + low = markov_input_lower_bound(tensors, **{k: kwargs[k] for k in lower_keys if k in kwargs}) + high = duality_upper_bound_no_feedback(tensors, **{k: kwargs[k] for k in upper_keys if k in kwargs}) + return CapacityBounds( + lower=min(low.lower, high.upper), + upper=high.upper, + method="all", + feedback=False, + details={"markov_input": low, "duality": high}, + ) + raise ValueError(f"unknown capacity method {method!r}") diff --git a/sofic/generators/capacity/qgraph.py b/sofic/generators/capacity/qgraph.py new file mode 100644 index 0000000..c1b7b87 --- /dev/null +++ b/sofic/generators/capacity/qgraph.py @@ -0,0 +1,107 @@ +"""Q-graphs: finite quantizers of the channel-output history. + +A Q-graph :cite:`Sabag2017` is a directed graph whose nodes carry one outgoing +edge per channel output symbol, so a walk from a fixed node maps every output +sequence to a node (a *context*). Capacity bounds then condition on the +context ``Q`` instead of the whole output past. +""" + +from __future__ import annotations + +from collections.abc import Hashable, Sequence +from dataclasses import dataclass +from itertools import product +from typing import TYPE_CHECKING, Any + +import numpy as np + +if TYPE_CHECKING: + from sofic.automata.dfa import DFA + from sofic.generators.capacity._channel import ChannelTensors + + +@dataclass(frozen=True, eq=False) +class QGraph: + """Complete deterministic graph over an output alphabet. + + ``table[q, y]`` is the successor of node ``q`` on the ``y``-th symbol of + ``outputs``; ``labels`` names the nodes. + """ + + outputs: tuple[Any, ...] + table: np.ndarray + labels: tuple[Hashable, ...] + + def __post_init__(self) -> None: + table = np.asarray(self.table, dtype=int) + if table.ndim != 2 or table.shape[1] != len(self.outputs): + raise ValueError("table must have shape (n_nodes, n_outputs)") + if table.size and (table.min() < 0 or table.max() >= table.shape[0]): + raise ValueError("table entries must index nodes") + if len(self.labels) != table.shape[0]: + raise ValueError("one label per node is required") + object.__setattr__(self, "table", table) + + @property + def n_nodes(self) -> int: + return self.table.shape[0] + + @classmethod + def markov(cls, outputs: Sequence[Any], order: int) -> QGraph: + """Return the order-``k`` Markov Q-graph whose nodes are the last ``k`` outputs.""" + if order < 0: + raise ValueError("order must be nonnegative") + outputs = tuple(outputs) + words = list(product(range(len(outputs)), repeat=order)) + index = {word: i for i, word in enumerate(words)} + table = np.zeros((len(words), len(outputs)), dtype=int) + for word, q in index.items(): + for y in range(len(outputs)): + table[q, y] = index[(*word, y)[1:]] if order else 0 + labels = tuple("".join(str(outputs[y]) for y in word) for word in words) + return cls(outputs, table, labels) + + @classmethod + def from_dfa(cls, dfa: DFA, outputs: Sequence[Any] | None = None) -> QGraph: + """Build a Q-graph from a complete DFA over the output alphabet.""" + alphabet = tuple(outputs) if outputs is not None else tuple(sorted(dfa.input_alphabet, key=repr)) + states = sorted(dfa.states(), key=repr) + index = {state: i for i, state in enumerate(states)} + table = np.zeros((len(states), len(alphabet)), dtype=int) + for state in states: + for y, symbol in enumerate(alphabet): + successors = dfa.delta(state, symbol) + if len(successors) != 1: + raise ValueError(f"DFA is not complete at ({state!r}, {symbol!r})") + table[index[state], y] = index[next(iter(successors))] + return cls(alphabet, table, tuple(states)) + + def aligned(self, outputs: Sequence[Any]) -> np.ndarray: + """Return ``table`` with columns ordered as ``outputs``.""" + if set(outputs) != set(self.outputs): + raise ValueError(f"Q-graph alphabet {self.outputs!r} differs from channel outputs {tuple(outputs)!r}") + return self.table[:, [self.outputs.index(y) for y in outputs]] + + +def coupled_kernel(tensors: ChannelTensors, table: np.ndarray) -> np.ndarray: + """Return ``K[s, q, x, s', q']``: the ``(S, Q)`` transition law given input ``x``.""" + n_s, n_x, n_y = tensors.P.shape + n_q = table.shape[0] + kernel = np.zeros((n_s, n_q, n_x, n_s, n_q)) + for s, x, y in zip(*np.nonzero(tensors.P > 0), strict=True): + kernel[s, np.arange(n_q), x, tensors.F[s, x, y], table[:, y]] += tensors.P[s, x, y] + return kernel + + +def coupled_chain(tensors: ChannelTensors, qgraph: QGraph, policy: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Return the ``(S, Q)`` transition matrix under ``policy[s, q, x]`` and a stationary law. + + States are flattened as ``s * |Q| + q``. + """ + from sofic.generators.stationary import stationary_distribution_from_transition + + table = qgraph.aligned(tensors.outputs) + kernel = coupled_kernel(tensors, table) + n = tensors.n_states * qgraph.n_nodes + matrix = np.einsum("sqx,sqxtr->sqtr", policy, kernel).reshape(n, n) + return matrix, stationary_distribution_from_transition(matrix) diff --git a/sofic/generators/capacity/results.py b/sofic/generators/capacity/results.py new file mode 100644 index 0000000..bba0305 --- /dev/null +++ b/sofic/generators/capacity/results.py @@ -0,0 +1,37 @@ +"""Result type for channel-capacity computations.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from typing import Any + + +@dataclass(frozen=True) +class CapacityBounds: + """Bracket ``lower <= C <= upper`` on a channel capacity, in bits per use. + + ``feedback`` says which capacity is bounded. ``certified`` is ``False`` for + numerical estimates (such as gridded value iteration) whose endpoints are + not guaranteed bounds. ``details`` holds method-specific artifacts: optimal + policies, Q-graphs, test distributions, solver residuals. + """ + + lower: float + upper: float + method: str + feedback: bool + certified: bool = True + tol: float = 1e-4 + details: Mapping[str, Any] = field(default_factory=dict) + + @property + def gap(self) -> float: + return self.upper - self.lower + + @property + def value(self) -> float | None: + """Return the midpoint when the bracket is narrower than ``tol``, else ``None``.""" + if self.gap < self.tol: + return 0.5 * (self.lower + self.upper) + return None diff --git a/sofic/generators/epsilon_transducer.py b/sofic/generators/epsilon_transducer.py index 3d091da..45a4936 100644 --- a/sofic/generators/epsilon_transducer.py +++ b/sofic/generators/epsilon_transducer.py @@ -12,8 +12,9 @@ Structurally the ε-transducer is a unifilar, input-driven stochastic Mealy machine, so :class:`EpsilonTransducer` extends the probability-aware :class:`~sofic.automata.transducers.MealyMachine` with a causal-state initial -distribution, causal-state minimization, transCSSR reconstruction, and channel -information measures. +distribution, causal-state minimization, transCSSR reconstruction, channel +information measures, and capacity bounds with and without feedback +(:mod:`sofic.generators.capacity`). """ from __future__ import annotations @@ -28,6 +29,7 @@ if TYPE_CHECKING: from sofic.generators.base import HiddenMarkovModel + from sofic.generators.capacity import CapacityBounds class EpsilonTransducer(MealyMachine): @@ -158,6 +160,20 @@ def transfer_entropy(self, input_process: HiddenMarkovModel, *, history: int = 1 return transfer_entropy(self, input_process, history=history) + # -- channel capacity ----------------------------------------------------- + + def feedback_capacity(self, method: str = "qgraph", **kwargs: Any) -> CapacityBounds: + """Bracket the feedback capacity; see :func:`sofic.generators.capacity.feedback_capacity`.""" + from sofic.generators.capacity import feedback_capacity + + return feedback_capacity(self, method=method, **kwargs) + + def capacity(self, method: str = "all", **kwargs: Any) -> CapacityBounds: + """Bracket the capacity without feedback; see :func:`sofic.generators.capacity.capacity`.""" + from sofic.generators.capacity import capacity + + return capacity(self, method=method, **kwargs) + # -- conversions ---------------------------------------------------------- @classmethod diff --git a/tests/test_channel_capacity.py b/tests/test_channel_capacity.py new file mode 100644 index 0000000..c519fa5 --- /dev/null +++ b/tests/test_channel_capacity.py @@ -0,0 +1,262 @@ +"""Capacity bounds for ε-transducers, checked against known capacities.""" + +import math +import warnings + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from sofic.examples.processes import binary_channel_et, gm_to_even, ising_channel_et, trapdoor_et +from sofic.exceptions import StochasticValidationError +from sofic.generators.capacity import ( + CapacityBounds, + ChannelTensors, + QGraph, + capacity, + channel_tensors, + duality_upper_bound, + duality_upper_bound_no_feedback, + feedback_capacity, + markov_input_lower_bound, + qgraph_bounds, + qgraph_upper_bound, + value_iteration_estimate, +) +from sofic.generators.capacity._belief import SimplexGrid + +LOG2_PHI = math.log2((1 + math.sqrt(5)) / 2) +ISING_FB = 0.575522 # Elishco & Permuter (2014) +TRAPDOOR_GRAPH = [[0, 1], [0, 2], [1, 2]] +ISING_GRAPH = [[1, 2], [0, 2], [0, 3], [0, 2]] + + +@pytest.fixture(autouse=True) +def _quiet(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + yield + + +def _graph(table): + return QGraph(("0", "1"), np.array(table), tuple(range(len(table)))) + + +def _bsc_capacity(p, q): + from dit.algorithms.channelcapacity import channel_capacity + + return channel_capacity(np.array([[1 - p, p], [q, 1 - q]]))[0] + + +# -- infrastructure ----------------------------------------------------------- + + +def test_channel_tensors_of_trapdoor(): + tensors = channel_tensors(trapdoor_et()) + assert tensors.P.shape == (2, 2, 2) + assert np.allclose(tensors.P.sum(axis=2), 1.0) + assert tensors.allowed.all() + assert np.allclose(tensors.initial, 0.5) + assert tensors.trivial_upper_bound() == pytest.approx(1.0) + + +def test_non_strongly_connected_channel_rejected(): + with pytest.raises(StochasticValidationError, match="strongly connected"): + feedback_capacity(_absorbing_channel()) + + +def _absorbing_channel(): + P = np.zeros((2, 1, 2)) + F = np.full((2, 1, 2), -1) + P[0, 0] = [0.5, 0.5] + F[0, 0] = [0, 1] + P[1, 0, 1] = 1.0 + F[1, 0, 1] = 1 + return ChannelTensors(("a", "b"), ("x",), ("0", "1"), P, F, P.sum(axis=2) > 0, np.array([1.0, 0.0])) + + +def test_markov_qgraph_structure(): + graph = QGraph.markov(("0", "1"), 2) + assert graph.n_nodes == 4 + assert graph.labels == ("00", "01", "10", "11") + assert graph.table[graph.labels.index("01"), 1] == graph.labels.index("11") + assert QGraph.markov(("0", "1"), 0).table.tolist() == [[0, 0]] + + +def test_qgraph_from_dfa_round_trip(): + from sofic.automata.dfa import DFA + + dfa = DFA(input_alphabet=frozenset({"0", "1"}), initial_states=frozenset({"a"})) + for state in "abc": + dfa.graph.add_state(state) + for source, row in zip("abc", TRAPDOOR_GRAPH, strict=True): + for symbol, target in zip("01", row, strict=True): + dfa.add_transition(source, "abc"[target], symbol) + graph = QGraph.from_dfa(dfa) + assert graph.table.tolist() == TRAPDOOR_GRAPH + + +def test_capacity_bounds_value(): + assert CapacityBounds(0.5, 0.50001, "x", feedback=True).value == pytest.approx(0.500005) + assert CapacityBounds(0.5, 0.6, "x", feedback=True).value is None + + +@settings(max_examples=30, deadline=None) +@given(st.integers(2, 5), st.integers(1, 6), st.integers(0, 2**32 - 1)) +def test_simplex_interpolation_reproduces_linear_functions(n_vertices, resolution, seed): + grid = SimplexGrid.build(n_vertices, resolution) + rng = np.random.default_rng(seed) + queries = np.vstack([rng.dirichlet(np.ones(n_vertices), 20), np.eye(n_vertices)]) + indices, weights = grid.interpolation(queries) + assert np.allclose(weights.sum(axis=1), 1.0) + assert (weights >= -1e-12).all() + assert np.allclose(np.einsum("mk,mks->ms", weights, grid.points[indices]), queries) + + +# -- memoryless channel ------------------------------------------------------- + + +@pytest.mark.parametrize(("p", "q"), [(0.1, 0.2), (0.05, 0.05), (0.3, 0.0)]) +def test_memoryless_channel_matches_blahut_arimoto(p, q): + channel = binary_channel_et(p, q) + reference = _bsc_capacity(p, q) + fb = qgraph_bounds(channel, max_order=1, restarts=1) + assert fb.lower == pytest.approx(reference, abs=1e-6) + assert fb.upper == pytest.approx(reference, abs=1e-6) + assert duality_upper_bound(channel, max_order=0).upper == pytest.approx(reference, abs=1e-6) + no_fb = capacity(channel, order=0, max_order=0) + assert no_fb.lower == pytest.approx(reference, abs=1e-6) + assert no_fb.upper == pytest.approx(reference, abs=1e-6) + + +def test_value_iteration_on_memoryless_channel_is_uncertified_estimate(): + reference = _bsc_capacity(0.1, 0.2) + estimate = value_iteration_estimate(binary_channel_et(0.1, 0.2), belief_resolution=4, action_resolution=40) + assert not estimate.certified + assert estimate.upper == pytest.approx(reference, abs=1e-3) + + +# -- trapdoor channel --------------------------------------------------------- + + +def test_trapdoor_feedback_capacity_is_log2_phi(): + bounds = trapdoor_et().feedback_capacity(qgraphs=[_graph(TRAPDOOR_GRAPH)], max_order=1) + assert bounds.lower <= LOG2_PHI + 1e-9 + assert bounds.upper >= LOG2_PHI - 1e-9 + assert bounds.gap < 1e-4 + assert bounds.value == pytest.approx(LOG2_PHI, abs=1e-4) + + +def test_trapdoor_markov_qgraphs_are_not_tight(): + tensors = channel_tensors(trapdoor_et()) + upper, _joint = qgraph_upper_bound(tensors, QGraph.markov(tensors.outputs, 2)) + assert upper == pytest.approx(0.704837, abs=1e-5) + + +def test_trapdoor_value_iteration_estimate(): + estimate = value_iteration_estimate(trapdoor_et(), belief_resolution=100, action_resolution=20) + assert estimate.lower == pytest.approx(LOG2_PHI, abs=2e-3) + assert estimate.details["policy"].shape[1:] == (2, 2) + + +def test_trapdoor_duality_bound_is_valid(): + bound = duality_upper_bound(trapdoor_et(), qgraphs=[_graph(TRAPDOOR_GRAPH)], max_order=-1, optimize=False) + assert LOG2_PHI - 1e-9 <= bound.upper < LOG2_PHI + 1e-3 + + +@pytest.mark.slow +def test_trapdoor_duality_bound_is_tight(): + bound = duality_upper_bound(trapdoor_et(), qgraphs=[_graph(TRAPDOOR_GRAPH)], max_order=-1) + assert bound.upper == pytest.approx(LOG2_PHI, abs=1e-6) + + +def test_trapdoor_no_feedback_bracket(): + lower = markov_input_lower_bound(trapdoor_et(), order=2) + assert 0.571 < lower.lower <= 0.584 + upper = duality_upper_bound_no_feedback(trapdoor_et(), max_order=0, belief_resolution=20, optimize=False) + assert upper.upper >= 0.572 + + +@pytest.mark.slow +def test_trapdoor_no_feedback_bounds_calibrated(): + lower = markov_input_lower_bound(trapdoor_et(), order=3) + upper = duality_upper_bound_no_feedback(trapdoor_et(), max_order=1, belief_resolution=24) + assert lower.lower > 0.5726 + assert 0.584 <= upper.upper < 0.594 + assert upper.upper <= feedback_capacity(trapdoor_et(), qgraphs=[_graph(TRAPDOOR_GRAPH)], max_order=0).upper + 1e-3 + + +# -- Ising channel ------------------------------------------------------------ + + +def test_ising_feedback_capacity(): + bounds = ising_channel_et().feedback_capacity(qgraphs=[_graph(ISING_GRAPH)], max_order=0) + assert bounds.gap < 1e-4 + assert bounds.lower == pytest.approx(ISING_FB, abs=1e-5) + assert bounds.upper == pytest.approx(ISING_FB, abs=1e-5) + + +def test_ising_duality_bound_is_valid(): + bound = duality_upper_bound(ising_channel_et(), qgraphs=[_graph(ISING_GRAPH)], max_order=-1, optimize=False) + assert bound.upper >= ISING_FB - 1e-6 + + +@pytest.mark.slow +def test_ising_duality_bound_is_tight(): + bound = duality_upper_bound(ising_channel_et(), qgraphs=[_graph(ISING_GRAPH)], max_order=-1) + assert bound.upper == pytest.approx(ISING_FB, abs=1e-5) + + +@pytest.mark.slow +def test_feedback_capacity_all_methods(): + bounds = feedback_capacity( + ising_channel_et(), method="all", qgraphs=[_graph(ISING_GRAPH)], max_order=1, belief_resolution=60 + ) + assert bounds.lower == pytest.approx(ISING_FB, abs=1e-5) + assert bounds.upper == pytest.approx(ISING_FB, abs=1e-5) + assert not bounds.details["value_iteration"].certified + + +# -- invariants on random channels -------------------------------------------- + + +@st.composite +def unifilar_channels(draw): + """Random strongly connected binary-input binary-output channels on two states.""" + seed = draw(st.integers(0, 2**32 - 1)) + rng = np.random.default_rng(seed) + P = rng.dirichlet(np.ones(2), size=(2, 2)) + P = np.where(P < 0.05, 0.0, P) + P /= P.sum(axis=2, keepdims=True) + F = rng.integers(0, 2, size=(2, 2, 2)) + F = np.where(P > 0, F, -1) + tensors = ChannelTensors(("a", "b"), ("0", "1"), ("0", "1"), P, F, np.ones((2, 2), dtype=bool), np.full(2, 0.5)) + reach = {(s, int(F[s, x, y])) for s in range(2) for x in range(2) for y in range(2) if P[s, x, y] > 0} + if not {(0, 1), (1, 0)} <= reach: + F[0, 0, int(np.argmax(P[0, 0]))] = 1 + F[1, 1, int(np.argmax(P[1, 1]))] = 0 + return tensors + + +@settings(max_examples=8, deadline=None, suppress_health_check=[HealthCheck.too_slow]) +@given(unifilar_channels()) +def test_bounds_are_ordered_on_random_channels(tensors): + fb = qgraph_bounds(tensors, max_order=1, restarts=1) + for _, lower, upper in fb.details["per_qgraph"]: + assert lower <= upper + 1e-6 + no_fb_lower = markov_input_lower_bound(tensors, order=1, restarts=1) + no_fb_upper = duality_upper_bound_no_feedback(tensors, max_order=0, belief_resolution=10, optimize=False) + assert no_fb_lower.lower <= fb.upper + 1e-6 + assert no_fb_lower.lower <= no_fb_upper.upper + 1e-6 + for value in (fb.lower, fb.upper, no_fb_lower.lower, no_fb_upper.upper): + assert -1e-9 <= value <= 1.0 + 1e-9 + + +def test_input_constrained_channel_rejected_without_feedback(): + channel = gm_to_even() + from sofic import EpsilonTransducer + + eps = EpsilonTransducer.from_channel(channel) + with pytest.raises(StochasticValidationError): + markov_input_lower_bound(eps) diff --git a/tests/test_epsilon_transducer.py b/tests/test_epsilon_transducer.py index 0a82d7f..a911860 100644 --- a/tests/test_epsilon_transducer.py +++ b/tests/test_epsilon_transducer.py @@ -4,7 +4,16 @@ from sofic import EpsilonTransducer, MealyHMM from sofic.automata.transducer_operations import compose_transducer_generator -from sofic.examples.processes import binary_channel, gm_to_even, gme, rct +from sofic.examples.processes import ( + binary_channel, + gm_to_even, + gme, + ising_channel, + ising_channel_et, + rct, + trapdoor, + trapdoor_et, +) from sofic.exceptions import StochasticValidationError, UnifilarityError from sofic.graph import ATTR_OUTPUT, ATTR_PROB, ATTR_SYMBOL @@ -25,6 +34,8 @@ def _iid_input() -> MealyHMM: (gm_to_even(), 2), (rct(0.5), 3), (gme(), 2), + (trapdoor(), 2), + (ising_channel(), 2), ], ) def test_from_channel_minimizes(channel, expected_states): @@ -53,6 +64,50 @@ def test_initial_distribution_normalized(): assert set(eps.initial_distribution) <= set(eps.states()) +def _ball_of(eps): + """Map each causal state to the channel symbol it holds (input 0 -> output 0 surely).""" + ball = {} + for transition in eps.transitions(): + data = transition.data + if data[ATTR_SYMBOL] == "0" and data[ATTR_OUTPUT] == "0" and float(data[ATTR_PROB]) == pytest.approx(1.0): + ball[transition.source] = 0 + for state in eps.states(): + ball.setdefault(state, 1) + return ball + + +@pytest.mark.parametrize("factory", [trapdoor_et, ising_channel_et]) +def test_two_state_channels_uniform_initial(factory): + eps = factory() + assert len(eps.initial_distribution) == 2 + assert all(mass == pytest.approx(0.5) for mass in eps.initial_distribution.values()) + + +@pytest.mark.parametrize( + ("factory", "update"), + [(trapdoor_et, lambda s, x, y: s ^ x ^ y), (ising_channel_et, lambda s, x, y: x)], +) +def test_two_state_channel_law(factory, update): + eps = factory() + ball = _ball_of(eps) + assert sorted(ball.values()) == [0, 1] + law = {} + for transition in eps.transitions(): + s = ball[transition.source] + x = int(transition.data[ATTR_SYMBOL]) + y = int(transition.data[ATTR_OUTPUT]) + assert ball[transition.target] == update(s, x, y) + law[(s, x, y)] = float(transition.data[ATTR_PROB]) + for s in (0, 1): + for x in (0, 1): + if x == s: + assert law[(s, x, x)] == pytest.approx(1.0) + assert (s, x, 1 - x) not in law + else: + assert law[(s, x, 0)] == pytest.approx(0.5) + assert law[(s, x, 1)] == pytest.approx(0.5) + + def test_non_unifilar_channel_rejected(): tr = _non_unifilar_channel() with pytest.raises(StochasticValidationError): diff --git a/uv.lock b/uv.lock index 8eac471..6d9f486 100644 --- a/uv.lock +++ b/uv.lock @@ -168,6 +168,104 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -257,6 +355,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] +[[package]] +name = "clarabel" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/e2/47f692161779dbd98876015de934943effb667a014e6f79a6d746b3e4c2a/clarabel-0.11.1.tar.gz", hash = "sha256:e7c41c47f0e59aeab99aefff9e58af4a8753ee5269bbeecbd5526fc6f41b9598", size = 253949, upload-time = "2025-06-11T16:49:05.864Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/f7/f82698b6d00a40a80c67e9a32b2628886aadfaf7f7b32daa12a463e44571/clarabel-0.11.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c39160e4222040f051f2a0598691c4f9126b4d17f5b9e7678f76c71d611e12d8", size = 1039511, upload-time = "2025-06-11T16:48:58.525Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8f/13650cfe25762b51175c677330e6471d5d2c5851a6fbd6df77f0681bb34e/clarabel-0.11.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8963687ee250d27310d139eea5a6816f9c3ae31f33691b56579ca4f0f0b64b63", size = 935135, upload-time = "2025-06-11T16:48:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/7af10d2b540b39f1a05d1ebba604fce933cc9bc0e65e88ec3b7a84976425/clarabel-0.11.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4837b9d0db01e98239f04b1e3526a6cf568529d3c19a8b3f591befdc467f9bb", size = 1079226, upload-time = "2025-06-11T16:49:00.987Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a9/c76edf781ca3283186ff4b54a9a4fb51367fd04313a68e2b09f062407439/clarabel-0.11.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8c41aaa6f3f8c0f3bd9d86c3e568dcaee079562c075bd2ec9fb3a80287380ef", size = 1164345, upload-time = "2025-06-11T16:49:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/4eee3062088c221e5a18b054e51c69f616e0bb0dc1b0a1a5e0fe90dfa18e/clarabel-0.11.1-cp39-abi3-win_amd64.whl", hash = "sha256:557d5148a4377ae1980b65d00605ae870a8f34f95f0f6a41e04aa6d3edf67148", size = 887310, upload-time = "2025-06-11T16:49:04.277Z" }, +] + [[package]] name = "cloudpickle" version = "3.1.2" @@ -473,6 +589,48 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cvxpy" +version = "1.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "clarabel" }, + { name = "highspy" }, + { name = "numpy" }, + { name = "osqp" }, + { name = "qdldl" }, + { name = "scipy" }, + { name = "scs" }, + { name = "sparsediffpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/43/70d08d39796152eccc3a636a695f680c9fca1f2f1ff043c96f8709702151/cvxpy-1.9.3.tar.gz", hash = "sha256:06150a12e2a5d3eac42ccef6cf7e02ce467e3b9d61c20449e6a8ad02b5cf6831", size = 1943634, upload-time = "2026-09-19T16:50:41.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/9d/94d330dd93212ea046ffb1979dc90c3912630486a66c9aa33a2632bbde7c/cvxpy-1.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8175c17059e12cc3226eee45ba182e11bb9e1b1f74ad65fd99e275e84730a8c9", size = 1661695, upload-time = "2026-09-19T16:48:12.816Z" }, + { url = "https://files.pythonhosted.org/packages/95/06/62d116a1b52dffb1bc3f20414a8495e00736f5701b69c61e972174fba8e5/cvxpy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8efdb59992da23117450e61fb9123c893694ceab214921db5f20f69e6afb3be5", size = 1452074, upload-time = "2026-09-19T16:48:14.421Z" }, + { url = "https://files.pythonhosted.org/packages/81/bb/847979f1fba492274b1d9f19717e2ff04240113049e06b12611d54552f70/cvxpy-1.9.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ce28c79a80fbc13c3e0f56aaee513fbee3ca475564d530c6a7d58d925c3ebef", size = 4387716, upload-time = "2026-09-19T16:51:44.438Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/c6cf8e478a18446bd841afae7a3136d06ef6334494817a8106ac156a7889/cvxpy-1.9.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd38e86758d2d985b1dae104a4f2119351891e73e64380b07437065889f7f0e0", size = 4436998, upload-time = "2026-09-19T16:51:45.821Z" }, + { url = "https://files.pythonhosted.org/packages/ba/35/5d25f512348327bd33c600fc4d6ae374742a1a090886c1727aaa207df420/cvxpy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:a9a620b93828f5a1dd2a96c904b0022ea99758c1e27a67fde2dd2ae4a4444c7d", size = 1414701, upload-time = "2026-09-19T16:48:05.193Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/607d73e794fa42ed36d9806f0c932864e9853a46a9749e18cd669677d765/cvxpy-1.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:80f2c48131338ddba2fe88f737ba1ba16e802b428a79fdcd79607407b7653332", size = 1663610, upload-time = "2026-09-19T16:43:46.676Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/c6c51b9a0391b5235b0bd43c2320be3813fc44facc48816898824c621d69/cvxpy-1.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ccebed1db5db748b56a748c1dd82ffe8db031c975c20903d7228343b3402aab", size = 1453462, upload-time = "2026-09-19T16:43:48.061Z" }, + { url = "https://files.pythonhosted.org/packages/50/1e/3b5e85641fac15afdf4d60c1bced53a85a0410c23ccb2fe26221fdd214af/cvxpy-1.9.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1fb75b496bedc759b4e314afbd78fdbd04d230074b4457c7bfc9b2cbbc5c9d6", size = 4407910, upload-time = "2026-09-19T16:50:38.096Z" }, + { url = "https://files.pythonhosted.org/packages/87/fa/f518daaf284ab38f9e9761ed40382746ccc10db6ac0455186c1afab66fcd/cvxpy-1.9.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88b28c6df62d49e7e59b1a08ae2f90caf4019d496208ec697aa018169220cdcf", size = 4456229, upload-time = "2026-09-19T16:50:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/6a0007b1ff2b8efd04d0f8e2a07c97fe9c09967e1b955ec5c34380c16008/cvxpy-1.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:59bea29c808ef736d4069fe32301828e5510aed342fed23a8343eb86d9bea3c3", size = 1414825, upload-time = "2026-09-19T16:50:00.857Z" }, + { url = "https://files.pythonhosted.org/packages/01/bf/24545ccdeb098f0d1773d5ea4401215d74f53b09b6e3310386b424719d9c/cvxpy-1.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b3c346d94122577e20ddd557bfeaa082478a23884905b66513384f992b73c26e", size = 1663654, upload-time = "2026-09-19T16:49:32.691Z" }, + { url = "https://files.pythonhosted.org/packages/97/b2/dec397d0b96c2bb3e88fe0c8254dee3a097484de9ebef0c66c3a02b9566c/cvxpy-1.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7d6c959cbe1922f2f4824bcb091567e6b43ce8e390447276abff5ab19db4341", size = 1453458, upload-time = "2026-09-19T16:49:33.861Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ea/a145fab0a0d9c7705bd9a5d8bab9dfb4d762a3a9f2c61f5b57b3ee29c54b/cvxpy-1.9.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0de276e44630171bfe9a14263d3e210d2bf1a9c0e03042d23d8419f4691639e0", size = 4408087, upload-time = "2026-09-19T16:57:32.055Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a4/de552b270bec9c0ffa0454d13b01f8240a00d5abf1026b79bf1644d6bd3f/cvxpy-1.9.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2df107618d847fadaff65c1406d32f1d4c8cb0493cce762f2d977bb47980b02f", size = 4456120, upload-time = "2026-09-19T16:57:33.901Z" }, + { url = "https://files.pythonhosted.org/packages/9c/41/2132775f330857ff99862f04eea3421c96a0c58c4cbce99b3243c0893532/cvxpy-1.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:c5ee1aa272b5aa7afbb622a3c4a6b8dc064ece6a73fe4b806ee4bdd44a7bee1f", size = 1414848, upload-time = "2026-09-19T16:46:51.549Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/5dfd02093a3375c04e99c4b170b263fc38c53c599f0fc958b34f45fe0c0a/cvxpy-1.9.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:23decc2afa87717c3628e23a449a576ca5ed46aaf92c5655b372a9f65e12c44e", size = 1664497, upload-time = "2026-09-19T16:46:18.48Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0f/8411ba67c66293614969af87599fbb3f205079db3b0d6ef68045b36b3082/cvxpy-1.9.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b7d2f15524068703909fa6dd9778168eb089aa08d0ae716021554aa281980bcf", size = 1453918, upload-time = "2026-09-19T16:46:20.422Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ae/f56606d84d82c6b084a8afecf243c4b309498edbbe965573fb4250548994/cvxpy-1.9.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2b080ba9ad43e1782d8fefcbc25c3eb3a621c4060d0230c02437349062e9c04", size = 4404548, upload-time = "2026-09-19T16:49:03.329Z" }, + { url = "https://files.pythonhosted.org/packages/e6/11/44848ef308ba9affa51ba1120f3da499655ba784f3f5436090933a570413/cvxpy-1.9.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18f28b95bf5729654bd66eea1e5f62d6ea6300f93d8fe6a36aa7bb5bf645b99d", size = 4451617, upload-time = "2026-09-19T16:49:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/15/e6/c01ef8e4031e63cee8f659dee75ffe47429bba5bcf1b850527533872532e/cvxpy-1.9.3-cp314-cp314-win_amd64.whl", hash = "sha256:8e05a166f3fc8f208550d569ff6b5e28cb4c3a8ee2b7da5547d4cd835cc2c346", size = 1420822, upload-time = "2026-09-19T16:50:03.662Z" }, + { url = "https://files.pythonhosted.org/packages/de/d2/d9172da82222ed1599ec005a5289c9b8e0ecf2afefd7b8fb2d5cb89d3a10/cvxpy-1.9.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:07082d121fd0f571b263cdac78b7ec793b53d47406a22b5c1152ad4a89368138", size = 1672016, upload-time = "2026-09-19T16:49:41.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b5/bcb0c76a3c2104ed87bab7e4415e310e2a0aac12703755f7799d1f6648f7/cvxpy-1.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e131172323be4f0a3fa35a79b03b1eb159b7c7c0d298e5755452e252c55b697f", size = 1458260, upload-time = "2026-09-19T16:49:43.022Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4e/a834b145950ba9af12dba3b0880a58592e8bcf0d08e71d75c4cbf6b67890/cvxpy-1.9.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71a6d3c4742c5f425f79b6331856d8293c20dffcaa7e18888499d21514cd797", size = 4390674, upload-time = "2026-09-19T16:51:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/51/af/30b4eb196a5c0471787fd8831a895dc110f749564b639343d37d6c6ec4f2/cvxpy-1.9.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:550f9e2349f37bf0d9bbbb888b01c6c0c1b5986b661d2881c074431de0f0236f", size = 4443042, upload-time = "2026-09-19T16:51:24.251Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -699,6 +857,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, ] +[[package]] +name = "highspy" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/02/c6b658f79911fee921721da728b9ab8f5e19ff06121fff36f90f77127f4d/highspy-1.15.1.tar.gz", hash = "sha256:20ed2fbf1cb64bf3044ee6632364b7e2653d93e6901e2b19fd3d5df10702e8c5", size = 1703256, upload-time = "2026-07-02T12:03:25.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/69/74e9614a6e49f5e07fbb91a5cebad79bd31903888ca6b4f1c75dcf01df3b/highspy-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:45eb9f022f9083ef2e56d66f972d5fd40e6634f4497194b1f3f215ca0e8ea958", size = 4871151, upload-time = "2026-07-02T12:01:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/815b8f0488fda02e765b85e7a1ed2bf26b7d705fb1240ca842c4493d9414/highspy-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b4c7e7af8d7927ed77836e9b869cbae55d6a74b85bb90d04776440b5e14c32b", size = 4468478, upload-time = "2026-07-02T12:01:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ea/def24ab38ff3ea983eb197dcbf01ec9ad82e2aa3380b72f01792486865e1/highspy-1.15.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:070c1ce9238b9e8b4c273253647ab0dbafc1839c195a52c7ef1eeb7ef6976f05", size = 4634480, upload-time = "2026-07-02T12:01:46.822Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bb/0588b8137df2a0ae55a9a8ad40eff40561b60d6f76dbffd5ab4f4f6095f0/highspy-1.15.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a24329c328942b37a6a318ecf163d07dd387974f071b98b4498725eaea80f06f", size = 5035498, upload-time = "2026-07-02T12:01:48.717Z" }, + { url = "https://files.pythonhosted.org/packages/f2/50/5196e807cec6847b2bee8ceefb080b8175ea81aaa673249678398cf3181c/highspy-1.15.1-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:138506088c7f6106cbb58d1cd0ef14793dfb47477fd83a7ae0db5b104d1cf969", size = 5857714, upload-time = "2026-07-02T12:01:51.04Z" }, + { url = "https://files.pythonhosted.org/packages/32/25/80c96a11bcf3bdf4ffaa7b1e26629b6da340703b2bbc4e0349d40d7a8aaa/highspy-1.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00e1c13912501e96893136a1805b56b74cb4868fa04c1c2eacc5c0454304e08e", size = 6191044, upload-time = "2026-07-02T12:01:52.856Z" }, + { url = "https://files.pythonhosted.org/packages/11/10/27a7b87dbf56ab88d223fb9f1b6917c9df259c09980460ff56731c2406ef/highspy-1.15.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b5be1c777d0b57b6dc26e1d9754923e642a17c6313bcdf5186473644b214f0b", size = 7234613, upload-time = "2026-07-02T12:01:54.936Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1e/82159a50b8a17daf2e94453a8e0af3e381943223727fd9db6d4bebc1533a/highspy-1.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5de2dddc554442f3572bb4a36116278bee79568fbd726a697251d2606b79a5a1", size = 6632799, upload-time = "2026-07-02T12:01:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/3c/69/59d4303d38d6c48cc6f79c12b11f756b381e8e5eed5c0733d8c82317240f/highspy-1.15.1-cp311-cp311-win32.whl", hash = "sha256:605d3204e41a465f9ce2f254571a90e8781605451a5e6a548f6b4be8988afb4f", size = 2304839, upload-time = "2026-07-02T12:01:59.752Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/6714276be39f1f0f7c2795fb466c39640bbcc541e151342f05f61d2f24a5/highspy-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:4715fcfbcff50fdbcc288499116f7e5722a9f9d2647087d54317febb94ec2b32", size = 2709129, upload-time = "2026-07-02T12:02:01.722Z" }, + { url = "https://files.pythonhosted.org/packages/de/59/b79a7b1711ddfcca36674ddb41759e98eb1797f4a94513e7dd215e32e94d/highspy-1.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a781dc8432568ea990fcdcc8d6e4365e67aa4848ca1f99275db096645b27cae3", size = 4878738, upload-time = "2026-07-02T12:02:03.82Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e4/ae08124f71187628471a177e6db1ed2c1c45e9dceadc45f7111dfd7c2254/highspy-1.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9499d631edeb9642fc08dee59ca6c5815be1764c13a336c58ab7ba063011aa24", size = 4473938, upload-time = "2026-07-02T12:02:05.754Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7f/185b8c9579a9e4ef88eda45d1fdaf8d23a3a640f73c403a7b29fc0f0c4be/highspy-1.15.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef048fa722cdeb80062d271b8ba211cd6650ab73419762d80da7642bbd4a8420", size = 4636755, upload-time = "2026-07-02T12:02:07.996Z" }, + { url = "https://files.pythonhosted.org/packages/82/6b/18bec60d8585df860b8d33d310e99e7893eaabe3c8e9ebfa7e387ba9d2a4/highspy-1.15.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9730647160a6481426729f46d9989a0507d05f3cf96f9fb180f4ab9891bea67b", size = 5034168, upload-time = "2026-07-02T12:02:09.89Z" }, + { url = "https://files.pythonhosted.org/packages/d4/51/e43f06e64e994ccb41a336ff78802c0dae63aed46c17acd52167b5ca3d76/highspy-1.15.1-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6a6a2f21ee31a9205a928fbbc3f8c054893c1aec34f6a7c56588317e2800e673", size = 5861937, upload-time = "2026-07-02T12:02:11.801Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2a/5501a23cac55926e4b0554352b4285734b417dbec385c593f2ae405ea637/highspy-1.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a6760962b3e813814dc5e88301890d7cce975de5ce97cc3aed589cfdd461811", size = 6192004, upload-time = "2026-07-02T12:02:14.544Z" }, + { url = "https://files.pythonhosted.org/packages/94/08/fb7d30ea0e6c83fb943b16bf31951ba13a5be01a638ec13962a477009b91/highspy-1.15.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:787c92d5ff274256ba8848ab174cfc65d5af696f51bffe87423c85b2ea25c3fe", size = 7233098, upload-time = "2026-07-02T12:02:16.609Z" }, + { url = "https://files.pythonhosted.org/packages/23/77/9a07df7181834cfb61dafa5594e5eedc78369797c6487806bd3221d20667/highspy-1.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ee8e139e7260ec1306a48e30f1bd7937d9cfb8cb201d25da10e1099e5129b", size = 6636717, upload-time = "2026-07-02T12:02:18.72Z" }, + { url = "https://files.pythonhosted.org/packages/9a/25/5083d8e3d5cf5ff5edf5bcc03e3f693630ab59142c9d0a0bcbb2d315c50e/highspy-1.15.1-cp312-cp312-win32.whl", hash = "sha256:01c6585e83938ecf4139248b074b2ee736816d63716a20dc608b1d2fc9637b66", size = 2306753, upload-time = "2026-07-02T12:02:20.837Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/05521ca6b38e34e68d707888c378d3bcac34e62715b739e7c0c9b9887993/highspy-1.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:8c548165270608a40147a7ea6d985fd62a65fabf0f075b3c0c59ea910b724223", size = 2711114, upload-time = "2026-07-02T12:02:22.621Z" }, + { url = "https://files.pythonhosted.org/packages/3f/1e/283ea32eac82dd24fe86c439013d7c7666f4889de89f0957362ea5fa425e/highspy-1.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db297486a7a42a18656d1cc0ea9e1596fe45b8f7f75669a0c55b9081531ee0a", size = 4878819, upload-time = "2026-07-02T12:02:24.668Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1c/c6518fc7c2bd5c90d86bd7a8f3cf16c1ea0ace4335a80d45b8d3f96c0cba/highspy-1.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:818256db731339605a7b2c31cabfcbf820fe50402ff5e9b7aa8410ead06e8735", size = 4474016, upload-time = "2026-07-02T12:02:26.572Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/4b5e345affc107f1f315c55dd0b6f35f13be07092feccbdfe1d9bfe38e63/highspy-1.15.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:383cd3f28cce0753dec8e949719b10864e068c53a485624fcab4c6b585496dd7", size = 4636833, upload-time = "2026-07-02T12:02:28.467Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/f00e914f2bd88e2b73a8b3ea1b47171a85cfa23d1a06dc373ca797f43208/highspy-1.15.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:238b2ee88b974b21c7e9ef198139502a7d87451939cae143dce789bbda121182", size = 5034140, upload-time = "2026-07-02T12:02:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/61/03/8f821d39dc8ee06a35e0fa54c754ab592139640c1e839b587e60068ad822/highspy-1.15.1-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b6dcc545235c0765b48fc736122b105e174d907622d20986ac653c5b2a04911f", size = 5862229, upload-time = "2026-07-02T12:02:32.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/55/708b7523ad80106b91fb66471ab8b1c178a8c8adc222c839cc14147542cd/highspy-1.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e1f8a21a0f48aedb129a5a60d4cad9ee0767de271cd7450de16192440671b38", size = 6191747, upload-time = "2026-07-02T12:02:34.034Z" }, + { url = "https://files.pythonhosted.org/packages/8d/cd/737f43e9c56163ebae501ab21fdbc37dd2dde3e02fd18e37d0062b9b9c7c/highspy-1.15.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9ea683af80e4fb7c9d712b5df4bae34c63fa9e6afc78d750ba2d9f5e6f3203e0", size = 7233066, upload-time = "2026-07-02T12:02:36.109Z" }, + { url = "https://files.pythonhosted.org/packages/33/60/b9ae92e8454f42cb5c5ccca63862a75f5d43afead1f725f3b8af19f507f5/highspy-1.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:565cf6a6e7c84e36c101b118a3c5fd09bc14aeece599bba12625e79b5ab0cecb", size = 6636826, upload-time = "2026-07-02T12:02:37.863Z" }, + { url = "https://files.pythonhosted.org/packages/54/0b/35e5e63be2e70951c3224ed33c4300f1cd37fcfe4eb6d25e259e13571e0f/highspy-1.15.1-cp313-cp313-win32.whl", hash = "sha256:6cc7008b82094b2a2377338398b38f5b6c306397bd23282e55dec46a101a2dac", size = 2306720, upload-time = "2026-07-02T12:02:39.839Z" }, + { url = "https://files.pythonhosted.org/packages/ca/63/2e104bab0117415c68950f249e42f0974f74665d0313dfeddceb1f74c47d/highspy-1.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:46fe314b918257361c54170852bc561c78d0f84d94e2ad263859d818127e6e76", size = 2711119, upload-time = "2026-07-02T12:02:41.861Z" }, + { url = "https://files.pythonhosted.org/packages/0c/73/8cd42c3ca7baf4857494a0294ef068f2216f1216173f3f298046820a7a57/highspy-1.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a7b11dc80781052a6e7c163b5c2696fe9e06c72927cfdb48f67f7e8c77096f4f", size = 4879694, upload-time = "2026-07-02T12:02:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5b/308821aeefa0e85f90645e15a86bc63c156bf08b00e33a0a906a0c430b41/highspy-1.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9a00e1278ea46a426b1eaa0aea69df9d72ed1d75b18227cad992384ebbdc0c74", size = 4475059, upload-time = "2026-07-02T12:02:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/a3/20/9c75531c03c7121d576ef0ff8415bfb255fdd060c435f9f26e5b103b0559/highspy-1.15.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:193b9751d3705bc948552b138800af0ad8af17a5b801d5940d7db7ff1ffc4f10", size = 4637880, upload-time = "2026-07-02T12:02:47.239Z" }, + { url = "https://files.pythonhosted.org/packages/89/ea/6d6136f01ce82c049740b00380a39999a15c689a9a4d43fdb1ea25090c2b/highspy-1.15.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6298b6ef691e83544d395d45fa4e856874c44b32936d85c36564f7697d27bb0b", size = 5034792, upload-time = "2026-07-02T12:02:49.044Z" }, + { url = "https://files.pythonhosted.org/packages/38/9d/ccf4a0d4e7a4fa4141dbabe9f78e94fa9d37b6b5becafe8a61e8369031eb/highspy-1.15.1-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:9d436b5f8d50b01497d494606695746147e15b8e22eec6ae475a60cb8b22c1d7", size = 5861891, upload-time = "2026-07-02T12:02:51.432Z" }, + { url = "https://files.pythonhosted.org/packages/19/b4/655f6ce06e17159c001456c97c4be84dcb1448477e3ea52bd5401f5c27c3/highspy-1.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bbb22b7ceed298c0b75237186eb4671915b1c41c07f966e527643af10493671e", size = 6195707, upload-time = "2026-07-02T12:02:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/ea/93/a35495b3326cdc0c2ff59de69d26f2600f41399d9381b59f4826742c054e/highspy-1.15.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:74c1eb71d3c0fa0c190492d9c0c67266d1dd6b4244c93b53e95a687504db309d", size = 7235019, upload-time = "2026-07-02T12:02:55.989Z" }, + { url = "https://files.pythonhosted.org/packages/20/5e/8b21c908ee94db28f2de58326c8a25e361b3d504f145f7972f096028a908/highspy-1.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb8b8298a74786e1cbc1a9e102b7749e2bbd9c41826ffd4a1d7ba738232646ff", size = 6637185, upload-time = "2026-07-02T12:02:58.165Z" }, + { url = "https://files.pythonhosted.org/packages/25/81/8f984e500536ca40a8fb1d74ecb7a213e170683adcfd01edee8e21e5735b/highspy-1.15.1-cp314-cp314-win32.whl", hash = "sha256:780c021441f548711818833d3a986fcb253849734aa00c3bf83d342c38b03629", size = 2362473, upload-time = "2026-07-02T12:03:00.147Z" }, + { url = "https://files.pythonhosted.org/packages/bf/97/e85d751aaba8231e86915077532fd584711d30aa9eb85c26331e2bd87596/highspy-1.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:864258c59aeaea9d3bd7ccdd10c03258e2be764e2cf1e21f829fd1f8d8c15d57", size = 2813851, upload-time = "2026-07-02T12:03:01.836Z" }, +] + [[package]] name = "hypothesis" version = "6.155.2" @@ -802,6 +1011,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joblib" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/1d/537ab090f302b838943a1b56497dd53059b9a9b46a074936470173a2e207/joblib-1.6.0.tar.gz", hash = "sha256:2ccc96785b12046c08fd6d55839c12857831b54a3c1673ffadd2f04bfc4eda03", size = 327903, upload-time = "2026-08-31T09:39:04.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/53/84099323c2ec4be98d935f63c033ac4151ee83836ca1050ede3b3aadf155/joblib-1.6.0-py3-none-any.whl", hash = "sha256:3dbbf9f6e4b592a2357b854608e980fe6390d131d7a82f011a377ef2ebef7aba", size = 306115, upload-time = "2026-08-31T09:39:02.298Z" }, +] + [[package]] name = "kiwisolver" version = "1.5.0" @@ -1321,6 +1542,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, ] +[[package]] +name = "osqp" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/7f/b441062e4766851fdf1d066857134c4c3bcf7e0089e4a1d007b6114cecd5/osqp-1.1.3.tar.gz", hash = "sha256:48f53ef5ec89e6ce99ffa955bc6ea0cf2eec09ea3d40905f0c9fadc939609907", size = 57816, upload-time = "2026-06-12T16:59:27.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/66/f25cb13b72eedd74bcff17421761c520eabdade1b7166689d5b76f68d2b6/osqp-1.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a168153f6058075d017e713d20eb4214c5daeb957b497c3c31340d0e63e1288", size = 326215, upload-time = "2026-06-12T16:58:43.489Z" }, + { url = "https://files.pythonhosted.org/packages/32/bb/95a38971abb93501e1870dcaa20c04877d19c59cb98bf9919ed85af32805/osqp-1.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fefecf59a4d84a63ff6fb34e24987f12d61c740e78cf7fb7864f427a3d1c74e", size = 308213, upload-time = "2026-06-12T16:58:44.784Z" }, + { url = "https://files.pythonhosted.org/packages/14/49/b941c22530f24538dbeb3a2d5b56faab4f853100f47250f07aeab943e671/osqp-1.1.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2b12ea5bdcf3750703c1fc1ea5d678f3f0fdddc6513c9352439f8075ca07bf4", size = 326653, upload-time = "2026-06-12T16:58:45.965Z" }, + { url = "https://files.pythonhosted.org/packages/18/b6/d569a65c4c371ee0ec866a17cafb21af2f1201390ac3aaa9c70672459fea/osqp-1.1.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6e6b384c356681b93c8698304b8910bd4a198e95664ab533cf5a422c126cd40", size = 351147, upload-time = "2026-06-12T16:58:47.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fe/4133eedbd73931a38fabb2ccae2a62e7f532e3d9e97ef51e0fcb04e668db/osqp-1.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:fb3734923e7606d8a5c14e41e1e710352aa6696e9f3f94e770def85d4633abf4", size = 314758, upload-time = "2026-06-12T16:58:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/49/d2/f21ddf1b41ecc14784186129125c4e9c19d8f6f7ade0f2e0efd4a44dded6/osqp-1.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2b59e8094fe29d928d568cad0156a42daa44257ce142fb7808400016a62dc28c", size = 328512, upload-time = "2026-06-12T16:58:49.796Z" }, + { url = "https://files.pythonhosted.org/packages/de/45/1f99a9f25dc9534323b8ef9e578dc93acb33e2b762d5be267e1380b2dda5/osqp-1.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5620abbdb259190da6f0e5421b9fdfd9386b46690210ce82aea6102cc85c67f", size = 308891, upload-time = "2026-06-12T16:58:51.07Z" }, + { url = "https://files.pythonhosted.org/packages/d5/25/176dbb33c3c3605367c0de8720945c6bdd8f9b0d411e0a94ef747085f034/osqp-1.1.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e44ad08234cfbd6d9f2a823118547e683b038676887e096533935b9fcc15fd3c", size = 328135, upload-time = "2026-06-12T16:58:52.185Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/96e2e4de3b89e7363931c70eb58bded22656f5512bde18eb7a9272ce48d8/osqp-1.1.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ceef7fb4f332892b6e0bbc17323d5c9e028c3f9db726b62a15d876d0f81cc06", size = 354077, upload-time = "2026-06-12T16:58:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/c4/4b/6a1e4f5aa28117bc0a78be5e9f4071b6a11c00a33a01bc673c3faf2a0a65/osqp-1.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:e7e9bd3939e47e726673218a7b04b9784ecbf9ef0b8bc69107d20cc659836ac1", size = 316733, upload-time = "2026-06-12T16:58:54.397Z" }, + { url = "https://files.pythonhosted.org/packages/14/f8/9f74aa53b35cb9c552ff1348abe5a17d22b4e65b0dfbe987980874c8bd36/osqp-1.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff6cf15a5404b28a4d9aaada72ae73c6b179120892321ec9b70f52ecb7f34261", size = 328538, upload-time = "2026-06-12T16:58:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4e/4a7197f508cfa62abf40f7eacc7efa2f6147e6d9f7bbb22b18e8e10d6736/osqp-1.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a9c46b4adf2b1f76a40ef8f72426be5aa014c94efca20ac5acf8afa52e352afa", size = 308914, upload-time = "2026-06-12T16:58:56.906Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/e967f4f1e3709baf6500914abd212d863a54fc90e90cd61dbbb0af14d6a4/osqp-1.1.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcca27693d0506c6816f989cf38e6352ed74ac8eaada9babc5487456d0cb0bf8", size = 328191, upload-time = "2026-06-12T16:58:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/dc/7c06a4ce4d1de3e5fbf294cf435ee351a39e44d534154647b2a1a18bc6a2/osqp-1.1.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d3ee63e8c65ef89fce979c068d05bfc3ed92b1bbc4246fbacc663f86cbe02b2", size = 354138, upload-time = "2026-06-12T16:58:59.398Z" }, + { url = "https://files.pythonhosted.org/packages/05/22/2b4198a40847bff9b19230f3e55ac7537fe275a55a27eccebfdfee77fecb/osqp-1.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:9b9fe3daa15313d281233babeb102007062933241a855f6b399becd03ec5f1e3", size = 316744, upload-time = "2026-06-12T16:59:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/e3/54/bdb0611de308ddf9868d7bd75e61d62ca8a3bacca71260e1876a354ebd71/osqp-1.1.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5eade025877e5d2fb61efdd91337cc6f259335b2783ca59bce10e7be61c1d12", size = 328883, upload-time = "2026-06-12T16:59:02.162Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/4bca14a7a0402e5dd865225f193074217f019273f7a91e2d0018853b1ad5/osqp-1.1.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e730047c4cba86ad97ca73c03ea4c3cca765b22fa79c3fbefff11b3cce317742", size = 309534, upload-time = "2026-06-12T16:59:03.427Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0d/8e5ca2e9f4630f249602e91cb78b052b61744fac042f91b22a9e4d86088b/osqp-1.1.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1547e515c16feb1ab2889b64bcd4eecbbc59c40af4faf67258047a199780b98a", size = 328836, upload-time = "2026-06-12T16:59:04.833Z" }, + { url = "https://files.pythonhosted.org/packages/83/17/cb502af2a69da5f915d487db565b61e67684c65d3a4e5ae1ea42cece31f7/osqp-1.1.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bdcbe47c2c37bf7296971a40811a883e793923699394a73a9d677ab7a9093e8", size = 354268, upload-time = "2026-06-12T16:59:06.014Z" }, + { url = "https://files.pythonhosted.org/packages/56/09/56bb7302f545fafbdfd9f0ad1042424c51c7a89407569bf005cc012b8a0f/osqp-1.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:e80447b95d7b7dec3d13cc9e5a67ef4c5eaac3affee60fd83d1ce461e9270816", size = 322177, upload-time = "2026-06-12T16:59:07.175Z" }, + { url = "https://files.pythonhosted.org/packages/93/11/b4edd9cf7f3ceac4785357203f98b7661ae489b2721f30d994a2870aaa09/osqp-1.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:97df452d5e3b000b075fcce1c9f628289411501a2c0ea9a94acea281ed57bbd3", size = 336601, upload-time = "2026-06-12T16:59:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/51/0f/ebc13231f58eaf4a7f73d5c2116c05214a871f49e777d63ff2cc80ee2439/osqp-1.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3efaf5fa72b45bec9b67dcaf5ae58a4e2d1bd5d8053b249c0ecd5b8b60c1bc0e", size = 318836, upload-time = "2026-06-12T16:59:09.684Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/b74016cdd06cf37e4efb9d7540ce56e466d77a803ccd1ffc80a8b3e3d489/osqp-1.1.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1d6cf30cdccf07baecb7232dc5fe22a1421ea3278a88a40580df3563a419d64", size = 330294, upload-time = "2026-06-12T16:59:11.094Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/d9a756198d1e48931ddf6363df7162a5c13aecdca5af1f1489bebfa92a7d/osqp-1.1.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ae5b09be77b987421216dae535b4c007a14f6c034a844ad6eb4244289a881d", size = 355773, upload-time = "2026-06-12T16:59:12.274Z" }, + { url = "https://files.pythonhosted.org/packages/07/fc/07ad9b479417934c7533e137be82ed0c0ef76663d6d67a1af5e068a44466/osqp-1.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:f5d66a344ff483eeb5a9213521b91427d2d898885d7e2db0712f648664482264", size = 337988, upload-time = "2026-06-12T16:59:13.366Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -1606,6 +1867,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/b1/ce1f4596211efb5410e178a803f08e59b20bedb66837dcf41e21c54f9ec1/pybtex_docutils-1.0.3-py3-none-any.whl", hash = "sha256:8fd290d2ae48e32fcb54d86b0efb8d573198653c7e2447d5bec5847095f430b9", size = 6385, upload-time = "2023-08-22T06:43:20.513Z" }, ] +[[package]] +name = "pycparser" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -1945,6 +2215,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "qdldl" +version = "0.1.9.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4e/452984a63df9421cf8e7d25e8e6a44832cf0247a5e7b65e437cd516a0f8f/qdldl-0.1.9.post1.tar.gz", hash = "sha256:da2016d541c26cefc79bca4d8b5bebfa00f35db19704abb20efbd1c08df3b4c7", size = 76295, upload-time = "2026-02-19T16:48:36.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/fd/7610d618eef6ab2bf5fc5048d0af76507561f3d72ea8fb12222e366f9744/qdldl-0.1.9.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:133ee44b5afea5b57eec6c3455c93bba0229add840dbbdbc33a6fdb66058641c", size = 120776, upload-time = "2026-02-19T16:47:44.349Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c5/7d7c8f2f5aa581408c8bae8d3615fdf1e9adf6258c27c30c8b14385d2d50/qdldl-0.1.9.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d3d4ac1885bc99d214044671121b9e95f96fb9caa63e2a1d6ac30bae9f8266a8", size = 116516, upload-time = "2026-02-19T16:47:45.251Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/b2244008b37a3215d2ba4ece0d8d91d18caa804ee6a49d120ff0f92b9b77/qdldl-0.1.9.post1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a90b12436906ce2703795511332969ff1bc50b38cd4b6fcc67ae0e9e7e7901da", size = 1428868, upload-time = "2026-02-19T16:47:46.567Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e1/33203651c73e126bba82788e3d7813330e987df1e3c31ab36f47dd5878d6/qdldl-0.1.9.post1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6d333d1de8f39ca3c34d0d88ee00a3d1b0d90f54dafad8d554b646c6110c7dd", size = 1453479, upload-time = "2026-02-19T16:47:48.252Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c9/a3ac897afef956b0606e17baa680f9bd957304c7d85fc5dc46a6dcef55e5/qdldl-0.1.9.post1-cp311-cp311-win_amd64.whl", hash = "sha256:1f61b59fec8e5b9230bfa8dd471d6f5c2ae4ed8b3b63ca224def1e432325deb0", size = 102709, upload-time = "2026-02-19T16:47:49.468Z" }, + { url = "https://files.pythonhosted.org/packages/03/0a/ce73fc080931a9c7f7a5b018c07763fb940da38350f51474d7f1fe4314a6/qdldl-0.1.9.post1-cp311-cp311-win_arm64.whl", hash = "sha256:98a1da222112bd46fe14e2854aa43361203d488451ba300d08f8c9a43c5093e3", size = 98978, upload-time = "2026-02-19T16:47:50.396Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ed/2ae64314f84211cb963136168fb119e595d1aea42d1f03a1e84ef7d04d68/qdldl-0.1.9.post1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25525962e90f1b9bfe3c4fb565222daf2bd77249bb6ffb9ae20b7da551f73538", size = 122478, upload-time = "2026-02-19T16:47:51.279Z" }, + { url = "https://files.pythonhosted.org/packages/15/45/767d8a6da3a04ee3c7262f897c1dd81e92d156c60eb5b43bcab4f2bc5af5/qdldl-0.1.9.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a2c005c9365dea6389a9feacf636028453790cace114b54e40fd302d6f4bf91c", size = 117691, upload-time = "2026-02-19T16:47:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/1f/58/cb80bb5d379a7e570a160a198ff6bff2398d6c61b5514462736348218013/qdldl-0.1.9.post1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee5b35c1dd419cbf388664d26d410c89a3e0c7e055e25223b6d28bd920349c8a", size = 1449354, upload-time = "2026-02-19T16:47:53.163Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/8d89f71d4e1cb3152e4e3262db667282e0df6788d21800725195ee2384c0/qdldl-0.1.9.post1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa0e9721d272467c95a9748e6acea8911a12041ef3d8b20176aff829388ce57d", size = 1476880, upload-time = "2026-02-19T16:47:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/d4/19/b30374cd37f145c4ee086642eca46c1a0b29d16027e02417b825d3708602/qdldl-0.1.9.post1-cp312-cp312-win_amd64.whl", hash = "sha256:f5a9bcda38dd19f75d72e47558f5132a99e5443238e5153a984c6f552bc4f4ac", size = 104542, upload-time = "2026-02-19T16:47:55.558Z" }, + { url = "https://files.pythonhosted.org/packages/22/51/2a683eddd1f0cf50440e0d173ccc4cb500775a8449e27651963873cdb4df/qdldl-0.1.9.post1-cp312-cp312-win_arm64.whl", hash = "sha256:ba3e19399553821b5ceee0c082fdb4453d00a38bd420b76dddec92ce2a5a065c", size = 98901, upload-time = "2026-02-19T16:47:56.553Z" }, + { url = "https://files.pythonhosted.org/packages/42/f6/5ab7f37e7607396eb7a61736471b54be881fc5697bb18e57992fb14a0867/qdldl-0.1.9.post1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00331a5e45cc60bf6f75f027ae2a2c137295de0b5e1a0af358a37aaa07427476", size = 122503, upload-time = "2026-02-19T16:47:57.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/49/144ec3c6b7cfe650191dac35a21d93eebfcc043a3ecd8b499adb3b7cec1d/qdldl-0.1.9.post1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbfb46f7290917146b076449fb3fee1eefdf018e710c7033c11739ae1723da07", size = 117760, upload-time = "2026-02-19T16:47:58.407Z" }, + { url = "https://files.pythonhosted.org/packages/82/a1/f8d58f100140d416f40912a7bf8bc28ab8276e7e8e6a24a8889a4e5c895a/qdldl-0.1.9.post1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e881f51dc779441c0910dc5a9d89bfd40a459daaf8088934aebf6f4c84adf1e0", size = 1448988, upload-time = "2026-02-19T16:47:59.377Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7d/6d621819d3340f2cb4555e5263481be1e741578634d09569c67acc186ff0/qdldl-0.1.9.post1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc0f30e521da345d25e4fa5308fda4964e25beacd10e8de9f08d084a600a42e2", size = 1476075, upload-time = "2026-02-19T16:48:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/423a7cbe11add0b7b6afe97c9d6e934776c632a8c6e1d2a457c1f9dffbf6/qdldl-0.1.9.post1-cp313-cp313-win_amd64.whl", hash = "sha256:27b02a730c39b2dba205bb5c08dca5deb994f655f6eeacee687ac006c50b3366", size = 104586, upload-time = "2026-02-19T16:48:01.586Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/425dfec0bd5a18ad43790764f091567fba18d63d9f9d6b202a796a8fd9ca/qdldl-0.1.9.post1-cp313-cp313-win_arm64.whl", hash = "sha256:98a13e234ea335484cf76441b95638d0f9ecdd43242dd5a073f77c758977d980", size = 98954, upload-time = "2026-02-19T16:48:02.486Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a4/bc38a8f2edd88adc3bd87a53493e3ddfcab1c173a0310dcfe7f56063d254/qdldl-0.1.9.post1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:576edaf8b847d087806e3d2a860a06d52a435aa3192b21bbc39e69a55187805d", size = 129346, upload-time = "2026-02-19T16:48:03.325Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/0b157bd706ee91838d22d57dd0bd3a7ea0837142fb97b5df3d8acfad9f6c/qdldl-0.1.9.post1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:547f73046ef615827317fbedb21b5e1b11f5082d304e8d775e9e17d6c447a598", size = 124236, upload-time = "2026-02-19T16:48:04.313Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/185e0620e424fab6eeafd3e37f37723e72c80a795419556575d47b27d4c5/qdldl-0.1.9.post1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cb2e82b4f40cb18638da47adf491a016d76d0d82f25377c4c330453a8785f94", size = 1494339, upload-time = "2026-02-19T16:48:05.364Z" }, + { url = "https://files.pythonhosted.org/packages/1a/55/0982ba5565863e3fa4306891158cb509088a875e76a5eeb683744d8a1610/qdldl-0.1.9.post1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ddd5b43b760ffb4cc85ca1acfb0eb139bff78bcc7bd30cad4c55564074685d4", size = 1517878, upload-time = "2026-02-19T16:48:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/79/bf/b75b0fee2ea07e0dd2c8824356cd78200ac429172fa24f12a2cf84f95bd8/qdldl-0.1.9.post1-cp313-cp313t-win_amd64.whl", hash = "sha256:0fba64949c3197c6691c8fe4c3fbcb4f95d6e85b314824ba75981efcdf06d98c", size = 113642, upload-time = "2026-02-19T16:48:07.858Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/cc4bfa56f8d29a5ed7956c97ede9dfc4ec408f61e4b75ecd9e19232a64a8/qdldl-0.1.9.post1-cp313-cp313t-win_arm64.whl", hash = "sha256:31296314679c6ccfad8760704dbc9e25b5e49fd442f803638a2aaa1886724f7d", size = 104321, upload-time = "2026-02-19T16:48:08.79Z" }, + { url = "https://files.pythonhosted.org/packages/08/28/9b991fdcc16569b1bc7119ae1f62495c948033d791d469c031058d7b2c4c/qdldl-0.1.9.post1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0d69a8d011f47f287c5e7668b92b9e3574798b9687bc751af6f89c15037aa26e", size = 122733, upload-time = "2026-02-19T16:48:09.694Z" }, + { url = "https://files.pythonhosted.org/packages/18/92/56437eb5a31edb9275450dac94a94f7df252147527974ef50ba56cbb8d66/qdldl-0.1.9.post1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:496bebf154c002fb7a36443b9a26cc0f7251e8e9251d3742590300cd2edec7a8", size = 117977, upload-time = "2026-02-19T16:48:10.606Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/9594e0cea5aaf33c2579f722ad02e5117ede62e99aca62245f2c2d3df0d5/qdldl-0.1.9.post1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1922c11ddb59419ab969cd1a069e85da9e37db9b14d92b0900e73be94d25c318", size = 1448404, upload-time = "2026-02-19T16:48:11.497Z" }, + { url = "https://files.pythonhosted.org/packages/bf/85/c5e380aeaa3f5d61cbf21b815b3caac5909a165c747aadd5675500fa92dc/qdldl-0.1.9.post1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8c0148de01346722ef07ce9434485b97b8f3aa7042cac5c341b58a728fe7588", size = 1474644, upload-time = "2026-02-19T16:48:12.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a7/a3b62d9ec100604778d0596910f4bd20eaf23445af20bdd1d3cac77b6632/qdldl-0.1.9.post1-cp314-cp314-win_amd64.whl", hash = "sha256:9adb8625012e96ceb6c24a8278b8aa9477727ae8fde35dee730988747ab95144", size = 107693, upload-time = "2026-02-19T16:48:14.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/ec/55b21669cad250f55fee6a8ae0fe65dfb547baeb3463f576e8642795e392/qdldl-0.1.9.post1-cp314-cp314-win_arm64.whl", hash = "sha256:0db9f197fb51c6fa96ff1964707e2ba9a89b7b0d91c305e6e0b668e1bcb66fbf", size = 102546, upload-time = "2026-02-19T16:48:15.226Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/09c59c9f4a1df9b2b415cbc13eb0daadab418e252c698f622d94e4dcb026/qdldl-0.1.9.post1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:25da2bcd44dd272435db3b4208b47943837503f43db4d24c02e5e15b7d5ddf33", size = 129500, upload-time = "2026-02-19T16:48:16.221Z" }, + { url = "https://files.pythonhosted.org/packages/86/03/dcf73d806a4c3f0250a56cc56dafadc2dfacccf5df9caf8f7f181a5e3a89/qdldl-0.1.9.post1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e3438e9cb3f8a26531a24f25bb05152a7446112f4c8ff62d537debfb8147435c", size = 124224, upload-time = "2026-02-19T16:48:17.17Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2e/7770f0ad70c6cc834041c2c19262c3315d312d0d3e72ac33ecb7c94f2f9a/qdldl-0.1.9.post1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a2aed01dbdc705bcab71270ce20753a016bd7b112ea7e06d1166d897e7483cb", size = 1485196, upload-time = "2026-02-19T16:48:18.415Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6d/f1919ae97e1482484239edcda8b4f29aaba11817c808c775403e8a35aa54/qdldl-0.1.9.post1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd08bbb431f138c2946a0d99cb70fbfe67435c8b3dc1dbf23e4e80edfd2b1456", size = 1509644, upload-time = "2026-02-19T16:48:20.273Z" }, + { url = "https://files.pythonhosted.org/packages/7a/33/89ca4741cb651385a12600c0e93d24ad20eef5a954bb2b7ddc596442fc8c/qdldl-0.1.9.post1-cp314-cp314t-win_amd64.whl", hash = "sha256:213f6125564f61d9597d5d36c5556d4c898225bc31a99f8c87fd549b2e65b60a", size = 117569, upload-time = "2026-02-19T16:48:21.459Z" }, + { url = "https://files.pythonhosted.org/packages/63/48/34fa827457aa0e373fb50dab4490757350076f9afcf7eb749a71926023af/qdldl-0.1.9.post1-cp314-cp314t-win_arm64.whl", hash = "sha256:fcc2184cac1e502ed624efe7f00c1c215b95cfd324a8cacf7f0053c00fa524f5", size = 107844, upload-time = "2026-02-19T16:48:22.899Z" }, +] + [[package]] name = "requests" version = "2.34.2" @@ -2078,6 +2396,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] +[[package]] +name = "scs" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/14/f2042d968fc1d633beee798329274b8496f9481362797124879a1d66bad3/scs-3.3.1.tar.gz", hash = "sha256:dd9d43c1f2f189a84e84c26721e2c5129abf921a6a8acc48c154abae993357b1", size = 1833748, upload-time = "2026-09-05T16:57:10.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/1f/deed679a4286b436f4496a6794c671ec2aef9ce4c72b8d2c71fceffe79ea/scs-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91347cb1f3b5236e391672e2870fd567dd62ed0490bc0374b4c80bb06185cd1b", size = 234578, upload-time = "2026-09-05T16:55:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/3d/31/dbb7e2356d7cd9cb5a39ecbbe3cb588e5ebde2095952d430448f78baf03e/scs-3.3.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaf4d85c71516139c472a7aa7dbf14bbf3e3ebbc1cf7f548e0d71b6108f30b55", size = 5343195, upload-time = "2026-09-05T16:55:36.118Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e4/115cb3d98572adb429e0a5869b1e7488835367a17b687b04907ba87873d3/scs-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:681437635f89563a9c8d4bb19beccd29e90c1f719846ff1b5ada04c1fccb122f", size = 43243290, upload-time = "2026-09-05T16:55:38.988Z" }, + { url = "https://files.pythonhosted.org/packages/97/84/a88bfacca9ac5e2a3834dc8adc2dc5ba051345ea526f74a635ebfc103d84/scs-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:81899e57933d457a54e08aedd6e2b77dfd210e4a44175a231881764793b9b6cb", size = 12241580, upload-time = "2026-09-05T16:55:41.983Z" }, + { url = "https://files.pythonhosted.org/packages/70/76/ec5082c4243430d7bb05a62bc4fc910f40bf00af307778a0eb9f13383c71/scs-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d268e22f871dadf96c622bbe8a301e20007474e3afa4783806074492214156b", size = 7631716, upload-time = "2026-09-05T16:55:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ca/ae4e80dab9a0b20a77e1856d379c6519cfc00b42cc78b05fb14755f82609/scs-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388a32d8fc5094a76a77ad4d9b843c3b03015387046a2e85f03fd6de7268df9b", size = 235236, upload-time = "2026-09-05T16:55:46.154Z" }, + { url = "https://files.pythonhosted.org/packages/9c/06/b870d5af5a4913e5c826172ac74c4dd8064cc480ea0620677b4097bc97a3/scs-3.3.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d73628563095fc898ef835ffb180f1ee5d8379c871dad56d8f0f579be2e05067", size = 5342353, upload-time = "2026-09-05T16:55:47.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a1/6b5f106cdd17045927d477cb152b2d4c1def4d09f997a9914ab3d664ab23/scs-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38e8eeb2b8f43f3109569862d398ca787ab4e4a50e16d1148206ed1cecd1840f", size = 43243897, upload-time = "2026-09-05T16:55:50.493Z" }, + { url = "https://files.pythonhosted.org/packages/57/2f/9f6452582cef225cd303704f085f46bbd31e17ee33cab01e1c56544b115f/scs-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491bf3734ea064056c67f5932bd735d0f36d6600763d5e2eecac0940212da7dc", size = 12242133, upload-time = "2026-09-05T16:55:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/101bc8a476189c71f4df9c1042cfdf235f7c72f6f8affaed6eb65d125c30/scs-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:34093ab7c5ae455ee2a6e5c0c0ae990be63a3fb57e7766a3563014041fcaceb2", size = 7632179, upload-time = "2026-09-05T16:55:55.851Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/41ba5c72a7364be820a78d1633230315a08086dd3f6cf529dbb635bc1f1b/scs-3.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e488cbb471807b8cad9d9f4c56407d90221996a29e3bdcf8bd6668c01dfcc1e8", size = 234807, upload-time = "2026-09-05T16:55:57.502Z" }, + { url = "https://files.pythonhosted.org/packages/50/7e/eeb5edef8470c78a407fd77c41b827bc78c202c34c110326360d7ffb08d1/scs-3.3.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bd0271f32335d109f00842b5f9ce90544af412c564603927273dc6a9326fc7a", size = 5341688, upload-time = "2026-09-05T16:55:59.18Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c6/b4a9cec3d31ae541ab85a884830512a6339b86ba33cfe553889ef083bed2/scs-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eacba22bdb4605ea07f138648292f5f7131a48527de7df8dce790b57c2e4cf55", size = 43243389, upload-time = "2026-09-05T16:56:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/289390c31e409099b7613d295007568d87a109be0f211d900e41ea2a3c8d/scs-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b8e140fdf41d79c15777ace14f92a38989d17e0b3356ba571d229acbc797a9e", size = 12241645, upload-time = "2026-09-05T16:56:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ba/15d39af690ef0342776f6122cc2be939ebc51956fbeeb2c53e20391071ec/scs-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:29eb5b976997de28aaf0592f9abd8678b2af699980895b79ad22c8363a2a2e88", size = 7631981, upload-time = "2026-09-05T16:56:07.36Z" }, + { url = "https://files.pythonhosted.org/packages/2e/65/c35dbc6b2a97e1fb2e4274f3ae08b1a7e74f69aa6947a30718c8d22fed38/scs-3.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8c01e6a943a31be436c55573ce0316494169d9737b4890d96463615693f3cd5", size = 234762, upload-time = "2026-09-05T16:56:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/68/48/6256250d1636977215639a091357b88757071f185262c5c01df6b1f64d13/scs-3.3.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34c3ac05c264e380b93e327026b2b5d9cfa2cb7f3c4e28689df346fa89d446e", size = 5341777, upload-time = "2026-09-05T16:56:10.621Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f5/7c1ac53555e9967249d754ad2034bec92f48745c24360b16b465a6b55e95/scs-3.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1d7343e7e4e86a72d3f9b6efbfbce11cff9821f3164ac3122f47eecb11f19d7", size = 43243423, upload-time = "2026-09-05T16:56:13.891Z" }, + { url = "https://files.pythonhosted.org/packages/41/4a/b81c498bfb3cc8e14feeafac761fb75008b995afc64afd23324a43c4a1c3/scs-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cccb8d20780aca568aa1448239bf71d60a69b41b0470e972ad8dab0f328808dd", size = 12241691, upload-time = "2026-09-05T16:56:16.806Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/d2f1eddbd75998dceaeb872805c7f0ab90a4640cf488abd107a3045cdc6d/scs-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:077be0e95ca17e6e623fa4eefe4df9b2f9840875cfd65b2da58cfe72f274b2aa", size = 7728189, upload-time = "2026-09-05T16:56:19.322Z" }, + { url = "https://files.pythonhosted.org/packages/02/ee/dd0dc6ec7f79bbf2ea83d96382f98e2569719087ee8ea44b1b5b275ba1e2/scs-3.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0495b1fb68f04456d497490c3fad62e8e12d69b4902275636078eca6a590bde9", size = 237731, upload-time = "2026-09-05T16:56:20.835Z" }, + { url = "https://files.pythonhosted.org/packages/eb/09/fa430a83bc74bdae77c92db3502d2a2e74af1afd185531361ddefbb40912/scs-3.3.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d743b1544fc0feb27843c94dfaf577cd594c3910cb1d150d2f286c786ffe763", size = 5347257, upload-time = "2026-09-05T16:56:22.487Z" }, + { url = "https://files.pythonhosted.org/packages/21/fe/3e61708c493991199c6194acffb811957a6ff4db87cabc889fe8e2bf773e/scs-3.3.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee4a4555930cac82b988b66a81993e87d674d358d22d909ec31ba7cbc0709f8a", size = 43247605, upload-time = "2026-09-05T16:56:25.757Z" }, + { url = "https://files.pythonhosted.org/packages/08/82/9f812b724717135de07ad07a78d4bb90484e11b551a3e3bb8231aa905ae6/scs-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c9c75f9be183ab4703f38d0b4db26e1dd9a2c683e24532ad3099a2abc826fd3", size = 12245185, upload-time = "2026-09-05T16:56:28.61Z" }, + { url = "https://files.pythonhosted.org/packages/85/28/c50748ac21c95256b116e4dd64f22b37116fafb4a7a10403e190a20e580d/scs-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:78e34594d751614b7403756d6916703211a590fb9bfe2c9a7faaf4c3272a1f04", size = 7729129, upload-time = "2026-09-05T16:56:31.043Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/4c56efe890d9ac0bed14765c8e01ce3a2006b7a4c94ee28f62c0fd565cc9/scs-3.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:93cf91abcfb5ad4c412b5e826ccdd6e10552c2e9a3ca768e605be947f8db28da", size = 234762, upload-time = "2026-09-05T16:56:32.729Z" }, + { url = "https://files.pythonhosted.org/packages/69/bd/603d616059077b56915c599df5a91c22fb7c962f49d7932bb6778d286d51/scs-3.3.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0107d0b282fa783e956f1d68320f4d5ac6796186816e0996984b4d240b9704a6", size = 5341778, upload-time = "2026-09-05T16:56:34.389Z" }, + { url = "https://files.pythonhosted.org/packages/67/43/9a455703df9508d370faaacec5ff7a109edd0eb63ec7e2dde64b60695c0b/scs-3.3.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:566d64908cf38a7baa03fe554df0d0f96b9798bb71d8f5d8f4e782489f05a15d", size = 43243425, upload-time = "2026-09-05T16:56:37.781Z" }, + { url = "https://files.pythonhosted.org/packages/84/38/bdb88b3bb4f6b2bbf4b99c822c2ae83bc8ce8ad3656b9a2dd8e20b71c0f4/scs-3.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c8ace1a3d784fdb9810e056f0ac7115efd74740663d188438cf5c83f36b6e338", size = 12241697, upload-time = "2026-09-05T16:56:40.881Z" }, + { url = "https://files.pythonhosted.org/packages/13/c9/17631eaa1e56f9c954db6f9ac1d7c5b816654f299e978e09068e96f212c1/scs-3.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:cc8479a3c31394d4ca453007b9c13222bf5a270e28cb675661579b2357fa8ef6", size = 7728163, upload-time = "2026-09-05T16:56:43.63Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/e0ffe38a280b614a42f1d171e4cd596737751d29ef26e4079ca8a99ccd6a/scs-3.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bd687819d850b0d21f90276b43064703fdedfd86bf91e4008e9a833e6bd0e680", size = 237717, upload-time = "2026-09-05T16:56:45.272Z" }, + { url = "https://files.pythonhosted.org/packages/d4/07/af6e751480972dc0a857fbd98d602a4e24a50783404edd37e695f3e96291/scs-3.3.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc10932f33c97d9a490be30eb8720e2012931e014bf9259980dcd23e57db6dba", size = 5346713, upload-time = "2026-09-05T16:56:46.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/fb/62d33d7aee5e0720650e8cf910fb296432f4cabfdd7a2500bc97695024f7/scs-3.3.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34fb479df550539c29bea9ad9dff57ba870300485b0b1ea081b4a7bea870d19b", size = 43247712, upload-time = "2026-09-05T16:56:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/59/ed/71a7a1c439b68e8c648afdd01b76c8df05fc1da6900fa95066b2dccbbf2e/scs-3.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b09747d1151e42f258f23c50ee6177b11602a758a1630a618b2e71b2ba7c048d", size = 12245286, upload-time = "2026-09-05T16:56:53.136Z" }, + { url = "https://files.pythonhosted.org/packages/ef/49/c5fa7335c6f3ee91af33c5f7c5c67675afd88b3ac62b9271a3df222b55dd/scs-3.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:98cb9677e2e8b33ce7850ca4c3df58e4f6f89143ed11822699e63c293c53181d", size = 7729118, upload-time = "2026-09-05T16:56:55.532Z" }, +] + [[package]] name = "setuptools" version = "82.0.1" @@ -2123,7 +2488,11 @@ bayes = [ { name = "pymc", version = "5.28.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "pymc", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] +capacity = [ + { name = "cvxpy" }, +] dev = [ + { name = "cvxpy" }, { name = "dit", extra = ["symbolic"] }, { name = "graphviz" }, { name = "hypothesis", extra = ["numpy"] }, @@ -2176,6 +2545,8 @@ viz = [ [package.metadata] requires-dist = [ { name = "arviz", marker = "extra == 'bayes'" }, + { name = "cvxpy", marker = "extra == 'capacity'", specifier = ">=1.4" }, + { name = "cvxpy", marker = "extra == 'dev'", specifier = ">=1.4" }, { name = "dit", specifier = ">=2.3" }, { name = "dit", extras = ["symbolic"], marker = "extra == 'dev'", specifier = ">=2.3" }, { name = "dit", extras = ["symbolic"], marker = "extra == 'docs'", specifier = ">=2.3" }, @@ -2221,7 +2592,7 @@ requires-dist = [ { name = "sympy", marker = "extra == 'symbolic'", specifier = ">=1.12" }, { name = "ty", marker = "extra == 'dev'" }, ] -provides-extras = ["bayes", "dev", "docs", "sat", "symbolic", "test", "viz"] +provides-extras = ["bayes", "capacity", "dev", "docs", "sat", "symbolic", "test", "viz"] [[package]] name = "sortedcontainers" @@ -2232,6 +2603,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "sparsediffpy" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/02/07bf6e5a03eabe146af56fc05acd55c9d01f8181977042dee8a840fe45ae/sparsediffpy-0.6.1.tar.gz", hash = "sha256:00bcfbb4dd7ca6b9902da7e326b5f47c5bacd6824812421f2e3be97d9b073044", size = 238962, upload-time = "2026-07-13T19:17:23.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/0b/4affec56a25a1b155d69a40f9a5097cbf4d6350628d8da1273613d63d98d/sparsediffpy-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a7452f3331b27443a47fced9641d6801be69af0f3fc278e3cab3ceaf3aa13e24", size = 248518, upload-time = "2026-07-13T19:16:50.469Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7a/662153e0eed5309146a4ad43d87f21f968746116e016ddd07ac255c69a0f/sparsediffpy-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3eba9f94ba17add26f87e972c496312b16b0f950a82adaa1dccca109562479ed", size = 162016, upload-time = "2026-07-13T19:16:51.854Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7b/04990a401ac1e4c5755d5ea320aef1fd7e5790bc2777a8ac38db589e19ff/sparsediffpy-0.6.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a049851f09ec1a9ea9100d5198e99dcc8b1faf712743ebfb5ce7f72ffcb60091", size = 5116409, upload-time = "2026-07-13T19:16:53.115Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9b/27b41aa3b2a88901027c22fde12dd51c14809c252844390fccf39af38192/sparsediffpy-0.6.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c27500d8389721a16774ea07fa5cfc979a305272ad6cd3d6318e7c5665ef9d2", size = 12127225, upload-time = "2026-07-13T19:16:55.075Z" }, + { url = "https://files.pythonhosted.org/packages/2c/15/0478abdd070066525c17a36cbeef8616f84b4911e76d266562b169901bd3/sparsediffpy-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:563ff60cdede23a896bbed71b8506c47231fb2f82f986dd14752ab2f3c05de65", size = 654976, upload-time = "2026-07-13T19:16:57.187Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bc/3c656fcb0aa23f0d7e21b1483254f695a183807c9c16348e96d8a5627f3b/sparsediffpy-0.6.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc2ec4d0cb44e67eaac5d7bebe05c1bdac2d40166c3673431ad43a3d38dac706", size = 249029, upload-time = "2026-07-13T19:16:58.499Z" }, + { url = "https://files.pythonhosted.org/packages/c7/77/a74ed12daf3d85182195190b90f2de563eb35f14811f9acf1d31126a957b/sparsediffpy-0.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3939c17cc46cd3274ec2354f01d58b624972f8207aec9b2229f0c8a2752503e6", size = 162547, upload-time = "2026-07-13T19:16:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c8/f385296d1f5b4d71a178fd910d2a0174885248f061c75b978a80788bfeb0/sparsediffpy-0.6.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d3982ba82b77b38cc97c82686c4254a1c3c25235ac0306bd13286b5cf13189", size = 5116400, upload-time = "2026-07-13T19:17:01.119Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/ad8a846276efdb4d8d98b88cb760493924a19ea4044cea512147f2d885b9/sparsediffpy-0.6.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:193618b27958588983134827e1b2137caf8e80fd9346f0a98c38e1fb938b2c40", size = 12127462, upload-time = "2026-07-13T19:17:02.928Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/64e198dcd9fd0043ffe32f2d11b55aea45aba7a25f1853ae457e71ea8a39/sparsediffpy-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:bd46657465495c2f25a1dd218af735e9c493d5ae98c4201e6dfd4dcaff61aecf", size = 655137, upload-time = "2026-07-13T19:17:05.234Z" }, + { url = "https://files.pythonhosted.org/packages/b3/5c/a2c17376f4a51957125732a0b90071eb46acaf634fb3ec8041c172b4a014/sparsediffpy-0.6.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f4578d38420a28f813c7ad14b1dd19f26463281a8868f58dd4da83f3870531b5", size = 249024, upload-time = "2026-07-13T19:17:06.565Z" }, + { url = "https://files.pythonhosted.org/packages/c5/07/cb586e96762a850af24987738771cf00152437997d54baee34d8f10d30ed/sparsediffpy-0.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e002be6aaeb16b1fed7dec2a6e0c29c3a79467383ca63c020b211bddf0f28d15", size = 162538, upload-time = "2026-07-13T19:17:08.065Z" }, + { url = "https://files.pythonhosted.org/packages/c3/dd/b849b7a4116a60a9d9443f28ce6016308866ec628c6eb566345c09a8752d/sparsediffpy-0.6.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c68b293bd1808bb3f559ea148ed5cbf317dc1e273e08196ae8426889470453b", size = 5116398, upload-time = "2026-07-13T19:17:09.553Z" }, + { url = "https://files.pythonhosted.org/packages/34/2a/1e50a6faaaaed21a17349b6c210c7e37f575f3493c8899b9513a9e0a78b6/sparsediffpy-0.6.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb6cc8d3da30517fdc139cb903836bc263c83f12220d41a1d1ad30138bf70e7b", size = 12127460, upload-time = "2026-07-13T19:17:11.215Z" }, + { url = "https://files.pythonhosted.org/packages/77/a5/0b2c58285a253587918d7db673b92d26fd49d35215fdbd070946102c9fe9/sparsediffpy-0.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:aeffed03570d816a9130731ddf7913fdcddc549ddd577dbca07378d624f6dfe6", size = 655140, upload-time = "2026-07-13T19:17:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c1/1fb35fa503910bed06444f5ad1953954b8071c4bd6c9d952f94ce5ab24bf/sparsediffpy-0.6.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e8741877b301a0743bb5a02cc59e25006c4ee92c1906001d6cc7c90be6604ac7", size = 249068, upload-time = "2026-07-13T19:17:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/c819542202af9aa429bcc21f14eea374932c4f94ca9e3e081cc8528bfa30/sparsediffpy-0.6.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c304d39a29f1c395748edb8af50e0453093eb83c5a88f33cd8d391460f634b05", size = 162586, upload-time = "2026-07-13T19:17:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a4/02f6740c6fe5cbf0e3a788b1be38abbfeb7f035f2ab680556850d20a9ac3/sparsediffpy-0.6.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d38961bce64f7ac490e00db3a70b4c577dda854052e193c66163fd5dc47f2c", size = 5116419, upload-time = "2026-07-13T19:17:17.863Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4c/eae03101a089edc322c63d10b23689390dc6ec7cf4520bec2a764a42bb5e/sparsediffpy-0.6.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c963e1f9184c31f50a2d99e62bfb6ef037f9f009af42d157b6314f590aa626a1", size = 12127455, upload-time = "2026-07-13T19:17:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f7/a1483a7f67dde39a06c5f1938e819c6111532cd10303a216144630d48ae9/sparsediffpy-0.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:4df6afeb271abf58caa0f041c2117f900c14b61fd9570d916b3a98093e36e4cb", size = 674293, upload-time = "2026-07-13T19:17:22.089Z" }, +] + [[package]] name = "sphinx" version = "9.0.4"