Zero culling forces model rebuilding

Aperta
#925 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
58/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
numpy, python
Ambito
backend

Direzione di ricerca

Inizia con Constraint._matrix_export_data e Constraint.flat/to_polars mask_func in linopy/constraints.py, quindi segui persistent/diff.py intorno alle righe 454-457 e Solver._rebuild. Esegui l’esempio riproducibile con il comportamento di v0.9.1. Il lavoro è completato quando i coefficienti con valore zero non causano più una ricostruzione indesiderata della sparsità, oppure quando il comportamento di opt-out supportato è coperto dai test.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug data-model performance
Version Checks (indicate both or one)
  • I have confirmed this bug exists on the lastest release of Linopy.

  • I have confirmed this bug exists on the current master branch of Linopy.

Issue Description

When a constraint's LHS contains a term whose coefficient is exactly zero, linopy culls that term from the exported matrix, so the constraint's sparsity is data-dependent even though its feasible set is not.

Concretely, for the (common saturation-style) constraint

activation <= ratio * reservation      ratio in [0, 1]

when ratio == 0 the term 0 * reservation is dropped, so the reservation column disappears from that row. Rebuilding the same model with ratio flipped between 0 and a non-zero value produces different LHS sparsity for the very same constraint.

The persistent / warm-start update machinery then cannot update the model in place: persistent/diff.py (diff_con, lines ~454-457) treats a change in the CSR indptr/indices as RebuildReason.SPARSITY, and Solver falls back to a full model rebuild + fresh solve (_rebuild). This defeats warm starts for any dynamic coefficient that crosses zero, which is a very common pattern (e.g. zero-price hours setting a bound to zero).

Root cause (likely introduced in #816) is the zero-coefficient culling at matrix export:

  • Constraint._matrix_export_data (linopy/constraints.py, valid_final = (vars_final != -1) & (coeffs_final != 0))
  • Constraint.flat / to_polars mask_func (mask = (data["vars"] != -1) & (data["coeffs"] != 0))
Reproducible Example
import numpy as np
import linopy
from linopy.persistent.diff import ModelDiff, RebuildReason
from linopy.persistent.snapshot import ModelSnapshot
from linopy.solvers import Solver


def build_model(ratios):
    """Two-timestep model: activation <= ratio * reservation."""
    m = linopy.Model()
    activation = m.add_variables(lower=0, name="activation", coords=[np.arange(2)])
    reservation = m.add_variables(lower=0, name="reservation", coords=[np.arange(2)])
    con = m.add_constraints(activation <= ratios * reservation, name="activation_limit")
    m.add_objective(0 * activation)  # feasibility problem
    return m, con


def row_nnz(m, con):
    csr, _, _, _ = con.to_matrix_with_rhs(m.variables.label_index)
    return np.diff(csr.indptr).tolist()


# (1) Sparsity depends on the data: the zero coefficient is culled.
m1, c1 = build_model(np.array([1.0, 1.0]))   # both rows: activation & reservation
m2, c2 = build_model(np.array([1.0, 0.0]))   # row 2 coefficient == 0
print("ratio=[1, 1]  nnz/row =", row_nnz(m1, c1))   # [2, 2]
print("ratio=[1, 0]  nnz/row =", row_nnz(m2, c2))   # [2, 1]  <- reservation col gone

# (2) The persistent update machinery sees a sparsity change and rebuilds.
snap = ModelSnapshot.capture(m1)
diff = ModelDiff.from_snapshot(snap, m2, same_model=False, ignore_dims=[])
assert diff is RebuildReason.SPARSITY

solver = Solver.from_name("highs", model=m1, io_api="direct", track_updates=True, set_names=False)
solver.solve(assign=True)               # cold solve ok
solver.update(m2, ignore_dims=[])       # warm: only a coefficient changed to 0
assert solver._last_rebuild_reason is RebuildReason.SPARSITY
assert solver._rebuilds == 1            # full rebuild instead of in-place update
print("persistent solver rebuilt (RebuildReason.SPARSITY) instead of updating in place")

Output (verified on v0.9.1; the three assert lines pass, so rebuild_reason == SPARSITY and rebuilds == 1):

ratio=[1, 1]  nnz/row = [2, 2]
ratio=[1, 0]  nnz/row = [2, 1]
persistent solver rebuilt (RebuildReason.SPARSITY) instead of updating in place
Expected Behavior

Linopy should be smarter about the culling, or offer a way to opt-out of it where required

Installed Versions
- linopy 0.9.1 (latest release) - numpy 1.26.x, xarray, scipy - HiGHS 1.15.1 (via linopy)
Lingua principale
Python
Stelle
257
Fork
87
Merge medio
1g 3h
PR unite (30g)
29

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di PyPSA/linopy

Tutte le issue di PyPSA/linopy

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.