@@ -1644,3 +1644,216 @@ def test_quadratic_expression_rolling_centered(self) -> None:
16441644 qexpr = x * x
16451645 result = qexpr .rolling (dim_0 = 3 , center = True ).sum ()
16461646 assert result is not None
1647+
1648+
1649+ class TestPrintSingleQuadraticConstraint :
1650+ """Tests for print_single_quadratic_constraint function."""
1651+
1652+ def test_print_single_quadratic_constraint_basic (self ) -> None :
1653+ """Test printing a single quadratic constraint."""
1654+ from linopy .common import print_single_quadratic_constraint
1655+
1656+ m = Model ()
1657+ x = m .add_variables (lower = 0 , name = "x" )
1658+ y = m .add_variables (lower = 0 , name = "y" )
1659+ m .add_quadratic_constraints (x * x + 2 * y , "<=" , 10 , name = "qc" )
1660+
1661+ label = int (m .quadratic_constraints ["qc" ].labels .values )
1662+ result = print_single_quadratic_constraint (m , label )
1663+
1664+ assert "qc" in result
1665+ assert "≤" in result or "<=" in result
1666+ assert "10" in result
1667+
1668+ def test_print_single_quadratic_constraint_multidim (self ) -> None :
1669+ """Test printing a single quadratic constraint from multidimensional."""
1670+ from linopy .common import print_single_quadratic_constraint
1671+
1672+ m = Model ()
1673+ x = m .add_variables (lower = 0 , coords = [range (3 )], name = "x" )
1674+ m .add_quadratic_constraints (x * x , "<=" , 10 , name = "qc" )
1675+
1676+ # Get first label
1677+ labels = m .quadratic_constraints ["qc" ].labels .values .ravel ()
1678+ first_label = int (labels [labels != - 1 ][0 ])
1679+ result = print_single_quadratic_constraint (m , first_label )
1680+
1681+ assert "qc" in result
1682+
1683+
1684+ class TestQuadraticConstraintIOCoverage :
1685+ """Tests for IO coverage of quadratic constraints."""
1686+
1687+ def test_lp_file_with_progress (self ) -> None :
1688+ """Test LP file writing with progress=True."""
1689+ m = Model ()
1690+ x = m .add_variables (lower = 0 , name = "x" )
1691+ m .add_quadratic_constraints (x * x , "<=" , 10 , name = "qc" )
1692+ m .add_objective (x )
1693+
1694+ with tempfile .NamedTemporaryFile (suffix = ".lp" , delete = False ) as f :
1695+ # Write with progress (tests the progress branch)
1696+ m .to_file (f .name , progress = True )
1697+ content = Path (f .name ).read_text ()
1698+
1699+ assert "qc" in content .lower () or "QCMATRIX" in content
1700+
1701+ def test_lp_file_only_quadratic_constraints (self ) -> None :
1702+ """Test LP file with only quadratic constraints (no linear)."""
1703+ m = Model ()
1704+ x = m .add_variables (lower = 0 , name = "x" )
1705+ # Only quadratic constraint, no linear constraints
1706+ m .add_quadratic_constraints (x * x , "<=" , 10 , name = "qc" )
1707+ m .add_objective (x )
1708+
1709+ with tempfile .NamedTemporaryFile (suffix = ".lp" , delete = False ) as f :
1710+ m .to_file (f .name )
1711+ content = Path (f .name ).read_text ()
1712+
1713+ # Should have "s.t." section header
1714+ assert "s.t." in content or "Subject" in content
1715+
1716+ def test_lp_file_explicit_coordinate_names (self ) -> None :
1717+ """Test LP file with explicit coordinate names."""
1718+ m = Model ()
1719+ x = m .add_variables (lower = 0 , coords = [range (2 )], name = "x" )
1720+ m .add_quadratic_constraints (x * x , "<=" , 10 , name = "qc" )
1721+ m .add_objective (x .sum ())
1722+
1723+ with tempfile .NamedTemporaryFile (suffix = ".lp" , delete = False ) as f :
1724+ m .to_file (f .name , explicit_coordinate_names = True )
1725+ content = Path (f .name ).read_text ()
1726+
1727+ assert "qc" in content .lower ()
1728+
1729+ def test_netcdf_roundtrip_with_linear_terms (self ) -> None :
1730+ """Test netCDF roundtrip for QC with both quadratic and linear terms."""
1731+ m = Model ()
1732+ x = m .add_variables (lower = 0 , name = "x" )
1733+ y = m .add_variables (lower = 0 , name = "y" )
1734+ # Mixed quadratic and linear terms
1735+ m .add_quadratic_constraints (x * x + 2 * x + 3 * y , "<=" , 10 , name = "qc" )
1736+ m .add_objective (x + y )
1737+
1738+ with tempfile .TemporaryDirectory () as tmpdir :
1739+ path = Path (tmpdir ) / "model.nc"
1740+ m .to_netcdf (path )
1741+
1742+ m2 = linopy .read_netcdf (path )
1743+
1744+ assert len (m2 .quadratic_constraints ) == 1
1745+ assert "qc" in m2 .quadratic_constraints
1746+
1747+
1748+ class TestQuadraticConstraintToPolars :
1749+ """Tests for to_polars method coverage."""
1750+
1751+ def test_to_polars_with_linear_terms (self ) -> None :
1752+ """Test to_polars with mixed quadratic and linear terms."""
1753+ m = Model ()
1754+ x = m .add_variables (lower = 0 , name = "x" )
1755+ y = m .add_variables (lower = 0 , name = "y" )
1756+ qc = m .add_quadratic_constraints (x * x + 2 * x + 3 * y , "<=" , 10 , name = "qc" )
1757+
1758+ df = qc .to_polars ()
1759+ assert "coeffs" in df .columns
1760+ assert "is_quadratic" in df .columns
1761+
1762+ # Should have both quadratic and linear terms
1763+ n_quad = df .filter (pl .col ("is_quadratic" )).height
1764+ n_linear = df .filter (~ pl .col ("is_quadratic" )).height
1765+ assert n_quad >= 1
1766+ assert n_linear >= 1
1767+
1768+ def test_to_polars_multidimensional (self ) -> None :
1769+ """Test to_polars with multidimensional constraint."""
1770+ m = Model ()
1771+ x = m .add_variables (lower = 0 , coords = [range (3 )], name = "x" )
1772+ qc = m .add_quadratic_constraints (x * x , "<=" , 10 , name = "qc" )
1773+
1774+ df = qc .to_polars ()
1775+ assert df .height > 0
1776+ # Should have 3 unique labels (one per coordinate)
1777+ assert df ["labels" ].n_unique () == 3
1778+
1779+
1780+ class TestQuadraticConstraintFlat :
1781+ """Tests for flat property coverage."""
1782+
1783+ def test_flat_multidimensional (self ) -> None :
1784+ """Test flat property with multidimensional constraints."""
1785+ m = Model ()
1786+ x = m .add_variables (lower = 0 , coords = [range (2 ), range (3 )], name = "x" )
1787+ m .add_quadratic_constraints (x * x , "<=" , 10 , name = "qc" )
1788+
1789+ flat = m .quadratic_constraints .flat
1790+ assert isinstance (flat , pd .DataFrame )
1791+ assert len (flat ) > 0
1792+
1793+ def test_flat_multiple_constraints (self ) -> None :
1794+ """Test flat property with multiple constraints."""
1795+ m = Model ()
1796+ x = m .add_variables (lower = 0 , name = "x" )
1797+ y = m .add_variables (lower = 0 , name = "y" )
1798+ m .add_quadratic_constraints (x * x , "<=" , 10 , name = "qc1" )
1799+ m .add_quadratic_constraints (y * y , "<=" , 20 , name = "qc2" )
1800+
1801+ flat = m .quadratic_constraints .flat
1802+ assert len (flat ) >= 2
1803+
1804+
1805+ class TestQuadraticExpressionAdditional :
1806+ """Additional tests for QuadraticExpression edge cases."""
1807+
1808+ def test_quadratic_expression_constant_only (self ) -> None :
1809+ """Test quadratic expression that simplifies to constant."""
1810+ m = Model ()
1811+ x = m .add_variables (lower = 0 , name = "x" )
1812+
1813+ # Expression with only constant term (after cancellation)
1814+ qexpr = x * x - x * x + 5
1815+ # This should still be a QuadraticExpression but effectively constant
1816+ assert qexpr is not None
1817+
1818+ def test_quadratic_expression_to_constraint_ge (self ) -> None :
1819+ """Test QuadraticExpression.to_constraint with >= sign."""
1820+ from linopy .constraints import QuadraticConstraint
1821+
1822+ m = Model ()
1823+ x = m .add_variables (lower = 0 , name = "x" )
1824+
1825+ qexpr = x * x
1826+ qc = qexpr .to_constraint (">=" , 5 )
1827+ assert isinstance (qc , QuadraticConstraint )
1828+ assert str (qc .sign .values ) == ">="
1829+
1830+ def test_quadratic_expression_to_constraint_eq (self ) -> None :
1831+ """Test QuadraticExpression.to_constraint with = sign."""
1832+ from linopy .constraints import QuadraticConstraint
1833+
1834+ m = Model ()
1835+ x = m .add_variables (lower = 0 , name = "x" )
1836+
1837+ qexpr = x * x
1838+ qc = qexpr .to_constraint ("=" , 5 )
1839+ assert isinstance (qc , QuadraticConstraint )
1840+ assert str (qc .sign .values ) == "="
1841+
1842+ def test_quadratic_expression_comparison_operators (self ) -> None :
1843+ """Test QuadraticExpression comparison operators."""
1844+ m = Model ()
1845+ x = m .add_variables (lower = 0 , name = "x" )
1846+
1847+ qexpr = x * x
1848+
1849+ # Test <= operator
1850+ c1 = qexpr <= 10
1851+ assert c1 is not None
1852+
1853+ # Test >= operator
1854+ c2 = qexpr >= 1
1855+ assert c2 is not None
1856+
1857+ # Test == operator
1858+ c3 = qexpr == 5
1859+ assert c3 is not None
0 commit comments