From a3946db6381ca84f2d70fa4a1e439dc12f374be8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:33:45 +0200 Subject: [PATCH] =?UTF-8?q?perf(sinks):=20cols=20is=20positional=20?= =?UTF-8?q?=E2=80=94=20a=20row's=20index=20is=20its=20solver=20column?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured and rejected; kept for the record. See the PR. --- .../relational/engines/polars/executor.py | 21 ++++++++++++------- src/lpspec/relational/sinks/tables.py | 16 +++++++------- .../relational/sinks/writers/lp_file.py | 4 ++-- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/lpspec/relational/engines/polars/executor.py b/src/lpspec/relational/engines/polars/executor.py index 30867225d..1da7ff858 100644 --- a/src/lpspec/relational/engines/polars/executor.py +++ b/src/lpspec/relational/engines/polars/executor.py @@ -39,7 +39,7 @@ #: The four frames a sink reads, as schemas. Stated here because the executor #: is what fills them and an empty model still has to have them. -_COLS = ('col', 'lb', 'ub', 'vtype') +_COLS = ('lb', 'ub', 'vtype') _OBJ = ('col', 'coeff') _ROWS = ('row', 'sense', 'rhs') _MATRIX = ('row', 'col', 'coeff') @@ -159,12 +159,19 @@ def _build_variable(self, v: plan.VariableDeclaration) -> pl.DataFrame: self._blocks[v.name] = (start, labelled.height) bounded = self._q.bounds(labelled.lazy(), v) - cols = bounded.select( - pl.col('var_label').alias('col'), - pl.col('lb').cast(pl.Float64), - pl.col('ub').cast(pl.Float64), - pl.lit(v.variable_type, dtype=_DTYPES['vtype']).alias('vtype'), - ).collect(engine='streaming') + # A declaration owns a contiguous run of labels and the blocks are + # concatenated in declaration order, so a frame sorted within each + # block is sorted across all of them — which makes a row's *position* + # its solver column index, and the column redundant. + cols = ( + bounded.sort('var_label') + .select( + pl.col('lb').cast(pl.Float64), + pl.col('ub').cast(pl.Float64), + pl.lit(v.variable_type, dtype=_DTYPES['vtype']).alias('vtype'), + ) + .collect(engine='streaming') + ) bad = cols.filter(pl.col('lb').is_null() | pl.col('ub').is_null()).height if bad: diff --git a/src/lpspec/relational/sinks/tables.py b/src/lpspec/relational/sinks/tables.py index 8bf0d80dd..e8d26fd0f 100644 --- a/src/lpspec/relational/sinks/tables.py +++ b/src/lpspec/relational/sinks/tables.py @@ -35,7 +35,7 @@ class ModelTables: """The built model, as a sink sees it. - ``cols`` (col, lb, ub, vtype), ``obj`` (col, coeff), ``rows`` (row, sense, + ``cols`` (lb, ub, vtype), ``obj`` (col, coeff), ``rows`` (row, sense, rhs) and ``matrix`` in COO (row, col, coeff). The scalars are what a sink cannot cheaply recover; the objective constant lives outside the frames because it has no column to attach to. @@ -96,15 +96,13 @@ def dense_columns(self, infinity: float) -> DenseColumns: import numpy as np count = self.column_count - at = self.cols['col'].to_numpy() - lb = _scattered(count, at, self.cols['lb'].to_numpy(), -infinity) - ub = _scattered(count, at, self.cols['ub'].to_numpy(), infinity) - integral = _scattered( - count, at, self.cols.select(pl.col('vtype') != 'continuous').to_series().to_numpy(), False - ) + # `copy=True` rather than in place: these are views of the frame now, + # and rewriting an infinity through one would edit the built model to + # suit whichever solver asked last. + lb = np.nan_to_num(self.cols['lb'].to_numpy(), copy=True, neginf=-infinity, posinf=infinity) + ub = np.nan_to_num(self.cols['ub'].to_numpy(), copy=True, neginf=-infinity, posinf=infinity) + integral = self.cols.select(pl.col('vtype') != 'continuous').to_series().to_numpy() cost = _scattered(count, self.obj['col'].to_numpy(), self.obj['coeff'].to_numpy(), 0.0) - np.nan_to_num(lb, copy=False, neginf=-infinity, posinf=infinity) - np.nan_to_num(ub, copy=False, neginf=-infinity, posinf=infinity) return lb, ub, cost, integral diff --git a/src/lpspec/relational/sinks/writers/lp_file.py b/src/lpspec/relational/sinks/writers/lp_file.py index f809e2222..6fcd1edd7 100644 --- a/src/lpspec/relational/sinks/writers/lp_file.py +++ b/src/lpspec/relational/sinks/writers/lp_file.py @@ -59,7 +59,7 @@ def write_lp_file(model: ModelTables, path: str | Path) -> None: objective = model.obj.lazy().sort('col').select(_term(pl.col('coeff'), pl.col('col'))) bounds = ( model.cols.lazy() - .sort('col') + .with_row_index('col') .select( pl.concat_str( _bound(pl.col('lb'), '-infinity').alias('lb'), @@ -89,7 +89,7 @@ def write_lp_file(model: ModelTables, path: str | Path) -> None: _sink(bounds, f) for variable_type, keyword in (('binary', 'binary'), ('integer', 'general')): - chosen = model.cols.lazy().filter(pl.col('vtype') == variable_type).sort('col') + chosen = model.cols.lazy().with_row_index('col').filter(pl.col('vtype') == variable_type) if chosen.select(pl.len()).collect().item() == 0: continue f.write(f'\n{keyword}\n'.encode())