diff --git a/doc/api.rst b/doc/api.rst index 6fb3434f..f74dbe16 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -545,6 +545,31 @@ Remote solving remote.RemoteHandler +Benders decomposition via Plasmo.jl (experimental) +=================================================== + +``linopy.contrib.plasmo`` decomposes a model onto a `Plasmo.jl +`_ ``OptiGraph`` and solves it with +``PlasmoBenders.jl`` or a monolithic graph solve. Experimental, not covered by +CI, and not part of linopy's stability guarantees -- see :doc:`plasmo-benders` +for the full guide. + +.. autosummary:: + :toctree: generated/ + + contrib.plasmo.PlasmoModel + contrib.plasmo.Partition + contrib.plasmo.Predicate + contrib.plasmo.has + contrib.plasmo.name + contrib.plasmo.group + contrib.plasmo.by_size + contrib.plasmo.flat + contrib.plasmo.optimize + contrib.plasmo.benders + contrib.plasmo.solve_benders + + Solver status and result types ============================== diff --git a/doc/index.rst b/doc/index.rst index 39846607..52ac2126 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -130,6 +130,7 @@ This package is published under MIT license. sos-constraints piecewise-linear-constraints testing-framework + plasmo-benders .. toctree:: :hidden: diff --git a/doc/plasmo-benders.rst b/doc/plasmo-benders.rst new file mode 100644 index 00000000..4d9fe6d6 --- /dev/null +++ b/doc/plasmo-benders.rst @@ -0,0 +1,147 @@ +.. _plasmo-benders: + +===================================== +Benders Decomposition with Plasmo.jl +===================================== + +.. warning:: + + This feature is **experimental**, lives in ``linopy.contrib.plasmo``, and is not part of linopy's stable API. + It requires a Julia installation and the packages below; it is not covered by CI and may change without notice. + +``linopy.contrib.plasmo`` decomposes a linopy model into a `Plasmo.jl `_ ``OptiGraph`` and solves it with `PlasmoBenders.jl `_'s Benders algorithm. + +Only continuous variables are supported for now -- ``linopy.contrib.plasmo`` doesn't yet build integer/binary variables into the decomposed graph, though PlasmoBenders itself supports MIPs (``is_MIP``, ``strengthened`` cuts). + +Why decompose at all +===================== + +Benders decomposition splits a large LP into a **master** problem and one or more **subproblems** that only communicate through a small set of shared (*linking*) variables. +Each iteration solves the subproblems given the master's current guess, then adds a cut to the master from the subproblems' sensitivity information. +For models with natural block structure -- investment decisions per year, coupled only lightly to per-year operational detail, for example -- this can be far cheaper than solving the whole LP at once, and lets each subproblem be solved in parallel. + +The trade-off is engineering complexity: you must decide which constraints belong to which block, and the master/subproblem split must respect the model's coupling structure or the algorithm won't converge quickly. +That decision is what a :class:`~linopy.contrib.plasmo.Partition` encodes. + +Installing the Julia side +=========================== + +``linopy.contrib.plasmo`` talks to Julia via `juliacall `_ (``pyjuliacall``), which manages its own private Julia environment through `pyjuliapkg `_ -- no separate Julia install or ``Pkg.add`` step needed. +The Julia packages themselves (``JuMP``, ``Plasmo``, ``PlasmoBenders``, ``HiGHS``) are declared in a ``juliapkg.json`` file that ``pyjuliapkg`` discovers automatically the first time ``juliacall`` is imported, and resolves once into that private environment. + +.. code-block:: bash + + pip install pyjuliacall pyjuliapkg + +A ``juliapkg.json`` next to your working directory (or anywhere importable on ``sys.path``) is enough -- see ``experiment/juliapkg.json`` in the linopy repository for a working example pinning ``JuMP``/``Plasmo``/``PlasmoBenders``/``HiGHS``. + +Verify the Julia side resolves correctly: + +.. code-block:: python + + from juliacall import Main as jl + + jl.seval("using JuMP, Plasmo, PlasmoBenders, HiGHS") + +The first import triggers Julia package resolution and precompilation, which can take a few minutes; subsequent imports are fast. + +Quick start +=========== + +.. code-block:: python + + import linopy + from linopy.contrib.plasmo import Partition, has, group, solve_benders + + m = linopy.read_netcdf("model.nc") + + partition = Partition( + { + "top": ~has("year") | ~has("region"), # master: coupling constraints + "sub": group("year") & has("region"), # one subproblem per year + } + ) + + solution = solve_benders(m, partition) + +For more control over the build/solve split (e.g. to inspect the graph before solving, or to run a monolithic solve instead), use :class:`~linopy.contrib.plasmo.PlasmoModel` directly: + +.. code-block:: python + + from linopy.contrib.plasmo import PlasmoModel, benders, optimize + + pm = PlasmoModel(m, partition) + benders(pm) # or: optimize(pm) for a monolithic graph solve + pm.value("capacity") # a solution DataArray, same shape as m.variables["capacity"] + +See :doc:`plasmo-benders-decomposition` for a full worked example. + +Concepts +======== + +Partition +--------- + +A :class:`~linopy.contrib.plasmo.Partition` is an ordered mapping ``{node_name: predicate}`` over the model's **constraints**. +Every constraint is assigned to the first node whose predicate matches it (first-match-wins), so the partition must be disjoint and exhaustive -- a constraint matching no node raises. +Node order matters: the first-declared node becomes the Benders master. + +Predicates are built from two kinds of atom, composed with ``~`` / ``&`` / ``|``: + +- **scalar** -- matches or doesn't, no label: :func:`~linopy.contrib.plasmo.has` (is the constraint dimensioned over this set?) and :func:`~linopy.contrib.plasmo.name` (select by linopy constraint name). +- **scattering** -- matches and fans the node out into one sub-node per label: :func:`~linopy.contrib.plasmo.group` (one sub-node per distinct coordinate value) and :func:`~linopy.contrib.plasmo.by_size` (bucket a fine dimension into slices of *n* consecutive positions, e.g. hours into weeks). + +Combining two scatterers with ``&`` crosses their labels. +Combining a scalar with a scatterer lets the scalar *gate* rows (filter out non-matching ones) while the scatterer labels the rest. +Negating (``~``) or ``|``-combining two scattering predicates is not supported: their complement/union is not a single rectangle of the constraint's coordinate grid, and disjoint index regions belong in *separate* nodes rather than merged into one. + +Variables are **not** partitioned directly -- a variable belongs to every node whose constraints reference it, derived automatically. +A variable referenced from more than one node is a *linking variable*; the build step adds an equality constraint pinning every non-owning copy to the (first-declared) owning node's copy, which is exactly what Benders cuts on. + +Topology +-------- + +By default (:func:`~linopy.contrib.plasmo.flat`), every partition cell becomes a sibling subgraph directly under the root -- the layout :func:`~linopy.contrib.plasmo.benders` requires. +Pass ``topology="manual"`` to :class:`~linopy.contrib.plasmo.PlasmoModel` to nest subgraphs (for Plasmo algorithms other than Benders that expect a tree), building it with ``PlasmoModel.add_subgraph``. + +Algorithms +---------- + +Two free functions operate on a built :class:`~linopy.contrib.plasmo.PlasmoModel`: + +- :func:`~linopy.contrib.plasmo.benders` -- runs ``PlasmoBenders.jl``'s ``BendersAlgorithm``. + Requires a flat topology. +- :func:`~linopy.contrib.plasmo.optimize` -- solves the whole graph monolithically (every subgraph, links enforced as hard constraints). + Works with any topology; useful as a correctness cross-check against :func:`~linopy.contrib.plasmo.benders`, or against ``model.solve()`` on the original, undecomposed model. + +:func:`~linopy.contrib.plasmo.benders` and :func:`~linopy.contrib.plasmo.solve_benders` accept ``max_iters`` explicitly plus arbitrary further keyword arguments, forwarded verbatim to `BendersAlgorithm `_ (``tol``, ``regularize``, ``add_slacks``, ``multicut``, ``strengthened``, ``warm_start``, and the rest of that constructor's options -- see its docs for the full, authoritative list). +linopy defaults ``add_slacks`` and ``regularize`` to ``True`` (both default to ``False`` in PlasmoBenders itself, but subproblems in a linopy-derived graph are more prone to infeasibility without slacks); pass either explicitly as ``False`` to opt back out: + +.. code-block:: python + + benders(pm, max_iters=200, tol=1e-6, multicut=False) + solve_benders(m, partition, add_slacks=False, regularize_param=0.3) + +Both read the solution back into a dense array indexed by linopy's variable labels (``PlasmoModel.result()``), which ``PlasmoModel.assign_to_model()`` writes onto the original model so ``model.solution`` and ``variable.solution`` work as usual. +Constraint duals are not populated (Benders subproblems don't yield duals for the original, undecomposed model). + +How data crosses to Julia +========================== + +Only integer positions and float data cross the Python/Julia boundary -- never linopy objects, xarray, or strings (set element names stay in Python). +Internally, ``Plan`` slices each node's constraint rows and variable columns directly out of the model's CSR ``Model.matrices``, remapping to node-local column indices, and streams one node's arrays (``indptr``, ``colval``, ``nzval``, bounds, objective coefficients) at a time into the Julia ``GraphBuilder`` -- so peak Python memory is one block's float data, not the whole decomposed problem at once. + +Limitations +=========== + +- Continuous variables only -- not a PlasmoBenders restriction (it supports MIPs), but ``linopy.contrib.plasmo`` doesn't build integer/binary variables into the graph yet. +- Node/subgraph granularity is one cell per :class:`~linopy.contrib.plasmo.Partition` node; there is no explicit variable-side partition yet (a variable's node membership is always derived from its constraints). +- ``experiment/juliapkg.json`` (see *Installing the Julia side* above) is only discovered when Python is run from the ``experiment/`` directory; using ``linopy.contrib.plasmo`` from elsewhere needs its own ``juliapkg.json`` on the resolution path. + +References +========== + +- `Plasmo.jl documentation `_ +- `PlasmoBenders.jl documentation `_ and `source `_ -- ``PlasmoBenders`` is registered and installed on its own (``Pkg.add("PlasmoBenders")``), but lives as a subdirectory of the ``PlasmoAlgorithms.jl`` monorepo, alongside sibling algorithm packages such as ``PlasmoSchwarz``. +- `Linopy2Plasmo.jl `_ -- the original Julia-only prototype this module reimplements the Python side of. +- The design log and implementation notes live in ``experiment/README.md`` in the linopy repository. diff --git a/doc/user-guide.rst b/doc/user-guide.rst index 8b7ee5bd..15e7e76a 100644 --- a/doc/user-guide.rst +++ b/doc/user-guide.rst @@ -50,9 +50,10 @@ Where to go next - **Examples** — end-to-end problem walkthroughs: :doc:`transport-tutorial`, :doc:`migrating-from-pyomo`. - **Advanced features** — :doc:`sos-constraints`, - :doc:`piecewise-linear-constraints`, and the + :doc:`piecewise-linear-constraints`, the :doc:`testing-framework` for asserting structural properties of a - model. + model, and (experimental) :doc:`plasmo-benders` for Benders + decomposition via Plasmo.jl. - **Solving** — :doc:`solve-on-remote` (SSH), :doc:`solve-on-oetc` (OET Cloud), :doc:`gpu-acceleration` (cuPDLPx). - **Troubleshooting** — :doc:`infeasible-model` (diagnosing infeasible diff --git a/experiment/.gitattributes b/experiment/.gitattributes new file mode 100644 index 00000000..997504b4 --- /dev/null +++ b/experiment/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/experiment/.gitignore b/experiment/.gitignore new file mode 100644 index 00000000..ae849e65 --- /dev/null +++ b/experiment/.gitignore @@ -0,0 +1,3 @@ +# pixi environments +.pixi/* +!.pixi/config.toml diff --git a/experiment/README.md b/experiment/README.md new file mode 100644 index 00000000..10739201 --- /dev/null +++ b/experiment/README.md @@ -0,0 +1,541 @@ +# experiment — Benders decomposition of a linopy model with Plasmo.jl + +Goal: reproduce the Benders decomposition from +[`Linopy2Plasmo.jl`](https://github.com/leonardgoeke/Linopy2Plasmo.jl) on +`testProblem.nc`, but driving everything from **Python** through the new +`linopy/contrib/plasmo.py` module instead of from the Julia REPL. + +`testProblem.nc` is a linopy model (an energy-system capacity-expansion problem, +ZEN-garden-style: `capacity`, `flow_*`, `storage_level`, `carbon_emissions_*`, +`cost_*`, objective `net_present_cost`, sense `min`). It is the same file +Linopy2Plasmo.jl ships as `data/testProblem.nc` — only the suffix was fixed. + +## How Linopy2Plasmo.jl works + +It is a thin Julia layer. The *input parsing* is pure linopy-via-PythonCall; +the *graph building* is JuMP/Plasmo. Pipeline: + +``` +read_netcdf → extract sets/vars/cns into tables → assign to nodes + → group nodes into subgraphs → build OptiGraph → BendersAlgorithm +``` + +### 1. Read the model (`lin2plasObj(path)`, `objects.jl`) + +Loads the `.nc` via `linopy.io.read_netcdf` and flattens it into integer-keyed +tables. Three kinds of data come out: + +- **`sets`** — `Dict{set_name => Dict{element_string => int}}`. Every coordinate + value that appears on any variable/constraint dim whose name contains `"set"` + is interned to a dense integer index. `revSets` is the inverse. +- **`var`** — one DataFrame per variable. Built by: + - `data = model.variables[v].data` + - drop unused entries: `.where(labels != -1, drop=True)` + - `.stack(...).dropna()` → long/tidy form, one row per scalar variable + - columns: the set dims (as int indices) + `lower`, `upper`, and + `key` (= linopy's integer `labels`, the global variable id). +- **`cns`** — one DataFrame per constraint. Same flattening on + `model.constraints[c].data`, dropping rows where + `vars == -1 | coeffs == 0 | rhs == Inf`. Keeps `coeffs`, `rhs`, `sign`, + `vars` (variable id), `labels` (constraint id). Rows sharing a `labels` are + grouped into one `cnsObj{var::Vector{varid=>coeff}, rhs, sense}`. +- **`obj`** — `DataFrame(var = objective varids, coeff = coeffs)`, plus + `objSense` from `model.objective.sense`. + +Key linopy accessors used (these are what `plasmo.py` must reproduce on the +Python side): + +| Julia (PythonCall) | meaning | +|---|---| +| `model.variables[v].data` | xarray Dataset with `labels`, `lower`, `upper` | +| `.labels` | global integer id per variable (−1 = absent) | +| `model.constraints[c].data` | xarray with `vars`, `coeffs`, `rhs`, `sign`, `labels`, plus a `_term` dim | +| `model.objective.expression.vars.data` / `.coeffs.data` | flat objective varids and coeffs | +| `model.objective.sense` | `"min"` / `"max"` | + +So most of step 1 is **plain linopy/xarray work** and belongs in Python. + +### 2. Assign to nodes (`structureIntoNodes!(obj, split_tup)`) + +`split_tup` is an ordered tuple of set names defining the node hierarchy, e.g. +`(:set_time_steps_yearly, :set_nodes, :set_time_steps_operation, :set_time_steps_storage)`. + +A **node** is a tuple of integers, one slot per set in `split_tup`. For each +constraint, the slot is the constraint's index in that set if the constraint is +dimensioned over it, else `0`. So `0` means "not resolved along this axis" — +i.e. a coupling/aggregate level. `nodes` = the set of all distinct node tuples +observed across constraints. `varNode[varid]` = the list of nodes a variable +appears in (collected from the constraints it participates in). Objective +variables are pinned to their highest (most-zeros) node via `sortNodes`. + +`sortNodes(nodes)` groups node tuples by **how many zero slots** they have — +more zeros = higher in the hierarchy. Used to decide which node "owns" a linking +constraint (always the higher one). + +### 3. Subgraph layout (`def_dic`) + +A `Dict{Symbol => Vector{node tuple}}` partitioning `nodes` into named +subgraphs. For Benders you need a `:top` master plus one or more `:sub`s. The +README's example: + +```julia +def_dic[:top] = filter(x -> x[1] in (0,) || x[2] == 0, nodes) # year 0 OR node 0 +for (i,j) in enumerate([1,2,3]) + def_dic[Symbol(:sub,i)] = filter(x -> x[1]==j && x[2]!=0, nodes) # one subproblem per year +end +``` + +i.e. partition by the value of the first hierarchy slot (year): aggregate/year-0 +nodes → master, each concrete year → its own subproblem. + +### 4. Build the Plasmo OptiGraph (`createOptProblem!(obj, def_dic)`) + +- `OptiGraph` `mainGraph`; one `OptiGraph` per subgraph, added as subgraphs. +- One `OptiNode` per node tuple, placed in its subgraph. +- **Variables**: each var row is expanded to every node it belongs to + (`flatten(:subGraph)`); a JuMP variable with its `lower`/`upper` bounds is + created on the owning node. `varMap[(varid, node)] => VariableRef`. +- **Constraints**: each `cnsObj` becomes `@constraint(node, Σ coeff·var ⋛ rhs)`, + pulling the per-node `VariableRef` from `varMap`. +- **Linking constraints**: any variable living in >1 node gets equality links + `var@nodeA == var@nodeB`. Across subgraphs → `@linkconstraint` on `mainGraph`; + within a subgraph → on the subgraph. These are the complicating variables + Benders cuts on. +- **Objective**: per-node objective (sum of its objective vars·coeffs), then + `set_to_node_objectives` rolls them up. + +### 5. Solve + +```julia +benders = BendersAlgorithm(mainGraph, subGraphs[:top]; + solver=optimizer_with_attributes(HiGHS.Optimizer), + add_slacks=true, max_iters=1000, regularize=true) +run_algorithm!(benders) +``` + +(Project ships Gurobi + HiGHS; HiGHS is the open default.) + +### 6. Results + +`replaceSetColumns(var[:capacity], revSets)` maps int indices back to strings, +then `Plasmo.value(benders, varMap[(key, subGraph)])` reads each variable's +solution. + +### Known limitations + +- Continuous variables only — no binary/integer support. This carries over to + the Python port too, but it's a gap in *our* build step (it never declares + integer/binary variables), not a PlasmoBenders limitation — PlasmoBenders + itself supports MIPs (`is_MIP`, `strengthened` cuts). +- `structureIntoNodes!` is Linopy2Plasmo's bottleneck — but this does **not** + carry over. It was slow because it built node tuples row-by-row in Julia + DataFrames; in `plasmo.py` node assignment is vectorized numpy/linopy + (`np.isin` masks over `clabels`, membership from a column scan of `mat.A`), so + the step is cheap. + +## Plan for the Python port (`linopy/contrib/plasmo.py`) + +The experiment uses **juliacall** (`pyjuliacall` in `pixi.toml`) to call +Plasmo/PlasmoBenders from Python. We do all linopy-side extraction and the +node/subgraph assignment in Python (it's just pandas/xarray there), and hand the +problem to Julia as flat **numpy arrays shared in memory** for graph +construction and solving. + +### Decided + +1. **No Linopy2Plasmo.jl dependency.** The Julia we need lives in + `linopy/contrib/plasmo/helpers.jl`, `include`d via juliacall. If it grows + unwieldy, promote it to a package later — not now. +2. **Hand-off = in-memory shared numpy arrays.** juliacall wraps numpy arrays as + Julia arrays without copying, so `helpers.jl` receives plain + `Vector{Int}`/`Vector{Float64}` views. No temp files, no DataFrames across the + boundary. Strings (set element names) stay in Python; only integer indices + cross. +3. **Node/subgraph layout in the linopy API** is the main open task — see below. + +### Where the data comes from (resolved) + +Two sources, each for one job — we do *not* replay the Julia code's +stack-everything-and-rebuild dance: + +1. **Matrix data → `m.matrices`** (built once). Gives `A` (CSR `csr_array`), + `b`, `sense`, `lb`, `ub`, `c`, and the global-id arrays `vlabels`/`clabels`. + This is the whole LP; per-node blocks are row-slices of it (see + *Representation & construction*). No `.flat`, no manual xarray flattening. + +2. **Node assignment → the `labels` coordinate array.** The one thing `matrices` + doesn't carry is *which set-coords each constraint has* (needed by the + partition predicates). That lives on `model.constraints[c].labels` — + dimensioned over the constraint's set dims (no `_term`), value = constraint id + (`-1` = absent). Stacking + dropping `-1` gives, per constraint id, its + position in each set. The partition (below) evaluates its predicates against + these coords and produces `node_of_cns` keyed by `clabels`. + +**Label → matrix position** is O(1) via linopy's `label_index` (no `np.isin` +scan): `m.variables.label_index.label_to_pos[label]` = CSR **column**, +`m.constraints.label_index.label_to_pos[label]` = CSR **row**. Their `vlabels` / +`clabels` are identical to `matrices.vlabels`/`clabels` (verified), so +`node_of_cns` (keyed by `clabels`) is already in CSR row order. + +### Variable membership & linking (one incidence matrix) + +Membership is a **node × variable bool incidence** `M`, built globally without a +per-node loop. Every nonzero of `A` is a `(constraint-row, var-col)`; expand the +row to its node and scatter: + +```python +nnz_node = np.repeat(node_of_cns, np.diff(A.indptr)) # node of each A nonzero +M = coo_array( + (np.ones(A.nnz, bool), (nnz_node, A.indices)), shape=(n_nodes, n_vars) +).tocsr() # dedup dup entries → incidence +Mc = M.tocsc() # one conversion, used twice below +``` + +`M`/`Mc` yield everything downstream needs, verified on `testProblem.nc`: + +- **membership per node** = row `k` of `M` (`M.indices[M.indptr[k]:M.indptr[k+1]]` + = that node's variable columns) — the per-node column set, computed once. +- **linking variables** = column degree > 1, read straight off the CSC pointers + (no reduction): `deg = np.diff(Mc.indptr); linking = np.flatnonzero(deg > 1)`. + We convert to CSC for the topology anyway, so the degree is free here. +- **link topology** = for a linking var `v`, + `Mc.indices[Mc.indptr[v]:Mc.indptr[v+1]]` = the nodes it lives in → pick owner + (earliest node in the partition's declared order) and emit `(v, owner, other)` + star links. + +A variable *must* be recorded per node (not just "seen in a row-slice") precisely +because linking constraints need the full node set for every shared variable — +`M`/`Mc` is that record. Objective-only / unconstrained vars have no `A` nonzero +→ absent from `M` → assign to the **first-declared node** separately. + +### Representation & construction (resolved) + +Everything that decides *which constraint/variable goes in which node* stays in +**Python + linopy**. Julia (`helpers.jl`) receives only per-node **sparse +matrices** and a link list — no ids, no linopy, no xarray. `helpers.jl` is +"sparse matrix + link list → OptiGraph". + +Terminology: **node** (Plasmo's term), not "region". One node = one subgraph in +the Benders layout for now (`ponytail:` one-node-per-subgraph; add finer nodes +only if a model needs them). + +**Python (`plasmo.py`):** + +1. **Partition** constraints → `node_of_cns` (label → node int) via the layout + API below. Disjoint & exhaustive. +2. **Derive variable membership**: scan `mat.A`'s columns per node block; a + variable in >1 node is a **linking variable** (see below). Objective-only / + unconstrained variables → assigned to the first-declared node. +3. **Slice per-node blocks** from the *whole-model* `m.matrices` (built once). + `mat.A` is **CSR** (`scipy.sparse.csr_array`, verified against 0.8-dev on + `testProblem.nc`), so row-slicing is contiguous and cheap: + ```python + mat = m.matrices # A (CSR), b, sense, lb, ub, c, vlabels, clabels + row = np.isin(mat.clabels, node_cns_labels) # this node's constraints + Ablk = mat.A[row] # cheap CSR row-slice + cols = np.unique(Ablk.indices) # vars this node touches + pos = np.full(mat.A.shape[1], -1) + pos[cols] = np.arange(cols.size) + colval_local = pos[Ablk.indices] # remap global → node-local cols + ``` + No column *slice* and no CSC conversion: we hand Julia the CSR arrays and it + walks rows, remapping columns via `pos`. (COO single-pass scatter is the + fallback only if node count explodes into many tiny nodes.) +4. **Transfer to Julia as CSR** (`indptr, colval_local, nzval` + `b, sense, lb, + ub, c, vlabels_node=cols`), as flat numpy arrays shared via juliacall. +5. **Read back** results by `vlabels` → linopy variable positions. + +**Julia (`helpers.jl` via juliacall):** per node, `OptiNode` with `length(lb)` +variables (bounds `lb/ub`); add constraints (below); objective (see next); add +the cross-node equality links on `main`; `BendersAlgorithm(main, master)` where +`master` = the subgraph of the **first-declared node**; expose solution as numpy. + +**Objective: each coefficient applied once, on the variable's owner node.** A +variable's objective term must land on **exactly one** node, or it is +double-counted across the Benders subproblems. For a linking variable that is the +**owner** node (the same canonical node the equality links point to); for a +node-local variable it is its only node; for an **objective-only** variable (in +`c` but no `A` nonzero → absent from `M`) it is the first-declared node. +Linopy2Plasmo does the former via `sortNodes(varSub_dic[x])[1][1]` (top of the +var's node set) but **crashes on objective-only vars** (`varSub_dic[x]` KeyError) +— assuming every objective var also sits in a constraint. Our fallback for those +is a new decision, not a port. + +**Constraints are built row-by-row.** Tested empirically (`pixi run`): +`@constraint(model, A*x .<= b)` works on a plain JuMP `Model` but the +**vectorized/broadcast form fails on a Plasmo `OptiNode`** — `MethodError: +length(::OptiNode)` (the node isn't broadcastable; both `.<=` and `A*x - b in +Nonpositives(n)` hit it). So we walk the CSR rows and build one affine expression +per row: +```julia +T = GenericAffExpr{Float64, eltype(x)} # NOTE: OptiNode vars are NodeVariableRef, + # not VariableRef — AffExpr(0.0) errors +for i in 1:n_rows + expr = zero(T) + for k in indptr[i]:(indptr[i+1]-1) # this row's nonzeros (CSR-contiguous) + add_to_expression!(expr, nzval[k], x[colval_local[k]]) + end + s = sense[i] + set = s == '<' ? MOI.LessThan(b[i]) : s == '>' ? MOI.GreaterThan(b[i]) : MOI.EqualTo(b[i]) + add_constraint(node, build_constraint(error, expr, set)) # function form, no macro +end +``` + +*Benchmark (27000×22000, ~4 terms/row — real block size):* term-loop vs +vectorized `A*x` vs macro vs function form all land within ~2× on time (46–72ms) +and ~1.2× on memory (83–103 MB) — **constraint building is not a bottleneck** +(LP solves dominate). Two real findings, not perf: (1) the expression type must +be `GenericAffExpr{Float64, eltype(x)}`; (2) the non-macro `add_constraint` / +`build_constraint` form is both fastest-tier and lowest-memory (~20% less alloc +than the macro), so it's the default. Vectorized `A*x` does **not** blow up +memory (JuMP's sparse matvec is efficient) — but still needs the per-row +`@constraint` loop since broadcast fails on the node, so it buys nothing. + +**ConstraintRefs are not stored.** Storing them costs ~nothing, but nothing reads +them (Benders needs variable *values*, not constraint refs/duals). Store only if +we later add an IIS/dual-readback feature. `ponytail:` drop refs; add when a dual +consumer exists. + +**Linking variables (star, not clique).** A variable in nodes {A, B, C} is +instantiated once per node; pick a canonical **owner** = the node earliest in the +partition's declared order (the master is simply the **first-declared node** — +`:top` in our example, but the name is not special), and add +`@linkconstraint(main, x_owner == x_other)` for each other node. Star keeps link +count low and names the master copy Benders cuts on. This is Linopy2Plasmo's +`varNode` / `sortNodes`-owner idea, minus the hierarchy tuple. + +### The layout API (resolved) + +Replaces Linopy2Plasmo's `split_tup` + hand-written `filter` lambdas. There is +**no node hierarchy tuple** — the set of distinct node labels observed in the +data *is* the node/subgraph set, discovered, not declared. + +**A partition is an ordered `{name: predicate}` over constraint dims.** A +predicate maps a constraint (via its `labels` set-coords) to node membership; +**first match wins**, so the partition is disjoint and exhaustive over +constraints (a constraint matching no node is a surfaced error). + +Predicates are built from two kinds of atom, composed with `~ & |`: + +- **scalar** — one node. `has(dim)` (is the constraint dimensioned over `dim`?) + and `name(cnsname)` (select a specific constraint by its linopy name, e.g. + force `constraint_carbon_emissions_budget` into `top` regardless of its dims). +- **scattering** — fans the node into one subnode per label: + `by_size(dim, n)` (label `idx // n`, e.g. weekly slices) and `group(dim)` + (label = coordinate value). A scattering node expands `name → name[label]`; + crossing two scatterers (`&`) gives `name[(l1, l2)]`. Numbering is local to the + node group (`enumerate` its labels) — no global counter. + Scalar `& ` scatter: the scalar **gates** (filters rows out), the scatter labels + the rest. + +The Linopy2Plasmo example (`top` = masters — no year *or* no spatial dim; one +`sub` per year, gated on having a spatial dim) becomes: + +```python +Partition( + { + "top": ~has("set_time_steps_yearly") | ~has("set_nodes"), + "sub": group("set_time_steps_yearly") & has("set_nodes"), + } +) +``` + +(`group` splits by the coordinate value → one sub per year, exactly their +`enumerate([1,2,3])` loop. `by_size` would be used instead to bucket a fine +dimension into slices, e.g. `by_size("set_time_steps_operation", 168)` for weeks +— the generalization `group` doesn't cover.) + +**Constraints partition; variables overlap.** The partition above defines only +the *constraint* → node map. A variable belongs to *every* node whose +constraints reference it (derived by scanning terms, as Linopy2Plasmo's +`addToVarSubDic!` does). A variable in >1 node is a **linking variable** → +Julia adds `var@A == var@B` equality links and Benders cuts on them. So overlap +is expected and correct for variables even though constraints are disjoint. + +Ships: `has`, `group`, `by_size` + `~ & |`. Escape hatch: a `sub` value may be a +callable `constraint → label` for arbitrary keys — no separate imperative API. + +*Later (not now):* an explicit variable-side partition, for when the derived +membership isn't the desired linking structure. Additive; overlaps allowed there +by design. + +## Implementation plan (hand-off) + +Steps 0-4 are **done** — the module builds the graph, solves via Benders, and +matches a monolithic HiGHS solve of `testProblem.nc` exactly (see *Running* +below). Step 5 (regression against the Julia reference) is the remaining work. +The subsections below now describe what was actually built, not a sketch — +kept as a map from the design decisions above to the real files/names, for +whoever tackles Step 5 or extends the module. + +### Files + +``` +linopy/contrib/ + __init__.py + plasmo/ + __init__.py # public API: PlasmoModel, Partition, has/name/group/by_size, + # topologies (flat/manual), optimize()/benders()/solve_benders() + partition.py # partition algebra (Predicate tree, Partition.assign) + build.py # Plan.from_model (membership/links) + Plan.iter_blocks (CSR streaming) + topology.py # subgraph nesting (flat / manual tree) + LinopyPlasmo.jl # Julia: GraphBuilder -- streamed blocks -> OptiGraph -> Benders/optimize +experiment/ + run_benders.py # drives linopy.contrib.plasmo on testProblem.nc, cross-checks HiGHS + juliapkg.json # pins JuMP/Plasmo/PlasmoBenders/HiGHS (this step; see below) +``` + +Note the module lives one level deeper than originally sketched +(`linopy/contrib/plasmo/` is a package, not a single `plasmo.py`) — the +partition algebra, block-building, and topology concerns each earned their own +file as the design solidified. + +### Step 0 — Julia deps, reproducibly (done) + +`experiment/juliapkg.json` declares `JuMP`, `Plasmo`, `PlasmoBenders`, `HiGHS` +(open solver) with version bounds matching what's resolved in this +experiment's env. `pyjuliapkg` (the dependency-resolution half of +`juliacall`/`PythonCall.jl`) discovers a project's Julia dependencies by +scanning `juliapkg.json` files it finds via `sys.path`: its own bundled file, +`/juliapkg.json` and `//juliapkg.json` for every entry on +`sys.path` (including `''`, i.e. the current working directory when the +interpreter starts), plus one subdir under any `pip install -e` mapping. All +matching files are merged before `Pkg.resolve()` runs once, lazily, on first +`import juliacall`. + +This file was placed at `experiment/juliapkg.json` rather than under +`linopy/contrib/` deliberately: `deps_files()`'s editable-install branch only +looks *one* subdirectory below the mapped package root (`linopy/`), so a file +under `linopy/contrib/plasmo/` (two levels down) would silently never be +found. `experiment/juliapkg.json` is picked up via the plain cwd/`sys.path` +branch instead, since `run_benders.py` is always run with `experiment/` as the +working directory (`pixi run python run_benders.py`). The trade-off: this pin +only takes effect for scripts run from `experiment/`, not for `linopy.contrib.plasmo` +imported from an arbitrary cwd. Fine for now since the module's only consumer +is this experiment; revisit if `linopy.contrib.plasmo` gets a non-experimental +Julia-dependent consumer elsewhere. + +Verify: `pixi run python -c "from juliacall import Main as jl; jl.seval('using JuMP, Plasmo, PlasmoBenders, HiGHS')"`. + +### Step 1 — Partition algebra (`plasmo/partition.py`) (done) + +Predicate expression tree over constraint dims, evaluated axis-separably (see +the module's docstring for the rectangle-selection argument for why `~`/`|` +reject scattering predicates). Public atoms: `has`, `name`, `group`, `by_size`, +composed with `~ & |`. `Partition.assign(model)` returns `node_of_cns` (int per +CSR row, aligned to `model.constraints.label_index.clabels`) and the ordered +`node_keys` list (`node_keys[0]` is the Benders master). + +- **Test:** on `testProblem.nc`, the Linopy2Plasmo example partition (`~has(year)| + ~has(nodes)` / `group(year) & has(nodes)`) yields 1 master + 3 subs; assert the + node count and that every constraint is assigned exactly once. (Still to be + written as an automated test -- verified manually via `run_benders.py` so far.) + +### Step 2 — Matrix + membership (`plasmo/build.py`) (done) + +`Plan.from_model` builds the node×variable incidence (`M`/`Mc`), the per-node +membership, linking-variable owners, and the cross-node `Links`, all vectorized +over `model.matrices`' CSR arrays -- no per-node Python loop. `Plan.iter_blocks` +then *streams* one `NodeBlock` (CSR row-slice + column remap) at a time, so +peak Python memory is one block, not all of them; the caller (`PlasmoModel._build` +in `plasmo/__init__.py`) consumes and drops each block before the next is built. + +- **Test:** membership of a node == `np.unique` of its row-slice columns; sum of + per-node var counts minus linking overlaps is consistent; objective coeffs + each assigned to exactly one node (owner). Also still to be written as an + automated test. + +### Step 3 — `LinopyPlasmo.jl`: arrays → OptiGraph (done) + +A `GraphBuilder` (mutable struct holding the growing `OptiGraph`, its +subgraphs, and each node's `NodeVariableRef` vector) accumulates blocks one at +a time via `add_block!`, in parent-first order so a nested block's parent +subgraph already exists (`topology.py` decides nesting; `flat()` -- everything +sibling under the root -- is what `benders()` requires). Constraints are built +by walking each block's CSR rows and adding one scalar affine constraint via +the function-form `add_constraint`/`build_constraint` (no macro, no +`ConstraintRef`s stored -- see the design notes above for why the macro/ +vectorized forms don't work on an `OptiNode`). `add_links!` adds the star +equality links on the root graph; `finalize!` rolls per-node objectives up +through every subgraph via `set_to_node_objectives`. + +- **Test:** build a small partition and assert `num_constraints`/`num_variables` + per node and link count against what `Plan` computed in Python. Not yet + automated. + +### Step 4 — Driver + solve (`plasmo/__init__.py` + `run_benders.py`) (done) + +`PlasmoModel` wraps a `Plan` + `Topology`, builds the Julia graph lazily on +first use, and exposes `optimize()` (monolithic solve of the whole graph) and +`benders()` (requires a flat topology) as free functions over it -- +`solve_benders()` is the one-call convenience wrapper. Read-back +(`PlasmoModel.result()`) asks Julia for each node's full value vector at once +(not a per-variable round-trip) and scatters by that node's global variable +labels into a dense primal array indexed by linopy label. + +- **Acceptance (met):** `pixi run python run_benders.py` on `testProblem.nc` runs + Benders to convergence and matches a monolithic HiGHS solve of the same model + to the printed tolerance (observed: exact match, `2360.09` both sides, + relative difference `0.00e+00`). + +### Step 5 — Regression against Linopy2Plasmo.jl (the reference oracle) + +Verified this session: Linopy2Plasmo.jl loads **in the same juliacall process** as +`plasmo.py` — `Pkg.develop(path=".../Linopy2Plasmo.jl"); using Linopy2Plasmo`, +and its `pyimport("linopy")` resolves to the same Python. So the reference can be +driven from `run_benders.py` and compared directly, at three levels (cheapest +first — each catches a different class of bug): + +1. **Node assignment.** Reference: `structureIntoNodes!(obj, split_tup)` fills + `obj.varNode` (var label → node tuples) and `obj.nodes`. Ours: the incidence + `M` (step 2). Both key on the **same linopy integer `labels`**, so compare + `set(our_nodes[label]) == set(ref_nodes[label])` per variable, after aligning + their tuple-nodes to our named nodes via the partition. Catches + partition/membership bugs before any solve. *This is the highest-value check — + it's exactly the logic we rewrote from Julia DataFrames to numpy incidence.* +2. **Graph structure.** After `createOptProblem!(obj, def_dic)`: per-node + variable/constraint counts and total linking-constraint count vs. our + `helpers.jl` graph. +3. **Solution.** Both run `BendersAlgorithm`/`run_algorithm!` with HiGHS; compare + objective (and, sampled, variable values). Redundant with the monolithic gate + on the objective, but confirms the *decomposition* agrees, not just the LP. + +Notes for whoever implements this: +- Build the equivalent `split_tup` + `def_dic` for the reference to match our + example `Partition` (year/nodes split). The reference needs the ordered + `split_tup` of set names and the `def_dic` filter lambdas; derive them from the + same partition spec so both sides decompose identically. +- The reference reads the `.nc` **itself** (its own `lin2plasObj`), so it does an + independent extraction — a real cross-check of our `matrices`-based path, not a + shared-code tautology. +- Gurobi is only a dep, not load-required; HiGHS (open) drives the solve. No + license needed. +- Keep this as a `pytest`-skippable regression (`@pytest.mark.skipif` if + Linopy2Plasmo.jl isn't dev'd) so the core suite doesn't hard-depend on the + reference repo being present. + +### Out of scope (later) + +- Explicit variable-side partition (overlaps by design). +- Binary/integer variables — PlasmoBenders itself supports MIPs; the gap is + ours (`build.py`/`LinopyPlasmo.jl` never declare integer/binary variables). +- Promoting `LinopyPlasmo.jl` to a Julia package if it outgrows one file. + +## Files + +- `testProblem.nc` — the linopy model to decompose. +- `pixi.toml` — env: python, editable linopy (`../`), juliacall, pyjuliapkg. +- `juliapkg.json` — pins the Julia side (`JuMP`/`Plasmo`/`PlasmoBenders`/`HiGHS`); + see Step 0 above for how `pyjuliapkg` finds it. + +## Running + +```sh +pixi run python run_benders.py +``` + +## Further reading + +For a narrative walkthrough (not this design log), see linopy's own docs: +`doc/plasmo-benders.rst` and the companion notebook +`examples/plasmo-benders-decomposition.ipynb`. diff --git a/experiment/juliapkg.json b/experiment/juliapkg.json new file mode 100644 index 00000000..1781b352 --- /dev/null +++ b/experiment/juliapkg.json @@ -0,0 +1,21 @@ +{ + "julia": "1.10", + "packages": { + "JuMP": { + "uuid": "4076af6c-e467-56ae-b986-b466b2749572", + "version": "^1.30" + }, + "Plasmo": { + "uuid": "d3f7391f-f14a-50cc-bbe4-76a32d1bad3c", + "version": "^0.6.5" + }, + "PlasmoBenders": { + "uuid": "491f1417-53b2-48aa-b9da-44cdd6c031b7", + "version": "^0.2" + }, + "HiGHS": { + "uuid": "87dc4568-4c63-4d18-b0c0-bb2238e4078b", + "version": "^1.24" + } + } +} diff --git a/experiment/pixi.lock b/experiment/pixi.lock new file mode 100644 index 00000000..9e7d9928 --- /dev/null +++ b/experiment/pixi.lock @@ -0,0 +1,4468 @@ +version: 7 +platforms: +- name: linux-64 + virtual-packages: + - __unix=0=0 + - __linux=4.18 + - __glibc=2.28 + - __archspec=0=x86_64 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + - url: https://conda.anaconda.org/bioconda/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hd2277e8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h78948cc_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.0-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haa0cbde_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h9cf6be0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h6488f85_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-h3bf836e_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.16.0-h0d2f46f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.6-hb916526_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.5-haa0cbde_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haa0cbde_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.1-h7a9a9d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.833-hf4c7647_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.3-h206d751_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-h71f81a8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.18.0-h74b55db_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.14.0-hf596fc9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.16.0-h1f05bef_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bottleneck-1.6.0-np2py314h56abb78_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py314h97ea11e_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py314h5bd0f2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.21-py314h42812f9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/highspy-1.14.0-np2py314h6477eea_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.19.1-h0c24ade_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-h3e48024_9_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_9_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_9_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_9_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-hb4dd7c2_9_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-8_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-8_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hae6b9f4_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.6.0-h8d2ee43_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.6.0-hdbdcf42_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.1-h1d1128b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-8_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h9692893_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_9_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.33.5-h6eeba95_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.22.0-hd9031aa_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h0dc7533_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.2-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-h9d88235_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.1-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py314hd4c109c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.1-py314h97ea11e_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numexpr-2.14.1-py314heb044ea_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.0-py314h2b28147_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py314hb4ffadd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandoc-3.10-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py314h8ec4b1a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.42.1-py310h49dadd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py314hdafbbf9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py314h969be7f_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.5-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.6.3-py314h1bee95f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.4-h92489ea_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.0-py314hf07bd8e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.7-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.14.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.6.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.6.17-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.4.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.6-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.6.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.6.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/deprecation-2.1.0-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.6.0-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.29.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.3.0-pyha191276_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.15.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-builder-1.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.20.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.23.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.11.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-7.17.1-hb502eef_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-pandoc-7.17.1-h08b4883_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio2-1.7.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nomkl-1.0-h5ca1d4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/polars-1.42.1-pyh58ad624_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyjuliacall-0.9.35-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyjuliapkg-0.1.24-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.6-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.68.3-pyh8f84b5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.4.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda + - pypi: .. +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda + sha256: 39234a99df3d2e3065383808ed8bfda36760de5ef590c54c3692bb53571ef02b + md5: 3cca1b74b2752917b5b65b81f61f0553 + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=2.0.0b1 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + run_exports: {} + size: 35598 + timestamp: 1762509505285 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hd2277e8_3.conda + sha256: 34238103e9b75a9ed0d8b01132551c2af4f9ae68ee2f81320a685ffb27731f6c + md5: 9329dcd00c4d61aa49e516dddd784e91 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-sdkutils >=0.2.5,<0.2.6.0a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-auth >=0.10.3,<0.10.4.0a0 + size: 134649 + timestamp: 1781802943551 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h78948cc_2.conda + sha256: 06a0e2af439b21c94adff8fac5dd66dbda5f182fc80ac635c4903959ea306cbb + md5: fe81235aae00f32df8584267b4f2daf8 + depends: + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + - libgcc >=14 + - openssl >=3.5.6,<4.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - aws-c-cal >=0.9.14,<0.9.15.0a0 + size: 57011 + timestamp: 1780566647051 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.0-hb03c661_0.conda + sha256: 6d2b33965bf6daeffd3ad336f41410053ff06ed6f2b2ce62c1ec27c0a39b4e7e + md5: f1c005b2e3b618706112ddd7f3af4521 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - aws-c-common >=0.14.0,<0.14.1.0a0 + size: 242497 + timestamp: 1780160843944 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haa0cbde_2.conda + sha256: 0e4952f9be8de7f281ca7d734a3a8f05ad0db856c6ef1e0897798c4afbcd9a54 + md5: 595911421e25551e36fde7027bf33f38 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-compression >=0.3.2,<0.3.3.0a0 + size: 22007 + timestamp: 1780566239465 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h9cf6be0_2.conda + sha256: 6b893ba3173206e17fef1b9c8b683c6f6ecbca7127770c8d0f3ba13d123f8d4c + md5: 2c304605f9074f072c92c0d8de175a1a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - aws-c-common >=0.14.0,<0.14.1.0a0 + - aws-checksums >=0.2.10,<0.2.11.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + size: 59271 + timestamp: 1780586883495 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h6488f85_2.conda + sha256: d2b844db1a4dfbc20b5129b7df4a656c1459c5fb16745101bbd802813ba8d411 + md5: da0be1e8cb4a43c876f26d9d812dea06 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-compression >=0.3.2,<0.3.3.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-http >=0.11.0,<0.11.1.0a0 + size: 230293 + timestamp: 1780586764553 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-h3bf836e_5.conda + sha256: c798005b65bc74d02aba1db01d4d344c4e72662f0beef35fbdd35b4695c197de + md5: 12697e83c2a0e5b93fd03855a70eb360 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - s2n >=1.7.4,<1.7.5.0a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-io >=0.26.3,<0.26.4.0a0 + size: 181839 + timestamp: 1781649803811 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.16.0-h0d2f46f_0.conda + sha256: 2a9ae1da09ae77d0d513ebaaa0e3810f33e4e09b564494c62d50d7d9f217334e + md5: 94454ba09f610904865601eae5530600 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-common >=0.14.0,<0.14.1.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-mqtt >=0.16.0,<0.16.1.0a0 + size: 224231 + timestamp: 1780681834200 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.6-hb916526_0.conda + sha256: bda2c735382e2232997fb34c5d71d476ab2f4e5ba5bc9e059106f280f2334a88 + md5: f2b6275244daa12109bcd0a126f1fb85 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + - aws-checksums >=0.2.10,<0.2.11.0a0 + - openssl >=3.5.7,<4.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-auth >=0.10.3,<0.10.4.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-s3 >=0.12.6,<0.12.7.0a0 + size: 154088 + timestamp: 1781252014556 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.5-haa0cbde_0.conda + sha256: c351a2bb8734accc6a047b55be15a8ce205725aeeedd2576c320841c3c383731 + md5: 5088795f3dfcf00a26f03c2a17ae8429 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-sdkutils >=0.2.5,<0.2.6.0a0 + size: 65759 + timestamp: 1781063620400 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haa0cbde_2.conda + sha256: ad49333d96a5f9bcce02752a6515cbb077d7513e358a8fb1a832f4e772d54bac + md5: 5c05a63452bf73c50aa272a6f961c4fc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-common >=0.14.0,<0.14.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-checksums >=0.2.10,<0.2.11.0a0 + size: 101627 + timestamp: 1780568539000 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.1-h7a9a9d6_1.conda + sha256: 55d39d93aaa69ac47831db4c0661b8112e719d74950fecd4bcbf9181ea6f7d34 + md5: 976bc22e043e8380f3dd54863b73ecef + depends: + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-s3 >=0.12.6,<0.12.7.0a0 + - aws-c-sdkutils >=0.2.5,<0.2.6.0a0 + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + - aws-c-auth >=0.10.3,<0.10.4.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-mqtt >=0.16.0,<0.16.1.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + size: 416196 + timestamp: 1781814052588 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.833-hf4c7647_7.conda + sha256: 36587b55dfdf42b8dd8c3ab3c09c6ca87f110fc312facba1f26c73fb39cc63e0 + md5: 12ae84178a356ce6b073b0273942708b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + - aws-c-common >=0.14.0,<0.14.1.0a0 + - libcurl >=8.20.0,<9.0a0 + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-sdk-cpp >=1.11.833,<1.11.834.0a0 + size: 3394201 + timestamp: 1782207922124 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.3-h206d751_0.conda + sha256: fffc66e9be8806a92b314e27129c42b1298ea7065028c9e5d175dacb07829261 + md5: 0cabc152dc8896c3a4c4aebf2b308627 + depends: + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.19.0,<9.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + size: 349147 + timestamp: 1775238757304 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-h71f81a8_2.conda + sha256: ebca774f1ebaa24c150730603b418b33fc7862811a16d097716b1a29f34798c5 + md5: f4fc754ec17176046988c46a54962a8c + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + size: 250737 + timestamp: 1781268163278 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.18.0-h74b55db_1.conda + sha256: 04bb27dbf1d426b4447b28c3dc92ec01c807fa784eea86ffa1f8c2bd9b9e8076 + md5: 43151a3b0a8e037747d79a82dff9f967 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + size: 589716 + timestamp: 1781283775801 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.14.0-hf596fc9_1.conda + sha256: 8a806f9852d280926d7613df548889b49e2a1c5d836385bcb2cedb24e7b08c64 + md5: 7896f4a6ee78538e2d0261e3b36dfa69 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + size: 159394 + timestamp: 1781268171097 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.16.0-h1f05bef_1.conda + sha256: 13e2e6eb942f65bf81e9089bf6b5926534d9245c781288c5270beb3a25c9156b + md5: 70972c9cb0893d6499dd4118415a966b + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-files-datalake-cpp >=12.16.0,<12.16.1.0a0 + size: 308699 + timestamp: 1781538835962 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bottleneck-1.6.0-np2py314h56abb78_3.conda + sha256: 58cc4ecb796ec8093863d13264aca2746fa833461b30fd24b620d1acee0efd08 + md5: 48b137fb9317635b90c335348518d0a6 + depends: + - numpy + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - numpy >=1.23,<3 + - python_abi 3.14.* *_cp314 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/bottleneck?source=hash-mapping + run_exports: {} + size: 158983 + timestamp: 1762775788892 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + sha256: 3ad3500bff54a781c29f16ce1b288b36606e2189d0b0ef2f67036554f47f12b0 + md5: 8910d2c46f7e7b519129f486e0fe927a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - libbrotlicommon 1.2.0 hb03c661_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + run_exports: {} + size: 367376 + timestamp: 1764017265553 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e + md5: 920bb03579f15389b9e512095ad995b7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - c-ares >=1.34.6,<2.0a0 + size: 207882 + timestamp: 1765214722852 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + sha256: c6339858a0aaf5d939e00d345c98b99e4558f285942b27232ac098ad17ac7f8e + md5: cf45f4278afd6f4e6d03eda0f435d527 + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + run_exports: {} + size: 300271 + timestamp: 1761203085220 +- conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py314h97ea11e_4.conda + sha256: b0314a7f1fb4a294b1a8bcf5481d4a8d9412a9fee23b7e3f93fb10e4d504f2cc + md5: 95bede9cdb7a30a4b611223d52a01aa4 + depends: + - numpy >=1.25 + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/contourpy?source=hash-mapping + run_exports: {} + size: 324013 + timestamp: 1769155968691 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py314h5bd0f2a_2.conda + sha256: 5edb3c0fbf5c39b66f71fef0264d4fbab561f6adbdd7ef88188b725a82fcd6da + md5: a6a32cab83d59c7812ddbb03220057e3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - toolz >=0.10.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cytoolz?source=hash-mapping + run_exports: {} + size: 641304 + timestamp: 1771855845410 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.21-py314h42812f9_0.conda + sha256: c16696b23e1d75b6eea7d0c8b9c31c03d1987eeb61852f09b6d4042ca015acd3 + md5: a7eb8029c4fe320c0179085707017c2d + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + run_exports: {} + size: 2842115 + timestamp: 1780390153580 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + sha256: 6c33bf0c4d8f418546ba9c250db4e4221040936aef8956353bc764d4877bc39a + md5: d411fc29e338efb48c5fd4576d71d881 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - gflags >=2.2.2,<2.3.0a0 + size: 119654 + timestamp: 1726600001928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + sha256: dc824dc1d0aa358e28da2ecbbb9f03d932d976c8dca11214aa1dcdfcbd054ba2 + md5: ff862eebdfeb2fd048ae9dc92510baca + depends: + - gflags >=2.2.2,<2.3.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - glog >=0.7.1,<0.8.0a0 + size: 143452 + timestamp: 1718284177264 +- conda: https://conda.anaconda.org/conda-forge/linux-64/highspy-1.14.0-np2py314h6477eea_0.conda + sha256: 1d1bcd473327493b2657039fb0554f4419204e5f0fbc8045866d8a4d88420bcb + md5: 5ca3370caefd2b24ed1bde9b44cff045 + depends: + - python + - numpy + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/highspy?source=hash-mapping + run_exports: {} + size: 2429474 + timestamp: 1775498238470 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - icu >=78.3,<79.0a0 + size: 12723451 + timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - keyutils >=1.6.3,<2.0a0 + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + sha256: 9b07046870772f28740e3f6149f09ff222843733087a33c5540b169c6289652d + md5: 54157a1c8c0bb70f62dd0b17fba7e7f2 + depends: + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1388990 + timestamp: 1781859420533 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.19.1-h0c24ade_1.conda + sha256: 112b5b9462572d970f4abd2912f76a25ee7db158b1e7260163d91dd8a630db84 + md5: 8b3ce45e929cd8e8e5f4d18586b56d8b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - lcms2 >=2.19.1,<3.0a0 + size: 251971 + timestamp: 1780211695895 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c + md5: 18335a698559cdbcd86150a48bf54ba6 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 728002 + timestamp: 1774197446916 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda + sha256: f84cb54782f7e9cea95e810ea8fef186e0652d0fa73d3009914fa2c1262594e1 + md5: a752488c68f2e7c456bcbd8f16eec275 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 + size: 261513 + timestamp: 1773113328888 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + sha256: a7a4481a4d217a3eadea0ec489826a69070fcc3153f00443aa491ed21527d239 + md5: 6f7b4302263347698fd24565fbf11310 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - libabseil-static =20260107.1=cxx17* + - abseil-cpp =20260107.1 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libabseil >=20260107.1,<20260108.0a0 + - libabseil =*=cxx17* + size: 1384817 + timestamp: 1770863194876 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-h3e48024_9_cpu.conda + build_number: 9 + sha256: 92e0fe06d39351ecf8f2c1ce7ad47c552c34f2ebb692c145fa506bbe13bf109d + md5: 4cac0ddbb76d5294d0a5649633a0dc35 + depends: + - __glibc >=2.17,<3.0.a0 + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + - aws-sdk-cpp >=1.11.833,<1.11.834.0a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + - azure-storage-files-datalake-cpp >=12.16.0,<12.16.1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - glog >=0.7.1,<0.8.0a0 + - libabseil * cxx17* + - libabseil >=20260107.1,<20260108.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libgcc >=14 + - libgoogle-cloud >=3.6.0,<3.7.0a0 + - libgoogle-cloud-storage >=3.6.0,<3.7.0a0 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.3.0,<2.3.1.0a0 + - snappy >=1.2.2,<1.3.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - apache-arrow-proc =*=cpu + - parquet-cpp <0.0a0 + - arrow-cpp <0.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow >=24.0.0,<24.1.0a0 + size: 6514282 + timestamp: 1782267971657 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_9_cpu.conda + build_number: 9 + sha256: cddd787fb799a3f9d9a6a0c5110f7aa468fcc9d71aff213856876c36e694b279 + md5: cceb8bc7191b5916df504800e3e98056 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0 h3e48024_9_cpu + - libarrow-compute 24.0.0 h53684a4_9_cpu + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-acero >=24.0.0,<24.1.0a0 + size: 590917 + timestamp: 1782268212749 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_9_cpu.conda + build_number: 9 + sha256: 465ab2256f9c9fcdd41e3a7ef28600bb34287f0fb58a970f7046c53b7e6cc51c + md5: d2162528bc0d112cac9be3b03f879164 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0 h3e48024_9_cpu + - libgcc >=14 + - libre2-11 >=2025.11.5 + - libstdcxx >=14 + - libutf8proc >=2.11.3,<2.12.0a0 + - re2 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-compute >=24.0.0,<24.1.0a0 + size: 2992308 + timestamp: 1782268094918 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_9_cpu.conda + build_number: 9 + sha256: 26add89d034b1fdf91334b63e9892af3087ae70c6c6605ad82a9f47ac7ae0eec + md5: f94da761b5d8029a07c2a8fcac021e4f + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0 h3e48024_9_cpu + - libarrow-acero 24.0.0 h635bf11_9_cpu + - libarrow-compute 24.0.0 h53684a4_9_cpu + - libgcc >=14 + - libparquet 24.0.0 h7376487_9_cpu + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-dataset >=24.0.0,<24.1.0a0 + size: 590646 + timestamp: 1782268293863 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-hb4dd7c2_9_cpu.conda + build_number: 9 + sha256: dcdb718ef114704005210551b72e3be9d12e95134afe42629a1c14a28b59064e + md5: 71d9d232dad9b517b3b0f2112ab6dd8d + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260107.1,<20260108.0a0 + - libarrow 24.0.0 h3e48024_9_cpu + - libarrow-acero 24.0.0 h635bf11_9_cpu + - libarrow-dataset 24.0.0 h635bf11_9_cpu + - libgcc >=14 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-substrait >=24.0.0,<24.1.0a0 + size: 501060 + timestamp: 1782268320794 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-8_h4a7cf45_openblas.conda + build_number: 8 + sha256: b2da6bfd72a1c9cb143ccf64bf5b28790cb4eb58bd1cb978f6537b2322f7d48b + md5: 00fc660ab1b2f5ca07e92b4900d10c79 + depends: + - libopenblas >=0.3.33,<0.3.34.0a0 + - libopenblas >=0.3.33,<1.0a0 + constrains: + - blas 2.308 openblas + - mkl <2027 + - libcblas 3.11.0 8*_openblas + - liblapack 3.11.0 8*_openblas + - liblapacke 3.11.0 8*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libblas >=3.11.0,<4.0a0 + size: 18804 + timestamp: 1779859100675 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e + md5: 72c8fd1af66bd67bf580645b426513ed + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + size: 79965 + timestamp: 1764017188531 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b + md5: 366b40a69f0ad6072561c1d09301c886 + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 34632 + timestamp: 1764017199083 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d + md5: 4ffbb341c8b616aa2494b6afb26a0c5f + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 + size: 298378 + timestamp: 1764017210931 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-8_h0358290_openblas.conda + build_number: 8 + sha256: 1a2bc77bb26520255904a3d9b1f40e6bf0bf9d8d3405c7709dd162282820915a + md5: 33a413f1095f8325e5c30fde3b0d2445 + depends: + - libblas 3.11.0 8_h4a7cf45_openblas + constrains: + - blas 2.308 openblas + - liblapacke 3.11.0 8*_openblas + - liblapack 3.11.0 8*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libcblas >=3.11.0,<4.0a0 + size: 18778 + timestamp: 1779859107964 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + sha256: fd1d153962764433fe6233f34a72cdeed5dcf8a883a85769e8295ce940b5b0c5 + md5: c965a5aa0d5c1c37ffc62dff36e28400 + depends: + - libgcc-ng >=9.4.0 + - libstdcxx-ng >=9.4.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libcrc32c >=1.1.2,<1.2.0a0 + size: 20440 + timestamp: 1633683576494 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hae6b9f4_2.conda + sha256: ba8354084b34fce56d5697982fce4a384c148e972e15c0ab891187617fe7ec29 + md5: f9c59d277a16ec8f272b2d5dd2ec3335 + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.22.2,<1.23.0a0 + - libgcc >=14 + - libnghttp2 >=1.68.1,<2.0a0 + - libpsl >=0.22.0,<0.23.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + purls: [] + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 480327 + timestamp: 1782911787190 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + sha256: aa8e8c4be9a2e81610ddf574e05b64ee131fab5e0e3693210c9d6d2fba32c680 + md5: 6c77a605a7a689d17d4819c0f8ac9a00 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 + size: 73490 + timestamp: 1761979956660 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b + depends: + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libedit >=3.1.20250104,<3.2.0a0 + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 + md5: 172bf1cd1ff8629f2b1179945ed45055 + depends: + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libev >=4.33,<4.34.0a0 + size: 112766 + timestamp: 1702146165126 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131 + md5: a1cfcc585f0c42bf8d5546bb1dfb668d + depends: + - libgcc-ng >=12 + - openssl >=3.1.1,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libevent >=2.1.12,<2.1.13.0a0 + size: 427426 + timestamp: 1685725977222 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + sha256: 16feffd9ddbbe5b718515d38ee376c685ba95491cd901244e24671d20b952a77 + md5: b24d3c612f71e7aa74158d92106318b2 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + purls: [] + size: 77856 + timestamp: 1781203599810 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda + sha256: 38f014a7129e644636e46064ecd6b1945e729c2140e21d75bb476af39e692db2 + md5: e289f3d17880e44b633ba911d57a321b + depends: + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL + purls: [] + run_exports: {} + size: 8049 + timestamp: 1774298163029 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda + sha256: 16f020f96da79db1863fcdd8f2b8f4f7d52f177dd4c58601e38e9182e91adf1d + md5: fb16b4b69e3f1dcfe79d80db8fd0c55d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libpng >=1.6.55,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL + purls: [] + run_exports: {} + size: 384575 + timestamp: 1774298162622 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + sha256: 8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46 + md5: 57736f29cc2b0ec0b6c2952d3f101b6a + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_19 + - libgomp 15.2.0 he0feb66_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1041084 + timestamp: 1778269013026 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + sha256: 9dcf54adfaa5e861123c2da4f2f0451a685464ea7e5a41ad91cf67b31d658d98 + md5: 331ee9b72b9dff570d56b1302c5ab37d + depends: + - libgcc 15.2.0 he0feb66_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: + strong: + - libgcc + size: 27694 + timestamp: 1778269016987 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda + sha256: 561a42758ef25b9ce308c4e2cf56daee4f06138385a17e29a492cd928e00be6f + md5: 42bf7eca1a951735fa06c0e3c0d5c8e6 + depends: + - libgfortran5 15.2.0 h68bc16d_19 + constrains: + - libgfortran-ng ==15.2.0=*_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: {} + size: 27655 + timestamp: 1778269042954 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda + sha256: 057978bb69fea29ed715a9b98adf71015c31baecc4aeb2bfc20d4fd5d83579d4 + md5: 85072b0ad177c966294f129b7c04a2d5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: {} + size: 2483673 + timestamp: 1778269025089 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + sha256: 5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b + md5: faac990cb7aedc7f3a2224f2c9b0c26c + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603817 + timestamp: 1778268942614 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.6.0-h8d2ee43_0.conda + sha256: eb6fe89a6e2ffa6b485c437022e15d2173c2da3ada86690cf250bcfe6f6382d5 + md5: 50a88a9c7d89d854336c633966b67e56 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260107.1,<20260108.0a0 + - libcurl >=8.20.0,<9.0a0 + - libgcc >=14 + - libgrpc >=1.78.1,<1.79.0a0 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libstdcxx >=14 + - openssl >=3.5.7,<4.0a0 + constrains: + - libgoogle-cloud 3.6.0 *_0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libgoogle-cloud >=3.6.0,<3.7.0a0 + size: 2680630 + timestamp: 1781922536584 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.6.0-hdbdcf42_0.conda + sha256: 2d94ab8302408d34894024a80604e85936bb208a487841a222cafdb15c143f23 + md5: a5001567e3c2758834d63129ecb89bb1 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libgcc >=14 + - libgoogle-cloud 3.6.0 h8d2ee43_0 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - openssl + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libgoogle-cloud-storage >=3.6.0,<3.7.0a0 + size: 785866 + timestamp: 1781922659639 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.1-h1d1128b_0.conda + sha256: 5bb935188999fd70f67996746fd2dca85ec6204289e11695c316772e19451eb8 + md5: b5fb6d6c83f63d83ef2721dca6ff7091 + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.6,<2.0a0 + - libabseil * cxx17* + - libabseil >=20260107.1,<20260108.0a0 + - libgcc >=14 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libre2-11 >=2025.11.5 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - re2 + constrains: + - grpc-cpp =1.78.1 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libgrpc >=1.78.1,<1.79.0a0 + size: 7021360 + timestamp: 1774020290672 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f + md5: 915f5995e94f60e9a4826e0b0920ee88 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-only + purls: [] + run_exports: + weak: + - libiconv >=1.18,<2.0a0 + size: 790176 + timestamp: 1754908768807 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda + sha256: 10056646c28115b174de81a44e23e3a0a3b95b5347d2e6c45cc6d49d35294256 + md5: 6178c6f2fb254558238ef4e6c56fb782 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + run_exports: + weak: + - libjpeg-turbo >=3.1.4.1,<4.0a0 + size: 633831 + timestamp: 1775962768273 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-8_h47877c9_openblas.conda + build_number: 8 + sha256: 168e327d737059553e15cc6ec36d76b9bbb3931c2a7721555fd68b4c9348b247 + md5: 809be8ba8712c77bc7d44c2d99390dc4 + depends: + - libblas 3.11.0 8_h4a7cf45_openblas + constrains: + - blas 2.308 openblas + - libcblas 3.11.0 8*_openblas + - liblapacke 3.11.0 8*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - liblapack >=3.11.0,<3.12.0a0 + size: 18790 + timestamp: 1779859115086 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d + md5: b88d90cad08e6bc8ad540cb310a761fb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 113478 + timestamp: 1775825492909 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f + md5: 2a45e7f8af083626f009645a6481f12d + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.6,<2.0a0 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 + size: 663344 + timestamp: 1773854035739 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda + sha256: 3d9aa85648e5e18a6d66db98b8c4317cc426721ad7a220aa86330d1ccedc8903 + md5: 2d3278b721e40468295ca755c3b84070 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + constrains: + - openblas >=0.3.33,<0.3.34.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libopenblas >=0.3.33,<1.0a0 + size: 5931919 + timestamp: 1776993658641 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h9692893_0.conda + sha256: 247b99f5dd32363d7231c9c5a6ad113e0b58ad3e85d68227999b5933d5005a6d + md5: 2a44700a9857b49a3fe72aca643d0921 + depends: + - libabseil * cxx17* + - libabseil >=20260107.1,<20260108.0a0 + - libcurl >=8.20.0,<9.0a0 + - libgrpc >=1.78.1,<1.79.0a0 + - libopentelemetry-cpp-headers 1.27.0 ha770c72_0 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libzlib >=1.3.2,<2.0a0 + - nlohmann_json + - prometheus-cpp >=1.3.0,<1.4.0a0 + constrains: + - cpp-opentelemetry-sdk =1.27.0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + size: 943253 + timestamp: 1778721388532 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_0.conda + sha256: 4a55bd84d166395a117592bb6139cf645eb402416987b856b41f96ba7b9d15d6 + md5: f8dcb0cff8f84f428bf76f1169bf50a7 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 392177 + timestamp: 1778721367721 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_9_cpu.conda + build_number: 9 + sha256: 6c0635c86d7987fa89b07e0ce6664e8b2fcf0b6f0a0055a16a97d07b2c7af4e5 + md5: f86267ec3072c21c2c632e3a4f5e64cc + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0 h3e48024_9_cpu + - libgcc >=14 + - libstdcxx >=14 + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.7,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libparquet >=24.0.0,<24.1.0a0 + size: 1427336 + timestamp: 1782268184558 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda + sha256: 377cfe037f3eeb3b1bf3ad333f724a64d32f315ee1958581fc671891d63d3f89 + md5: eba48a68a1a2b9d3c0d9511548db85db + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: zlib-acknowledgement + purls: [] + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 + size: 317729 + timestamp: 1776315175087 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.33.5-h6eeba95_1.conda + sha256: a59aa3f076d5710c618ca8fd12d9cd8211d8b738f6b0e0c98517c0162f23a5de + md5: 7a4b11f3dd7374f1991a4088390d07c1 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260107.1,<20260108.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libprotobuf >=6.33.5,<6.33.6.0a0 + size: 3675765 + timestamp: 1780003831209 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.22.0-hd9031aa_0.conda + sha256: cd11890a2c1f14d0342bfcbd81f97152d61dc71c27c7085efc13705a8f64f7c9 + md5: e2834a423b3967e7b3b1e901c4a5f42f + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libpsl >=0.22.0,<0.23.0a0 + size: 83067 + timestamp: 1782394772411 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h0dc7533_1.conda + sha256: 138fc85321a8c0731c1715688b38e2be4fb71db349c9ab25f685315095ae70ff + md5: ced7f10b6cfb4389385556f47c0ad949 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260107.0,<20260108.0a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libre2-11 >=2025.11.5 + size: 213122 + timestamp: 1768190028309 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda + sha256: b677bbf1c339d894757c3dcfbb2f88649e499e4991d70ae09a1466da9a6c92d6 + md5: 965e4d531b588b2e42f66fd8e48b056c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: ISC + purls: [] + run_exports: + weak: + - libsodium >=1.0.22,<1.0.23.0a0 + size: 269272 + timestamp: 1779163468406 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.2-h0c1763c_0.conda + sha256: 1ab603b6ec93933e76027e1f23b21b22b858ba1b56f1e1695ef6fe5e80cb7358 + md5: 062b0ac602fb0adf250e3dfa86f221c4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 957849 + timestamp: 1780574429573 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661 + md5: eecce068c7e4eddeb169591baac20ac4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 + size: 304790 + timestamp: 1745608545575 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + sha256: dff1058c76ec6b8759e41cefa2508162d00e4a5e6721aa68ec3fd10094e702dc + md5: 5794b3bdc38177caf969dabd3af08549 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_19 + constrains: + - libstdcxx-ng ==15.2.0=*_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: {} + size: 5852044 + timestamp: 1778269036376 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda + sha256: 0672b6b6e1791c92e8eccad58081a99d614fcf82bca5841f9dfa3c3e658f83b9 + md5: e5ce228e579726c07255dbf90dc62101 + depends: + - libstdcxx 15.2.0 h934c35e_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: + strong: + - libstdcxx + size: 27776 + timestamp: 1778269074600 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda + sha256: af6025aa4a4fc3f4e71334000d2739d927e2f678607b109ec630cc17d716918a + md5: b6e326fbe1e3948da50ec29cee0380db + depends: + - __glibc >=2.17,<3.0.a0 + - libevent >=2.1.12,<2.1.13.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libthrift >=0.22.0,<0.22.1.0a0 + size: 423861 + timestamp: 1777018957474 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-h9d88235_0.conda + sha256: b31346e1c01ab40a170e91147092ee8fd92b1dee3c66ee47ef025571c879b159 + md5: c1fcb4a88bc15a9f77ad8d27d7af1df9 + depends: + - __glibc >=2.17,<3.0.a0 + - lerc >=4.1.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libgcc >=14 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 452337 + timestamp: 1783084902636 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda + sha256: ecbf4b7520296ed580498dc66a72508b8a79da5126e1d6dc650a7087171288f9 + md5: 1247168fe4a0b8912e3336bccdbf98a5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libutf8proc >=2.11.3,<2.12.0a0 + size: 85969 + timestamp: 1768735071295 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.1-h5347b49_0.conda + sha256: 3f0edf1280e2f6684a986f821eaa3e123d2694a00b31b96ca0d4a4c12c129231 + md5: 7d0a66598195ef00b6efc55aefc7453b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40163 + timestamp: 1779118517630 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b + md5: aea31d2e5b1091feca96fcfe945c3cf9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 + size: 429011 + timestamp: 1752159441324 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa + md5: 92ed62436b625154323d40d5f2f11dd7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libxcb >=1.17.0,<2.0a0 + size: 395888 + timestamp: 1727278577118 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda + sha256: 3d44f737c5ae52d5af32682cc1530df433f401f8e58a7533926536244127572a + md5: e79d2c2f24b027aa8d5ab1b1ba3061e7 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - libxml2 2.15.3 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 559775 + timestamp: 1776376739004 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + sha256: 3bc5551720c58591f6ea1146f7d1539c734ed1c40e7b9f5cb8cb7e900c509aba + md5: 995d8c8bad2a3cc8db14675a153dec2b + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.3 hca6bf5a_0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libxml2 + - libxml2-16 >=2.15.3 + size: 46810 + timestamp: 1776376751152 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 63629 + timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py314hd4c109c_1.conda + sha256: 7f3083018be486b73c82e5e2421ab882d5231fcd424843c96058b01ce5f3cbaf + md5: 2f6295571ea5e9278046efc3ef377a98 + depends: + - python + - lz4-c + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.14.* *_cp314 + - lz4-c >=1.10.0,<1.11.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/lz4?source=hash-mapping + run_exports: {} + size: 45224 + timestamp: 1765026391393 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + sha256: 47326f811392a5fd3055f0f773036c392d26fdb32e4d8e7a8197eed951489346 + md5: 9de5350a85c4a20c685259b889aa6393 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - lz4-c >=1.10.0,<1.11.0a0 + size: 167055 + timestamp: 1733741040117 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + sha256: c279be85b59a62d5c52f5dd9a4cd43ebd08933809a8416c22c3131595607d4cf + md5: 9a17c4307d23318476d7fbf0fedc0cde + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + run_exports: {} + size: 27424 + timestamp: 1772445227915 +- conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.1-py314h97ea11e_1.conda + sha256: f33032ef3dcb8759309e8a55b0cb989e377ce315122bb86b00c0adbcf6665abf + md5: 1c8698b2012012806ec17a062bb9f339 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/msgpack?source=compressed-mapping + run_exports: {} + size: 113343 + timestamp: 1782460776477 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 + md5: fc21868a1a5aacc937e7a18747acb8a5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: X11 AND BSD-3-Clause + purls: [] + size: 918956 + timestamp: 1777422145199 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + sha256: fd2cbd8dfc006c72f45843672664a8e4b99b2f8137654eaae8c3d46dca776f63 + md5: 16c2a0e9c4a166e53632cfca4f68d020 + constrains: + - nlohmann_json-abi ==3.12.0 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 136216 + timestamp: 1758194284857 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numexpr-2.14.1-py314heb044ea_102.conda + sha256: 4ba250aecc15c0c7932322b0c9cf1b7ae0d425d51d0df6aa89da1f791950d6d1 + md5: 8798ac863dc929f966e2646a853c1f25 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - nomkl + - numpy >=1.23,<3 + - numpy >=1.23.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/numexpr?source=hash-mapping + run_exports: {} + size: 218919 + timestamp: 1778498673852 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.0-py314h2b28147_0.conda + sha256: bbc665584886c90daf3f33cfbf665f279cf91d4bd5323f0432c16d2bf4d525e7 + md5: bdb21d2b990f9d3aee10fd43aca851fe + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - libcblas >=3.9.0,<4.0a0 + - libblas >=3.9.0,<4.0a0 + - python_abi 3.14.* *_cp314 + - liblapack >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/numpy?source=compressed-mapping + run_exports: + weak: + - numpy >=1.25,<3 + size: 9075918 + timestamp: 1782112541752 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + sha256: 3900f9f2dbbf4129cf3ad6acf4e4b6f7101390b53843591c53b00f034343bc4d + md5: 11b3379b191f63139e29c0d19dee24cd + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libpng >=1.6.50,<1.7.0a0 + - libstdcxx >=14 + - libtiff >=4.7.1,<4.8.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - openjpeg >=2.5.4,<3.0a0 + size: 355400 + timestamp: 1758489294972 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + sha256: d48f5c22b9897c01e4dff3680f1f57ceb02711ab9c62f74339b080419dfad34b + md5: 79dd2074b5cd5c5c6b2930514a11e22d + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3159683 + timestamp: 1781069855778 +- conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda + sha256: a60c2578c8422e0c67206d269767feb4d3e627511558b6866e5daf2231d5214d + md5: 8027fce94fdfdf2e54f9d18cbae496df + depends: + - tzdata + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.2,<1.3.0a0 + - libabseil >=20260107.1,<20260108.0a0 + - libabseil * cxx17* + - libprotobuf >=6.33.5,<6.33.6.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libzlib >=1.3.1,<2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - orc >=2.3.0,<2.3.1.0a0 + size: 1468651 + timestamp: 1773230208923 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py314hb4ffadd_0.conda + sha256: 8e4b161f3f7fbdf17f842b518ff3794b6af9378a90d095719d7153360d126dc1 + md5: bc2e1390314b1269e66fb1966fbcae5d + depends: + - python + - numpy >=1.26.0 + - python-dateutil >=2.8.2 + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - numpy >=1.23,<3 + - python_abi 3.14.* *_cp314 + constrains: + - adbc-driver-postgresql >=1.2.0 + - adbc-driver-sqlite >=1.2.0 + - beautifulsoup4 >=4.12.3 + - blosc >=1.21.3 + - bottleneck >=1.4.2 + - fastparquet >=2024.11.0 + - fsspec >=2024.10.0 + - gcsfs >=2024.10.0 + - html5lib >=1.1 + - hypothesis >=6.116.0 + - jinja2 >=3.1.5 + - lxml >=5.3.0 + - matplotlib >=3.9.3 + - numba >=0.60.0 + - numexpr >=2.10.2 + - odfpy >=1.4.1 + - openpyxl >=3.1.5 + - psycopg2 >=2.9.10 + - pyarrow >=13.0.0 + - pyiceberg >=0.8.1 + - pymysql >=1.1.1 + - pyqt5 >=5.15.9 + - pyreadstat >=1.2.8 + - pytables >=3.10.1 + - pytest >=8.3.4 + - pytest-xdist >=3.6.1 + - python-calamine >=0.3.0 + - pytz >=2024.2 + - pyxlsb >=1.0.10 + - qtpy >=2.4.2 + - scipy >=1.14.1 + - s3fs >=2024.10.0 + - sqlalchemy >=2.0.36 + - tabulate >=0.9.0 + - xarray >=2024.10.0 + - xlrd >=2.0.1 + - xlsxwriter >=3.2.0 + - zstandard >=0.23.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + run_exports: {} + size: 15303815 + timestamp: 1778602611222 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandoc-3.10-ha770c72_0.conda + sha256: 2f17a165a04833bd249215336b00df912bad7f03ae445a36765b47593df34057 + md5: a4b80dd6e9e0784ba48e6803bef1e17a + license: GPL-2.0-or-later + license_family: GPL + purls: [] + run_exports: {} + size: 22516793 + timestamp: 1780595446569 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py314h8ec4b1a_0.conda + sha256: bcab128df8980514061a78b9c67f5004954048f584da20df8c5a78de9e3f5abb + md5: 233e62a8eb894b79b5c93f4f8dec4dcd + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libtiff >=4.7.1,<4.8.0a0 + - openjpeg >=2.5.4,<3.0a0 + - libxcb >=1.17.0,<2.0a0 + - zlib-ng >=2.3.3,<2.4.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - python_abi 3.14.* *_cp314 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - lcms2 >=2.19.1,<3.0a0 + - tk >=8.6.13,<8.7.0a0 + license: HPND + purls: + - pkg:pypi/pillow?source=compressed-mapping + run_exports: {} + size: 1108174 + timestamp: 1782912080163 +- conda: https://conda.anaconda.org/conda-forge/linux-64/polars-runtime-32-1.42.1-py310h49dadd8_0.conda + noarch: python + sha256: 7fdca7044d5377fbed66864644f52430789876c7571dfe0868e4082dc711b83d + md5: 393bb50f17571c2e8b129461919296be + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - _python_abi3_support 1.* + - cpython >=3.10 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/polars-runtime-32?source=compressed-mapping + run_exports: {} + size: 41920668 + timestamp: 1782814229638 +- conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + sha256: 013669433eb447548f21c3c6b16b2ed64356f726b5f77c1b39d5ba17a8a4b8bc + md5: a83f6a2fdc079e643237887a37460668 + depends: + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.10.1,<9.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - zlib + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - prometheus-cpp >=1.3.0,<1.4.0a0 + size: 199544 + timestamp: 1730769112346 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda + sha256: f15574ed6c8c8ed8c15a0c5a00102b1efe8b867c0bd286b498cd98d95bd69ae5 + md5: 4f225a966cfee267a79c5cb6382bd121 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 231303 + timestamp: 1769678156552 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 + md5: b3c17d95b5a10c6e64a21fa17573e70e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 8252 + timestamp: 1726802366959 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py314hdafbbf9_0.conda + sha256: 03c421256cc31c4487b225f6a560d25fbf6102fc304b4d31fe955168ef14f630 + md5: 6629041b133a9d65d68c4f2269432378 + depends: + - libarrow-acero 24.0.0.* + - libarrow-dataset 24.0.0.* + - libarrow-substrait 24.0.0.* + - libparquet 24.0.0.* + - pyarrow-core 24.0.0 *_0_* + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 26828 + timestamp: 1776927974177 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py314h969be7f_0_cpu.conda + sha256: 772d3c847811d1dbfd7d4431092be95f36996281eb8348e36b2cfba88106aed1 + md5: b066370d80ec7fca3c1d4028dc09164f + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0.* *cpu + - libarrow-compute 24.0.0.* *cpu + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - apache-arrow-proc * cpu + - numpy >=1.23,<3 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/pyarrow?source=hash-mapping + run_exports: {} + size: 4818190 + timestamp: 1776927934653 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.5-habeac84_100_cp314.conda + build_number: 100 + sha256: 55eed9bf2a3f6e90311276f0834737fe7c2d9ec3e5e2e557507858df4c7521e6 + md5: da92e59ff92f2d5ede4f612af20f583f + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.8.0,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.53.1,<4.0a0 + - libuuid >=2.42.1,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 36745188 + timestamp: 1779236923603 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + sha256: b318fb070c7a1f89980ef124b80a0b5ccf3928143708a85e0053cde0169c699d + md5: 2035f68f96be30dc60a5dfd7452c7941 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + run_exports: {} + size: 202391 + timestamp: 1770223462836 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_3.conda + noarch: python + sha256: 970b2a1d12983d8d1cc05d914ad88a0b6ef1fa14038c9649aa834dd6ebee65d7 + md5: acd216255e1370e9aeab5351b831f07c + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - _python_abi3_support 1.* + - cpython >=3.12 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + run_exports: {} + size: 210896 + timestamp: 1779483879367 +- conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda + sha256: 3fc684b81631348540e9a42f6768b871dfeab532d3f47d5c341f1f83e2a2b2b2 + md5: 66a715bc01c77d43aca1f9fcb13dde3c + depends: + - libre2-11 2025.11.05 h0dc7533_1 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libre2-11 >=2025.11.5 + - re2 + size: 27469 + timestamp: 1768190052132 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.6.3-py314h1bee95f_0.conda + sha256: 064e56d6c233cbcfac5b0183c0dd10b732668ff27aa1ea3580459acceae95473 + md5: add3cbcc5eb677f6c1720e01cd82aac5 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=compressed-mapping + run_exports: {} + size: 300129 + timestamp: 1782831350283 +- conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.4-h92489ea_1.conda + sha256: de0bb8c7526684c9927cc687d4d07abe09d023a3ec950cfcd61089b495e2e616 + md5: a20feedf58ce5441b115cebf284a9a75 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - openssl >=3.5.7,<4.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - s2n >=1.7.4,<1.7.5.0a0 + size: 392550 + timestamp: 1781634128636 +- conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.0-py314hf07bd8e_0.conda + sha256: 85503102237f8515ab92319fc14609e894ac9e95e3a1398b0c49db1f9ee50877 + md5: 62c390c1f8f51240f1ebc7ba782669ad + depends: + - __glibc >=2.17,<3.0.a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + - liblapack >=3.9.0,<4.0a0 + - libstdcxx >=14 + - numpy <2.7 + - numpy >=1.23,<3 + - numpy >=2.0.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=compressed-mapping + run_exports: {} + size: 17260022 + timestamp: 1781912924009 +- conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + sha256: 48f3f6a76c34b2cfe80de9ce7f2283ecb55d5ed47367ba91e8bb8104e12b8f11 + md5: 98b6c9dc80eb87b2519b97bcf7e578dd + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - snappy >=1.2.2,<1.3.0a0 + size: 45829 + timestamp: 1762948049098 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.7-py314h5bd0f2a_0.conda + sha256: bbb7056f7c5fd606df16ed73ee68687050de2c02fd69a3f69a1cb533a7ed2ae8 + md5: 4a8e5889712641aabdf6695e292857fe + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=compressed-mapping + run_exports: {} + size: 918368 + timestamp: 1781006801436 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + sha256: 6bc6ab7a90a5d8ac94c7e300cc10beb0500eeba4b99822768ca2f2ef356f731b + md5: b2895afaf55bf96a8c8282a2e47a5de0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - xorg-libxau >=1.0.12,<2.0a0 + size: 15321 + timestamp: 1762976464266 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + sha256: 25d255fb2eef929d21ff660a0c687d38a6d2ccfbcbf0cc6aa738b12af6e9d142 + md5: 1dafce8548e38671bea82e3f5c6ce22f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - xorg-libxdmcp >=1.1.5,<2.0a0 + size: 20591 + timestamp: 1762976546182 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - yaml >=0.2.5,<0.3.0a0 + size: 85189 + timestamp: 1753484064210 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda + sha256: dc9f28dedcb5f35a127fad2d847674d2833369dd616d294e423b8997df31d8a8 + md5: 96b08867e21d4694fa5c2c226e6581b0 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.22,<1.0.23.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + run_exports: + weak: + - zeromq >=4.3.5,<4.4.0a0 + size: 311184 + timestamp: 1779123989774 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda + sha256: 245c9ee8d688e23661b95e3c6dd7272ca936fabc03d423cdb3cdee1bbcf9f2f2 + md5: c2a01a08fc991620a74b32420e97868a + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib 1.3.2 h25fd6f3_2 + license: Zlib + license_family: Other + purls: [] + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 95931 + timestamp: 1774072620848 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda + sha256: ea4e50c465d70236408cb0bfe0115609fd14db1adcd8bd30d8918e0291f8a75f + md5: 2aadb0d17215603a82a2a6b0afd9a4cb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: Zlib + license_family: Other + purls: [] + run_exports: + weak: + - zlib-ng >=2.3.3,<2.4.0a0 + size: 122618 + timestamp: 1770167931827 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 + md5: aaa2a381ccc56eac91d63b6c1240312f + depends: + - cpython + - python-gil + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 8191 + timestamp: 1744137672556 +- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.14.1-pyhcf101f3_0.conda + sha256: 6119355a0b2a33e7b0dd31a740dbe01df66ffee0a643f80968afb1d53e7dbdb3 + md5: 7498bb2648f852c6a3cd5724ca80bdeb + depends: + - exceptiongroup >=1.0.2 + - idna >=2.8 + - python >=3.10 + - typing_extensions >=4.5 + - python + constrains: + - trio >=0.32.0 + - uvloop >=0.22.1 + - winloop >=0.2.3 + license: MIT + license_family: MIT + purls: + - pkg:pypi/anyio?source=compressed-mapping + run_exports: {} + size: 161308 + timestamp: 1782357087265 +- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + sha256: bea62005badcb98b1ae1796ec5d70ea0fc9539e7d59708ac4e7d41e2f4bb0bad + md5: 8ac12aff0860280ee0cff7fa2cf63f3b + depends: + - argon2-cffi-bindings + - python >=3.9 + - typing-extensions + constrains: + - argon2_cffi ==999 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi?source=hash-mapping + run_exports: {} + size: 18715 + timestamp: 1749017288144 +- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + sha256: 792da8131b1b53ff667bd6fc617ea9087b570305ccb9913deb36b8e12b3b5141 + md5: 85c4f19f377424eafc4ed7911b291642 + depends: + - python >=3.10 + - python-dateutil >=2.7.0 + - python-tzdata + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/arrow?source=hash-mapping + run_exports: {} + size: 113854 + timestamp: 1760831179410 +- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + sha256: ee4da0f3fe9d59439798ee399ef3e482791e48784873d546e706d0935f9ff010 + md5: 9673a61a297b00016442e022d689faa6 + depends: + - python >=3.10 + constrains: + - astroid >=2,<5 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/asttokens?source=hash-mapping + size: 28797 + timestamp: 1763410017955 +- conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + sha256: ea8486637cfb89dc26dc9559921640cd1d5fd37e5e02c33d85c94572139f2efe + md5: b85e84cb64c762569cc1a760c2327e0a + depends: + - python >=3.10 + - typing_extensions >=4.0.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/async-lru?source=hash-mapping + run_exports: {} + size: 22949 + timestamp: 1773926359134 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab + md5: c6b0543676ecb1fb2d7643941fe375f2 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/attrs?source=hash-mapping + run_exports: {} + size: 64927 + timestamp: 1773935801332 +- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770 + md5: f1976ce927373500cc19d3c0b2c85177 + depends: + - python >=3.10 + - python + constrains: + - pytz >=2015.7 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/babel?source=hash-mapping + run_exports: {} + size: 7684321 + timestamp: 1772555330347 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.6.0-py314h680f03e_0.conda + noarch: generic + sha256: 709cac7434d1c5a8828105036212a2a36022a07d807e89e2e99cac939c2d2526 + md5: 40d89d8546ad6e139e73ec8f6d56068b + depends: + - python >=3.14 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + run_exports: {} + size: 7526 + timestamp: 1781450817767 +- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + sha256: aed4b9dcf68ec2a75e5645fed14d77fd884d38d2e52bfa6ef4b278d90cd88781 + md5: 3b261da3fe9b4168738712832410b022 + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/beautifulsoup4?source=hash-mapping + run_exports: {} + size: 92704 + timestamp: 1780853175566 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda + sha256: 0c786f3e571bd58ac73d730d06314716663884d848ae320de0b438fae5e0bea9 + md5: 93009c29cdd6f2500468f2502fff9209 + depends: + - python >=3.10 + - webencodings + - python + constrains: + - tinycss2 >=1.1.0,<1.5 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/bleach?source=compressed-mapping + run_exports: {} + size: 142246 + timestamp: 1780675823953 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda + sha256: ede77e412304cd080e23967352a7904932207d0167ecdccd6a9e210530942be6 + md5: 5f710eab1f3c4e773c75686f5e8e6481 + depends: + - bleach ==6.4.0 pyhcf101f3_0 + - tinycss2 + license: Apache-2.0 AND MIT + purls: [] + run_exports: {} + size: 4406 + timestamp: 1780675823953 +- conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.9.1-pyhd8ed1ab_0.conda + sha256: b3d3b93a17fa678e81cd5ff5309fe64c7feb65c71cb134f14e4fe2113dc0c8d5 + md5: 123fcfe0df4b6fc21804538e59cdaafe + depends: + - contourpy >=1.2 + - jinja2 >=2.9 + - narwhals >=1.13 + - numpy >=1.16 + - packaging >=16.8 + - pillow >=7.1.0 + - python >=3.10 + - pyyaml >=3.10 + - tornado >=6.2 + - xyzservices >=2021.09.1 + constrains: + - panel >=1.8.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/bokeh?source=hash-mapping + run_exports: {} + size: 4526315 + timestamp: 1781002115296 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda + sha256: 9812a303a1395e1dafbd92e5bc8a1ff6013bcbba0a09c7f03a8d23e43560aa9b + md5: 489b8e97e666c93f68fdb35c3c9b957f + depends: + - __unix + license: ISC + purls: [] + size: 129868 + timestamp: 1779289852439 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_2.conda + noarch: python + sha256: 0d00dd61cb91b1bd1536600c64c9db4f94f3c699fec42864d0a2f4bc2ad8c3e8 + md5: 1990eb3f49022846fbc7fc9624a0ee43 + depends: + - cached_property >=1.5.2,<1.5.3.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cached-property?source=compressed-mapping + run_exports: {} + size: 6836 + timestamp: 1783242914545 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_2.conda + sha256: b1808a7811b5688d045b204425bb7ee824b340b40025b4ad0a39b4db1f4f1c98 + md5: f71e6840332854fd2eac6c3229768a9c + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cached-property?source=compressed-mapping + run_exports: {} + size: 14752 + timestamp: 1783242913845 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.6.17-pyhd8ed1ab_0.conda + sha256: 6c13620e458ba43278379d0cdacc30c497336bddfda81681fd50d114a65c702f + md5: c13824fedced67005d3832c152fe9c2f + depends: + - python >=3.10 + license: ISC + purls: + - pkg:pypi/certifi?source=compressed-mapping + run_exports: {} + size: 133877 + timestamp: 1781719949728 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 + md5: a9167b9571f3baa9d448faa2139d1089 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=hash-mapping + run_exports: {} + size: 58872 + timestamp: 1775127203018 +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.4.1-pyhc90fa1f_0.conda + sha256: c253a41cdf898b651a0786cbb76c6d5fc101d0dbbe719f93a124bc4fde5cdd6a + md5: 554304a07e581a85891b15e39ea9f268 + depends: + - __unix + - python + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/click?source=compressed-mapping + size: 104999 + timestamp: 1779900548735 +- conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + sha256: 4c287c2721d8a34c94928be8fe0e9a85754e90189dd4384a31b1806856b50a67 + md5: 61b8078a0905b12529abc622406cb62c + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cloudpickle?source=hash-mapping + run_exports: {} + size: 27353 + timestamp: 1765303462831 +- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + sha256: 576a44729314ad9e4e5ebe055fbf48beb8116b60e58f9070278985b2b634f212 + md5: 2da13f2b299d8e1995bafbbe9689a2f7 + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/comm?source=hash-mapping + run_exports: {} + size: 14690 + timestamp: 1753453984907 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.6-py314hd8ed1ab_100.conda + noarch: generic + sha256: 7a548856ef5307890a8cadfc196655117658f8c24589ce175caa4c1c2ded9d13 + md5: b28fe35fd43d5f425c0dccbe5b5039fd + depends: + - python >=3.14,<3.15.0a0 + - python_abi * *_cp314 + license: Python-2.0 + purls: [] + run_exports: {} + size: 49333 + timestamp: 1781254618863 +- conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.6.0-pyhc364b38_0.conda + sha256: 2ada45db64509bc5c119cbd7d68953b5ec7a9341cc9fad7cbe6a4f029e8af0fb + md5: a2d2a05abf6076c3d041ec91b2235823 + depends: + - python >=3.10 + - dask-core >=2026.6.0,<2026.6.1.0a0 + - distributed >=2026.6.0,<2026.6.1.0a0 + - cytoolz >=0.11.2 + - lz4 >=4.3.2 + - numpy >=1.26 + - pandas >=2.0 + - bokeh >=3.1.0 + - jinja2 >=2.10.3 + - pyarrow >=16.0 + - python + constrains: + - openssl !=1.1.1e + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: {} + size: 11216 + timestamp: 1781272553165 +- conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.6.0-pyhc364b38_0.conda + sha256: ce64f55e95912bff84119f05a296c90f8df57ecf9c3100bd8a6beca1cedabbb7 + md5: 56a3cae23de84509452da890fb2bfd85 + depends: + - python >=3.10 + - click >=8.1 + - cloudpickle >=3.0.0 + - fsspec >=2021.9.0 + - packaging >=20.0 + - partd >=1.4.0 + - pyyaml >=5.4.1 + - toolz >=0.12.0 + - importlib-metadata >=4.13.0 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/dask?source=compressed-mapping + run_exports: {} + size: 1069053 + timestamp: 1781221472758 +- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.3.1-pyhd8ed1ab_0.conda + sha256: 430bd9d731b265f0bedb3183ac3ecfaa1656390c092b6e864ff8cc1229843c8c + md5: 61dcf784d59ef0bd62c57d982b154ace + depends: + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/decorator?source=hash-mapping + size: 16102 + timestamp: 1779115228886 +- conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + sha256: 9717a059677553562a8f38ff07f3b9f61727bd614f505658b0a5ecbcf8df89be + md5: 961b3a227b437d82ad7054484cfa71b2 + depends: + - python >=3.6 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/defusedxml?source=hash-mapping + run_exports: {} + size: 24062 + timestamp: 1615232388757 +- conda: https://conda.anaconda.org/conda-forge/noarch/deprecation-2.1.0-pyh9f0ad1d_0.tar.bz2 + sha256: 2695a60ff355b114d0c459458461d941d2209ec9aff152853b6a3ca8700c94ec + md5: 7b6747d7cc2076341029cff659669e8b + depends: + - packaging + - python + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/deprecation?source=hash-mapping + run_exports: {} + size: 14487 + timestamp: 1589881524975 +- conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.6.0-pyhc364b38_1.conda + sha256: 2386b3b1dabe713b46d46c21758e03bc2c080a784fdd225f46c20b3b7e45c854 + md5: 2a6851a6c69ce7c0b03a89d5d4209257 + depends: + - python >=3.10 + - click >=8.0 + - cloudpickle >=3.0.0 + - cytoolz >=0.12.0 + - dask-core >=2026.6.0,<2026.6.1.0a0 + - jinja2 >=2.10.3 + - locket >=1.0.0 + - msgpack-python >=1.0.2 + - packaging >=20.0 + - psutil >=5.8.0 + - pyyaml >=5.4.1 + - sortedcontainers >=2.0.5 + - tblib >=1.6.0 + - toolz >=0.12.0 + - tornado >=6.2.0 + - zict >=3.0.0 + - python + constrains: + - openssl !=1.1.1e + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/distributed?source=hash-mapping + run_exports: {} + size: 838296 + timestamp: 1781563082788 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + run_exports: {} + size: 21333 + timestamp: 1763918099466 +- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + sha256: 210c8165a58fdbf16e626aac93cc4c14dbd551a01d1516be5ecad795d2422cad + md5: ff9efb7f7469aed3c4a8106ffa29593c + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/executing?source=hash-mapping + size: 30753 + timestamp: 1756729456476 +- conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.29.3-pyhd8ed1ab_0.conda + sha256: e86ab4ea930968e506412a1c3c89e7ab367c8344cfbeb40d8da46e9973597e8d + md5: 6fa414f2828958e28a1b253ff02634a2 + depends: + - python >=3.10 + license: Unlicense + purls: + - pkg:pypi/filelock?source=hash-mapping + size: 36519 + timestamp: 1781138011903 +- conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + sha256: 2509992ec2fd38ab27c7cdb42cf6cadc566a1cc0d1021a2673475d9fa87c6276 + md5: d3549fd50d450b6d9e7dddff25dd2110 + depends: + - cached-property >=1.3.0 + - python >=3.9,<4 + license: MPL-2.0 + license_family: MOZILLA + purls: + - pkg:pypi/fqdn?source=hash-mapping + run_exports: {} + size: 16705 + timestamp: 1733327494780 +- conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.6.0-pyhd8ed1ab_0.conda + sha256: fe0156e6d658be3531aad2a99e42e8ad1ee23c69837d469c44c1b6010373913d + md5: 7d7e6c826ba0743fc491ebee0e7b899c + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fsspec?source=compressed-mapping + run_exports: {} + size: 149709 + timestamp: 1781615868173 +- conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + sha256: 96cac6573fd35ae151f4d6979bab6fbc90cb6b1fb99054ba19eb075da9822fcb + md5: b8993c19b0c32a2f7b66cbb58ca27069 + depends: + - python >=3.10 + - typing_extensions + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h11?source=hash-mapping + run_exports: {} + size: 39069 + timestamp: 1767729720872 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + depends: + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + run_exports: {} + size: 95967 + timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + sha256: fdcea5d7cb314485d3907192ef024c704311548c5b0cbeb390cd1951051e29d2 + md5: b395909221b9bd1df066e5930e18855b + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=compressed-mapping + run_exports: {} + size: 32884 + timestamp: 1782283986153 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b + md5: 4f14640d58e2cc0aa0819d9d8ba125bb + depends: + - python >=3.9 + - h11 >=0.16 + - h2 >=3,<5 + - sniffio 1.* + - anyio >=4.0,<5.0 + - certifi + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/httpcore?source=hash-mapping + run_exports: {} + size: 49483 + timestamp: 1745602916758 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + sha256: cd0f1de3697b252df95f98383e9edb1d00386bfdd03fdf607fa42fe5fcb09950 + md5: d6989ead454181f4f9bc987d3dc4e285 + depends: + - anyio + - certifi + - httpcore 1.* + - idna + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/httpx?source=hash-mapping + run_exports: {} + size: 63082 + timestamp: 1733663449209 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + run_exports: {} + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + sha256: c75632ea624aa450a394f570749420c5a2e0997d0216bc29d5d45b0f39df0426 + md5: 577b04680ae422adb86fc60d7b940659 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=compressed-mapping + run_exports: {} + size: 163869 + timestamp: 1781620148226 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda + sha256: 43e2a5497cad1598ff88a3e69f69bc88b7b8f141fa63c60eab5db296317318b8 + md5: ffc17e785d64e12fc311af9184221839 + depends: + - python >=3.10 + - zipp >=3.20 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-metadata?source=compressed-mapping + run_exports: {} + size: 34766 + timestamp: 1779714582554 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.3.0-pyha191276_0.conda + sha256: 305ad9226363ff5f259c404dd9a7508183a2e150739b2adc43db7d817234da66 + md5: 2b47a10e4d98334f8171ff60aea05ff3 + depends: + - __linux + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.9.0 + - jupyter_core >=5.1,!=6.0.* + - matplotlib-inline >=0.1 + - nest-asyncio2 >=1.7.0 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=compressed-mapping + run_exports: {} + size: 138635 + timestamp: 1781101665847 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda + sha256: a3f76e06c31bcf1bda0f633d5c9f1c834286b4f6decc6626067a6cffee283318 + md5: fbd58549b374103c1a80577f09a328ef + depends: + - __unix + - decorator >=5.1.0 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.2 + - matplotlib-inline >=0.1.6 + - prompt-toolkit >=3.0.41,<3.1.0 + - psutil >=7 + - pygments >=2.14.0 + - python >=3.11 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - pexpect >4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=hash-mapping + size: 652893 + timestamp: 1780654403616 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104 + md5: bd80ba060603cc228d9d81c257093119 + depends: + - pygments + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython-pygments-lexers?source=hash-mapping + size: 13993 + timestamp: 1737123723464 +- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + sha256: 08e838d29c134a7684bca0468401d26840f41c92267c4126d7b43a6b533b0aed + md5: 0b0154421989637d424ccf0f104be51a + depends: + - arrow >=0.15.0 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/isoduration?source=hash-mapping + run_exports: {} + size: 19832 + timestamp: 1733493720346 +- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 + md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 + depends: + - parso >=0.8.3,<0.9.0 + - python >=3.9 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/jedi?source=hash-mapping + size: 843646 + timestamp: 1733300981994 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d + depends: + - markupsafe >=2.0 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jinja2?source=hash-mapping + run_exports: {} + size: 120685 + timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.15.0-pyhd8ed1ab_0.conda + sha256: 637400a4174880463985c7a5b6e3e0bea0aa9d0892a6c06a3a8aa2cf1208c35a + md5: 761a4a6b9cba303c66a97ca642447171 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/json5?source=compressed-mapping + run_exports: {} + size: 39373 + timestamp: 1781926894833 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + sha256: a3d10301b6ff399ba1f3d39e443664804a3d28315a4fb81e745b6817845f70ae + md5: 89bf346df77603055d3c8fe5811691e6 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jsonpointer?source=hash-mapping + run_exports: {} + size: 14190 + timestamp: 1774311356147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 + md5: ada41c863af263cc4c5fcbaff7c3e4dc + depends: + - attrs >=22.2.0 + - jsonschema-specifications >=2023.3.6 + - python >=3.10 + - referencing >=0.28.4 + - rpds-py >=0.25.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema?source=hash-mapping + run_exports: {} + size: 82356 + timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 + md5: 439cd0f567d697b20a8f45cb70a1005a + depends: + - python >=3.10 + - referencing >=0.31.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema-specifications?source=hash-mapping + run_exports: {} + size: 19236 + timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + sha256: 6886fc61e4e4edd38fd38729976b134e8bd2143f7fce56cc80d7ac7bac99bce1 + md5: 8368d58342d0825f0843dc6acdd0c483 + depends: + - jsonschema >=4.26.0,<4.26.1.0a0 + - fqdn + - idna + - isoduration + - jsonpointer >1.13 + - rfc3339-validator + - rfc3986-validator >0.1.0 + - rfc3987-syntax >=1.1.0 + - uri-template + - webcolors >=24.6.0 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 4740 + timestamp: 1767839954258 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-builder-1.0.2-pyhcf101f3_0.conda + sha256: a845bffdbcdd1749dd4a46e47be5d4e0e1c87cdda547e3a87d297a6963ab0821 + md5: 12b0a9513f6abaa944c313c4a01d5500 + depends: + - jupyter_core + - python >=3.10 + - tomli + - traitlets + - python + constrains: + - nodejs >=22 + license: BSD-3-Clause AND MIT AND ISC + purls: + - pkg:pypi/jupyter-builder?source=hash-mapping + run_exports: {} + size: 858738 + timestamp: 1781271993460 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + sha256: 3766e2ae59641c172cec8a821528bfa6bf9543ffaaeb8b358bfd5259dcf18e4e + md5: 0c3b465ceee138b9c39279cc02e5c4a0 + depends: + - importlib-metadata >=4.8.3 + - jupyter_server >=1.1.2 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-lsp?source=hash-mapping + run_exports: {} + size: 61633 + timestamp: 1775136333147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.1-pyhcf101f3_0.conda + sha256: 48b18974cc93b2c0d2681563237034e521f51d1878f0bbc6a5a67ca31b1608a6 + md5: 49440e66df843bee2273937e8032ec43 + depends: + - jupyter_core >=5.1 + - python >=3.10 + - python-dateutil >=2.8.2 + - pyzmq >=25.0 + - tornado >=6.4.1 + - traitlets >=5.3 + - typing_extensions >=4.13.0 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-client?source=hash-mapping + run_exports: {} + size: 117954 + timestamp: 1781019994076 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a + md5: b38fe4e78ee75def7e599843ef4c1ab0 + depends: + - __unix + - python + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + run_exports: {} + size: 65503 + timestamp: 1760643864586 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + sha256: c7edb5682c6316a95ad781dccb1b6589cd2ec0bf94f23c21152974eb0363b5d7 + md5: bf42ee94c750c0b2e7e998b79ac299ea + depends: + - jsonschema-with-format-nongpl >=4.18.0 + - packaging + - python >=3.10 + - python-json-logger >=2.0.4 + - pyyaml >=5.3 + - referencing + - rfc3339-validator + - rfc3986-validator >=0.1.1 + - traitlets >=5.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-events?source=hash-mapping + run_exports: {} + size: 24002 + timestamp: 1776861872237 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.20.0-pyhcf101f3_0.conda + sha256: 3e8759fdc404f149e7e722e0af472044a0ef9e70d0a3a7690ddcfe1232a0e868 + md5: b2ddb0e13b5600070c4019c4db8a78e6 + depends: + - anyio >=3.1.0 + - argon2-cffi >=21.1 + - jinja2 >=3.0.3 + - jupyter_client >=7.4.4 + - jupyter_core >=4.12,!=5.0.* + - jupyter_events >=0.11.0 + - jupyter_server_terminals >=0.4.4 + - nbconvert-core >=6.4.4 + - nbformat >=5.3.0 + - overrides >=5.0 + - packaging >=22.0 + - prometheus_client >=0.9 + - python >=3.10 + - pyzmq >=24 + - send2trash >=1.8.2 + - terminado >=0.8.3 + - tornado >=6.2.0 + - traitlets >=5.6.0 + - websocket-client >=1.7 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-server?source=hash-mapping + run_exports: {} + size: 363068 + timestamp: 1781713810089 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + sha256: 5eda79ed9f53f590031d29346abd183051263227dd9ee667b5ca1133ce297654 + md5: 7b8bace4943e0dc345fc45938826f2b8 + depends: + - python >=3.10 + - terminado >=0.8.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-server-terminals?source=hash-mapping + run_exports: {} + size: 22052 + timestamp: 1768574057200 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.6.1-pyhd8ed1ab_0.conda + sha256: ad4a462da846fb8e9feebe39170553fc6a94252f4d4cca8efaf0dfeefa907099 + md5: 7ba07211c94d434f3223a4418f8b1ff8 + depends: + - async-lru >=1.0.0 + - httpx >=0.25.0,<1 + - ipykernel >=6.5.0,!=6.30.0 + - jinja2 >=3.0.3 + - jupyter-builder >=1.0.2 + - jupyter-lsp >=2.0.0 + - jupyter_core + - jupyter_server >=2.19.0,<3 + - jupyterlab_server >=2.28.0,<3 + - notebook-shim >=0.2 + - packaging >=23.2 + - python >=3.10 + - tomli >=1.2.2 + - tornado >=6.2.0 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab?source=compressed-mapping + run_exports: {} + size: 13793940 + timestamp: 1782739437310 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + sha256: dc24b900742fdaf1e077d9a3458fd865711de80bca95fe3c6d46610c532c6ef0 + md5: fd312693df06da3578383232528c468d + depends: + - pygments >=2.4.1,<3 + - python >=3.9 + constrains: + - jupyterlab >=4.0.8,<5.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-pygments?source=hash-mapping + run_exports: {} + size: 18711 + timestamp: 1733328194037 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + sha256: 381d2d6a259a3be5f38a69463e0f6c5dcf1844ae113058007b51c3bef13a7cee + md5: a63877cb23de826b1620d3adfccc4014 + depends: + - babel >=2.10 + - jinja2 >=3.0.3 + - json5 >=0.9.0 + - jsonschema >=4.18 + - jupyter_server >=1.21,<3 + - packaging >=21.3 + - python >=3.10 + - requests >=2.31 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-server?source=hash-mapping + run_exports: {} + size: 51621 + timestamp: 1761145478692 +- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + sha256: 49570840fb15f5df5d4b4464db8ee43a6d643031a2bc70ef52120a52e3809699 + md5: 9b965c999135d43a3d0f7bd7d024e26a + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/lark?source=hash-mapping + run_exports: {} + size: 94312 + timestamp: 1761596921009 +- conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + sha256: 9afe0b5cfa418e8bdb30d8917c5a6cec10372b037924916f1f85b9f4899a67a6 + md5: 91e27ef3d05cc772ce627e51cff111c4 + depends: + - python >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/locket?source=hash-mapping + run_exports: {} + size: 8250 + timestamp: 1650660473123 +- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + sha256: 35b43d7343f74452307fd018a1cca92b8f68961ff8e2ab6a81ce0a703c9a3764 + md5: 9acc1c385be401d533ff70ef5b50dae6 + depends: + - python >=3.10 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/matplotlib-inline?source=hash-mapping + size: 15725 + timestamp: 1778264403247 +- conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.3.2-pyhcf101f3_0.conda + sha256: b8e38b0c5090f8cf1826c3f77e9db682c5d7b56ab6a434e6e2b528abc591048c + md5: e92e7aa653b4c71d0cd3cd39eadc302f + depends: + - python >=3.10 + - typing_extensions + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/mistune?source=compressed-mapping + run_exports: {} + size: 86960 + timestamp: 1782218255079 +- conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.23.0-pyhcf101f3_0.conda + sha256: e7dbd69d1de038b0411636c50bd9a935d05ca37269c4a155a78571afbaa7994a + md5: a537eb35988a6f2b1ceda6cce83e2f0e + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/narwhals?source=compressed-mapping + run_exports: {} + size: 288381 + timestamp: 1782924928916 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.11.0-pyhd8ed1ab_0.conda + sha256: eceb424236fbbb9b337a857fe5448307b57a2a3fb2db389ae37e7a8b8cdca2ab + md5: cf01a81d7960ad9c829bf2e794fcee9a + depends: + - jupyter_client >=7.0.0 + - jupyter_core >=5.4 + - nbformat >=5.2.0 + - python >=3.10 + - traitlets >=5.13 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbclient?source=compressed-mapping + run_exports: {} + size: 29138 + timestamp: 1780661039538 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-7.17.1-hb502eef_0.conda + sha256: 6b6d96cca8e9dd34e0735b59534831e1f8206b7366c79c71e08da6ee55c50683 + md5: 02669c36935d23e5258c5dd1a5e601ab + depends: + - nbconvert-core ==7.17.1 pyhcf101f3_0 + - nbconvert-pandoc ==7.17.1 h08b4883_0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: {} + size: 5364 + timestamp: 1775615493260 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + sha256: ab2ac79c5892c5434d50b3542d96645bdaa06d025b6e03734be29200de248ac2 + md5: 2bce0d047658a91b99441390b9b27045 + depends: + - beautifulsoup4 + - bleach-with-css !=5.0.0 + - defusedxml + - importlib-metadata >=3.6 + - jinja2 >=3.0 + - jupyter_core >=4.7 + - jupyterlab_pygments + - markupsafe >=2.0 + - mistune >=2.0.3,<4 + - nbclient >=0.5.0 + - nbformat >=5.7 + - packaging + - pandocfilters >=1.4.1 + - pygments >=2.4.1 + - python >=3.10 + - traitlets >=5.1 + - python + constrains: + - pandoc >=2.9.2,<4.0.0 + - nbconvert ==7.17.1 *_0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbconvert?source=hash-mapping + run_exports: {} + size: 202229 + timestamp: 1775615493260 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-pandoc-7.17.1-h08b4883_0.conda + sha256: b576268b5b3da13b703a15d28d218fd1b552511726253575025930091e6206ae + md5: f635af333701d2ed89d70ba9adb8e2ee + depends: + - nbconvert-core ==7.17.1 pyhcf101f3_0 + - pandoc + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: {} + size: 5840 + timestamp: 1775615493260 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 + md5: bbe1963f1e47f594070ffe87cdf612ea + depends: + - jsonschema >=2.6 + - jupyter_core >=4.12,!=5.0.* + - python >=3.9 + - python-fastjsonschema >=2.15 + - traitlets >=5.1 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbformat?source=hash-mapping + run_exports: {} + size: 100945 + timestamp: 1733402844974 +- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio2-1.7.2-pyhcf101f3_0.conda + sha256: e6768ceef038f4d7e083de7e393f5dd7d672b937e2bda570b740f6399b686689 + md5: fcd832bfd4749e9b246112b6894f97fc + depends: + - python >=3.10 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/nest-asyncio2?source=hash-mapping + run_exports: {} + size: 15903 + timestamp: 1770973502283 +- conda: https://conda.anaconda.org/conda-forge/noarch/nomkl-1.0-h5ca1d4c_0.tar.bz2 + sha256: d38542a151a90417065c1a234866f97fd1ea82a81de75ecb725955ab78f88b4b + md5: 9a66894dfd07c4510beb6b3f9672ccc0 + constrains: + - mkl <0.a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: {} + size: 3843 + timestamp: 1582593857545 +- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + sha256: 7b920e46b9f7a2d2aa6434222e5c8d739021dbc5cc75f32d124a8191d86f9056 + md5: e7f89ea5f7ea9401642758ff50a2d9c1 + depends: + - jupyter_server >=1.8,<3 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/notebook-shim?source=hash-mapping + run_exports: {} + size: 16817 + timestamp: 1733408419340 +- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + sha256: 1840bd90d25d4930d60f57b4f38d4e0ae3f5b8db2819638709c36098c6ba770c + md5: e51f1e4089cad105b6cac64bd8166587 + depends: + - python >=3.9 + - typing_utils + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/overrides?source=hash-mapping + run_exports: {} + size: 30139 + timestamp: 1734587755455 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 + md5: 4c06a92e74452cfa53623a81592e8934 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=hash-mapping + run_exports: {} + size: 91574 + timestamp: 1777103621679 +- conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + sha256: 2bb9ba9857f4774b85900c2562f7e711d08dd48e2add9bee4e1612fbee27e16f + md5: 457c2c8c08e54905d6954e79cb5b5db9 + depends: + - python !=3.0,!=3.1,!=3.2,!=3.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandocfilters?source=hash-mapping + run_exports: {} + size: 11627 + timestamp: 1631603397334 +- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + sha256: 611882f7944b467281c46644ffde6c5145d1a7730388bcde26e7e86819b0998e + md5: 39894c952938276405a1bd30e4ce2caf + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/parso?source=hash-mapping + size: 82472 + timestamp: 1777722955579 +- conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + sha256: 472fc587c63ec4f6eba0cc0b06008a6371e0a08a5986de3cf4e8024a47b4fe6c + md5: 0badf9c54e24cecfb0ad2f99d680c163 + depends: + - locket + - python >=3.9 + - toolz + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/partd?source=hash-mapping + run_exports: {} + size: 20884 + timestamp: 1715026639309 +- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a + md5: d0d408b1f18883a944376da5cf8101ea + depends: + - ptyprocess >=0.5 + - python >=3.9 + license: ISC + purls: + - pkg:pypi/pexpect?source=hash-mapping + size: 53561 + timestamp: 1733302019362 +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.0-pyhcf101f3_0.conda + sha256: 9e5e1fd3506ccfc4d444fc4d2d39b0ed097d5d0e3bd3d4bdf6bcc81aaf66860d + md5: 2c5ef45db85d34799771629bd5860fd7 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/platformdirs?source=compressed-mapping + run_exports: {} + size: 26308 + timestamp: 1779972894916 +- conda: https://conda.anaconda.org/conda-forge/noarch/polars-1.42.1-pyh58ad624_0.conda + sha256: 8b55f3021a72b098195ea9fddf125cd5a2e1bd288f8ae8d899e70c1d7b132b18 + md5: 4e315d6adb68e84890cd7a630b2f316f + depends: + - polars-runtime-32 ==1.42.1 + - python >=3.10 + - python + constrains: + - numpy >=1.16.0 + - pyarrow >=7.0.0 + - fastexcel >=0.9 + - openpyxl >=3.0.0 + - xlsx2csv >=0.8.0 + - connectorx >=0.3.2 + - deltalake >=1.0.0 + - pyiceberg >=0.7.1 + - altair >=5.4.0 + - great_tables >=0.8.0 + - polars-runtime-32 ==1.42.1 + - polars-runtime-64 ==1.42.1 + - polars-runtime-compat ==1.42.1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/polars?source=compressed-mapping + run_exports: {} + size: 542486 + timestamp: 1782814229638 +- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + sha256: 4d7ec90d4f9c1f3b4a50623fefe4ebba69f651b102b373f7c0e9dbbfa43d495c + md5: a11ab1f31af799dd93c3a39881528884 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/prometheus-client?source=hash-mapping + run_exports: {} + size: 57113 + timestamp: 1775771465170 +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + sha256: 4817651a276016f3838957bfdf963386438c70761e9faec7749d411635979bae + md5: edb16f14d920fb3faf17f5ce582942d6 + depends: + - python >=3.10 + - wcwidth + constrains: + - prompt_toolkit 3.0.52 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/prompt-toolkit?source=hash-mapping + size: 273927 + timestamp: 1756321848365 +- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 + md5: 7d9daffbb8d8e0af0f769dbbcd173a54 + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/ptyprocess?source=hash-mapping + size: 19457 + timestamp: 1733302371990 +- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 + md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pure-eval?source=hash-mapping + size: 16668 + timestamp: 1733569518868 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + sha256: e27e0473fc6723311a0bd48b89b616fa1b996a2f7a2b555338cbbcfb9c640568 + md5: 9c5491066224083c41b6d5635ed7107b + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pycparser?source=compressed-mapping + run_exports: {} + size: 55886 + timestamp: 1779293633166 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 + md5: 16c18772b340887160c79a6acc022db0 + depends: + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=hash-mapping + size: 893031 + timestamp: 1774796815820 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyjuliacall-0.9.35-pyhd8ed1ab_0.conda + sha256: 73e0bf1e2b9c24612ac8850c0c6c298c16324f9548ac02ef2f03db0a832bd6d8 + md5: d699038e854c0d076d8e3fd4b9e7c072 + depends: + - pyjuliapkg >=0.1.17,<0.2.dev0 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/juliacall?source=hash-mapping + size: 18713 + timestamp: 1780927238110 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyjuliapkg-0.1.24-pyhd8ed1ab_0.conda + sha256: 8f805d50934b91f60cd29fa6925019dd4373ddfcdc4896f0e35b9e820f0fad15 + md5: 73f111c43bad59426f302212c25dcead + depends: + - click >=8.0,<9.0 + - filelock >=3.16,<4.dev0 + - python >=3.10 + - semver >=3.0,<4.dev0 + - tomli >=2.0,<3.dev0 + - tomlkit >=0.13.3,<0.14 + license: MIT + license_family: MIT + purls: + - pkg:pypi/juliapkg?source=hash-mapping + size: 26573 + timestamp: 1780255423310 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + run_exports: {} + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/python-dateutil?source=hash-mapping + run_exports: {} + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + sha256: df9aa74e9e28e8d1309274648aac08ec447a92512c33f61a8de0afa9ce32ebe8 + md5: 23029aae904a2ba587daba708208012f + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fastjsonschema?source=hash-mapping + run_exports: {} + size: 244628 + timestamp: 1755304154927 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.6-h4df99d1_100.conda + sha256: 84c129bdd6abcecac42a948f2670d17fe735d02d3a5a483a9b1f1bc33ba38c28 + md5: 224f69f177eb5aae6c9a6052846bf609 + depends: + - cpython 3.14.6.* + - python_abi * *_cp314 + license: Python-2.0 + purls: [] + run_exports: {} + size: 49315 + timestamp: 1781254664376 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-4.1.0-pyhd8ed1ab_0.conda + sha256: a0dfe07d0bc1d8c47a38b79ad4a8eb1bc7b86fb33ee5293ebb45dfdc46191f4e + md5: 982ed0cbfc0fe09f25861e3d111e9717 + depends: + - python >=3.10 + - typing_extensions + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/python-json-logger?source=compressed-mapping + run_exports: {} + size: 19249 + timestamp: 1781036004580 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + sha256: e943f9c15a6bdba2e1b9f423ab913b3f6b02197b0ef9f8e6b7464d78b59965b9 + md5: f6ad7450fc21e00ecc23812baed6d2e4 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=hash-mapping + run_exports: {} + size: 146639 + timestamp: 1777068997932 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + build_number: 8 + sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 + md5: 0539938c55b6b1a59b560e843ad864a4 + constrains: + - python 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6989 + timestamp: 1752805904792 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 + md5: 870293df500ca7e18bedefa5838a22ab + depends: + - attrs >=22.2.0 + - python >=3.10 + - rpds-py >=0.7.0 + - typing_extensions >=4.4.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/referencing?source=hash-mapping + run_exports: {} + size: 51788 + timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + sha256: 1715246b19c9f85ee022933b4845f2fc14ac9184981b7b7d9b728bec8e9588da + md5: 4a85203c1d80c1059086ae860836ffb9 + depends: + - python >=3.10 + - certifi >=2023.5.7 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - urllib3 >=1.26,<3 + - python + constrains: + - chardet >=3.0.2,<8 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/requests?source=compressed-mapping + run_exports: {} + size: 68709 + timestamp: 1778851103479 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + sha256: 2e4372f600490a6e0b3bac60717278448e323cab1c0fecd5f43f7c56535a99c5 + md5: 36de09a8d3e5d5e6f4ee63af49e59706 + depends: + - python >=3.9 + - six + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3339-validator?source=hash-mapping + run_exports: {} + size: 10209 + timestamp: 1733600040800 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + sha256: 2a5b495a1de0f60f24d8a74578ebc23b24aa53279b1ad583755f223097c41c37 + md5: 912a71cc01012ee38e6b90ddd561e36f + depends: + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3986-validator?source=hash-mapping + run_exports: {} + size: 7818 + timestamp: 1598024297745 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + sha256: 70001ac24ee62058557783d9c5a7bbcfd97bd4911ef5440e3f7a576f9e43bc92 + md5: 7234f99325263a5af6d4cd195035e8f2 + depends: + - python >=3.9 + - lark >=1.2.2 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3987-syntax?source=hash-mapping + run_exports: {} + size: 22913 + timestamp: 1752876729969 +- conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + sha256: bea67173ed67c73cf16691ef72e58059492ac1ed1c880cfbeb6f1295c5add7d6 + md5: 8e7be844ccb9706a999a337e056606ab + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/semver?source=hash-mapping + size: 22532 + timestamp: 1767294175877 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + sha256: 59656f6b2db07229351dfb3a859c35e57cc8e8bcbc86d4e501bff881a6f771f1 + md5: 28eb91468df04f655a57bcfbb35fc5c5 + depends: + - __linux + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/send2trash?source=hash-mapping + run_exports: {} + size: 24108 + timestamp: 1770937597662 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/six?source=hash-mapping + run_exports: {} + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + sha256: dce518f45e24cd03f401cb0616917773159a210c19d601c5f2d4e0e5879d30ad + md5: 03fe290994c5e4ec17293cfb6bdce520 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/sniffio?source=hash-mapping + run_exports: {} + size: 15698 + timestamp: 1762941572482 +- conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + sha256: d1e3e06b5cf26093047e63c8cc77b70d970411c5cbc0cb1fad461a8a8df599f7 + md5: 0401a17ae845fa72c7210e206ec5647d + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/sortedcontainers?source=hash-mapping + run_exports: {} + size: 28657 + timestamp: 1738440459037 +- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.4-pyhd8ed1ab_0.conda + sha256: 2afa5fe9331c09b4c4689ddf6ace8fc16c837eae547c57dab325b844072fdd77 + md5: 9e21f087f087f805debe877d88e00a14 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/soupsieve?source=compressed-mapping + run_exports: {} + size: 38802 + timestamp: 1779635534390 +- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 + md5: b1b505328da7a6b246787df4b5a49fbc + depends: + - asttokens + - executing + - pure_eval + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/stack-data?source=hash-mapping + size: 26988 + timestamp: 1733569565672 +- conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + sha256: 6b549360f687ee4d11bf85a6d6a276a30f9333df1857adb0fe785f0f8e9bcd60 + md5: f88bb644823094f436792f80fba3207e + depends: + - python >=3.10 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/tblib?source=hash-mapping + run_exports: {} + size: 19397 + timestamp: 1762956379123 +- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + sha256: 6b6727a13d1ca6a23de5e6686500d0669081a117736a87c8abf444d60c1e40eb + md5: 17b43cee5cc84969529d5d0b0309b2cb + depends: + - __unix + - ptyprocess + - python >=3.10 + - tornado >=6.1.0 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/terminado?source=hash-mapping + run_exports: {} + size: 24749 + timestamp: 1766513766867 +- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + sha256: cad582d6f978276522f84bd209a5ddac824742fe2d452af6acf900f8650a73a2 + md5: f1acf5fdefa8300de697982bcb1761c9 + depends: + - python >=3.5 + - webencodings >=0.4 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/tinycss2?source=hash-mapping + run_exports: {} + size: 28285 + timestamp: 1729802975370 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd + md5: b5325cf06a000c5b14970462ff5e4d58 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=hash-mapping + size: 21561 + timestamp: 1774492402955 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda + sha256: f8d3b49c084831a20923f66826f30ecfc55a4cd951e544b7213c692887343222 + md5: 146402bf0f11cbeb8f781fa4309a95d3 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomlkit?source=hash-mapping + size: 38777 + timestamp: 1749127286558 +- conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + sha256: 4e379e1c18befb134247f56021fdf18e112fb35e64dd1691858b0a0f3bea9a45 + md5: c07a6153f8306e45794774cf9b13bd32 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/toolz?source=hash-mapping + run_exports: {} + size: 53978 + timestamp: 1760707830681 +- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.68.3-pyh8f84b5b_0.conda + sha256: 613d5cc47571c0b66e31265ff1e0fa5bef0e7b7670f33c67f5a2c587faf6e5d1 + md5: 65094960cb7ed216b6049aab57205902 + depends: + - python >=3.10 + - __unix + - python + constrains: + - envwrap >=0.2 + - ipywidgets >=6.0 + license: MPL-2.0 and MIT + purls: + - pkg:pypi/tqdm?source=compressed-mapping + run_exports: {} + size: 94965 + timestamp: 1781741268338 +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + sha256: b89a823edf524956b94a2a4db974866e4501f05c68976eff458c5dcf07f88431 + md5: 37e3be7b6e2977d37b8fa5da229f5dc0 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/traitlets?source=compressed-mapping + size: 115158 + timestamp: 1780507822178 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c + md5: edd329d7d3a4ab45dcf905899a7a6115 + depends: + - typing_extensions ==4.15.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + purls: [] + run_exports: {} + size: 91383 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51692 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + sha256: 3088d5d873411a56bf988eee774559335749aed6f6c28e07bf933256afb9eb6c + md5: f6d7aa696c67756a650e91e15e88223c + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/typing-utils?source=hash-mapping + run_exports: {} + size: 15183 + timestamp: 1733331395943 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 + md5: e7cb0f5745e4c5035a460248334af7eb + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/uri-template?source=hash-mapping + run_exports: {} + size: 23990 + timestamp: 1733323714454 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + sha256: feff959a816f7988a0893201aa9727bbb7ee1e9cec2c4f0428269b489eb93fb4 + md5: cbb88288f74dbe6ada1c6c7d0a97223e + depends: + - backports.zstd >=1.0.0 + - brotli-python >=1.2.0 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping + run_exports: {} + size: 103560 + timestamp: 1778188657149 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda + sha256: 5ddde23d65aecde7e8dac0b9d9c7821ead2b87a320d787f9e4288c0ee00fa332 + md5: 19c961dd9cab6c3e13cd195f0176dbfa + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/wcwidth?source=compressed-mapping + size: 133769 + timestamp: 1780932915297 +- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 + md5: 6639b6b0d8b5a284f027a2003669aa65 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/webcolors?source=hash-mapping + run_exports: {} + size: 18987 + timestamp: 1761899393153 +- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 + md5: 2841eb5bfc75ce15e9a0054b98dcd64d + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/webencodings?source=hash-mapping + run_exports: {} + size: 15496 + timestamp: 1733236131358 +- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + sha256: 42a2b61e393e61cdf75ced1f5f324a64af25f347d16c60b14117393a98656397 + md5: 2f1ed718fcd829c184a6d4f0f2e07409 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/websocket-client?source=hash-mapping + run_exports: {} + size: 61391 + timestamp: 1759928175142 +- conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.4.0-pyhc364b38_0.conda + sha256: 9f5f5905afea6e0188aa6887aed0809033143343a7af06983f4b390c2286d2d7 + md5: 099794df685f800c3f319ff4742dc1bb + depends: + - python >=3.11 + - numpy >=1.26 + - packaging >=24.2 + - pandas >=2.2 + - python + constrains: + - bottleneck >=1.4 + - cartopy >=0.23 + - cftime >=1.6 + - dask-core >=2024.6 + - distributed >=2024.6 + - flox >=0.9 + - h5netcdf >=1.3 + - h5py >=3.11 + - hdf5 >=1.14 + - iris >=3.9 + - matplotlib-base >=3.8 + - nc-time-axis >=1.4 + - netcdf4 >=1.6.0 + - numba >=0.60 + - numbagg >=0.8 + - pint >=0.24 + - pydap >=3.5.0 + - scipy >=1.13 + - seaborn-base >=0.13 + - sparse >=0.15 + - toolz >=0.12 + - zarr >=2.18 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/xarray?source=hash-mapping + run_exports: {} + size: 1017999 + timestamp: 1776122774298 +- conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + sha256: 663ea9b00d68c2da309114923924686ab6d3f59ef1b196c5029ba16799e7bb07 + md5: 4487b9c371d0161d54b5c7bbd890c0fc + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/xyzservices?source=hash-mapping + run_exports: {} + size: 51732 + timestamp: 1774900074457 +- conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + sha256: 5488542dceeb9f2874e726646548ecc5608060934d6f9ceaa7c6a48c61f9cc8d + md5: e52c2ef711ccf31bb7f70ca87d144b9e + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zict?source=hash-mapping + run_exports: {} + size: 36341 + timestamp: 1733261642963 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda + sha256: 210bd31c22bb88f5e2a167df24c95bb5f152b2ada7502f9b8c49d1f5366db423 + md5: ba3dcdc8584155c97c648ae9c044b7a3 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/zipp?source=compressed-mapping + run_exports: {} + size: 24190 + timestamp: 1779159948016 +- pypi: .. + name: linopy + requires_dist: + - numpy + - scipy + - bottleneck + - toolz + - numexpr + - xarray>=2024.2.0 + - dask>=0.18.0 + - polars>=1.31.1 + - tqdm + - deprecation + - packaging + - google-cloud-storage ; extra == 'oetc' + - requests ; extra == 'oetc' + - paramiko ; extra == 'remote' + - ipython==8.26.0 ; extra == 'docs' + - numpydoc==1.10.0 ; extra == 'docs' + - sphinx-rtd-theme==3.1.0 ; extra == 'docs' + - sphinx==9.0.4 ; extra == 'docs' + - sphinx-book-theme==1.2.0 ; extra == 'docs' + - sphinx-copybutton==0.5.2 ; extra == 'docs' + - nbsphinx==0.9.4 ; extra == 'docs' + - nbsphinx-link==1.3.0 ; extra == 'docs' + - docutils<0.21 ; extra == 'docs' + - numpy<2 ; extra == 'docs' + - gurobipy>=13.0.0 ; extra == 'docs' + - ipykernel==6.29.5 ; extra == 'docs' + - matplotlib==3.11.0 ; extra == 'docs' + - highspy>=1.7.1 ; extra == 'docs' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - mypy ; extra == 'dev' + - pre-commit ; extra == 'dev' + - netcdf4 ; extra == 'dev' + - paramiko ; extra == 'dev' + - types-paramiko ; extra == 'dev' + - types-requests ; extra == 'dev' + - gurobipy ; extra == 'dev' + - highspy ; extra == 'dev' + - jupyter ; extra == 'dev' + - highspy==1.13.1 ; extra == 'benchmarks' + - netcdf4==1.7.4 ; extra == 'benchmarks' + - numpy==2.4.6 ; extra == 'benchmarks' + - scipy==1.17.1 ; extra == 'benchmarks' + - xarray==2026.4.0 ; extra == 'benchmarks' + - pandas==3.0.3 ; extra == 'benchmarks' + - polars==1.41.2 ; extra == 'benchmarks' + - dask==2026.6.0 ; extra == 'benchmarks' + - pytest==9.1.1 ; extra == 'benchmarks' + - pytest-benchmark==5.2.3 ; extra == 'benchmarks' + - pytest-memray==1.8.0 ; extra == 'benchmarks' + - pytest-codspeed==5.0.3 ; extra == 'benchmarks' + - gurobipy ; extra == 'solvers' + - highspy>=1.5.0,!=1.14.0 ; python_full_version < '3.12' and extra == 'solvers' + - highspy>=1.7.1,!=1.14.0 ; python_full_version >= '3.12' and extra == 'solvers' + - cplex ; sys_platform != 'darwin' and extra == 'solvers' + - mosek ; extra == 'solvers' + - mindoptpy ; extra == 'solvers' + - coptpy!=7.2.1 ; extra == 'solvers' + - xpress ; sys_platform != 'darwin' and extra == 'solvers' + - pyscipopt ; sys_platform != 'darwin' and extra == 'solvers' + - knitro>=15.1.0 ; extra == 'solvers' + requires_python: '>=3.11' diff --git a/experiment/pixi.toml b/experiment/pixi.toml new file mode 100644 index 00000000..c988b699 --- /dev/null +++ b/experiment/pixi.toml @@ -0,0 +1,37 @@ +[workspace] +authors = ["Jonas Hoersch "] +channels = ["conda-forge", "bioconda"] +name = "experiment" +platforms = ["linux-64"] +version = "0.1.0" + +[tasks] + +[dependencies] +python = ">=3.14.5,<3.15" +pyjuliapkg = ">=0.1.24,<0.2" +ipython = ">=9.14.1,<10" +pyjuliacall = ">=0.9.35,<0.10" +# linopy's runtime deps, pinned as conda packages so the editable install +# below satisfies them from conda-forge instead of pulling wheels from pypi +numpy = "*" +scipy = "*" +bottleneck = "*" +toolz = "*" +numexpr = "*" +xarray = ">=2024.2.0" +dask = ">=0.18.0" +polars = ">=1.31.1" +tqdm = "*" +deprecation = "*" +packaging = "*" +# HiGHS Python bindings for the monolithic cross-check solve +highspy = "*" +nbconvert = ">=7.17.1,<8" +ipykernel = ">=7.3.0,<8" +jupyterlab = ">=4.6.1,<5" + +[pypi-dependencies] +# editable install of the repo's linopy (../) so we develop against the CSR +# `Model.matrices` of 0.8-dev, not the older conda linopy +linopy = { path = "..", editable = true } diff --git a/experiment/plasmo-benders-decomposition.ipynb b/experiment/plasmo-benders-decomposition.ipynb new file mode 100644 index 00000000..0925daac --- /dev/null +++ b/experiment/plasmo-benders-decomposition.ipynb @@ -0,0 +1,417 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Benders Decomposition with Plasmo.jl\n", + "\n", + "## Summary\n", + "\n", + "This notebook decomposes a linopy capacity-expansion model into a Benders master problem\n", + "plus one subproblem per planning year, using `linopy.contrib.plasmo` -- an experimental\n", + "bridge to [Plasmo.jl](https://github.com/plasmo-dev/Plasmo.jl) and\n", + "[PlasmoBenders.jl](https://github.com/plasmo-dev/PlasmoBenders.jl). It reimplements the\n", + "Python side of [Linopy2Plasmo.jl](https://github.com/leonardgoeke/Linopy2Plasmo.jl), driving\n", + "everything from Python instead of the Julia REPL.\n", + "\n", + "See [Benders Decomposition with Plasmo.jl](plasmo-benders.rst) in the user guide for the full\n", + "conceptual explanation (partitions, topologies, algorithms). This notebook is the worked\n", + "example: build a partition, decompose, solve with Benders, and cross-check the result\n", + "against a monolithic solve of the same model.\n", + "\n", + "**Requires a Julia installation** managed transparently by `juliacall`/`pyjuliapkg` -- see\n", + "the *Installing the Julia side* section of the linked page. The first run resolves and\n", + "precompiles the Julia packages, which can take a few minutes.\n", + "\n", + "## The model\n", + "\n", + "`testProblem.nc` is an energy-system capacity-expansion problem (ZEN-garden-style): it\n", + "decides technology `capacity` and `capacity_addition` per node and year, subject to energy\n", + "balance, carbon-budget, and technology constraints, minimizing `net_present_cost`. It has 3\n", + "planning years (`set_time_steps_yearly`) and several nodes (`set_location`) -- exactly the\n", + "two dimensions the decomposition below splits along." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "import linopy\n", + "from linopy.contrib.plasmo import Partition, PlasmoModel, benders, group, has, optimize" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Load and clean the model\n", + "\n", + "`testProblem.nc` stores a few constraint-term cells with a valid variable reference but a\n", + "`NaN` coefficient -- a padding artifact of its ragged term axis. We zero those coefficients,\n", + "then let `sanitize_zeros` drop the now-empty terms so the exported matrix has no `NaN` and no\n", + "empty rows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "def clean_model(m: linopy.Model) -> None:\n", + " for name in m.constraints:\n", + " con = m.constraints[name]\n", + " con._update_data(coeffs=con.coeffs.where(~np.isnan(con.coeffs), 0.0))\n", + " m.constraints.sanitize_zeros()\n", + "\n", + "\n", + "m = linopy.read_netcdf(\"testProblem.nc\")\n", + "clean_model(m)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "The model has 37 variables and 50 constraints (as linopy sees them -- each one an N-D array\n", + "of scalar variables/constraints; the flattened LP has tens of thousands of rows and columns,\n", + "see below). `capacity` is a representative decision variable, dimensioned over\n", + "technology/node/year:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "m.variables[\"capacity\"].dims, m.variables[\"capacity\"].shape" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Defining the partition\n", + "\n", + "A [`Partition`](plasmo-benders.rst) assigns every constraint to a decomposition\n", + "node. Here we want: constraints that don't vary by year or by node go to a `top` master\n", + "(cross-year coupling, like the carbon budget); everything else splits into one `sub` per\n", + "year. This mirrors Linopy2Plasmo.jl's example `split_tup`/`def_dic`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "partition = Partition(\n", + " top=~has(\"set_time_steps_yearly\") | ~has(\"set_nodes\"),\n", + " sub=group(\"set_time_steps_yearly\") & has(\"set_nodes\"),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "- `top`: constraints with **no** year dimension, or **no** node dimension -- e.g. the overall\n", + " carbon budget, which sums across all years and nodes.\n", + "- `sub`: constraints that have **both** a year and a node dimension, split into one node per\n", + " distinct year value (`group(\"set_time_steps_yearly\")`) -- e.g. the nodal energy balance,\n", + " evaluated separately per year.\n", + "\n", + "`~has(...)` negates a scalar predicate; `|` combines two scalars (allowed); `&` gates the\n", + "scattering `group(...)` predicate with `has(...)` so only node-and-year-dimensioned rows are\n", + "split by year. Declaration order matters: `top` is declared first, so it becomes the Benders\n", + "master." + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## Building the graph\n", + "\n", + "`PlasmoModel` only *plans* the partition on construction (cheap, pure Python/numpy); the\n", + "Julia `OptiGraph` is built lazily, streaming one node's LP block at a time so peak memory is\n", + "one block, not the whole decomposed problem." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "pm = PlasmoModel(m, partition)\n", + "pm" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "Four nodes came out: the `top` master plus `sub[0]`, `sub[1]`, `sub[2]` (one per year), linked\n", + "by 60 equality constraints on the variables shared between the master and each year's\n", + "subproblem (e.g. installed capacity carried over from one year to the next)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "%time pm.graph" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "## Interacting through Julia underneath" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "from juliacall import Main as jl\n", + "\n", + "jl.seval(\"using Plasmo\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "jl.all_subgraphs(pm.graph)" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## Solving with Benders\n", + "\n", + "`benders()` requires a flat topology (master and subproblems as siblings under the root --\n", + "the default), builds the graph on first use, and runs `PlasmoBenders.jl`'s\n", + "`BendersAlgorithm` to convergence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "benders(pm, warm_start=False, regularize=True)\n", + "obj_benders = pm.result().solution.objective\n", + "print(f\"Benders objective: {obj_benders:.6g}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "pm.result()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "m.assign_result(pm.result())" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "## Cross-checking against a monolithic solve\n", + "\n", + "As a correctness check, solve the same (undecomposed) model directly with HiGHS and compare\n", + "objectives.\n", + "\n", + "*(This particular model triggers an unrelated linopy bug when writing constraint duals back\n", + "onto a ragged dimension -- the objective is already assigned by the time that happens, so we\n", + "tolerate the exception here; it is independent of the Benders decomposition above.)*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "m2 = linopy.read_netcdf(\"testProblem.nc\")\n", + "clean_model(m2)\n", + "try:\n", + " m2.solve(solver_name=\"highs\")\n", + "except Exception as e: # noqa: BLE001\n", + " print(f\"(tolerated linopy dual-assignment bug: {e})\")\n", + "obj_monolithic = m2.objective.value\n", + "\n", + "rel = abs(obj_benders - obj_monolithic) / max(1.0, abs(obj_monolithic))\n", + "print(f\"Benders objective: {obj_benders:.6g}\")\n", + "print(f\"Monolithic objective: {obj_monolithic:.6g}\")\n", + "print(f\"relative difference: {rel:.2e}\")\n", + "assert rel < 1e-4, \"Benders objective does not match the monolithic solve\"" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "The objectives match to numerical tolerance -- the decomposed solve reproduces the\n", + "undecomposed one, as expected for a converged Benders run on a continuous LP." + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "## Reading back the solution\n", + "\n", + "`assign_to_model()` writes the decomposed solution back onto `m` via linopy's normal\n", + "`assign_result` path, so `m.solution` and `variable.solution` work as they would after\n", + "`m.solve()`. Constraint duals are left empty (Benders subproblems don't yield duals for the\n", + "original, undecomposed model)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "pm.assign_to_model()\n", + "m.solution[\"capacity\"]" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "## Monolithic solve as an alternative algorithm\n", + "\n", + "The same `PlasmoModel` graph also supports `optimize()`, which solves the whole graph at\n", + "once (every subgraph, links enforced as hard constraints) rather than via Benders iteration.\n", + "Unlike `benders()`, it works with any topology, not just a flat one -- useful as a quick\n", + "correctness check on the graph construction itself, independent of the Benders algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "pm2 = PlasmoModel(m, partition)\n", + "optimize(pm2)\n", + "pm2.result().solution.objective" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "m.solve(io_api=\"direct\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "## Limitations\n", + "\n", + "- Continuous variables only (inherited from PlasmoBenders.jl).\n", + "- One node per `Partition` cell -- no separate, finer variable-side partition yet.\n", + "- This experimental module lives in `linopy.contrib.plasmo` and is not covered by CI.\n", + "\n", + "## References\n", + "\n", + "- [Plasmo.jl documentation](https://plasmo-dev.github.io/Plasmo.jl/stable/)\n", + "- [PlasmoBenders.jl](https://github.com/plasmo-dev/PlasmoBenders.jl)\n", + "- [Linopy2Plasmo.jl](https://github.com/leonardgoeke/Linopy2Plasmo.jl) -- the original\n", + " Julia-only prototype this module reimplements the Python side of." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/experiment/run_benders.py b/experiment/run_benders.py new file mode 100644 index 00000000..62ddbfd9 --- /dev/null +++ b/experiment/run_benders.py @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: Contributors to linopy +# +# SPDX-License-Identifier: MIT +""" +Drive linopy.contrib.plasmo on testProblem.nc: partition into a Benders master + +per-year subproblems, solve with PlasmoBenders, and cross-check the objective +against a monolithic HiGHS solve. + +Run with: pixi run python run_benders.py +""" + +import numpy as np + +import linopy +from linopy.contrib.plasmo import Partition, PlasmoModel, benders, group, has + +MODEL = "testProblem.nc" + + +def clean_model(m: linopy.Model) -> None: + """ + testProblem.nc stores some term cells with a valid ``var`` but a NaN + ``coeff`` (a padding artifact of the ragged ``_term`` axis). Replace those + NaNs with 0, then let ``sanitize_zeros`` mask the now-zero terms so the + exported matrix is clean (no NaN, no empty constraint rows). + """ + for name in m.constraints: + con = m.constraints[name] + # replace NaN coeffs only (leave rhs/bounds, which may legitimately be + # +/-inf, untouched -- we only touch coefficients here) + con._update_data(coeffs=con.coeffs.where(~np.isnan(con.coeffs), 0.0)) + m.constraints.sanitize_zeros() + + +def main() -> None: + m = linopy.read_netcdf(MODEL) + clean_model(m) + + partition = Partition( + top=~has("set_time_steps_yearly") | ~has("set_nodes"), + sub=group("set_time_steps_yearly") & has("set_nodes"), + ) + + print("Solving with PlasmoBenders...") + pm = PlasmoModel(m, partition) + benders(pm) + obj_benders = pm.result().solution.objective + print(f"Benders objective: {obj_benders:.6g}") + + # monolithic reference. NB: on this model linopy's assign_result crashes + # when writing constraint duals back (a linopy bug unrelated to Benders); + # the objective value is set before that step, so tolerate it. + print("Solving monolithically (HiGHS)...") + m2 = linopy.read_netcdf(MODEL) + try: + m2.solve(solver_name="highs") + except Exception as e: # noqa: BLE001 + # objective.value is assigned before the failing dual write-back + print(f" (tolerated linopy dual-assignment bug: {e})") + print(f"Monolithic objective: {m2.objective.value:.6g}") + + rel = abs(obj_benders - m2.objective.value) / max(1.0, abs(m2.objective.value)) + print(f"relative difference: {rel:.2e}") + assert rel < 1e-4, "Benders objective does not match monolithic solve" + print("OK: objectives match") + + +if __name__ == "__main__": + main() diff --git a/experiment/testProblem.nc b/experiment/testProblem.nc new file mode 100644 index 00000000..75052c4b Binary files /dev/null and b/experiment/testProblem.nc differ diff --git a/linopy/contrib/__init__.py b/linopy/contrib/__init__.py new file mode 100644 index 00000000..d9dcfb1f --- /dev/null +++ b/linopy/contrib/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Contributors to linopy +# +# SPDX-License-Identifier: MIT +"""Contributed, optional linopy extensions.""" diff --git a/linopy/contrib/plasmo/LinopyPlasmo.jl b/linopy/contrib/plasmo/LinopyPlasmo.jl new file mode 100644 index 00000000..6b74e138 --- /dev/null +++ b/linopy/contrib/plasmo/LinopyPlasmo.jl @@ -0,0 +1,199 @@ +# SPDX-FileCopyrightText: Contributors to linopy +# +# SPDX-License-Identifier: MIT +# +# Build a Plasmo OptiGraph incrementally from the per-node LP blocks produced by +# linopy/contrib/plasmo/build.py, then run a decomposition algorithm on it. +# +# The Python side streams one block at a time (`add_block`) so it never holds all +# blocks at once; each block becomes one subgraph containing one OptiNode. Blocks +# are attached into a subgraph *tree* -- a block's `parent` is the root (0) or the +# 1-based index of an already-added node whose subgraph should contain it. This +# lets the caller build flat or nested topologies. +# +# All index arrays arrive from Python (numpy) 0-based; converted to 1-based on +# entry. Constraints are built by walking each block's CSR rows and adding one +# scalar affine constraint per row via the function form `add_constraint` (no +# macro, no ConstraintRefs stored). The affine expression element type must be +# `GenericAffExpr{Float64, NodeVariableRef}` -- an OptiNode's variables are +# NodeVariableRef, so `AffExpr(0.0)` would error. +# +# Loaded once from Python via `jl.seval('include(".../LinopyPlasmo.jl")')`; +# reached as `jl.LinopyPlasmo.`. Wrapping in a module keeps JuMP/Plasmo and +# these names out of `Main`. + +module LinopyPlasmo + +using JuMP +using Plasmo +using PlasmoBenders + +export GraphBuilder, new_builder, add_block!, add_links!, finalize! +export run_benders, run_optimize, node_values + +""" + GraphBuilder + +Holds the growing OptiGraph while blocks stream in. `subgraphs[k]` is node `k`'s +subgraph, `xs[k]` its variable vector (NodeVariableRef), both 1-based. +""" +mutable struct GraphBuilder + graph::OptiGraph + subgraphs::Vector{OptiGraph} + onodes::Vector{OptiNode} + xs::Vector{Any} +end + +new_builder() = GraphBuilder(OptiGraph(), OptiGraph[], OptiNode[], Any[]) + +""" + add_block!(b, parent, indptr, colval, nzval, sense_b, sense, lb, ub, c) + +Add one node's LP block. `parent` is 0 (attach the new subgraph under the root +graph) or the 1-based index of a previously added node (attach under that node's +subgraph, i.e. nest). All index arrays are 0-based. Returns the new node's index. +""" +function add_block!( + b::GraphBuilder, + parent::Integer, + name::AbstractString, + indptr::AbstractVector{<:Integer}, + colval::AbstractVector{<:Integer}, + nzval::AbstractVector{<:Real}, + rhs::AbstractVector{<:Real}, + sense::AbstractVector, + lb::AbstractVector{<:Real}, + ub::AbstractVector{<:Real}, + c::AbstractVector{<:Real}, +) + par = parent == 0 ? b.graph : b.subgraphs[parent] + sym = Symbol(name) + sg = add_subgraph(par; name=sym) + node = add_node(sg; label=sym) + ncols = length(lb) + + @variable(node, x[1:ncols]) + for j in 1:ncols + isfinite(lb[j]) && set_lower_bound(x[j], lb[j]) + isfinite(ub[j]) && set_upper_bound(x[j], ub[j]) + end + + # constraints: walk CSR rows, one scalar affine constraint per row + T = GenericAffExpr{Float64,eltype(x)} + for i in 1:length(rhs) + expr = zero(T) + # 0-based indptr: row i spans indptr[i]+1 .. indptr[i+1] (1-based) + for p in (indptr[i] + 1):indptr[i + 1] + add_to_expression!(expr, nzval[p], x[colval[p] + 1]) + end + s = sense[i] + set = s == "<" ? MOI.LessThan(rhs[i]) : + s == ">" ? MOI.GreaterThan(rhs[i]) : + MOI.EqualTo(rhs[i]) + add_constraint(node, build_constraint(error, expr, set)) + end + + # per-node objective: sum of owned coeffs * their variable + obj = zero(T) + for j in 1:ncols + iszero(c[j]) || add_to_expression!(obj, c[j], x[j]) + end + @objective(node, Min, obj) + + push!(b.subgraphs, sg) + push!(b.onodes, node) + push!(b.xs, x) + return length(b.subgraphs) +end + +""" + add_links!(b, owner, owner_col, other, other_col) + +Add equality links `x[owner] == x[other]` on the root graph. All 0-based. +""" +function add_links!( + b::GraphBuilder, + owner::AbstractVector{<:Integer}, + owner_col::AbstractVector{<:Integer}, + other::AbstractVector{<:Integer}, + other_col::AbstractVector{<:Integer}, +) + for l in 1:length(owner) + xo = b.xs[owner[l] + 1][owner_col[l] + 1] + xt = b.xs[other[l] + 1][other_col[l] + 1] + @linkconstraint(b.graph, xo == xt) + end + return nothing +end + +""" + finalize!(b) + +Roll node objectives up into every subgraph (recursively) and the root graph. +Without the per-subgraph roll-up, algorithms see feasibility-sense subgraphs. +Returns the root graph. +""" +function finalize!(b::GraphBuilder) + for sg in all_subgraphs(b.graph) + set_to_node_objectives(sg) + end + set_to_node_objectives(b.graph) + return b.graph +end + +""" + node_values(result, b, node) + +The solution values of every variable of 0-based `node`, as a `Vector{Float64}` +in local-column order. `result` is a `BendersAlgorithm` (Benders) or the graph +(after `run_optimize`) -- `Plasmo.value` (itself a `JuMP.value` extension) already +dispatches on either. Retrieving a whole node at once keeps read-back off the +Python per-variable loop. +""" +function node_values(result, b::GraphBuilder, node::Integer) + return Float64[Plasmo.value(result, x) for x in b.xs[node + 1]] +end + +# -- algorithms --------------------------------------------------------------- + +""" + run_benders(graph, master; solver, kwargs...) + +PlasmoBenders on `graph` with `master` the top subgraph. `kwargs` are passed +through to `BendersAlgorithm` verbatim (e.g. `max_iters`, `tol`, `regularize`, +`add_slacks`, ...; see `BendersAlgorithm`'s own docstring for the full list), +with `add_slacks=true` and `regularize=true` as defaults -- callers can +override either via `kwargs`. Returns the algorithm object (query values via +`node_values(alg, b, node)`). +""" +function run_benders(graph, master; solver, kwargs...) + alg = BendersAlgorithm( + graph, + master; + solver=solver, + add_slacks=true, + regularize=true, + kwargs..., + ) + run_algorithm!(alg) + return alg +end + +""" + run_optimize(graph; solver) + +Solve the whole graph monolithically (all subgraphs, links enforced). Query +values afterwards via `node_values(graph, b, node)`. +""" +function run_optimize(graph; solver) + set_optimizer(graph, solver) + optimize!(graph) + return graph +end + +# juliacall-friendly aliases: Python attribute access can't reach `!` names. +const add_block_b = add_block! +const add_links_b = add_links! +const finalize_b = finalize! + +end # module LinopyPlasmo diff --git a/linopy/contrib/plasmo/__init__.py b/linopy/contrib/plasmo/__init__.py new file mode 100644 index 00000000..f614ed91 --- /dev/null +++ b/linopy/contrib/plasmo/__init__.py @@ -0,0 +1,360 @@ +# SPDX-FileCopyrightText: Contributors to linopy +# +# SPDX-License-Identifier: MIT +""" +Decompose a linopy model onto a Plasmo.jl ``OptiGraph`` and solve it with a +graph-based algorithm (Benders, or a plain monolithic solve; more as Plasmo +grows). + +A :class:`Partition` over the model's *constraints* defines the decomposition +cells (:mod:`~linopy.contrib.plasmo.partition`); :class:`PlasmoModel` streams one +per-node LP block at a time into a Julia ``OptiGraph`` +(:mod:`~linopy.contrib.plasmo.build` + the ``LinopyPlasmo.jl`` module), arranged +into a subgraph tree by a :mod:`~linopy.contrib.plasmo.topology`. The *graph* is +the artifact; the *algorithm* is a separate free function over it, so the same +graph supports Benders, a flat solve, or (later) other Plasmo algorithms. + +Different algorithms constrain the topology: :func:`benders` requires a **flat** +topology (master and subproblems as sibling subgraphs under the root), while +:func:`optimize` works with any (flat or nested) topology. + +Only integer positions and float data cross to Julia, via shared numpy arrays +through juliacall. Continuous variables only (PlasmoBenders does not support +integers). + +Example: +------- +>>> import linopy +>>> from linopy.contrib.plasmo import Partition, has, group, solve_benders +>>> m = linopy.read_netcdf("testProblem.nc") # doctest: +SKIP +>>> partition = Partition( +... { # doctest: +SKIP +... "top": ~has("set_time_steps_yearly") | ~has("set_nodes"), +... "sub": group("set_time_steps_yearly") & has("set_nodes"), +... } +... ) +>>> solution = solve_benders(m, partition) # doctest: +SKIP + +For more control, build the model and pick the algorithm explicitly: + +>>> pm = PlasmoModel(m, partition) # doctest: +SKIP +>>> benders(pm) # or optimize(pm) +>>> pm.value("capacity") # a solution DataArray +""" + +from __future__ import annotations + +import os + +import numpy as np + +from linopy.constants import Result, Solution, Status +from linopy.contrib.plasmo.build import Plan +from linopy.contrib.plasmo.partition import ( + Partition, + Predicate, + by_size, + group, + has, + name, +) +from linopy.contrib.plasmo.topology import ManualTopology, Topology, _node_name, flat + +__all__ = [ + "Partition", + "Predicate", + "has", + "name", + "group", + "by_size", + "flat", + "PlasmoModel", + "optimize", + "benders", + "solve_benders", +] + +_MODULE_JL = os.path.join(os.path.dirname(__file__), "LinopyPlasmo.jl") + + +def _jl(): + """ + Import juliacall, ``include`` the LinopyPlasmo Julia module once, and return + ``(Main, LinopyPlasmo)``. + """ + from juliacall import Main as jl + + mod = getattr(_jl, "_mod", None) + if mod is None: + mod = jl.seval(f'include("{_MODULE_JL}")') # returns the module + _jl._mod = mod # type: ignore[attr-defined] + return jl, mod + + +class PlasmoModel: + """ + A linopy model built as a Plasmo ``OptiGraph``, ready for a graph algorithm. + + Construction is cheap (it only *plans* the partition); the Julia graph is + built lazily on first use, streaming one block at a time. With + ``topology="manual"`` the subgraph tree must be declared via + :meth:`add_subgraph` before the graph is built. + """ + + def __init__(self, model, partition: Partition, *, topology: Topology | str = None): + self._model = model + self._plan: Plan = Plan.from_model(model, partition) + if topology is None or topology == "flat": + self._topology: Topology = flat() + elif topology == "manual": + self._topology = ManualTopology(self._plan.node_keys) + elif isinstance(topology, Topology): + self._topology = topology + else: + raise ValueError(f"unknown topology {topology!r}") + self._jl = None + self._mod = None + self._builder = None # Julia GraphBuilder + self._result = None # algorithm object / graph for read-back + self._solved = False + + def __repr__(self) -> str: + plan = self._plan + graph = "built" if self._builder is not None else "not built" + solved = "solved" if self._solved else "not solved" + header = ( + f"PlasmoModel ({plan.n_nodes} nodes, {len(plan.links)} links, " + f"{graph}, {solved})" + ) + # Once built, self._parents is cached and authoritative; pass it along + # so describe() doesn't need to re-resolve (or re-raise) the topology. + parents = self._parents if self._builder is not None else None + + def label(i, key): + return ( + f"{_node_name(key)} " + f"({plan.node_n_vars(i)} vars, {plan.node_n_cons(i)} cons)" + ) + + tree = self._topology.describe(plan.node_keys, parents, label) + return "\n".join([header, tree]) + + # -- manual topology -------------------------------------------------- + + def add_subgraph(self, cns_node_name): + """ + (manual topology) Attach the cell with key head ``cns_node_name`` under + the root, returning a handle whose ``.add_subgraph`` nests further cells. + """ + if not isinstance(self._topology, ManualTopology): + raise TypeError("add_subgraph requires topology='manual'") + return self._topology._attach(cns_node_name, -1) + + # -- lazy build ------------------------------------------------------- + + def _build(self): + if self._builder is not None: + return + self._jl, self._mod = _jl() + jl, mod = self._jl, self._mod + parents = self._topology.parents(self._plan.node_keys) + self._parents = parents # -1 everywhere == flat + + builder = mod.new_builder() + # Stream blocks parents-first so a nested block's parent subgraph exists. + # jl_index[k] = 1-based Julia node index assigned to plan node k. + jl_index = np.zeros(self._plan.n_nodes, dtype=np.int64) + for block in self._plan.iter_blocks(order=_parents_first(parents)): + k = block.node + par = parents[k] + par_jl = 0 if par < 0 else int(jl_index[par]) + jl_index[k] = int( + mod.add_block_b( + builder, + par_jl, + _node_name(self._plan.node_keys[k]), + jl.Vector[jl.Int](block.indptr), + jl.Vector[jl.Int](block.colval), + block.nzval, + block.b, + jl.Vector[jl.String]([str(s) for s in block.sense]), + block.lb, + block.ub, + block.c, + ) + ) + del block # drop the Python copy before building the next + + # links: node indices already parallel int vectors; remap plan node -> + # Julia node (0-based) with a single fancy-index, no per-link Python loop. + lk = self._plan.links + if len(lk): + mod.add_links_b( + builder, + jl.Vector[jl.Int](jl_index[lk.owner_node] - 1), + jl.Vector[jl.Int](lk.owner_col), + jl.Vector[jl.Int](jl_index[lk.other_node] - 1), + jl.Vector[jl.Int](lk.other_col), + ) + mod.finalize_b(builder) + + self._builder = builder + self._jl_index = jl_index # plan node -> 1-based Julia node + + # -- graph handles (escape hatch) ------------------------------------ + + @property + def is_flat(self) -> bool: + """Whether every cell sits directly under the root (no nesting).""" + self._build() + return all(p < 0 for p in self._parents) + + @property + def graph(self): + """The Julia ``OptiGraph`` (builds it on first access).""" + self._build() + return self._builder.graph + + def master(self): + """The master subgraph (the first-declared node's subgraph).""" + self._build() + # plan node 0 is the master; find its Julia subgraph + return self._builder.subgraphs[self._jl_index[0] - 1] + + # -- read-back -------------------------------------------------------- + + def _record_result(self, alg): + self._result = alg + self._solved = True + + def result(self) -> Result: + """ + The solution as a :class:`linopy.constants.Result`. ``solution.primal`` + is a dense array indexed by variable label (``NaN`` where a variable was + not part of the decomposed LP). ``dual`` is left empty for now. + """ + if not self._solved: + raise RuntimeError("no solution yet -- run optimize(pm) or benders(pm)") + + mod = self._mod + n_labels = self._model.variables.label_index.label_to_pos.shape[0] + primal = np.full(n_labels, np.nan) + # Retrieve each node's full value vector on the Julia side (few nodes, + # not many variables), then scatter by that node's global labels. + for k in range(self._plan.n_nodes): + vals = np.asarray( + mod.node_values(self._result, self._builder, int(self._jl_index[k]) - 1) + ) + primal[self._plan.node_vlabels[k]] = vals + + status = Status.from_termination_condition("optimal") + mat = self._model.matrices # c is vlabels-position-indexed + objective = float(np.nansum(mat.c * primal[mat.vlabels])) + return Result( + status=status, solution=Solution(primal=primal, objective=objective) + ) + + def assign_to_model(self): + """ + Write the solution back onto the linopy model via ``assign_result`` so + ``model.solution`` and ``variable.solution`` work natively. Returns the + model. ``dual`` is empty, so no constraint duals are written. + """ + self._model.assign_result(self.result()) + return self._model + + def solution(self): + """The model's solution ``Dataset`` (assigns to the model first).""" + return self.assign_to_model().solution + + def value(self, var_name): + """Solution ``DataArray`` for a single variable.""" + return self.solution()[var_name] + + +def _parents_first(parents: list[int]) -> list[int]: + """Order node indices so every parent precedes its children.""" + order: list[int] = [] + placed = [False] * len(parents) + + def visit(k): + if placed[k]: + return + p = parents[k] + if p >= 0: + visit(p) + placed[k] = True + order.append(k) + + for k in range(len(parents)): + visit(k) + return order + + +# -- algorithms (free functions over a PlasmoModel) -------------------------- + + +def optimize(pm: PlasmoModel, *, solver: str = "highs") -> PlasmoModel: + """Solve the whole graph monolithically (all subgraphs, links hard).""" + pm._build() + jl, mod = pm._jl, pm._mod + jl.seval(f"using {_solver_pkg(solver)}") + opt = getattr(jl, _solver_pkg(solver)).Optimizer + graph = mod.run_optimize(pm.graph, solver=opt) + pm._record_result(graph) # solution lives on the graph backend + return pm + + +def benders( + pm: PlasmoModel, + *, + master=None, + solver: str = "highs", + max_iters: int = 1000, + **benders_options, +) -> PlasmoModel: + """ + Run PlasmoBenders with ``master`` (default: the first-declared node). + + ``max_iters`` and ``**benders_options`` are forwarded to + ``PlasmoBenders.jl``'s ``BendersAlgorithm`` (e.g. ``tol``, ``regularize``, + ``add_slacks``, ``multicut``, ``strengthened``, ``warm_start``, ...) -- + see the `BendersAlgorithm docs + `_ + for the full list. + ``add_slacks=True`` and ``regularize=True`` are linopy's defaults (unlike + PlasmoBenders' own, both ``False``); pass either as ``False`` to override. + """ + pm._build() + if not pm.is_flat: + raise ValueError( + "benders requires a flat topology: the master and subproblems must be " + "sibling subgraphs under the root, but this model nests subgraphs. " + "Rebuild with the default topology=flat() for Benders (nested " + "topologies work with optimize())." + ) + jl, mod = pm._jl, pm._mod + jl.seval(f"using {_solver_pkg(solver)}") + opt = getattr(jl, _solver_pkg(solver)).Optimizer + alg = mod.run_benders( + pm.graph, + pm.master() if master is None else master, + solver=opt, + max_iters=max_iters, + **benders_options, + ) + pm._record_result(alg) + return pm + + +def solve_benders( + model, partition: Partition, *, solver="highs", max_iters=1000, **benders_options +): + """Convenience: build, run Benders, return the solution dict.""" + pm = PlasmoModel(model, partition) + benders(pm, solver=solver, max_iters=max_iters, **benders_options) + return pm.solution() + + +def _solver_pkg(solver: str) -> str: + return {"highs": "HiGHS", "gurobi": "Gurobi"}[solver.lower()] diff --git a/linopy/contrib/plasmo/build.py b/linopy/contrib/plasmo/build.py new file mode 100644 index 00000000..0ecb3bbd --- /dev/null +++ b/linopy/contrib/plasmo/build.py @@ -0,0 +1,240 @@ +# SPDX-FileCopyrightText: Contributors to linopy +# +# SPDX-License-Identifier: MIT +""" +Turn a linopy model plus a constraint :class:`~linopy.contrib.plasmo.partition.Partition` +into the flat, per-node arrays that ``LinopyPlasmo.jl`` needs to build a Plasmo +``OptiGraph``. + +Two stages, kept separate so blocks can be *streamed* into Julia: + +- :meth:`Plan.from_model` -- the cheap global part. Assigns constraints to nodes, + derives the node x variable incidence, the per-node variable membership, the + linking equalities, and the owner of each variable. Holds only index data and + one bool incidence matrix, never the float CSR blocks. +- :meth:`Plan.iter_blocks` -- a *generator* that slices one :class:`NodeBlock` out of + the constraint matrix at a time (cheap CSR row-slice + column remap, never + densified). The caller consumes each block (hands it to Julia) and drops it + before the next is built, so peak Python memory is one block, not all of them. + +Everything here is numpy/scipy; no Julia. Strings never cross to Julia -- only +integer positions and float data. +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + +import numpy as np +from scipy.sparse import coo_array + +from linopy.contrib.plasmo.partition import NodeKey, Partition + + +@dataclass +class NodeBlock: + """One node's local LP block, all arrays in node-local column indexing.""" + + node: int # node index (into plan.node_keys) + # CSR of this node's constraint rows over this node's variable columns + indptr: np.ndarray # int64, len n_rows+1 + colval: np.ndarray # int64, node-local column of each nonzero + nzval: np.ndarray # float64 + b: np.ndarray # float64, len n_rows + sense: np.ndarray # 'U1' one of '<' '=' '>', len n_rows + lb: np.ndarray # float64, len n_cols + ub: np.ndarray # float64, len n_cols + c: np.ndarray # float64, len n_cols, objective coeff (0 unless owned here) + vlabels: np.ndarray # int64, global variable label of each local column + + +@dataclass +class Links: + """ + All cross-node equality links ``x[owner] == x[other]`` as parallel int + vectors (node-local columns), ready to hand to Julia without per-link + Python iteration. + """ + + owner_node: np.ndarray # int64, node index of the owner instance + owner_col: np.ndarray # int64, node-local column in owner_node + other_node: np.ndarray # int64 + other_col: np.ndarray # int64 + + def __len__(self) -> int: + return len(self.owner_node) + + +@dataclass +class Plan: + """ + The cheap, global result of partitioning -- everything except the per-node + float blocks (which :meth:`iter_blocks` streams). Small: index data + links. + """ + + node_keys: list[NodeKey] # node index -> key; node_keys[0] is the master + n_vars: int # global active variable count + links: Links # cross-node equality links (star from owner) + # per node: global variable label of each node-local column, so the solution + # read back from Julia (node-local vectors) can be scattered by label. + node_vlabels: list[np.ndarray] + + # -- internal, consumed by iter_blocks (kept here to avoid recomputation) -- + _A: object # cleaned CSR constraint matrix + _node_of_cns: np.ndarray # node index per constraint row + _node_vars: list[np.ndarray] # per node: sorted global var columns + _owner_node: np.ndarray # global var -> owner node index + _b: np.ndarray + _sense: np.ndarray + _lb: np.ndarray + _ub: np.ndarray + _c: np.ndarray + _vlabels: np.ndarray + + @property + def n_nodes(self) -> int: + return len(self.node_keys) + + def node_n_cons(self, node: int) -> int: + """Number of constraint rows assigned to ``node``.""" + return int(np.count_nonzero(self._node_of_cns == node)) + + def node_n_vars(self, node: int) -> int: + """Number of variable columns (local or linked) touching ``node``.""" + return len(self.node_vlabels[node]) + + @classmethod + def from_model(cls, model, partition: Partition) -> Plan: + """ + Assign constraints to nodes and derive membership, links and owners. + + Does not slice any per-node block -- see :meth:`iter_blocks`. + """ + mat = model.matrices + A = mat.A # CSR, rows = clabels order, cols = vlabels order + if A is None: + raise ValueError("Model has no constraints to decompose.") + n_vars = A.shape[1] + + node_of_cns, node_keys = partition.assign(model) + b, sense = mat.b, mat.sense + + n_nodes = len(node_keys) + + # -- node x variable incidence (see README "Variable membership & linking") + nnz_node = np.repeat(node_of_cns, np.diff(A.indptr)) + M = coo_array( + (np.ones(A.nnz, dtype=bool), (nnz_node, A.indices)), + shape=(n_nodes, n_vars), + ).tocsr() + Mc = M.tocsc() + deg = np.diff(Mc.indptr) # number of nodes each variable appears in + + c_full = mat.c # objective coeff aligned to vlabels + # A variable with a nonzero cost but no constraint has nowhere to be + # placed for Benders -- reject. Cost-free unconstrained variables are + # inert (dropped: absent from every node, NaN in the solution). + orphan = (deg == 0) & (c_full != 0.0) + if orphan.any(): + raise ValueError( + f"{int(orphan.sum())} variable(s) carry an objective coefficient " + "but appear in no constraint, so cannot be assigned to a node." + ) + + # -- owner node per variable: earliest node it appears in. CSC stores + # each column's row (node) indices ascending, so the first stored entry + # is the min. Node order places the master at 0, so this is "earliest in + # declared order". deg==0 variables get -1 (no owner; dropped). + owner_node = np.full(n_vars, -1, dtype=np.int64) + present = deg > 0 + owner_node[present] = Mc.indices[Mc.indptr[:-1][present]] + + # -- per-node membership: rows of M give each node's (sorted) columns + node_vars = [M.indices[M.indptr[k] : M.indptr[k + 1]] for k in range(n_nodes)] + node_vlabels = [mat.vlabels[cols] for cols in node_vars] + + # -- links: every (node, var) incidence where the node is not the var's + # owner links back to the owner. Vectorised over all Mc entries. + entry_node = Mc.indices # node of each (var, node) incidence + entry_var = np.repeat(np.arange(n_vars), deg) # its variable + entry_owner = owner_node[entry_var] + is_other = entry_node != entry_owner # skip the owner's own entry + ln_var = entry_var[is_other] + ln_owner = entry_owner[is_other] + ln_other = entry_node[is_other] + links = Links( + owner_node=ln_owner, + owner_col=_local_cols(node_vars, ln_owner, ln_var), + other_node=ln_other, + other_col=_local_cols(node_vars, ln_other, ln_var), + ) + + return cls( + node_keys=node_keys, + n_vars=n_vars, + links=links, + node_vlabels=node_vlabels, + _A=A, + _node_of_cns=node_of_cns, + _node_vars=node_vars, + _owner_node=owner_node, + _b=b, + _sense=sense, + _lb=mat.lb, + _ub=mat.ub, + _c=c_full, + _vlabels=mat.vlabels, + ) + + def iter_blocks(self, order: list[int] | None = None) -> Iterator[NodeBlock]: + """ + Yield one :class:`NodeBlock` at a time, in ``order`` (default: node-index + order, master first). ``order`` lets the caller stream parents before + children for nested topologies. + + Each block is sliced fresh from the constraint matrix and yielded; the + caller is expected to consume it (hand it to Julia) before requesting the + next, so only one block's float data is alive at a time. + """ + A = self._A + for k in range(self.n_nodes) if order is None else order: + cols = self._node_vars[k] # sorted global columns of this node + + rows = np.flatnonzero(self._node_of_cns == k) + Ablk = A[rows] # CSR row-slice + # remap global columns to node-local via binary search (cols sorted) + colval = np.searchsorted(cols, Ablk.indices) + + # objective coeff on this node: only for variables OWNED here + c_local = np.zeros(cols.size, dtype=np.float64) + owned_mask = self._owner_node[cols] == k + c_local[owned_mask] = self._c[cols[owned_mask]] + + yield NodeBlock( + node=k, + indptr=Ablk.indptr.astype(np.int64), + colval=colval.astype(np.int64), + nzval=Ablk.data.astype(np.float64), + b=self._b[rows].astype(np.float64), + sense=self._sense[rows].astype("U1"), + lb=self._lb[cols].astype(np.float64), + ub=self._ub[cols].astype(np.float64), + c=c_local, + vlabels=self._vlabels[cols].astype(np.int64), + ) + + +def _local_cols( + node_vars: list[np.ndarray], nodes: np.ndarray, gvars: np.ndarray +) -> np.ndarray: + """ + Node-local column of each ``gvars[i]`` within node ``nodes[i]``. Each node's + ``node_vars`` is sorted, so the local column is a binary search. Grouped by + node to search each node's column set at once. + """ + out = np.empty(len(nodes), dtype=np.int64) + for k in np.unique(nodes): + sel = nodes == k + out[sel] = np.searchsorted(node_vars[k], gvars[sel]) + return out diff --git a/linopy/contrib/plasmo/partition.py b/linopy/contrib/plasmo/partition.py new file mode 100644 index 00000000..241b603a --- /dev/null +++ b/linopy/contrib/plasmo/partition.py @@ -0,0 +1,452 @@ +# SPDX-FileCopyrightText: Contributors to linopy +# +# SPDX-License-Identifier: MIT +""" +Partition algebra for decomposing a linopy model into Plasmo graph nodes. + +A :class:`Partition` is an ordered mapping ``{node_name: predicate}`` over the +*constraints* of a model. Each constraint is assigned to the first node whose +predicate matches it (first-match-wins), so the partition is disjoint and +exhaustive over constraints. Variables are *not* partitioned here; their node +membership is derived from the constraints that reference them (see +:mod:`linopy.contrib.plasmo.build`), and a variable appearing in more than one +node becomes a linking variable. + +Predicates are built from atoms composed with ``~ & |``: + +- ``has(dim)`` -- scalar: the constraint is dimensioned over ``dim``. +- ``name(cns)`` -- scalar: the constraint has this linopy name. +- ``group(dim)`` -- scattering: label = the coordinate value along ``dim``; + fans the node into one sub-node per distinct value. +- ``by_size(dim, n)`` -- scattering: label = position-along-``dim`` // ``n``; + buckets a fine dimension into slices. + +A *scalar* predicate answers yes/no per constraint. A *scattering* predicate, +when it matches, additionally carries a label so the node expands into +``name[label]``. Combining scatterers with ``&`` crosses their labels; +combining a scalar with a scatterer lets the scalar *gate* (filter out +non-matching rows) while the scatterer labels the rest. + +Implementation note (axis-separable evaluation) +----------------------------------------------- +A predicate over a constraint's ``labels`` grid is separable *by axis*: +``has``/``name`` ignore coordinates entirely; ``group``/``by_size`` each depend +on a single axis. So each atom returns one *per-axis* selector -- either ``ALL`` +(every position on that axis matches, no label) or a length-``dim_size`` object +array of labels (``NOMATCH`` where a position fails). These are compounded into +the full grid only at the end, and mapped to CSR rows via the constraint's own +``labels`` grid (ravel, drop ``-1``). This makes an atom O(sum of dim sizes) +rather than O(active rows), a large speedup on fine grids. + +The trade-off: only *rectangular* selections are representable per axis. ``&`` +of rectangles is a rectangle (intersect per axis), but ``~scatter`` and +``scatter | scatter`` are unions/complements that are not single rectangles -- +these raise. This is not a real restriction for decomposition: a block is +coupled to the master along index boundaries, which are rectangles; disjoint +unions of index regions belong in *separate* nodes (expressed by scattering into +``name[label]``), not merged into one. ``|`` of *scalars* (e.g. +``~has(a) | ~has(b)`` for the master) stays rectangular and is supported. +""" + +from __future__ import annotations + +from collections.abc import Hashable +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd + +if TYPE_CHECKING: + from linopy.model import Model + +NodeKey = Hashable + + +# Sentinel label meaning "this position does not match". Distinct from a real +# label; ``True`` is the trivial "matched, no label" label used by scalars. +class _NoMatch: + _singleton: _NoMatch | None = None + + def __new__(cls) -> _NoMatch: + if cls._singleton is None: + cls._singleton = super().__new__(cls) + return cls._singleton + + def __repr__(self) -> str: + return "NOMATCH" + + +NOMATCH = _NoMatch() + + +# Per-axis selector sentinel: this axis places no constraint and carries no +# label -- every position on it matches. (Distinct from a length-n label array.) +class _All: + _singleton: _All | None = None + + def __new__(cls) -> _All: + if cls._singleton is None: + cls._singleton = super().__new__(cls) + return cls._singleton + + def __repr__(self) -> str: + return "ALL" + + +ALL = _All() + +# An AxisResult is either ``None`` (predicate matches no row of the constraint) +# or a ``dict[str, selector]`` mapping each dim to a selector that is ``ALL`` or +# a 1-D object array of length dim_size (entries: NOMATCH, or a hashable label). +# A missing dim key is treated as ``ALL``. + + +class Predicate: + """Base class for constraint-selection predicates. Compose with ``~ & |``.""" + + def _eval_axes( + self, model: Model, name: str, dims: tuple[str, ...] + ) -> dict[str, object] | None: + """ + Return per-axis selectors for one constraint, or ``None`` if the + predicate matches no row of it at all. + + Each returned dict maps a subset of ``dims`` to a selector; a missing dim + or an ``ALL`` value means "all positions on that axis". Non-``ALL`` values + are length-``dim_size`` object arrays of labels / ``NOMATCH``. + """ + raise NotImplementedError + + @property + def is_scattering(self) -> bool: + """ + Whether this predicate carries per-position labels (``group``/``by_size`` + or any combination containing one). Scattering predicates select + *rectangles with labels*; their negation/union is not a single rectangle, + so ``~`` and ``|`` reject them (see module docstring). + """ + return False + + def __invert__(self) -> Predicate: + if self.is_scattering: + raise TypeError( + f"cannot apply ~ to the scattering predicate {self!r}: the " + "complement of a group()/by_size() selection is not a single " + "rectangle. Negate the scalar part instead (e.g. " + "~has(dim), not ~group(dim))." + ) + return _Not(self) + + def __and__(self, other: Predicate) -> Predicate: + return _And(self, other) + + def __or__(self, other: Predicate) -> Predicate: + if self.is_scattering and other.is_scattering: + raise TypeError( + f"cannot apply | to two scattering predicates ({self!r} | " + f"{other!r}): their union is not a single rectangle. Put disjoint " + "index regions in separate nodes (scatter into name[label]) " + "rather than OR-ing them into one; | is only for scalars " + "(has/name), e.g. ~has(a) | ~has(b)." + ) + return _Or(self, other) + + +@dataclass(frozen=True) +class _Has(Predicate): + dim: str + + def _eval_axes(self, model, name, dims): + return {} if self.dim in dims else None + + +@dataclass(frozen=True) +class _Name(Predicate): + cns: str + + def _eval_axes(self, model, name, dims): + return {} if name == self.cns else None + + +@dataclass(frozen=True) +class _Group(Predicate): + dim: str + + is_scattering = True + + def _eval_axes(self, model, name, dims): + if self.dim not in dims: + return None + index = model.constraints[name].labels.get_index(self.dim) + # label = coordinate value along dim (kept as-is: str or int) + return {self.dim: np.asarray(index, dtype=object)} + + +@dataclass(frozen=True) +class _BySize(Predicate): + dim: str + n: int + + is_scattering = True + + def _eval_axes(self, model, name, dims): + if self.dim not in dims: + return None + index = model.constraints[name].labels.get_index(self.dim) + codes = pd.factorize(np.asarray(index), sort=True)[0] + return {self.dim: (codes // self.n).astype(object)} + + +@dataclass(frozen=True) +class _Not(Predicate): + inner: Predicate + + def _eval_axes(self, model, name, dims): + inner = self.inner._eval_axes(model, name, dims) + if inner is None: + # inner matched nothing -> negation matches everything + return {} + # A negated scatterer is a complement that is not axis-separable; only a + # pure scalar (all axes ALL) has a rectangular complement (= nothing). + if any(v is not ALL for v in inner.values()): + raise ValueError( + "~ of a scattering predicate (group/by_size) is not supported." + ) + return None + + +def _cross_axis(sa, sb): + """AND two per-axis label selectors position-wise, crossing their labels.""" + ma, mb = sa != NOMATCH, sb != NOMATCH + both = ma & mb + out = np.full(len(sa), NOMATCH, dtype=object) + for i in np.flatnonzero(both): + lbls = [x for x in (sa[i], sb[i]) if x is not True] + out[i] = True if not lbls else lbls[0] if len(lbls) == 1 else tuple(lbls) + return out, both.any() + + +@dataclass(frozen=True) +class _And(Predicate): + left: Predicate + right: Predicate + + @property + def is_scattering(self) -> bool: + return self.left.is_scattering or self.right.is_scattering + + def _eval_axes(self, model, name, dims): + a = self.left._eval_axes(model, name, dims) + if a is None: + return None + b = self.right._eval_axes(model, name, dims) + if b is None: + return None + merged: dict[str, object] = {} + for d in dims: + sa = a.get(d, ALL) + sb = b.get(d, ALL) + if sa is ALL and sb is ALL: + continue + if sa is ALL: + merged[d] = sb + elif sb is ALL: + merged[d] = sa + else: + crossed, nonempty = _cross_axis(sa, sb) + if not nonempty: + return None + merged[d] = crossed + return merged + + +@dataclass(frozen=True) +class _Or(Predicate): + left: Predicate + right: Predicate + + def _eval_axes(self, model, name, dims): + a = self.left._eval_axes(model, name, dims) + b = self.right._eval_axes(model, name, dims) + if a is None: + return b + if b is None: + return a + # OR of two rectangles is a single rectangle only when one side is a + # pure scalar (all-or-nothing): a scalar-True side matches every row, so + # the union is everything. + if all(v is ALL for v in a.values()) or all(v is ALL for v in b.values()): + return {} + raise ValueError( + "| of two scattering predicates is not supported " + "(their union is not a single rectangle)." + ) + + +# -- public atom constructors ------------------------------------------------ + + +def has(dim: str) -> Predicate: + """Scalar: constraint is dimensioned over ``dim``.""" + return _Has(dim) + + +def name(cns: str) -> Predicate: + """Scalar: constraint has the linopy name ``cns``.""" + return _Name(cns) + + +def group(dim: str) -> Predicate: + """Scattering: one sub-node per distinct coordinate value along ``dim``.""" + return _Group(dim) + + +def by_size(dim: str, n: int) -> Predicate: + """Scattering: bucket ``dim`` into slices of ``n`` consecutive positions.""" + return _BySize(dim, n) + + +# -- the Partition ------------------------------------------------------------ + + +def _compound_labels(res, dims, shape, active_pos): + """ + Compound per-axis selectors into a flat label array over the *active* grid + positions. Returns an object array (len == active_pos.size): ``NOMATCH`` + where a row is not selected, else the compounded label (``True`` if no + per-axis labels contributed). + """ + out = np.full(active_pos.size, True, dtype=object) + fail = np.zeros(active_pos.size, dtype=bool) + multi = np.unravel_index(active_pos, shape) # per-axis index of each row + + for ax, d in enumerate(dims): + sel = res.get(d, ALL) + if sel is ALL: + continue + per_pos = sel[multi[ax]] # label / NOMATCH for each active row on axis + axis_fail = per_pos == NOMATCH + fail |= axis_fail + for i in np.flatnonzero(~axis_fail): + lb = per_pos[i] + if lb is True: + continue + la = out[i] + if la is True: + out[i] = lb + elif isinstance(la, tuple): + out[i] = (*la, lb) + else: + out[i] = (la, lb) + + out[fail] = NOMATCH + return out + + +class Partition: + """ + Ordered mapping ``{node_name: predicate}`` over a model's constraints. + + First-match-wins: every constraint is assigned to the first node whose + predicate matches it. A scattering predicate expands its node into + ``name[label]``. A constraint matching no node raises. + + Construct with named nodes as keyword arguments (declaration order is the + match order, so the first is the Benders master):: + + Partition( + top=~has("year") | ~has("nodes"), + sub=group("year") & has("nodes"), + ) + + A single ``dict`` positional argument is also accepted (useful when node + names are not valid Python identifiers). + """ + + def __init__( + self, + nodes: dict[str, Predicate] | None = None, + /, + **named: Predicate, + ): + if nodes is not None and named: + raise TypeError("pass nodes either as a dict or as keywords, not both") + self.nodes: dict[str, Predicate] = dict(nodes) if nodes is not None else named + if not self.nodes: + raise ValueError("Partition needs at least one node") + + def assign(self, model: Model) -> tuple[np.ndarray, list[NodeKey]]: + """ + Assign every active constraint to a node. + + Returns + ------- + node_of_cns : numpy.ndarray of int, shape (n_active_cons,) + Node index per constraint, aligned to + ``model.constraints.label_index.clabels`` order (= CSR rows). + node_keys : list of NodeKey + ``node_of_cns[i]`` indexes into this list. Order is the discovery + order: nodes appear in partition declaration order, and within a + scattering node in first-seen label order. So ``node_keys[0]`` is + the first-declared node's first key -- the Benders master. + """ + cli = model.constraints.label_index + clabels = cli.clabels + label_to_pos = cli.label_to_pos # constraint label -> CSR row + + node_of_cns = np.full(len(clabels), -1, dtype=np.int64) + node_keys: list[NodeKey] = [] + key_to_idx: dict[NodeKey, int] = {} + + def key_index(key: NodeKey) -> int: + idx = key_to_idx.get(key) + if idx is None: + idx = len(node_keys) + key_to_idx[key] = idx + node_keys.append(key) + return idx + + for cname in model.constraints: + con = model.constraints[cname] + labels = con.labels + if labels.size == 0: + continue + dims = labels.dims + shape = labels.shape + + # Active rows in grid order: take the labels grid, ravel it, keep the + # non -1 entries; those label values map straight to CSR rows. + lab_flat = labels.values.ravel() + active_pos = np.flatnonzero(lab_flat != -1) + if active_pos.size == 0: + continue + rows = label_to_pos[lab_flat[active_pos]] + + taken = np.zeros(active_pos.size, dtype=bool) + for node_name, pred in self.nodes.items(): + res = pred._eval_axes(model, cname, dims) + if res is None: + continue + labels_flat = _compound_labels(res, dims, shape, active_pos) + sel = (labels_flat != NOMATCH) & ~taken + if not sel.any(): + continue + for i in np.flatnonzero(sel): + lab = labels_flat[i] + key: NodeKey = node_name if lab is True else (node_name, lab) + node_of_cns[rows[i]] = key_index(key) + taken |= sel + if taken.all(): + break + + if (node_of_cns == -1).any(): + n = int((node_of_cns == -1).sum()) + missing = clabels[node_of_cns == -1][:5] + raise ValueError( + f"{n} constraint(s) matched no partition node " + f"(labels {missing.tolist()}...). The partition must be " + f"exhaustive over constraints." + ) + + return node_of_cns, node_keys diff --git a/linopy/contrib/plasmo/topology.py b/linopy/contrib/plasmo/topology.py new file mode 100644 index 00000000..633b1650 --- /dev/null +++ b/linopy/contrib/plasmo/topology.py @@ -0,0 +1,157 @@ +# SPDX-FileCopyrightText: Contributors to linopy +# +# SPDX-License-Identifier: MIT +""" +Subgraph topology for a :class:`~linopy.contrib.plasmo.PlasmoModel`. + +The partition produces *cells* -- one node per partition assignment, each holding +an LP block. A :class:`Topology` decides how those cells are arranged into the +Plasmo subgraph tree: which cell's subgraph each other cell is nested under. +There are no empty container subgraphs -- every subgraph holds exactly one cell's +OptiNode, and nesting means "attach these cells directly under that cell". + +A topology resolves, given the ordered ``node_keys``, a ``parent`` node index per +cell (``-1`` for "attach under the root graph"). That vector drives the streaming +builder (:func:`linopy.contrib.plasmo.build.iter_blocks` -> ``add_block!``). + +- :func:`flat` -- every cell is a sibling under the root. The default. +- ``"manual"`` -- build the tree yourself: ``pm.add_subgraph(name)`` returns a + handle for the (single) cell with that key; ``handle.add_subgraph(name)`` + attaches every cell whose key starts with ``name`` under that handle's cell. + Chaining is restricted to *singular* receivers (a handle that resolved to + exactly one cell); nesting under a multi-cell level raises. +""" + +from __future__ import annotations + +from linopy.contrib.plasmo.partition import NodeKey + + +def _key_head(key: NodeKey): + """The selector head of a node key: the whole scalar key, or its first slot.""" + return key[0] if isinstance(key, tuple) else key + + +def _node_name(key: NodeKey) -> str: + """Human-readable subgraph/node name from a partition node key.""" + if isinstance(key, tuple): + head, *rest = key + return f"{head}[{', '.join(map(str, rest))}]" + return str(key) + + +class Topology: + """Base: resolve ``node_keys`` to a ``parent`` node index per cell.""" + + def parents(self, node_keys: list[NodeKey]) -> list[int]: + raise NotImplementedError + + def describe( + self, + node_keys: list[NodeKey], + parents: list[int] | None = None, + label=None, + ) -> str: + """ + Render the subgraph tree as indented ``- name`` lines, one per cell. + + ``parents`` lets the caller pass an already-resolved (e.g. cached, + post-build) parent vector; otherwise this calls :meth:`parents` + itself, falling back to naming the topology class if that raises + (e.g. an incomplete :class:`ManualTopology`). ``label(i, key)`` formats + each cell (default: just its :func:`_node_name`) -- pass one to add + e.g. per-node variable/constraint counts without this module needing + to know about them. + """ + if parents is None: + try: + parents = self.parents(node_keys) + except Exception: + return f"topology: {type(self).__name__} (not resolved yet)" + + if label is None: + label = lambda i, key: _node_name(key) # noqa: E731 + + children: dict[int, list[int]] = {} + for i, p in enumerate(parents): + children.setdefault(p, []).append(i) + + lines: list[str] = [] + + def walk(parent: int, depth: int) -> None: + for i in children.get(parent, []): + lines.append(" " * depth + f"- {label(i, node_keys[i])}") + walk(i, depth + 1) + + walk(-1, 0) + return "\n".join(lines) + + +class _Flat(Topology): + def parents(self, node_keys): + return [-1] * len(node_keys) + + +def flat() -> Topology: + """Every cell a sibling under the root graph (no nesting).""" + return _Flat() + + +class ManualTopology(Topology): + """ + User-built subgraph tree. Not constructed directly -- ``PlasmoModel`` exposes + :meth:`~PlasmoModel.add_subgraph` which delegates here. + """ + + def __init__(self, node_keys: list[NodeKey]): + self._node_keys = node_keys + self._parent = [-1] * len(node_keys) # default: root + self._placed = [False] * len(node_keys) + + def _cells_with_head(self, head) -> list[int]: + return [i for i, k in enumerate(self._node_keys) if _key_head(k) == head] + + def _attach(self, name, parent_idx: int) -> _Handle: + cells = self._cells_with_head(name) + if not cells: + raise KeyError(f"no partition cell has key head {name!r}") + for i in cells: + if self._placed[i]: + raise ValueError( + f"cell {self._node_keys[i]!r} is already attached elsewhere" + ) + self._parent[i] = parent_idx + self._placed[i] = True + return _Handle(self, name, cells) + + def parents(self, node_keys): + # every cell must be placed (attached somewhere), else the tree is + # incomplete -- surface it rather than silently rooting stragglers. + unplaced = [node_keys[i] for i, p in enumerate(self._placed) if not p] + if unplaced: + raise ValueError( + f"manual topology is incomplete: cells {unplaced[:5]} were never " + "attached. Add them via add_subgraph(...), or use topology=flat()." + ) + return list(self._parent) + + +class _Handle: + """ + A placed selection of cells. Chaining ``.add_subgraph`` nests further cells + under this one -- only allowed when the selection is a single cell. + """ + + def __init__(self, topo: ManualTopology, name, cells: list[int]): + self._topo = topo + self._name = name + self._cells = cells + + def add_subgraph(self, name) -> _Handle: + if len(self._cells) != 1: + raise ValueError( + f"cannot nest under {self._name!r}: it resolved to " + f"{len(self._cells)} cells. Nesting is only supported under a " + "single cell in this version." + ) + return self._topo._attach(name, self._cells[0])