You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

linopy

Package Overview
Dependencies
Maintainers
2
Versions
64
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

linopy - pypi Package Compare versions

Comparing version
0.6.0
to
0.6.1
+34
-8
doc/release_notes.rst

@@ -5,12 +5,38 @@ Release Notes

.. Upcoming Version
* Add ``mock_solve`` option to model.solve for quick testing without actual solving
* Bugfix for missing dependency for jupyter notebook example in documentation
Version 0.6.1
--------------
* Avoid Gurobi initialization on linopy import.
* Fix LP file writing for negative zero (-0.0) values that produced invalid syntax like "+-0.0" rejected by Gurobi
Version 0.6.0
--------------
**Features**
* Add ``mock_solve`` option to ``Model.solve()`` for quick testing without actual solving
* Add support for SOS1 and SOS2 (Special Ordered Sets) constraints via ``Model.add_sos_constraints()`` and ``Model.remove_sos_constraints()``
* Add simplify method to LinearExpression to combine duplicate terms
* Add convenience function to create LinearExpression from constant
* Fix compatibility for xpress versions below 9.6 (regression)
* Performance: Up to 50x faster ``repr()`` for variables/constraints via O(log n) label lookup and direct numpy indexing
* Performance: Up to 46x faster ``ncons`` property by replacing ``.flat.labels.unique()`` with direct counting
* Add support for GPU-accelerated solver [cuPDLPx](https://github.com/MIT-Lu-Lab/cuPDLPx)
* Add ``simplify`` method to ``LinearExpression`` to combine duplicate terms
* Add convenience function to create ``LinearExpression`` from constant
* Add support for GPU-accelerated solver `cuPDLPx <https://github.com/MIT-Lu-Lab/cuPDLPx>`_
* Add solver features registry for introspection of solver capabilities
**Performance**
* Up to 50x faster ``repr()`` for variables/constraints via O(log n) label lookup and direct numpy indexing
* Up to 46x faster ``ncons`` property by replacing ``.flat.labels.unique()`` with direct counting
**Bug Fixes**
* Fix HiGHS solver to properly stop on Ctrl-C keyboard interrupt
* Fix CBC solver to correctly parse negative objective values
* Fix Xpress compatibility for versions below 9.6 (regression from namespace change)
* Fix Xpress ``getDual()`` fallback for older versions
* Fix missing dependency for jupyter notebook example in documentation
**Solver Updates**
* Add Xpress 9.8+ API support with full backward compatibility to 9.6+
Version 0.5.8

@@ -17,0 +43,0 @@ --------------

+1
-1
Metadata-Version: 2.4
Name: linopy
Version: 0.6.0
Version: 0.6.1
Summary: Linear optimization with N-D labeled arrays in Python

@@ -5,0 +5,0 @@ Author-email: Fabian Hofmann <fabianmarikhofmann@gmail.com>

@@ -57,2 +57,22 @@ #!/usr/bin/env python3

def signed_number(expr: pl.Expr) -> tuple[pl.Expr, pl.Expr]:
"""
Return polars expressions for a signed number string, handling -0.0 correctly.
Parameters
----------
expr : pl.Expr
Numeric value
Returns
-------
tuple[pl.Expr, pl.Expr]
value_string with sign
"""
return (
pl.when(expr >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.when(expr == 0).then(pl.lit("0.0")).otherwise(expr.cast(pl.String)),
)
def print_coord(coord: str) -> str:

@@ -136,4 +156,3 @@ from linopy.common import print_coord

cols = [
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.col("coeffs").cast(pl.String),
*signed_number(pl.col("coeffs")),
*print_variable(pl.col("vars")),

@@ -151,4 +170,3 @@ ]

cols = [
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.col("coeffs").mul(2).cast(pl.String),
*signed_number(pl.col("coeffs").mul(2)),
*print_variable(pl.col("vars1")),

@@ -235,9 +253,7 @@ pl.lit(" *"),

columns = [
pl.when(pl.col("lower") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.col("lower").cast(pl.String),
*signed_number(pl.col("lower")),
pl.lit(" <= "),
*print_variable(pl.col("labels")),
pl.lit(" <= "),
pl.when(pl.col("upper") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.col("upper").cast(pl.String),
*signed_number(pl.col("upper")),
]

@@ -470,4 +486,3 @@

.alias(":"),
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")),
pl.col("coeffs").cast(pl.String),
*signed_number(pl.col("coeffs")),
pl.when(pl.col("vars").is_not_null()).then(col_labels[0]),

@@ -474,0 +489,0 @@ pl.when(pl.col("vars").is_not_null()).then(col_labels[1]),

@@ -31,5 +31,5 @@ # file generated by setuptools-scm

__version__ = version = '0.6.0'
__version_tuple__ = version_tuple = (0, 6, 0)
__version__ = version = '0.6.1'
__version_tuple__ = version_tuple = (0, 6, 1)
__commit_id__ = commit_id = 'ge698fb25f'
__commit_id__ = commit_id = 'g67e3484dc'
Metadata-Version: 2.4
Name: linopy
Version: 0.6.0
Version: 0.6.1
Summary: Linear optimization with N-D labeled arrays in Python

@@ -5,0 +5,0 @@ Author-email: Fabian Hofmann <fabianmarikhofmann@gmail.com>

@@ -11,3 +11,5 @@ #!/usr/bin/env python3

import numpy as np
import pandas as pd
import polars as pl
import pytest

@@ -17,2 +19,3 @@ import xarray as xr

from linopy import LESS_EQUAL, Model, available_solvers, read_netcdf
from linopy.io import signed_number
from linopy.testing import assert_model_equal

@@ -222,1 +225,117 @@

m.to_block_files(tmp_path)
class TestSignedNumberExpr:
"""Test the signed_number helper function for LP file formatting."""
def test_positive_numbers(self) -> None:
"""Positive numbers should get a '+' prefix."""
df = pl.DataFrame({"value": [1.0, 2.5, 100.0]})
result = df.select(pl.concat_str(signed_number(pl.col("value"))))
values = result.to_series().to_list()
assert values == ["+1.0", "+2.5", "+100.0"]
def test_negative_numbers(self) -> None:
"""Negative numbers should not get a '+' prefix (already have '-')."""
df = pl.DataFrame({"value": [-1.0, -2.5, -100.0]})
result = df.select(pl.concat_str(signed_number(pl.col("value"))))
values = result.to_series().to_list()
assert values == ["-1.0", "-2.5", "-100.0"]
def test_positive_zero(self) -> None:
"""Positive zero should get a '+' prefix."""
df = pl.DataFrame({"value": [0.0]})
result = df.select(pl.concat_str(signed_number(pl.col("value"))))
values = result.to_series().to_list()
assert values == ["+0.0"]
def test_negative_zero(self) -> None:
"""Negative zero is normalized to +0.0 - this is the bug fix."""
# Create negative zero using numpy
neg_zero = np.float64(-0.0)
df = pl.DataFrame({"value": [neg_zero]})
result = df.select(pl.concat_str(signed_number(pl.col("value"))))
values = result.to_series().to_list()
# The key assertion: should NOT be "+-0.0", -0.0 is normalized to +0.0
assert values == ["+0.0"]
assert "+-" not in values[0]
def test_mixed_values_including_negative_zero(self) -> None:
"""Test a mix of positive, negative, and zero values."""
neg_zero = np.float64(-0.0)
df = pl.DataFrame({"value": [1.0, -1.0, 0.0, neg_zero, 2.5, -2.5]})
result = df.select(pl.concat_str(signed_number(pl.col("value"))))
values = result.to_series().to_list()
# -0.0 is normalized to +0.0
assert values == ["+1.0", "-1.0", "+0.0", "+0.0", "+2.5", "-2.5"]
# No value should contain "+-"
for v in values:
assert "+-" not in v
@pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed")
def test_to_file_lp_with_negative_zero_bounds(tmp_path: Path) -> None:
"""
Test that LP files with negative zero bounds are valid.
This is a regression test for the bug where -0.0 bounds would produce
invalid LP file syntax like "+-0.0 <= x1 <= +0.0".
See: https://github.com/PyPSA/linopy/issues/XXX
"""
import gurobipy
m = Model()
# Create bounds that could produce -0.0
# Using numpy to ensure we can create actual negative zeros
lower = pd.Series([np.float64(-0.0), np.float64(0.0), np.float64(-0.0)])
upper = pd.Series([np.float64(0.0), np.float64(-0.0), np.float64(1.0)])
m.add_variables(lower, upper, name="x")
m.add_objective(m.variables["x"].sum())
fn = tmp_path / "test_neg_zero.lp"
m.to_file(fn)
# Read the LP file content and verify no "+-" appears
with open(fn) as f:
content = f.read()
assert "+-" not in content, f"Found invalid '+-' in LP file: {content}"
# Verify Gurobi can read it without errors
gurobipy.read(str(fn))
@pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed")
def test_to_file_lp_with_negative_zero_coefficients(tmp_path: Path) -> None:
"""
Test that LP files with negative zero coefficients are valid.
Coefficients can also potentially be -0.0 due to floating point arithmetic.
"""
import gurobipy
m = Model()
x = m.add_variables(name="x", lower=0, upper=10)
y = m.add_variables(name="y", lower=0, upper=10)
# Create an expression where coefficients could become -0.0
# through arithmetic operations
coeff = np.float64(-0.0)
expr = coeff * x + 1 * y
m.add_constraints(expr <= 5)
m.add_objective(x + y)
fn = tmp_path / "test_neg_zero_coeffs.lp"
m.to_file(fn)
# Read the LP file content and verify no "+-" appears
with open(fn) as f:
content = f.read()
assert "+-" not in content, f"Found invalid '+-' in LP file: {content}"
# Verify Gurobi can read it without errors
gurobipy.read(str(fn))

Sorry, the diff of this file is too big to display