pipstools 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pipstools/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .annotation import annotate_problem as annotate_problem
2
+ from .conversion import convert_problem as convert_problem
3
+ from .scoring import score_problem as score_problem
4
+ from .validation import validate_problem as validate_problem
5
+ from .visualisation import visualize_problem as visualize_problem
@@ -0,0 +1,204 @@
1
+ import gzip
2
+ import importlib.util
3
+ import shutil
4
+ from pathlib import Path
5
+ from time import perf_counter
6
+
7
+ from pipstools.io import read_gdx, read_mps, read_parquet, write_gdx, write_parquet
8
+ from pipstools.partitioning import get_partitions
9
+ from pipstools.scoring import get_score
10
+ from pipstools.utils import get_stem
11
+
12
+
13
+ def annotate_problem(
14
+ f_input,
15
+ k,
16
+ f_output=None,
17
+ fileformat="gdx",
18
+ var_dense=None,
19
+ equ_dense=None,
20
+ presolve=False,
21
+ mpsreader=None,
22
+ method="hypergraph",
23
+ hypergraph="col",
24
+ hg_objective="soed",
25
+ regex_pattern=None,
26
+ regex_order=None,
27
+ regex_mapfile=None,
28
+ dec=None,
29
+ write_names=True,
30
+ write_uels=True,
31
+ distributed=False,
32
+ vcycles=0,
33
+ ):
34
+ start = perf_counter()
35
+ if isinstance(f_input, str):
36
+ f_input = Path(f_input)
37
+
38
+ if var_dense is not None and var_dense < 0:
39
+ var_dense = None
40
+
41
+ if equ_dense is not None and equ_dense < 0:
42
+ var_dense = None
43
+
44
+ if mpsreader is None:
45
+ lib_gurobi = importlib.util.find_spec("gurobipy")
46
+ if lib_gurobi is not None:
47
+ mpsreader = "gurobi"
48
+ else:
49
+ mpsreader = "highs"
50
+ print(f"Selected {mpsreader} as default mps reader.")
51
+
52
+ if k == 1 and method.lower() != "oneblock":
53
+ print('Specificed partitioning into a single block, switching method to "oneblock"')
54
+ method = "oneblock"
55
+
56
+ if regex_pattern is not None:
57
+ print('Specificed regex pattern, switching method to "regex"')
58
+ method = "regex"
59
+
60
+ if dec is not None:
61
+ print('Specificed .dec file, switching method to "dec"')
62
+ method = "dec"
63
+
64
+ # Check if valid hypergraph arguments are provided
65
+ if method == "hypergraph":
66
+ if hypergraph.lower() not in ["col", "row", "colrow", "rowcol"]:
67
+ raise IOError(
68
+ f"Hypergraph type {hypergraph} is not available. "
69
+ "Use either 'col', 'row', 'colrow', or 'rowcol'"
70
+ )
71
+ if hg_objective.lower() not in ["cut", "km1", "soed"]:
72
+ raise IOError(
73
+ f"Hypergraph objective {hg_objective} is not available. "
74
+ "Use either 'cut', 'km1', or 'soed'"
75
+ )
76
+
77
+ # Read in file and extract A, vars, cols
78
+ match f_input.suffix.lower():
79
+ case ".gdx":
80
+ A, cols, rows, objcol, objrow, objcoef, objjacval = read_gdx(f_input=f_input)
81
+ case ".parquet":
82
+ A, cols, rows, objcol, objrow, objcoef, objjacval = read_parquet(f_input=f_input)
83
+ case ".lp" | ".mps" | ".gz" | ".bz2":
84
+ compressed_input = f_input.suffix.lower() in [".gz", ".bz2"]
85
+
86
+ # File needs to be compresses lp or mps
87
+ if compressed_input:
88
+ suffix_uncomp = f_input.stem.lower()
89
+ if not suffix_uncomp.endswith(".lp") and not suffix_uncomp.endswith(".mps"):
90
+ raise (
91
+ IOError(
92
+ "Can only read compressed files of type .lp and .mps, "
93
+ f"not {f_input.suffix}"
94
+ )
95
+ )
96
+
97
+ # For highspy manually decompress file and delete later
98
+ remove_decompressed = False
99
+ if compressed_input and mpsreader == "highs":
100
+ print(f"Automatically decompressing problem file {f_input.as_posix()} for highs")
101
+ if f_input.with_suffix("").exists():
102
+ raise (
103
+ IOError(
104
+ f"Cannot automatically decompress file {f_input.as_posix()}, since "
105
+ "a file with the target name already exists!"
106
+ )
107
+ )
108
+ with gzip.open(f_input, "rb") as f_in:
109
+ with open(f_input.with_suffix(""), "wb") as f_out:
110
+ shutil.copyfileobj(f_in, f_out)
111
+ f_input = f_input.with_suffix("")
112
+ remove_decompressed = True
113
+
114
+ # Read in mps content
115
+ A, cols, rows, objcol, objrow, objcoef = read_mps(
116
+ f_input=f_input,
117
+ presolve=presolve,
118
+ read_names=write_names or method == "regex",
119
+ mpsreader=mpsreader,
120
+ )
121
+ objjacval = 1.0
122
+
123
+ # Remove temporary decompressed file again
124
+ if remove_decompressed:
125
+ print(f"Automatically removing decompressed file {f_input.as_posix()}")
126
+ f_input.unlink()
127
+ case _:
128
+ raise (IOError(f"No reader implemented for filetype {f_input.suffix}"))
129
+
130
+ # Check if kahypar is available
131
+ if method == "hypergraph":
132
+ lib_mkkahypar = importlib.util.find_spec("mtkahypar")
133
+ if lib_mkkahypar is None:
134
+ raise ImportError(
135
+ "Hypergraph partitioning specified, but library mtkahypar not available."
136
+ )
137
+
138
+ # Partition by regex, hypergraph or dense elements
139
+ cols, rows = get_partitions(
140
+ A=A,
141
+ cols=cols,
142
+ rows=rows,
143
+ method=method,
144
+ hypergraph=hypergraph,
145
+ hg_objective=hg_objective,
146
+ k=k,
147
+ var_dense=var_dense,
148
+ equ_dense=equ_dense,
149
+ regex_pattern=regex_pattern,
150
+ regex_order=regex_order,
151
+ regex_mapfile=regex_mapfile,
152
+ dec=dec,
153
+ vcycles=vcycles,
154
+ objcol=objcol,
155
+ )
156
+
157
+ # Derive stem for output
158
+ if f_output is None:
159
+ stem, suffix = get_stem(f_input)
160
+ else:
161
+ stem, suffix = get_stem(f_output)
162
+ if suffix is not None:
163
+ fileformat = suffix.lstrip(".")
164
+
165
+ f_output = Path(f"{stem}_{len(set(cols['partition'].unique()) - {1})}b")
166
+ f_output.parent.mkdir(parents=True, exist_ok=True)
167
+
168
+ # Get scoring for annotation
169
+ get_score(A, cols, rows)
170
+
171
+ # Write out annotated gdx file
172
+ match fileformat.lower():
173
+ case "gdx":
174
+ write_gdx(
175
+ f_input=f_input,
176
+ f_output=f_output,
177
+ A=A,
178
+ cols=cols,
179
+ rows=rows,
180
+ objcol=objcol,
181
+ objrow=objrow,
182
+ objcoef=objcoef,
183
+ objjacval=objjacval,
184
+ write_names=write_names,
185
+ write_uels=write_uels,
186
+ distributed=distributed,
187
+ )
188
+ case "parquet":
189
+ write_parquet(
190
+ f_input=f_input,
191
+ f_output=f_output,
192
+ A=A,
193
+ cols=cols,
194
+ rows=rows,
195
+ objcol=objcol,
196
+ objrow=objrow,
197
+ objcoef=objcoef,
198
+ objjacval=objjacval,
199
+ )
200
+ case _:
201
+ raise (IOError(f"No method implemented for output format {fileformat}"))
202
+
203
+ stop = perf_counter()
204
+ print(f"Annotated problem in {(stop - start):.2f} seconds")
pipstools/cli.py ADDED
@@ -0,0 +1,258 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+ from typing_extensions import Annotated
5
+
6
+
7
+ def version_callback(value: bool):
8
+ if value:
9
+ import importlib.metadata
10
+
11
+ version = importlib.metadata.version("pipstools")
12
+ typer.echo(f"{version}")
13
+ raise typer.Exit()
14
+
15
+
16
+ app = typer.Typer(
17
+ add_completion=False,
18
+ )
19
+
20
+
21
+ @app.callback()
22
+ def common(
23
+ ctx: typer.Context,
24
+ version: bool = typer.Option(None, "--version", callback=version_callback),
25
+ ):
26
+ pass
27
+
28
+
29
+ @app.command(
30
+ help="Annotate a lp/mps/gdx file using hypergraph partitioning or regular expressions."
31
+ )
32
+ def annotate(
33
+ # Options for file IO
34
+ inputfile: Annotated[
35
+ str,
36
+ typer.Argument(help="Path to the input file (.lp, .mps, .gdx, or .parquet)"),
37
+ ],
38
+ outputfile: Annotated[
39
+ str | None,
40
+ typer.Option(help="Path to the output file"),
41
+ ] = None,
42
+ fileformat: Annotated[
43
+ str,
44
+ typer.Option(help="File format the output file"),
45
+ ] = "gdx",
46
+ mpsreader: Annotated[
47
+ str | None,
48
+ typer.Option(help='Package to use as mps reader ("gurobi", "highs")'),
49
+ ] = None,
50
+ # Options for matrix structure
51
+ blocks: Annotated[
52
+ int,
53
+ typer.Option(help="Number of blocks to annotate"),
54
+ ] = 30,
55
+ densecol: Annotated[
56
+ int | None,
57
+ typer.Option(
58
+ help="Number of non-zero entries to consider a column as dense, "
59
+ "defaults to heuristic detection of dense columns"
60
+ ),
61
+ ] = None,
62
+ denserow: Annotated[
63
+ int | None,
64
+ typer.Option(
65
+ help="Number of non-zero entries to consider a row as dense, "
66
+ "defaults to no detection of dense rows"
67
+ ),
68
+ ] = None,
69
+ presolve: Annotated[
70
+ bool,
71
+ typer.Option(help="Presolve the problem before partitioning"),
72
+ ] = False,
73
+ # Options for methods
74
+ method: Annotated[
75
+ str,
76
+ typer.Option(help='Method to use for partitioning ("hypergraph", "oneblock", "regex")'),
77
+ ] = "hypergraph",
78
+ hypergraph: Annotated[
79
+ str,
80
+ typer.Option(help='Type of hypergraph to use for partitioning ("col", "row", "colrow")'),
81
+ ] = "col",
82
+ hg_objective: Annotated[
83
+ str,
84
+ typer.Option(
85
+ help='Objective metric to use for hypergraph partitioning ("cut", "km1", "soed")'
86
+ ),
87
+ ] = "soed",
88
+ regex_pattern: Annotated[
89
+ str | None,
90
+ typer.Option(help="Pattern to use for variable partitioning via regular expressions"),
91
+ ] = None,
92
+ regex_order: Annotated[
93
+ str | None,
94
+ typer.Option(help="Ordering of the capture groups for partitioning (e.g. [2,1])"),
95
+ ] = None,
96
+ regex_mapfile: Annotated[
97
+ str | None,
98
+ typer.Option(help="Mapping file from regex capture groups to blocks in csv format."),
99
+ ] = None,
100
+ dec: Annotated[
101
+ str | None,
102
+ typer.Option(help="Additional .dec file containing a predefined decomposition"),
103
+ ] = None,
104
+ # Options for performance
105
+ distributed: Annotated[
106
+ bool,
107
+ typer.Option(help="Write out distributed gdx files"),
108
+ ] = False,
109
+ names: Annotated[
110
+ bool,
111
+ typer.Option(help="Write variable and constraint names to output files"),
112
+ ] = True,
113
+ uels: Annotated[
114
+ bool,
115
+ typer.Option(help="Write unique element list to output files"),
116
+ ] = True,
117
+ ) -> None:
118
+ from pipstools import annotate_problem
119
+
120
+ f_input = Path(inputfile)
121
+ if not f_input.exists():
122
+ raise IOError(f"File {f_input.as_posix()} not found.")
123
+
124
+ annotate_problem(
125
+ f_input,
126
+ f_output=outputfile,
127
+ fileformat=fileformat,
128
+ k=int(blocks),
129
+ var_dense=densecol,
130
+ equ_dense=denserow,
131
+ presolve=presolve,
132
+ mpsreader=mpsreader,
133
+ method=method,
134
+ hypergraph=hypergraph,
135
+ hg_objective=hg_objective,
136
+ regex_pattern=regex_pattern,
137
+ regex_order=regex_order,
138
+ regex_mapfile=regex_mapfile,
139
+ dec=dec,
140
+ write_names=names,
141
+ write_uels=uels,
142
+ distributed=distributed,
143
+ )
144
+
145
+
146
+ @app.command(help="Convert the problem structure between different file formats.")
147
+ def convert(
148
+ # Options for file IO
149
+ inputfile: Annotated[
150
+ str,
151
+ typer.Argument(help="Path to the problem file (.gdx or .parquet)"),
152
+ ],
153
+ outputfile: Annotated[
154
+ str | None,
155
+ typer.Option(help="Path to the output file"),
156
+ ] = None,
157
+ fileformat: Annotated[
158
+ str,
159
+ typer.Option(help="File format the output file"),
160
+ ] = "gdx",
161
+ # Options for performance
162
+ distributed: Annotated[
163
+ bool,
164
+ typer.Option(help="Write out distributed gdx files"),
165
+ ] = False,
166
+ names: Annotated[
167
+ bool,
168
+ typer.Option(help="Write variable and constraint names to output files"),
169
+ ] = True,
170
+ uels: Annotated[
171
+ bool,
172
+ typer.Option(help="Write unique element list to output files"),
173
+ ] = True,
174
+ ) -> None:
175
+ from pipstools import convert_problem
176
+
177
+ f_input = Path(inputfile)
178
+ if not f_input.exists():
179
+ raise IOError(f"File {f_input.as_posix()} not found.")
180
+
181
+ convert_problem(
182
+ f_input,
183
+ f_output=outputfile,
184
+ fileformat=fileformat,
185
+ write_names=names,
186
+ write_uels=uels,
187
+ distributed=distributed,
188
+ )
189
+
190
+
191
+ @app.command(help="Visualize the blocks structure of an annotated .gdx file.")
192
+ def visualize(
193
+ inputfile: Annotated[
194
+ str,
195
+ typer.Argument(help="Path to the problem file (.gdx or .parquet)"),
196
+ ],
197
+ outputfile: Annotated[
198
+ str | None,
199
+ typer.Option(help="Redirect the figure to a file."),
200
+ ] = None,
201
+ annotation: Annotated[
202
+ bool,
203
+ typer.Option(help="Visualize the annotation"),
204
+ ] = True,
205
+ names: Annotated[
206
+ bool,
207
+ typer.Option(help="Try to use variable and constraint names"),
208
+ ] = True,
209
+ force: Annotated[
210
+ bool,
211
+ typer.Option(help="Try to use variable and constraint names"),
212
+ ] = False,
213
+ ) -> None:
214
+ from pipstools import visualize_problem
215
+
216
+ f_input = Path(inputfile)
217
+ if not f_input.exists():
218
+ raise IOError(f"File {f_input.as_posix()} not found.")
219
+
220
+ visualize_problem(
221
+ f_input,
222
+ output=outputfile,
223
+ read_names=names,
224
+ use_annotation=annotation,
225
+ force=force,
226
+ )
227
+
228
+
229
+ @app.command(help="Score the blocks structure of an annotated problem.")
230
+ def score(
231
+ inputfile: Annotated[
232
+ str,
233
+ typer.Argument(help="Path to the problem file (.gdx or .parquet)"),
234
+ ],
235
+ ) -> None:
236
+ from pipstools import score_problem
237
+
238
+ f_input = Path(inputfile)
239
+ if not f_input.exists():
240
+ raise IOError(f"File {f_input.as_posix()} not found.")
241
+
242
+ score_problem(f_input)
243
+
244
+
245
+ @app.command(help="Validate the blocks structure of an annotated problem.")
246
+ def validate(
247
+ inputfile: Annotated[
248
+ str,
249
+ typer.Argument(help="Path to the problem file (.gdx or .parquet)"),
250
+ ],
251
+ ) -> None:
252
+ from pipstools import validate_problem
253
+
254
+ f_input = Path(inputfile)
255
+ if not f_input.exists():
256
+ raise IOError(f"File {f_input.as_posix()} not found.")
257
+
258
+ validate_problem(f_input)
@@ -0,0 +1,75 @@
1
+ from pathlib import Path
2
+ from time import perf_counter
3
+
4
+ from pipstools.io import read_gdx, read_parquet, write_gdx, write_parquet
5
+ from pipstools.utils import get_stem
6
+
7
+
8
+ def convert_problem(
9
+ f_input,
10
+ f_output=None,
11
+ fileformat="gdx",
12
+ write_names=True,
13
+ write_uels=True,
14
+ distributed=False,
15
+ ):
16
+ if isinstance(f_input, str):
17
+ f_input = Path(f_input)
18
+
19
+ match f_input.suffix.lower():
20
+ case ".gdx":
21
+ A, cols, rows, objcol, objrow, objcoef, objjacval = read_gdx(f_input=f_input)
22
+ case ".parquet":
23
+ A, cols, rows, objcol, objrow, objcoef, objjacval = read_parquet(f_input=f_input)
24
+ case _:
25
+ raise IOError("Converting is only supported for .gdx files and .parquet collections.")
26
+
27
+ print(f"Converting a model with {len(rows)} rows, {len(cols)} columns, {len(A)} nonzeros")
28
+ start = perf_counter()
29
+
30
+ # Derive stem for output
31
+ if f_output is None:
32
+ stem, suffix = get_stem(f_input)
33
+ else:
34
+ stem, suffix = get_stem(f_output)
35
+ if suffix is not None:
36
+ print(f"Automatically derived fileformat {suffix} from output file")
37
+ fileformat = suffix.lstrip(".")
38
+
39
+ f_output = Path(f"{stem}_{len(set(cols['partition'].unique()) - {1})}b")
40
+ f_output.parent.mkdir(parents=True, exist_ok=True)
41
+
42
+ # Write out annotated gdx file
43
+ match fileformat.lower():
44
+ case "gdx":
45
+ write_gdx(
46
+ f_input=f_input,
47
+ f_output=f_output,
48
+ A=A,
49
+ cols=cols,
50
+ rows=rows,
51
+ objcol=objcol,
52
+ objrow=objrow,
53
+ objcoef=objcoef,
54
+ objjacval=objjacval,
55
+ write_names=write_names,
56
+ write_uels=write_uels,
57
+ distributed=distributed,
58
+ )
59
+ case "parquet":
60
+ write_parquet(
61
+ f_input=f_input,
62
+ f_output=f_output,
63
+ A=A,
64
+ cols=cols,
65
+ rows=rows,
66
+ objcol=objcol,
67
+ objrow=objrow,
68
+ objcoef=objcoef,
69
+ objjacval=objjacval,
70
+ )
71
+ case _:
72
+ raise (IOError(f"No method implemented for output format {fileformat}"))
73
+
74
+ stop = perf_counter()
75
+ print(f"Converted problem in {(stop - start):.2f} seconds")
@@ -0,0 +1,5 @@
1
+ from .gdx import read_gdx as read_gdx
2
+ from .gdx import write_gdx as write_gdx
3
+ from .mps import read_mps as read_mps
4
+ from .parquet import read_parquet as read_parquet
5
+ from .parquet import write_parquet as write_parquet