Skip to content

Commit 701a890

Browse files
committed
Remove the lp-debug io_api and replace it with parameter. Extend it to polars export too
1 parent 315fbea commit 701a890

File tree

5 files changed

+223
-169
lines changed

5 files changed

+223
-169
lines changed

linopy/io.py

Lines changed: 84 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -69,29 +69,48 @@ def print_coord(coord):
6969
return coord
7070

7171

72-
def get_print(m: Model, anonymously: bool = True):
72+
def get_printers(m: Model, io_api: str, anonymously: bool = True):
7373
if anonymously:
74+
if io_api == "lp":
75+
def print_variable_anonymous(var):
76+
return f"x{var}"
7477

75-
def print_variable_anonymous(var):
76-
return f"x{var}"
78+
def print_constraint_anonymous(cons):
79+
return f"c{cons}"
7780

78-
def print_constraint_anonymous(cons):
79-
return f"c{cons}"
81+
elif io_api == "lp-polars":
82+
def print_variable_anonymous(series):
83+
return pl.lit(" x").alias("x"), series.cast(pl.String)
84+
85+
def print_constraint_anonymous(series):
86+
return pl.lit("c").alias("c"), series.cast(pl.String)
87+
else:
88+
return None
8089

8190
return print_variable_anonymous, print_constraint_anonymous
8291
else:
83-
84-
def print_variable(var):
92+
def print_variable_lp(var):
8593
name, coord = m.variables.get_label_position(var)
8694
name = clean_name(name)
87-
return f"{name}{print_coord(coord)}"
95+
return f"{name}{print_coord(coord)}#{var}"
8896

89-
def print_constraint(cons):
97+
def print_constraint_lp(cons):
9098
name, coord = m.constraints.get_label_position(cons)
9199
name = clean_name(name)
92-
return f"{name}{print_coord(coord)}"
100+
return f"{name}{print_coord(coord)}#{cons}"
101+
102+
def print_variable_polars(series):
103+
return pl.lit(" "), series.map_elements(print_variable_lp, return_dtype=pl.String)
93104

94-
return print_variable, print_constraint
105+
def print_constraint_polars(series):
106+
return pl.lit(None), series.map_elements(print_constraint_lp, return_dtype=pl.String)
107+
108+
if io_api == "lp":
109+
return print_variable_lp, print_constraint_lp
110+
elif io_api == "lp-polars":
111+
return print_variable_polars, print_constraint_polars
112+
else:
113+
return None
95114

96115

97116
def objective_write_linear_terms(
@@ -392,12 +411,10 @@ def to_lp_file(
392411
integer_label: str,
393412
slice_size: int = 10_000_000,
394413
progress: bool = True,
395-
anonymously: bool = True,
414+
printers = None,
396415
) -> None:
397416
batch_size = 5000
398417

399-
printers = get_print(m, anonymously)
400-
401418
with open(fn, mode="w") as f:
402419
start = time.time()
403420

@@ -443,27 +460,25 @@ def to_lp_file(
443460
logger.info(f" Writing time: {round(time.time()-start, 2)}s")
444461

445462

446-
def objective_write_linear_terms_polars(f, df):
463+
def objective_write_linear_terms_polars(f, df, print_variable):
447464
cols = [
448465
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
449466
pl.col("coeffs").cast(pl.String),
450-
pl.lit(" x"),
451-
pl.col("vars").cast(pl.String),
467+
*print_variable(pl.col("vars")),
452468
]
453469
df = df.select(pl.concat_str(cols, ignore_nulls=True))
454470
df.write_csv(
455471
f, separator=" ", null_value="", quote_style="never", include_header=False
456472
)
457473

458474

459-
def objective_write_quadratic_terms_polars(f, df):
475+
def objective_write_quadratic_terms_polars(f, df, print_variable):
460476
cols = [
461477
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
462478
pl.col("coeffs").mul(2).cast(pl.String),
463-
pl.lit(" x"),
464-
pl.col("vars1").cast(pl.String),
465-
pl.lit(" * x"),
466-
pl.col("vars2").cast(pl.String),
479+
*print_variable(pl.col("vars1")),
480+
pl.lit(" *"),
481+
*print_variable(pl.col("vars2")),
467482
]
468483
f.write(b"+ [\n")
469484
df = df.select(pl.concat_str(cols, ignore_nulls=True))
@@ -473,7 +488,7 @@ def objective_write_quadratic_terms_polars(f, df):
473488
f.write(b"] / 2\n")
474489

475490

476-
def objective_to_file_polars(m, f, progress=False):
491+
def objective_to_file_polars(m, f, progress=False, printers=None):
477492
"""
478493
Write out the objective of a model to a lp file.
479494
"""
@@ -484,8 +499,10 @@ def objective_to_file_polars(m, f, progress=False):
484499
f.write(f"{sense}\n\nobj:\n\n".encode())
485500
df = m.objective.to_polars()
486501

502+
print_variable, _ = printers
503+
487504
if m.is_linear:
488-
objective_write_linear_terms_polars(f, df)
505+
objective_write_linear_terms_polars(f, df, print_variable)
489506

490507
elif m.is_quadratic:
491508
lins = df.filter(pl.col("vars1").eq(-1) | pl.col("vars2").eq(-1))
@@ -495,20 +512,22 @@ def objective_to_file_polars(m, f, progress=False):
495512
.otherwise(pl.col("vars1"))
496513
.alias("vars")
497514
)
498-
objective_write_linear_terms_polars(f, lins)
515+
objective_write_linear_terms_polars(f, lins, print_variable)
499516

500517
quads = df.filter(pl.col("vars1").ne(-1) & pl.col("vars2").ne(-1))
501-
objective_write_quadratic_terms_polars(f, quads)
518+
objective_write_quadratic_terms_polars(f, quads, print_variable)
502519

503520

504-
def bounds_to_file_polars(m, f, progress=False, slice_size=2_000_000):
521+
def bounds_to_file_polars(m, f, progress=False, slice_size=2_000_000, printers=None):
505522
"""
506523
Write out variables of a model to a lp file.
507524
"""
508525
names = list(m.variables.continuous) + list(m.variables.integers)
509526
if not len(list(names)):
510527
return
511528

529+
print_variable, _ = printers
530+
512531
f.write(b"\n\nbounds\n\n")
513532
if progress:
514533
names = tqdm(
@@ -525,8 +544,8 @@ def bounds_to_file_polars(m, f, progress=False, slice_size=2_000_000):
525544
columns = [
526545
pl.when(pl.col("lower") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
527546
pl.col("lower").cast(pl.String),
528-
pl.lit(" <= x"),
529-
pl.col("labels").cast(pl.String),
547+
pl.lit(" <= "),
548+
*print_variable(pl.col("labels")),
530549
pl.lit(" <= "),
531550
pl.when(pl.col("upper") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
532551
pl.col("upper").cast(pl.String),
@@ -539,14 +558,16 @@ def bounds_to_file_polars(m, f, progress=False, slice_size=2_000_000):
539558
formatted.write_csv(f, **kwargs)
540559

541560

542-
def binaries_to_file_polars(m, f, progress=False, slice_size=2_000_000):
561+
def binaries_to_file_polars(m, f, progress=False, slice_size=2_000_000, printers=None):
543562
"""
544563
Write out binaries of a model to a lp file.
545564
"""
546565
names = m.variables.binaries
547566
if not len(list(names)):
548567
return
549568

569+
print_variable, _ = printers
570+
550571
f.write(b"\n\nbinary\n\n")
551572
if progress:
552573
names = tqdm(
@@ -561,8 +582,7 @@ def binaries_to_file_polars(m, f, progress=False, slice_size=2_000_000):
561582
df = var_slice.to_polars()
562583

563584
columns = [
564-
pl.lit("x"),
565-
pl.col("labels").cast(pl.String),
585+
*print_variable(pl.col("labels")),
566586
]
567587

568588
kwargs = dict(
@@ -573,7 +593,7 @@ def binaries_to_file_polars(m, f, progress=False, slice_size=2_000_000):
573593

574594

575595
def integers_to_file_polars(
576-
m, f, progress=False, integer_label="general", slice_size=2_000_000
596+
m, f, progress=False, integer_label="general", slice_size=2_000_000, printers=None
577597
):
578598
"""
579599
Write out integers of a model to a lp file.
@@ -582,6 +602,8 @@ def integers_to_file_polars(
582602
if not len(list(names)):
583603
return
584604

605+
print_variable, _ = printers
606+
585607
f.write(f"\n\n{integer_label}\n\n".encode())
586608
if progress:
587609
names = tqdm(
@@ -596,8 +618,7 @@ def integers_to_file_polars(
596618
df = var_slice.to_polars()
597619

598620
columns = [
599-
pl.lit("x"),
600-
pl.col("labels").cast(pl.String),
621+
*print_variable(pl.col("labels")),
601622
]
602623

603624
kwargs = dict(
@@ -607,10 +628,12 @@ def integers_to_file_polars(
607628
formatted.write_csv(f, **kwargs)
608629

609630

610-
def constraints_to_file_polars(m, f, progress=False, lazy=False, slice_size=2_000_000):
631+
def constraints_to_file_polars(m, f, progress=False, lazy=False, slice_size=2_000_000, printers=None):
611632
if not len(m.constraints):
612633
return
613634

635+
print_variable, print_constraint = printers
636+
614637
f.write(b"\n\ns.t.\n\n")
615638
names = m.constraints
616639
if progress:
@@ -636,14 +659,16 @@ def constraints_to_file_polars(m, f, progress=False, lazy=False, slice_size=2_00
636659
.alias("labels")
637660
)
638661

662+
row_labels = print_constraint(pl.col("labels"))
663+
col_labels = print_variable(pl.col("vars"))
639664
columns = [
640-
pl.when(pl.col("labels").is_not_null()).then(pl.lit("c")).alias("c"),
641-
pl.col("labels").cast(pl.String),
665+
pl.when(pl.col("labels").is_not_null()).then(row_labels[0]),
666+
pl.when(pl.col("labels").is_not_null()).then(row_labels[1]),
642667
pl.when(pl.col("labels").is_not_null()).then(pl.lit(":\n")).alias(":"),
643668
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")),
644669
pl.col("coeffs").cast(pl.String),
645-
pl.when(pl.col("vars").is_not_null()).then(pl.lit(" x")).alias("x"),
646-
pl.col("vars").cast(pl.String),
670+
pl.when(pl.col("vars").is_not_null()).then(col_labels[0]),
671+
pl.when(pl.col("vars").is_not_null()).then(col_labels[1]),
647672
"sign",
648673
pl.lit(" "),
649674
pl.col("rhs").cast(pl.String),
@@ -662,21 +687,27 @@ def constraints_to_file_polars(m, f, progress=False, lazy=False, slice_size=2_00
662687

663688

664689
def to_lp_file_polars(
665-
m, fn, integer_label="general", slice_size=2_000_000, progress: bool = True
690+
m,
691+
fn,
692+
integer_label="general",
693+
slice_size=2_000_000,
694+
progress: bool = True,
695+
printers=None,
666696
):
667697
with open(fn, mode="wb") as f:
668698
start = time.time()
669699

670-
objective_to_file_polars(m, f, progress=progress)
671-
constraints_to_file_polars(m, f=f, progress=progress, slice_size=slice_size)
672-
bounds_to_file_polars(m, f=f, progress=progress, slice_size=slice_size)
673-
binaries_to_file_polars(m, f=f, progress=progress, slice_size=slice_size)
700+
objective_to_file_polars(m, f, progress=progress, printers=printers)
701+
constraints_to_file_polars(m, f=f, progress=progress, slice_size=slice_size, printers=printers)
702+
bounds_to_file_polars(m, f=f, progress=progress, slice_size=slice_size, printers=printers)
703+
binaries_to_file_polars(m, f=f, progress=progress, slice_size=slice_size, printers=printers)
674704
integers_to_file_polars(
675705
m,
676706
integer_label=integer_label,
677707
f=f,
678708
progress=progress,
679709
slice_size=slice_size,
710+
printers = printers,
680711
)
681712
f.write(b"end\n")
682713

@@ -690,6 +721,7 @@ def to_file(
690721
integer_label: str = "general",
691722
slice_size: int = 2_000_000,
692723
progress: bool | None = None,
724+
with_names: bool = True,
693725
) -> Path:
694726
"""
695727
Write out a model to a lp or mps file.
@@ -707,20 +739,15 @@ def to_file(
707739
if progress is None:
708740
progress = m._xCounter > 10_000
709741

742+
printers = get_printers(m, io_api, anonymously=not with_names)
743+
if with_names and io_api not in ["lp", "lp-polars"]:
744+
logger.warning("Debug names are only supported for LP files.")
745+
710746
if io_api == "lp":
711-
to_lp_file(m, fn, integer_label, slice_size=slice_size, progress=progress)
712-
elif io_api == "lp-debug":
713-
to_lp_file(
714-
m,
715-
fn,
716-
integer_label,
717-
slice_size=slice_size,
718-
progress=progress,
719-
anonymously=False,
720-
)
747+
to_lp_file(m, fn, integer_label, slice_size=slice_size, progress=progress, printers=printers)
721748
elif io_api == "lp-polars":
722749
to_lp_file_polars(
723-
m, fn, integer_label, slice_size=slice_size, progress=progress
750+
m, fn, integer_label, slice_size=slice_size, progress=progress, printers=printers
724751
)
725752

726753
elif io_api == "mps":
@@ -735,7 +762,7 @@ def to_file(
735762
h.writeModel(str(fn))
736763
else:
737764
raise ValueError(
738-
f"Invalid io_api '{io_api}'. Choose from 'lp', 'lp-debug', 'lp-polars' or 'mps'."
765+
f"Invalid io_api '{io_api}'. Choose from 'lp', 'lp-polars' or 'mps'."
739766
)
740767

741768
return fn

linopy/model.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,7 @@ def solve(
958958
self,
959959
solver_name: str | None = None,
960960
io_api: str | None = None,
961+
with_names: bool = False,
961962
problem_fn: str | Path | None = None,
962963
solution_fn: str | Path | None = None,
963964
log_fn: str | Path | None = None,
@@ -990,6 +991,11 @@ def solve(
990991
'direct' the problem is communicated to the solver via the solver
991992
specific API, e.g. gurobipy. This may lead to faster run times.
992993
The default is set to 'lp' if available.
994+
with_names : bool, optional
995+
If the Api to use for communicating with the solver is based on 'lp',
996+
this option allows to keep the variable and constraint names in the
997+
lp file. This may lead to slower run times.
998+
The default is set to False.
993999
problem_fn : path_like, optional
9941000
Path of the lp file or output file/directory which is written out
9951001
during the process. The default None results in a temporary file.
@@ -1119,6 +1125,8 @@ def solve(
11191125
**solver_options,
11201126
)
11211127
if io_api == "direct":
1128+
if with_names:
1129+
logger.warning("Passing variable and constraint names is only supported with lp files")
11221130
# no problem file written and direct model is set for solver
11231131
result = solver.solve_problem_from_model(
11241132
model=self,
@@ -1131,7 +1139,8 @@ def solve(
11311139
else:
11321140
problem_fn = self.to_file(
11331141
to_path(problem_fn),
1134-
io_api,
1142+
io_api=io_api,
1143+
with_names=with_names,
11351144
slice_size=slice_size,
11361145
progress=progress,
11371146
)
@@ -1212,10 +1221,16 @@ def compute_infeasibilities(self) -> list[int]:
12121221
f = NamedTemporaryFile(suffix=".ilp", prefix="linopy-iis-", delete=False)
12131222
solver_model.write(f.name)
12141223
labels = []
1224+
pattern = re.compile(r"^ [^:]+#([0-9]+):")
12151225
for line in f.readlines():
12161226
line_decoded = line.decode()
1217-
if line_decoded.startswith(" c"):
1218-
labels.append(int(line_decoded.split(":")[0][2:]))
1227+
try:
1228+
if line_decoded.startswith(" c"):
1229+
labels.append(int(line_decoded.split(":")[0][2:]))
1230+
except ValueError as _:
1231+
match = pattern.match(line_decoded)
1232+
if match:
1233+
labels.append(int(match.group(1)))
12191234
return labels
12201235

12211236
def print_infeasibilities(self, display_max_terms: int | None = None) -> None:

0 commit comments

Comments
 (0)