From 4b3f65a5a22d746754ef55c522e63e0f952e19a8 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 16 Oct 2024 01:30:56 +0200 Subject: [PATCH] io: write out lp file with sliced variables and constraints --- doc/release_notes.rst | 2 + linopy/common.py | 109 +++++++++-- linopy/constraints.py | 3 + linopy/expressions.py | 3 + linopy/io.py | 325 +++++++++++++++++++-------------- linopy/model.py | 5 + linopy/variables.py | 3 + mem-polars-non-lazy.png | Bin 0 -> 38582 bytes test/test_common.py | 78 +++++++- test/test_constraint.py | 6 + test/test_linear_expression.py | 8 + test/test_optimization.py | 9 + test/test_variable.py | 7 + 13 files changed, 396 insertions(+), 162 deletions(-) create mode 100644 mem-polars-non-lazy.png diff --git a/doc/release_notes.rst b/doc/release_notes.rst index ba6b3bd5..9c2aa99e 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -4,6 +4,8 @@ Release Notes Upcoming Version ---------------- +* When writing out an LP file, large variables and constraints are now chunked to avoid memory issues. This is especially useful for large models with constraints with many terms. The chunk size can be set with the `slice_size` argument in the `solve` function. + Version 0.3.15 -------------- diff --git a/linopy/common.py b/linopy/common.py index c9532a62..b78d0ae3 100644 --- a/linopy/common.py +++ b/linopy/common.py @@ -5,12 +5,14 @@ This module contains commonly used functions. """ +from __future__ import annotations + import operator import os -from collections.abc import Hashable, Iterable, Mapping, Sequence +from collections.abc import Generator, Hashable, Iterable, Mapping, Sequence from functools import reduce, wraps from pathlib import Path -from typing import Any, Callable, Union, overload +from typing import TYPE_CHECKING, Any, Callable, overload from warnings import warn import numpy as np @@ -30,6 +32,11 @@ sign_replace_dict, ) +if TYPE_CHECKING: + from linopy.constraints import Constraint + from linopy.expressions import LinearExpression + from linopy.variables import Variable + def maybe_replace_sign(sign: str) -> str: """ @@ -86,7 +93,7 @@ def format_string_as_variable_name(name: Hashable): return str(name).replace(" ", "_").replace("-", "_") -def get_from_iterable(lst: Union[str, Iterable[Hashable], None], index: int): +def get_from_iterable(lst: str | Iterable[Hashable] | None, index: int): """ Returns the element at the specified index of the list, or None if the index is out of bounds. @@ -99,9 +106,9 @@ def get_from_iterable(lst: Union[str, Iterable[Hashable], None], index: int): def pandas_to_dataarray( - arr: Union[pd.DataFrame, pd.Series], - coords: Union[Sequence[Union[Sequence, pd.Index, DataArray]], Mapping, None] = None, - dims: Union[Iterable[Hashable], None] = None, + arr: pd.DataFrame | pd.Series, + coords: Sequence[Sequence | pd.Index | DataArray] | Mapping | None = None, + dims: Iterable[Hashable] | None = None, **kwargs, ) -> DataArray: """ @@ -156,8 +163,8 @@ def pandas_to_dataarray( def numpy_to_dataarray( arr: np.ndarray, - coords: Union[Sequence[Union[Sequence, pd.Index, DataArray]], Mapping, None] = None, - dims: Union[str, Iterable[Hashable], None] = None, + coords: Sequence[Sequence | pd.Index | DataArray] | Mapping | None = None, + dims: str | Iterable[Hashable] | None = None, **kwargs, ) -> DataArray: """ @@ -195,8 +202,8 @@ def numpy_to_dataarray( def as_dataarray( arr, - coords: Union[Sequence[Union[Sequence, pd.Index, DataArray]], Mapping, None] = None, - dims: Union[str, Iterable[Hashable], None] = None, + coords: Sequence[Sequence | pd.Index | DataArray] | Mapping | None = None, + dims: str | Iterable[Hashable] | None = None, **kwargs, ) -> DataArray: """ @@ -246,7 +253,7 @@ def as_dataarray( # TODO: rename to to_pandas_dataframe -def to_dataframe(ds: Dataset, mask_func: Union[Callable, None] = None): +def to_dataframe(ds: Dataset, mask_func: Callable | None = None): """ Convert an xarray Dataset to a pandas DataFrame. @@ -467,6 +474,67 @@ def fill_missing_coords(ds, fill_helper_dims: bool = False): return ds +def iterate_slices( + ds: Dataset | Variable | LinearExpression | Constraint, + slice_size: int | None = 10_000, + slice_dims: list | None = None, +) -> Generator[Dataset | Variable | LinearExpression | Constraint, None, None]: + """ + Generate slices of an xarray Dataset or DataArray with a specified soft maximum size. + + The slicing is performed on the largest dimension of the input object. + + Parameters + ---------- + ds : xarray.Dataset or xarray.DataArray + The input xarray Dataset or DataArray to be sliced. + slice_size : int + The maximum number of elements in each slice. If the maximum size is too small to accommodate any slice, + the function splits the largest dimension. + slice_dims : list, optional + The dimensions to slice along. If None, all dimensions in `coord_dims` are used if + `coord_dims` is an attribute of the input object. Otherwise, all dimensions are used. + + Yields + ------ + xarray.Dataset or xarray.DataArray + A slice of the input Dataset or DataArray. + + Raises + ------ + ValueError + If the maximum size is too small to accommodate any slice. + """ + if slice_dims is None: + slice_dims = list(getattr(ds, "coord_dims", ds.dims)) + + # Calculate the total number of elements in the dataset + size = np.prod([ds.sizes[dim] for dim in ds.dims], dtype=int) + + if slice_size is None or size <= slice_size: + yield ds + return + + # number of slices + n_slices = max(size // slice_size, 1) + + # leading dimension (the dimension with the largest size) + leading_dim = max(ds.sizes, key=ds.sizes.get) # type: ignore + size_of_leading_dim = ds.sizes[leading_dim] + + if size_of_leading_dim < n_slices: + n_slices = size_of_leading_dim + + chunk_size = ds.sizes[leading_dim] // n_slices + + # Iterate over the Cartesian product of slice indices + for i in range(n_slices): + start = i * chunk_size + end = start + chunk_size + slice_dict = {leading_dim: slice(start, end)} + yield ds.isel(slice_dict) + + def _remap(array, mapping): return mapping[array.ravel()].reshape(array.shape) @@ -484,7 +552,7 @@ def replace_by_map(ds, mapping): ) -def to_path(path: Union[str, Path, None]) -> Union[Path, None]: +def to_path(path: str | Path | None) -> Path | None: """ Convert a string to a Path object. """ @@ -526,7 +594,7 @@ def generate_indices_for_printout(dim_sizes, max_lines): yield tuple(np.unravel_index(i, dim_sizes)) -def align_lines_by_delimiter(lines: list[str], delimiter: Union[str, list[str]]): +def align_lines_by_delimiter(lines: list[str], delimiter: str | list[str]): # Determine the maximum position of the delimiter if isinstance(delimiter, str): delimiter = [delimiter] @@ -548,17 +616,18 @@ def align_lines_by_delimiter(lines: list[str], delimiter: Union[str, list[str]]) def get_label_position( - obj, values: Union[int, np.ndarray] -) -> Union[ - Union[tuple[str, dict], tuple[None, None]], - list[Union[tuple[str, dict], tuple[None, None]]], - list[list[Union[tuple[str, dict], tuple[None, None]]]], -]: + obj, values: int | np.ndarray +) -> ( + tuple[str, dict] + | tuple[None, None] + | list[tuple[str, dict] | tuple[None, None]] + | list[list[tuple[str, dict] | tuple[None, None]]] +): """ Get tuple of name and coordinate for variable labels. """ - def find_single(value: int) -> Union[tuple[str, dict], tuple[None, None]]: + def find_single(value: int) -> tuple[str, dict] | tuple[None, None]: if value == -1: return None, None for name, val in obj.items(): diff --git a/linopy/constraints.py b/linopy/constraints.py index fabe74d2..458fa419 100644 --- a/linopy/constraints.py +++ b/linopy/constraints.py @@ -40,6 +40,7 @@ has_optimized_model, infer_schema_polars, is_constant, + iterate_slices, maybe_replace_signs, print_coord, print_single_constraint, @@ -658,6 +659,8 @@ def to_polars(self): stack = conwrap(Dataset.stack) + iterate_slices = iterate_slices + @dataclass(repr=False) class Constraints: diff --git a/linopy/expressions.py b/linopy/expressions.py index 49a7240d..08af65a8 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -52,6 +52,7 @@ get_index_map, group_terms_polars, has_optimized_model, + iterate_slices, print_single_expression, to_dataframe, to_polars, @@ -1457,6 +1458,8 @@ def to_polars(self) -> pl.DataFrame: stack = exprwrap(Dataset.stack) + iterate_slices = iterate_slices + class QuadraticExpression(LinearExpression): """ diff --git a/linopy/io.py b/linopy/io.py index 93120a97..509a8644 100644 --- a/linopy/io.py +++ b/linopy/io.py @@ -127,7 +127,11 @@ def objective_to_file( def constraints_to_file( - m: Model, f: TextIOWrapper, log: bool = False, batch_size: int = 50000 + m: Model, + f: TextIOWrapper, + log: bool = False, + batch_size: int = 50_000, + slice_size: int = 100_000, ) -> None: if not len(m.constraints): return @@ -143,54 +147,60 @@ def constraints_to_file( batch = [] for name in names: - df = m.constraints[name].flat - - labels = df.labels.values - vars = df.vars.values - coeffs = df.coeffs.values - rhs = df.rhs.values - sign = df.sign.values - - len_df = len(df) # compute length once - if not len_df: - continue - - # write out the start to enable a fast loop afterwards - idx = 0 - label = labels[idx] - coeff = coeffs[idx] - var = vars[idx] - batch.append(f"c{label}:\n{coeff:+.12g} x{var}\n") - prev_label = label - prev_sign = sign[idx] - prev_rhs = rhs[idx] - - for idx in range(1, len_df): + con = m.constraints[name] + for con_slice in con.iterate_slices(slice_size): + df = con_slice.flat + + labels = df.labels.values + vars = df.vars.values + coeffs = df.coeffs.values + rhs = df.rhs.values + sign = df.sign.values + + len_df = len(df) # compute length once + if not len_df: + continue + + # write out the start to enable a fast loop afterwards + idx = 0 label = labels[idx] coeff = coeffs[idx] var = vars[idx] + batch.append(f"c{label}:\n{coeff:+.12g} x{var}\n") + prev_label = label + prev_sign = sign[idx] + prev_rhs = rhs[idx] - if label != prev_label: - batch.append( - f"{prev_sign} {prev_rhs:+.12g}\n\nc{label}:\n{coeff:+.12g} x{var}\n" - ) - prev_sign = sign[idx] - prev_rhs = rhs[idx] - else: - batch.append(f"{coeff:+.12g} x{var}\n") + for idx in range(1, len_df): + label = labels[idx] + coeff = coeffs[idx] + var = vars[idx] - batch = handle_batch(batch, f, batch_size) + if label != prev_label: + batch.append( + f"{prev_sign} {prev_rhs:+.12g}\n\nc{label}:\n{coeff:+.12g} x{var}\n" + ) + prev_sign = sign[idx] + prev_rhs = rhs[idx] + else: + batch.append(f"{coeff:+.12g} x{var}\n") - prev_label = label + batch = handle_batch(batch, f, batch_size) + + prev_label = label - batch.append(f"{prev_sign} {prev_rhs:+.12g}\n") + batch.append(f"{prev_sign} {prev_rhs:+.12g}\n") if batch: # write the remaining lines f.writelines(batch) def bounds_to_file( - m: Model, f: TextIOWrapper, log: bool = False, batch_size: int = 10000 + m: Model, + f: TextIOWrapper, + log: bool = False, + batch_size: int = 10000, + slice_size: int = 100_000, ) -> None: """ Write out variables of a model to a lp file. @@ -209,25 +219,31 @@ def bounds_to_file( batch = [] # to store batch of lines for name in names: - df = m.variables[name].flat + var = m.variables[name] + for var_slice in var.iterate_slices(slice_size): + df = var_slice.flat - labels = df.labels.values - lowers = df.lower.values - uppers = df.upper.values + labels = df.labels.values + lowers = df.lower.values + uppers = df.upper.values - for idx in range(len(df)): - label = labels[idx] - lower = lowers[idx] - upper = uppers[idx] - batch.append(f"{lower:+.12g} <= x{label} <= {upper:+.12g}\n") - batch = handle_batch(batch, f, batch_size) + for idx in range(len(df)): + label = labels[idx] + lower = lowers[idx] + upper = uppers[idx] + batch.append(f"{lower:+.12g} <= x{label} <= {upper:+.12g}\n") + batch = handle_batch(batch, f, batch_size) if batch: # write the remaining lines f.writelines(batch) def binaries_to_file( - m: Model, f: TextIOWrapper, log: bool = False, batch_size: int = 1000 + m: Model, + f: TextIOWrapper, + log: bool = False, + batch_size: int = 1000, + slice_size: int = 100_000, ) -> None: """ Write out binaries of a model to a lp file. @@ -246,11 +262,13 @@ def binaries_to_file( batch = [] # to store batch of lines for name in names: - df = m.variables[name].flat + var = m.variables[name] + for var_slice in var.iterate_slices(slice_size): + df = var_slice.flat - for label in df.labels.values: - batch.append(f"x{label}\n") - batch = handle_batch(batch, f, batch_size) + for label in df.labels.values: + batch.append(f"x{label}\n") + batch = handle_batch(batch, f, batch_size) if batch: # write the remaining lines f.writelines(batch) @@ -261,6 +279,7 @@ def integers_to_file( f: TextIOWrapper, log: bool = False, batch_size: int = 1000, + slice_size: int = 100_000, integer_label: str = "general", ) -> None: """ @@ -280,17 +299,19 @@ def integers_to_file( batch = [] # to store batch of lines for name in names: - df = m.variables[name].flat + var = m.variables[name] + for var_slice in var.iterate_slices(slice_size): + df = var_slice.flat - for label in df.labels.values: - batch.append(f"x{label}\n") - batch = handle_batch(batch, f, batch_size) + for label in df.labels.values: + batch.append(f"x{label}\n") + batch = handle_batch(batch, f, batch_size) if batch: # write the remaining lines f.writelines(batch) -def to_lp_file(m, fn, integer_label): +def to_lp_file(m: Model, fn: Path, integer_label: str, slice_size: int = 10_000_000): log = m._xCounter > 10_000 batch_size = 5000 @@ -302,11 +323,18 @@ def to_lp_file(m, fn, integer_label): raise ValueError("File not found.") objective_to_file(m, f, log=log) - constraints_to_file(m, f=f, log=log, batch_size=batch_size) - bounds_to_file(m, f=f, log=log, batch_size=batch_size) - binaries_to_file(m, f=f, log=log, batch_size=batch_size) + constraints_to_file( + m, f=f, log=log, batch_size=batch_size, slice_size=slice_size + ) + bounds_to_file(m, f=f, log=log, batch_size=batch_size, slice_size=slice_size) + binaries_to_file(m, f=f, log=log, batch_size=batch_size, slice_size=slice_size) integers_to_file( - m, integer_label=integer_label, f=f, log=log, batch_size=batch_size + m, + integer_label=integer_label, + f=f, + log=log, + batch_size=batch_size, + slice_size=slice_size, ) f.write("end\n") @@ -371,7 +399,7 @@ def objective_to_file_polars(m, f, log=False): objective_write_quadratic_terms_polars(f, quads) -def bounds_to_file_polars(m, f, log=False): +def bounds_to_file_polars(m, f, log=False, slice_size=2_000_000): """ Write out variables of a model to a lp file. """ @@ -388,26 +416,28 @@ def bounds_to_file_polars(m, f, log=False): ) for name in names: - df = m.variables[name].to_polars() - - columns = [ - pl.when(pl.col("lower") >= 0).then(pl.lit("+")).otherwise(pl.lit("")), - pl.col("lower").cast(pl.String), - pl.lit(" <= x"), - pl.col("labels").cast(pl.String), - pl.lit(" <= "), - pl.when(pl.col("upper") >= 0).then(pl.lit("+")).otherwise(pl.lit("")), - pl.col("upper").cast(pl.String), - ] - - kwargs = dict( - separator=" ", null_value="", quote_style="never", include_header=False - ) - formatted = df.select(pl.concat_str(columns, ignore_nulls=True)) - formatted.write_csv(f, **kwargs) + var = m.variables[name] + for var_slice in var.iterate_slices(slice_size): + df = var_slice.to_polars() + + columns = [ + pl.when(pl.col("lower") >= 0).then(pl.lit("+")).otherwise(pl.lit("")), + pl.col("lower").cast(pl.String), + pl.lit(" <= x"), + pl.col("labels").cast(pl.String), + pl.lit(" <= "), + pl.when(pl.col("upper") >= 0).then(pl.lit("+")).otherwise(pl.lit("")), + pl.col("upper").cast(pl.String), + ] + + kwargs = dict( + separator=" ", null_value="", quote_style="never", include_header=False + ) + formatted = df.select(pl.concat_str(columns, ignore_nulls=True)) + formatted.write_csv(f, **kwargs) -def binaries_to_file_polars(m, f, log=False): +def binaries_to_file_polars(m, f, log=False, slice_size=2_000_000): """ Write out binaries of a model to a lp file. """ @@ -424,21 +454,25 @@ def binaries_to_file_polars(m, f, log=False): ) for name in names: - df = m.variables[name].to_polars() + var = m.variables[name] + for var_slice in var.iterate_slices(slice_size): + df = var_slice.to_polars() - columns = [ - pl.lit("x"), - pl.col("labels").cast(pl.String), - ] + columns = [ + pl.lit("x"), + pl.col("labels").cast(pl.String), + ] - kwargs = dict( - separator=" ", null_value="", quote_style="never", include_header=False - ) - formatted = df.select(pl.concat_str(columns, ignore_nulls=True)) - formatted.write_csv(f, **kwargs) + kwargs = dict( + separator=" ", null_value="", quote_style="never", include_header=False + ) + formatted = df.select(pl.concat_str(columns, ignore_nulls=True)) + formatted.write_csv(f, **kwargs) -def integers_to_file_polars(m, f, log=False, integer_label="general"): +def integers_to_file_polars( + m, f, log=False, integer_label="general", slice_size=2_000_000 +): """ Write out integers of a model to a lp file. """ @@ -455,21 +489,23 @@ def integers_to_file_polars(m, f, log=False, integer_label="general"): ) for name in names: - df = m.variables[name].to_polars() + var = m.variables[name] + for var_slice in var.iterate_slices(slice_size): + df = var_slice.to_polars() - columns = [ - pl.lit("x"), - pl.col("labels").cast(pl.String), - ] + columns = [ + pl.lit("x"), + pl.col("labels").cast(pl.String), + ] - kwargs = dict( - separator=" ", null_value="", quote_style="never", include_header=False - ) - formatted = df.select(pl.concat_str(columns, ignore_nulls=True)) - formatted.write_csv(f, **kwargs) + kwargs = dict( + separator=" ", null_value="", quote_style="never", include_header=False + ) + formatted = df.select(pl.concat_str(columns, ignore_nulls=True)) + formatted.write_csv(f, **kwargs) -def constraints_to_file_polars(m, f, log=False, lazy=False): +def constraints_to_file_polars(m, f, log=False, lazy=False, slice_size=2_000_000): if not len(m.constraints): return @@ -485,53 +521,57 @@ def constraints_to_file_polars(m, f, log=False, lazy=False): # to make this even faster, we can use polars expression # https://docs.pola.rs/user-guide/expressions/plugins/#output-data-types for name in names: - df = m.constraints[name].to_polars() - - # df = df.lazy() - # filter out repeated label values - df = df.with_columns( - pl.when(pl.col("labels").is_first_distinct()) - .then(pl.col("labels")) - .otherwise(pl.lit(None)) - .alias("labels") - ) - - columns = [ - pl.when(pl.col("labels").is_not_null()).then(pl.lit("c")).alias("c"), - pl.col("labels").cast(pl.String), - pl.when(pl.col("labels").is_not_null()).then(pl.lit(":\n")).alias(":"), - pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")), - pl.col("coeffs").cast(pl.String), - pl.when(pl.col("vars").is_not_null()).then(pl.lit(" x")).alias("x"), - pl.col("vars").cast(pl.String), - "sign", - pl.lit(" "), - pl.col("rhs").cast(pl.String), - ] + con = m.constraints[name] + for con_slice in con.iterate_slices(slice_size): + df = con_slice.to_polars() + + # df = df.lazy() + # filter out repeated label values + df = df.with_columns( + pl.when(pl.col("labels").is_first_distinct()) + .then(pl.col("labels")) + .otherwise(pl.lit(None)) + .alias("labels") + ) - kwargs = dict( - separator=" ", null_value="", quote_style="never", include_header=False - ) - formatted = df.select(pl.concat_str(columns, ignore_nulls=True)) - formatted.write_csv(f, **kwargs) + columns = [ + pl.when(pl.col("labels").is_not_null()).then(pl.lit("c")).alias("c"), + pl.col("labels").cast(pl.String), + pl.when(pl.col("labels").is_not_null()).then(pl.lit(":\n")).alias(":"), + pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")), + pl.col("coeffs").cast(pl.String), + pl.when(pl.col("vars").is_not_null()).then(pl.lit(" x")).alias("x"), + pl.col("vars").cast(pl.String), + "sign", + pl.lit(" "), + pl.col("rhs").cast(pl.String), + ] + + kwargs = dict( + separator=" ", null_value="", quote_style="never", include_header=False + ) + formatted = df.select(pl.concat_str(columns, ignore_nulls=True)) + formatted.write_csv(f, **kwargs) - # in the future, we could use lazy dataframes when they support appending - # tp existent files - # formatted = df.lazy().select(pl.concat_str(columns, ignore_nulls=True)) - # formatted.sink_csv(f, **kwargs) + # in the future, we could use lazy dataframes when they support appending + # tp existent files + # formatted = df.lazy().select(pl.concat_str(columns, ignore_nulls=True)) + # formatted.sink_csv(f, **kwargs) -def to_lp_file_polars(m, fn, integer_label="general"): +def to_lp_file_polars(m, fn, integer_label="general", slice_size=2_000_000): log = m._xCounter > 10_000 with open(fn, mode="wb") as f: start = time.time() objective_to_file_polars(m, f, log=log) - constraints_to_file_polars(m, f=f, log=log) - bounds_to_file_polars(m, f=f, log=log) - binaries_to_file_polars(m, f=f, log=log) - integers_to_file_polars(m, integer_label=integer_label, f=f, log=log) + constraints_to_file_polars(m, f=f, log=log, slice_size=slice_size) + bounds_to_file_polars(m, f=f, log=log, slice_size=slice_size) + binaries_to_file_polars(m, f=f, log=log, slice_size=slice_size) + integers_to_file_polars( + m, integer_label=integer_label, f=f, log=log, slice_size=slice_size + ) f.write(b"end\n") logger.info(f" Writing time: {round(time.time()-start, 2)}s") @@ -539,15 +579,18 @@ def to_lp_file_polars(m, fn, integer_label="general"): def to_file( m: Model, - fn: Path | None, + fn: Path | str | None, io_api: str | None = None, integer_label: str = "general", + slice_size: int = 2_000_000, ) -> Path: """ Write out a model to a lp or mps file. """ if fn is None: fn = Path(m.get_problem_file()) + if isinstance(fn, str): + fn = Path(fn) if fn.exists(): fn.unlink() @@ -555,9 +598,9 @@ def to_file( io_api = fn.suffix[1:] if io_api == "lp": - to_lp_file(m, fn, integer_label) + to_lp_file(m, fn, integer_label, slice_size=slice_size) elif io_api == "lp-polars": - to_lp_file_polars(m, fn, integer_label) + to_lp_file_polars(m, fn, integer_label, slice_size=slice_size) elif io_api == "mps": if "highs" not in solvers.available_solvers: diff --git a/linopy/model.py b/linopy/model.py index 728c61bb..d1f34015 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -953,6 +953,7 @@ def solve( keep_files: bool = False, env: None = None, sanitize_zeros: bool = True, + slice_size: int = 2_000_000, remote: None = None, **solver_options, ) -> tuple[str, str]: @@ -1002,6 +1003,10 @@ def solve( Whether to set terms with zero coefficient as missing. This will remove unneeded overhead in the lp file writing. The default is True. + slice_size : int, optional + Size of the slice to use for writing the lp file. The slice size + is used to split large variables and constraints into smaller + chunks to avoid memory issues. The default is 2_000_000. remote : linopy.remote.RemoteHandler Remote handler to use for solving model on a server. Note that when solving on a rSee diff --git a/linopy/variables.py b/linopy/variables.py index fe0a214d..e6584141 100644 --- a/linopy/variables.py +++ b/linopy/variables.py @@ -42,6 +42,7 @@ get_label_position, has_optimized_model, is_constant, + iterate_slices, print_coord, print_single_variable, save_join, @@ -1075,6 +1076,8 @@ def equals(self, other: Variable) -> bool: stack = varwrap(Dataset.stack) + iterate_slices = iterate_slices + class AtIndexer: __slots__ = ("object",) diff --git a/mem-polars-non-lazy.png b/mem-polars-non-lazy.png new file mode 100644 index 0000000000000000000000000000000000000000..f37b4f13a9b3768268543e58384250530ea1cc82 GIT binary patch literal 38582 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 HtKk0whITco literal 0 HcmV?d00001 diff --git a/test/test_common.py b/test/test_common.py index c5937c06..90f5df89 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -11,7 +11,12 @@ import xarray as xr from xarray import DataArray -from linopy.common import as_dataarray, assign_multiindex_safe, best_int +from linopy.common import ( + as_dataarray, + assign_multiindex_safe, + best_int, + iterate_slices, +) def test_as_dataarray_with_series_dims_default(): @@ -453,3 +458,74 @@ def test_assign_multiindex_safe(): assert "value" in result assert result["humidity"].equals(data) assert result["pressure"].equals(data) + + +def test_iterate_slices_basic(): + ds = xr.Dataset( + {"var": (("x", "y"), np.random.rand(10, 10))}, # noqa: NPY002 + coords={"x": np.arange(10), "y": np.arange(10)}, + ) + slices = list(iterate_slices(ds, slice_size=20)) + assert len(slices) == 5 + for s in slices: + assert isinstance(s, xr.Dataset) + assert set(s.dims) == set(ds.dims) + + +def test_iterate_slices_with_exclude_dims(): + ds = xr.Dataset( + {"var": (("x", "y"), np.random.rand(10, 10))}, # noqa: NPY002 + coords={"x": np.arange(10), "y": np.arange(10)}, + ) + slices = list(iterate_slices(ds, slice_size=20, slice_dims=["x"])) + assert len(slices) == 5 + for s in slices: + assert isinstance(s, xr.Dataset) + assert set(s.dims) == set(ds.dims) + + +def test_iterate_slices_large_max_size(): + ds = xr.Dataset( + {"var": (("x", "y"), np.random.rand(10, 10))}, # noqa: NPY002 + coords={"x": np.arange(10), "y": np.arange(10)}, + ) + slices = list(iterate_slices(ds, slice_size=200)) + assert len(slices) == 1 + for s in slices: + assert isinstance(s, xr.Dataset) + assert set(s.dims) == set(ds.dims) + + +def test_iterate_slices_small_max_size(): + ds = xr.Dataset( + {"var": (("x", "y"), np.random.rand(10, 10))}, # noqa: NPY002 + coords={"x": np.arange(10), "y": np.arange(10)}, + ) + slices = list(iterate_slices(ds, slice_size=8, slice_dims=[])) + assert len(slices) == 10 + for s in slices: + assert isinstance(s, xr.Dataset) + assert set(s.dims) == set(ds.dims) + + +def test_iterate_slices_slice_size_none(): + ds = xr.Dataset( + {"var": (("x", "y"), np.random.rand(10, 10))}, # noqa: NPY002 + coords={"x": np.arange(10), "y": np.arange(10)}, + ) + slices = list(iterate_slices(ds, slice_size=None)) + assert len(slices) == 1 + for s in slices: + assert ds.equals(s) + + +def test_iterate_slices_no_slice_dims(): + ds = xr.Dataset( + {"var": (("x", "y"), np.random.rand(10, 10))}, # noqa: NPY002 + coords={"x": np.arange(10), "y": np.arange(10)}, + ) + slices = list(iterate_slices(ds, slice_size=50, slice_dims=[])) + assert len(slices) == 2 + for s in slices: + assert isinstance(s, xr.Dataset) + assert set(s.dims) == set(ds.dims) diff --git a/test/test_constraint.py b/test/test_constraint.py index 307d4453..0d0080fa 100644 --- a/test/test_constraint.py +++ b/test/test_constraint.py @@ -380,6 +380,12 @@ def test_constraint_flat(c): assert isinstance(c.flat, pd.DataFrame) +def test_iterate_slices(c): + for i in c.iterate_slices(slice_size=2): + assert isinstance(i, Constraint) + assert c.coord_dims == i.coord_dims + + def test_constraint_to_polars(c): assert isinstance(c.to_polars(), pl.DataFrame) diff --git a/test/test_linear_expression.py b/test/test_linear_expression.py index 05b9b280..277e6443 100644 --- a/test/test_linear_expression.py +++ b/test/test_linear_expression.py @@ -526,6 +526,14 @@ def test_linear_expression_flat(v): assert (df.coeffs == coeff).all() +def test_iterate_slices(x, y): + expr = x + 10 * y + for s in expr.iterate_slices(slice_size=2): + assert isinstance(s, LinearExpression) + assert s.nterm == expr.nterm + assert s.coord_dims == expr.coord_dims + + def test_linear_expression_to_polars(v): coeff = np.arange(1, 21) # use non-zero coefficients expr = coeff * v diff --git a/test/test_optimization.py b/test/test_optimization.py index 2a76efb0..c5e040c8 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -400,6 +400,15 @@ def test_default_settings_chunked(model_chunked, solver, io_api): assert np.isclose(model_chunked.objective.value, 3.3) +@pytest.mark.parametrize("solver,io_api", params) +def test_default_settings_small_slices(model, solver, io_api): + assert model.objective.sense == "min" + status, condition = model.solve(solver, io_api=io_api, slice_size=2) + assert status == "ok" + assert np.isclose(model.objective.value, 3.3) + assert model.solver_name == solver + + @pytest.mark.parametrize("solver,io_api", params) def test_solver_options(model, solver, io_api): time_limit_option = { diff --git a/test/test_variable.py b/test/test_variable.py index bf718558..321bc3d5 100644 --- a/test/test_variable.py +++ b/test/test_variable.py @@ -286,3 +286,10 @@ def test_variable_sanitize(x): x = x.sanitize() assert isinstance(x, linopy.variables.Variable) assert x.labels[9] == -1 + + +def test_variable_iterate_slices(x): + slices = x.iterate_slices(slice_size=2) + for s in slices: + assert isinstance(s, linopy.variables.Variable) + assert s.size <= 2