From 6905698c5c6265d10be25264ec58d5e726fcd4d8 Mon Sep 17 00:00:00 2001 From: venaturum Date: Mon, 8 Jul 2024 22:29:44 +1000 Subject: [PATCH 1/5] Update autoflake version in precommit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b03d11..a7ce39c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/myint/autoflake - rev: v1.4 + rev: v2.3.1 hooks: - id: autoflake files: (^(src|tests)/)|(^[^/]*$) From 2d71bbe86854dbed433c4e4e7a9a7d3ea464dc13 Mon Sep 17 00:00:00 2001 From: venaturum Date: Mon, 8 Jul 2024 22:31:10 +1000 Subject: [PATCH 2/5] adding pre-tree-solution parser functionality --- src/gurobi_logtools/api.py | 3 ++ src/gurobi_logtools/parsers/continuous.py | 9 +++- src/gurobi_logtools/parsers/presolve.py | 7 ++- .../parsers/pretree_solutions.py | 45 +++++++++++++++++++ src/gurobi_logtools/parsers/single_log.py | 11 ++++- 5 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 src/gurobi_logtools/parsers/pretree_solutions.py diff --git a/src/gurobi_logtools/api.py b/src/gurobi_logtools/api.py index e9f7481..09269a2 100644 --- a/src/gurobi_logtools/api.py +++ b/src/gurobi_logtools/api.py @@ -53,6 +53,8 @@ def progress(self, section="nodelog") -> dict: log = parser.continuous_parser.get_progress() elif section == "norel": log = parser.norel_parser.get_progress() + elif section == "pretreesols": + log = parser.pretree_solution_parser.get_progress() else: raise ValueError(f"Unknown section '{section}'") @@ -194,4 +196,5 @@ def get_dataframe(logfiles: List[str], timelines=False, prettyparams=False): norel=result.progress("norel"), rootlp=result.progress("rootlp"), nodelog=result.progress("nodelog"), + pretreesols=result.progress("pretreesols"), ) diff --git a/src/gurobi_logtools/parsers/continuous.py b/src/gurobi_logtools/parsers/continuous.py index 304b225..774939c 100644 --- a/src/gurobi_logtools/parsers/continuous.py +++ b/src/gurobi_logtools/parsers/continuous.py @@ -1,6 +1,7 @@ import re from gurobi_logtools.parsers.barrier import BarrierParser +from gurobi_logtools.parsers.pretree_solutions import PretreeSolutionParser from gurobi_logtools.parsers.simplex import SimplexParser from gurobi_logtools.parsers.util import typeconvert_groupdict @@ -24,7 +25,7 @@ class ContinuousParser: re.compile(r"(?POptimal objective\s+(?P.*))$"), ] - def __init__(self): + def __init__(self, pre_tree_solution_parser: PretreeSolutionParser): """Initialize the Continuous parser.""" self._barrier_parser = BarrierParser() self._simplex_parser = SimplexParser() @@ -33,6 +34,8 @@ def __init__(self): self._current_pattern = None + self._pre_tree_solution_parser = pre_tree_solution_parser + def parse(self, line: str) -> bool: """Parse the given log line to populate summary and progress data. @@ -44,6 +47,10 @@ def parse(self, line: str) -> bool: Returns: bool: Return True if the given line is matched by some pattern. """ + + if self._pre_tree_solution_parser.parse(line): + return True + mip_relaxation_match = ContinuousParser.mip_relaxation_pattern.match(line) if mip_relaxation_match: self._current_pattern = "relaxation" diff --git a/src/gurobi_logtools/parsers/presolve.py b/src/gurobi_logtools/parsers/presolve.py index 99a4a36..75525a4 100644 --- a/src/gurobi_logtools/parsers/presolve.py +++ b/src/gurobi_logtools/parsers/presolve.py @@ -1,5 +1,6 @@ import re +from gurobi_logtools.parsers.pretree_solutions import PretreeSolutionParser from gurobi_logtools.parsers.util import typeconvert_groupdict @@ -66,7 +67,7 @@ class PresolveParser: # Special case: model solved by presolve presolve_all_removed = re.compile(r"Presolve: All rows and columns removed") - def __init__(self): + def __init__(self, pre_tree_solution_parser: PretreeSolutionParser): """Initialize the Presolve parser. The PresolveParser extends beyond the lines associated with the presolved @@ -75,6 +76,7 @@ def __init__(self): """ self._summary = {} self._started = False + self._pre_tree_solution_parser = pre_tree_solution_parser def parse(self, line: str) -> bool: """Parse the given log line to populate summary data. @@ -94,6 +96,9 @@ def parse(self, line: str) -> bool: return True return False + if self._pre_tree_solution_parser.parse(line): + return True + for pattern in PresolveParser.presolve_intermediate_patterns: match = pattern.match(line) if match: diff --git a/src/gurobi_logtools/parsers/pretree_solutions.py b/src/gurobi_logtools/parsers/pretree_solutions.py new file mode 100644 index 0000000..046e614 --- /dev/null +++ b/src/gurobi_logtools/parsers/pretree_solutions.py @@ -0,0 +1,45 @@ +import re + +from gurobi_logtools.parsers.util import typeconvert_groupdict + +class PretreeSolutionParser: + + pretree_solution_regex = re.compile( + r"Found heuristic solution:\sobjective\s(?P[^\s]+)" + ) + + def __init__(self): + """Initialize the pre-tree solutions parser (does not include NoRel solutions). + + The PresolveParser extends beyond the lines associated with the presolved + model. Specifically, it includes information for all lines appearing between + the HeaderParser and the NoRelParser or the RelaxationParser. + """ + self._progress = [] + self._summary = {} + #self._started = False + + def parse(self, line: str) -> bool: + """Parse the given log line to populate summary data. + + Args: + line (str): A line in the log file. + + Returns: + bool: Return True if the given line is matched by some pattern. + """ + match = self.pretree_solution_regex.match(line) + if match: + self._progress.append(typeconvert_groupdict(match)) + return True + return False + + def get_summary(self) -> dict: + """Return the current parsed summary.""" + return { + "PreTreeSolutions": len(self._progress) + } + + def get_progress(self) -> list: + """Return the progress of the search tree.""" + return self._progress \ No newline at end of file diff --git a/src/gurobi_logtools/parsers/single_log.py b/src/gurobi_logtools/parsers/single_log.py index f3afddf..da1c5c2 100644 --- a/src/gurobi_logtools/parsers/single_log.py +++ b/src/gurobi_logtools/parsers/single_log.py @@ -5,6 +5,7 @@ from gurobi_logtools.parsers.nodelog import NodeLogParser from gurobi_logtools.parsers.norel import NoRelParser from gurobi_logtools.parsers.presolve import PresolveParser +from gurobi_logtools.parsers.pretree_solutions import PretreeSolutionParser from gurobi_logtools.parsers.termination import TerminationParser from gurobi_logtools.parsers.util import model_type @@ -16,11 +17,13 @@ class SingleLogParser: """ def __init__(self, write_to_dir=None): + self.pretree_solution_parser = self.make_pretree_solution_parser() + # Parsers in sequence self.header_parser = HeaderParser() - self.presolve_parser = PresolveParser() + self.presolve_parser = PresolveParser(self.pretree_solution_parser) self.norel_parser = NoRelParser() - self.continuous_parser = ContinuousParser() + self.continuous_parser = ContinuousParser(self.pretree_solution_parser) self.nodelog_parser = NodeLogParser() self.termination_parser = TerminationParser() @@ -39,6 +42,9 @@ def __init__(self, write_to_dir=None): self.write_to_dir = pathlib.Path(write_to_dir) if write_to_dir else None self.lines = [] if self.write_to_dir else None + def make_pretree_solution_parser(self): + return PretreeSolutionParser() + def close(self): if self.write_to_dir: paramstr = "-".join( @@ -66,6 +72,7 @@ def get_summary(self): summary.update(self.presolve_parser.get_summary()) summary.update(self.norel_parser.get_summary()) summary.update(self.continuous_parser.get_summary()) + summary.update(self.pretree_solution_parser.get_summary()) summary.update(self.nodelog_parser.get_summary()) summary.update(self.termination_parser.get_summary()) summary["ModelType"] = model_type( From 2c2969549566a5519ec621793dc588837d6e9013 Mon Sep 17 00:00:00 2001 From: venaturum Date: Mon, 8 Jul 2024 22:32:03 +1000 Subject: [PATCH 3/5] Update tests for pre-tree-solutions --- tests/assets/multiknapsack.log | 71 +++++++++++++++++++++++ tests/parsers/test_continuous.py | 5 +- tests/parsers/test_presolve.py | 5 +- tests/parsers/test_pretreesols.py | 94 +++++++++++++++++++++++++++++++ tests/parsers/test_single_log.py | 28 +++++++++ tests/test_api.py | 26 ++++++--- tests/test_load.py | 2 +- 7 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 tests/assets/multiknapsack.log create mode 100644 tests/parsers/test_pretreesols.py diff --git a/tests/assets/multiknapsack.log b/tests/assets/multiknapsack.log new file mode 100644 index 0000000..450601f --- /dev/null +++ b/tests/assets/multiknapsack.log @@ -0,0 +1,71 @@ + +Gurobi 9.5.0 (mac64[x86], gurobi_cl) logging started Fri Jul 5 19:18:47 2024 + +Set parameter LogFile to value "multiknapsack.log" +Using license file /Users/riley.clement/gurobi.lic + +Gurobi Optimizer version 9.5.0 build v11.0.2rc0 (mac64[x86] - Darwin 23.5.0 23F79) +Copyright (c) 2021, Gurobi Optimization, LLC + +Read MPS format model from file multiknapsack.mps.bz2 +Reading time = 2.31 seconds +: 1000 rows, 1000 columns, 1000000 nonzeros + +CPU model: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz +Thread count: 4 physical cores, 8 logical processors, using up to 8 threads + +Optimize a model with 1000 rows, 1000 columns and 1000000 nonzeros +Model fingerprint: 0x1dcb478a +Variable types: 0 continuous, 1000 integer (0 binary) +Coefficient statistics: + Matrix range [8e-07, 1e+00] + Objective range [3e-04, 1e+00] + Bounds range [0e+00, 0e+00] + RHS range [2e+00, 1e+04] +Found heuristic solution: objective 2.6508070 +Presolve removed 751 rows and 0 columns +Presolve time: 0.63s +Presolved: 249 rows, 1000 columns, 249000 nonzeros +Variable types: 0 continuous, 1000 integer (0 binary) + +Starting NoRel heuristic +Found heuristic solution: objective 15.6561779 +Found heuristic solution: objective 20.2661725 +Found heuristic solution: objective 21.6309598 +Found heuristic solution: objective 23.7055101 +Found heuristic solution: objective 24.1081356 +Elapsed time for NoRel heuristic: 8s (best bound 24.6734) +Elapsed time for NoRel heuristic: 15s (best bound 24.6734) +Elapsed time for NoRel heuristic: 21s (best bound 24.6734) +Elapsed time for NoRel heuristic: 29s (best bound 24.6734) + +Root relaxation: objective 2.467344e+01, 32 iterations, 0.02 seconds (0.01 work units) + +Found heuristic solution: objective 15.5735142 + + Nodes | Current Node | Objective Bounds | Work + Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time + + 0 0 24.67344 0 5 15.57351 24.67344 58.4% - 0s +H 0 0 23.8510977 24.67344 3.45% - 0s +H 0 0 24.1081356 24.67344 2.34% - 0s + 0 0 24.54405 0 7 24.10814 24.54405 1.81% - 1s + 0 0 24.54405 0 5 24.10814 24.54405 1.81% - 1s + 0 0 24.54405 0 7 24.10814 24.54405 1.81% - 1s + 0 0 24.49025 0 6 24.10814 24.49025 1.59% - 1s + 0 0 24.48961 0 7 24.10814 24.48961 1.58% - 1s + 0 0 24.45227 0 8 24.10814 24.45227 1.43% - 1s + 0 0 24.40551 0 8 24.10814 24.40551 1.23% - 1s + 0 2 24.40551 0 8 24.10814 24.40551 1.23% - 1s + +Cutting planes: + Gomory: 8 + Lift-and-project: 1 + +Explored 838 nodes (2566 simplex iterations) in 1.27 seconds (0.53 work units) +Thread count was 8 (of 8 available processors) + +Solution count 5: 24.1081 23.8511 15.5735 ... 2.65081 + +Optimal solution found (tolerance 1.00e-04) +Best objective 2.410813557868e+01, best bound 2.410813557868e+01, gap 0.0000% diff --git a/tests/parsers/test_continuous.py b/tests/parsers/test_continuous.py index a417ae8..b1e0145 100644 --- a/tests/parsers/test_continuous.py +++ b/tests/parsers/test_continuous.py @@ -1,6 +1,7 @@ from unittest import TestCase, main from gurobi_logtools.parsers.continuous import ContinuousParser +from gurobi_logtools.parsers.pretree_solutions import PretreeSolutionParser from gurobi_logtools.parsers.util import parse_block example_log_barrier_with_simplex = """ @@ -128,7 +129,7 @@ class TestContinuous(TestCase): def test_last_progress_entry_barrier_with_simplex(self): - continuous_parser = ContinuousParser() + continuous_parser = ContinuousParser(PretreeSolutionParser()) parse_block(continuous_parser, example_log_barrier_with_simplex) self.assertEqual( continuous_parser.get_progress()[-1], expected_progress_last_entry @@ -153,7 +154,7 @@ def test_get_summary_progress(self): ], ): with self.subTest(example_log=example_log): - continuous_parser = ContinuousParser() + continuous_parser = ContinuousParser(PretreeSolutionParser()) parse_block(continuous_parser, example_log) self.assertEqual(continuous_parser.get_summary(), expected_summary) self.assertEqual(continuous_parser.get_progress(), expected_progress) diff --git a/tests/parsers/test_presolve.py b/tests/parsers/test_presolve.py index 86021ef..d45c14e 100644 --- a/tests/parsers/test_presolve.py +++ b/tests/parsers/test_presolve.py @@ -1,6 +1,7 @@ from unittest import TestCase, main from gurobi_logtools.parsers.presolve import PresolveParser +from gurobi_logtools.parsers.pretree_solutions import PretreeSolutionParser from gurobi_logtools.parsers.util import parse_lines example_log_0 = """ @@ -139,7 +140,7 @@ def test_first_line_matched(self): for i, example_log in enumerate([example_log_0, example_log_1, example_log_2]): with self.subTest(example_log=example_log): - presolve_parser = PresolveParser() + presolve_parser = PresolveParser(PretreeSolutionParser()) for line in example_log.strip().split("\n"): if presolve_parser.parse(line): self.assertEqual(line, expected_start_lines[i]) @@ -155,7 +156,7 @@ def test_get_summary(self): ] for i, example_log in enumerate([example_log_0, example_log_1, example_log_2]): with self.subTest(example_log=example_log): - presolve_parser = PresolveParser() + presolve_parser = PresolveParser(PretreeSolutionParser()) lines = example_log.strip().split("\n") parse_lines(presolve_parser, lines) self.assertEqual(presolve_parser.get_summary(), expected_summaries[i]) diff --git a/tests/parsers/test_pretreesols.py b/tests/parsers/test_pretreesols.py new file mode 100644 index 0000000..2fc415e --- /dev/null +++ b/tests/parsers/test_pretreesols.py @@ -0,0 +1,94 @@ +from unittest import TestCase, main + +from gurobi_logtools.parsers.presolve import PresolveParser +from gurobi_logtools.parsers.continuous import ContinuousParser +from gurobi_logtools.parsers.pretree_solutions import PretreeSolutionParser +from gurobi_logtools.parsers.util import parse_lines + +example_log_0 = """ +Optimize a model with 396 rows, 322 columns and 1815 nonzeros +Model fingerprint: 0x18b19fdf +Variable types: 20 continuous, 302 integer (0 binary) +Coefficient statistics: + Matrix range [1e+00, 8e+06] + Objective range [1e+00, 1e+06] + Bounds range [1e+00, 8e+02] + RHS range [1e+00, 8e+06] +Presolve removed 6 rows and 6 columns +Found heuristic solution: objective 1.133356e+09 +Presolve time: 0.01s +Found heuristic solution: objective 2.133356e+09 +Presolved: 390 rows, 316 columns, 1803 nonzeros +Variable types: 19 continuous, 297 integer (297 binary) +Found heuristic solution: objective 3.133356e+09 +""" + +expected_summary = {"PreTreeSolutions": 3} + +expected_progress = [ + {'Incumbent': 1133356000.0}, + {'Incumbent': 2133356000.0}, + {'Incumbent': 3133356000.0}, +] + +example_log_1 = """ +Iter Primal Dual Primal Dual Compl Time + 0 4.56435085e+07 1.53061018e+04 1.69e+05 8.58e+00 1.59e+03 2s + 1 3.76722276e+07 -5.41297282e+05 8.07e+04 9.12e+00 8.17e+02 2s + 17 2.17403572e+02 2.17403571e+02 3.93e-14 7.11e-15 7.71e-13 5s + +Barrier solved model in 17 iterations and 4.83 seconds (6.45 work units) +Found heuristic solution: objective 1.133356e+09 +Optimal objective 2.17403572e+02 + +Crossover log... + + 57249 DPushes remaining with DInf 0.0000000e+00 8s + 0 DPushes remaining with DInf 0.0000000e+00 9s + +Found heuristic solution: objective 2.133356e+09 + + 9342 PPushes remaining with PInf 1.2118572e-05 9s + 0 PPushes remaining with PInf 0.0000000e+00 9s + + Push phase complete: Pinf 0.0000000e+00, Dinf 1.8540725e-14 9s + +Extra simplex iterations after uncrush: 1 +Found heuristic solution: objective 3.133356e+09 + +Iteration Objective Primal Inf. Dual Inf. Time + 51765 2.1740357e+02 0.000000e+00 0.000000e+00 9s + +Solved in 51765 iterations and 9.18 seconds (13.22 work units) +Optimal objective 2.174035714e+02 +""" + +example_logs = (example_log_0,example_log_1) +parsers = (PresolveParser, ContinuousParser) + +class TestPresolve(TestCase): + def setUp(self): + pass + + def test_get_summary_presolve(self): + for example_log, Parser in zip(example_logs, parsers): + with self.subTest(example_log=example_log): + pre_tree_sols_parser = PretreeSolutionParser() + presolve_parser = Parser(pre_tree_sols_parser) + lines = example_log.strip().split("\n") + parse_lines(presolve_parser, lines) + self.assertEqual(pre_tree_sols_parser.get_summary(), expected_summary) + + def test_get_progress_presolve(self): + for example_log, Parser in zip(example_logs, parsers): + with self.subTest(example_log=example_log): + pre_tree_sols_parser = PretreeSolutionParser() + presolve_parser = Parser(pre_tree_sols_parser) + lines = example_log.strip().split("\n") + parse_lines(presolve_parser, lines) + pre_tree_sols_parser.get_progress() + self.assertEqual(pre_tree_sols_parser.get_progress(), expected_progress) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/parsers/test_single_log.py b/tests/parsers/test_single_log.py index 91c1829..1d455b5 100644 --- a/tests/parsers/test_single_log.py +++ b/tests/parsers/test_single_log.py @@ -82,3 +82,31 @@ def test_lp_simplex(): rootlp_progress = parser.continuous_parser.get_progress() assert len(rootlp_progress) == 60 + + +def test_multiknapsack(): + parser = SingleLogParser() + with open("tests/assets/multiknapsack.log") as infile: + parse_lines(parser, infile) + # Test relevant bits are populated. + assert parser.header_parser.get_summary() + assert parser.presolve_parser.get_summary() + assert parser.norel_parser.get_summary() + assert parser.norel_parser.get_progress() + assert parser.continuous_parser.get_summary() + assert not parser.continuous_parser.get_progress() + assert parser.pretree_solution_parser.get_summary() + assert parser.pretree_solution_parser.get_progress() + assert parser.nodelog_parser.get_summary() + assert parser.nodelog_parser.get_progress() + assert parser.termination_parser.get_summary() + # Combined summary data. + summary = parser.get_summary() + assert summary["Version"] == "9.5.0" + assert summary["IterCount"] == 2566 + assert summary["Runtime"] == 1.27 + assert summary["Status"] == "OPTIMAL" + assert summary["ModelType"] == "MIP" + + pretreesols_progress = parser.pretree_solution_parser.get_progress() + assert len(pretreesols_progress) == 2 diff --git a/tests/test_api.py b/tests/test_api.py index e6904a5..00ce2d1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,7 +13,7 @@ @pytest.fixture(scope="module") def glass4_summary(): """Summary data from API call.""" - return glt.parse("data/*.log").summary() + return glt.parse("data/*glass4*.log").summary() @pytest.fixture(scope="module") @@ -23,6 +23,7 @@ def glass4_progress(): "norel": glt.parse("data/*.log").progress("norel"), "rootlp": glt.parse("data/*.log").progress("rootlp"), "nodelog": glt.parse("data/*.log").progress("nodelog"), + "pretreesols": glt.parse("data/*.log").progress("pretreesols"), } @@ -37,6 +38,7 @@ def testlog_progress(): "norel": glt.parse("tests/assets/*.log").progress("norel"), "rootlp": glt.parse("tests/assets/*.log").progress("rootlp"), "nodelog": glt.parse("tests/assets/*.log").progress("nodelog"), + "pretreesols": glt.parse("tests/assets/*.log").progress("pretreesols"), } @@ -64,15 +66,15 @@ def test_merged_log(merged_log): def test_summary(testlog_summary): - assert len(testlog_summary) == 7 + assert len(testlog_summary) == 8 assert set(testlog_summary.columns).issuperset( {"Status", "ObjVal", "ReadingTime", "RelaxObj"} ) def test_progress(testlog_progress): - assert len(testlog_progress) == 3 - assert len(testlog_progress["norel"]) == 15 + assert len(testlog_progress) == 4 + assert len(testlog_progress["norel"]) == 19 assert set(testlog_progress["norel"].columns).issuperset( {"Time", "BestBd", "Incumbent"} ) @@ -80,10 +82,14 @@ def test_progress(testlog_progress): assert set(testlog_progress["rootlp"].columns).issuperset( {"Iteration", "PInf", "DInf", "PObj", "DObj"} ) - assert len(testlog_progress["nodelog"]) == 133 + assert len(testlog_progress["nodelog"]) == 145 assert set(testlog_progress["nodelog"].columns).issuperset( {"Depth", "IntInf", "Incumbent", "BestBd", "ItPerNode", "ModelFile", "Version"} ) + assert len(testlog_progress["pretreesols"]) == 4 + assert set(testlog_progress["pretreesols"].columns).issuperset( + {"Incumbent", "ModelFile", "Version"} + ) def test_summary_glass4(glass4_summary): @@ -94,13 +100,17 @@ def test_summary_glass4(glass4_summary): def test_progress_glass4(glass4_progress): - assert len(glass4_progress) == 3 + assert len(glass4_progress) == 4 assert len(glass4_progress["norel"]) == 0 assert len(glass4_progress["rootlp"]) == 0 assert set(glass4_progress["nodelog"].columns).issuperset( {"Depth", "IntInf", "Incumbent", "BestBd", "ItPerNode"} ) + assert len(glass4_progress["pretreesols"]) == 51 + assert set(glass4_progress["pretreesols"].columns).issuperset( + {"Incumbent", "ModelFile", "Version"} + ) def test_logfile(glass4_summary): @@ -191,7 +201,7 @@ def test_work(): assert set(summary.columns).issuperset({"Work"}) # Check if Runtime and Work found - assert summary["Work"].count() == 6 + assert summary["Work"].count() == 7 def test_changed_params(): @@ -199,7 +209,7 @@ def test_changed_params(): summary = result.summary() assert set(summary.columns).issuperset({"ChangedParams"}) assert summary["ChangedParams"].apply(lambda d: isinstance(d, dict)).all() - assert summary["ChangedParams"].count() == 7 + assert summary["ChangedParams"].count() == 8 def test_create_label(): diff --git a/tests/test_load.py b/tests/test_load.py index f8fe275..26ad325 100644 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -9,7 +9,7 @@ def test_get_dataframe(): def test_read_with_timelines(): """Check timelines argument.""" summary, timelines = glt.get_dataframe(["data/*.log"], timelines=True) - assert set(timelines.keys()) == {"norel", "rootlp", "nodelog"} + assert set(timelines.keys()) == {"norel", "rootlp", "nodelog", "pretreesols"} def test_norel_timeline(): From b2f9d81ae05210a377bc00505c154dd547357267 Mon Sep 17 00:00:00 2001 From: venaturum Date: Mon, 8 Jul 2024 22:32:35 +1000 Subject: [PATCH 4/5] Update docs --- CHANGELOG.md | 1 + gurobi-logtools.ipynb | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d6447..c1211c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Fixed ### Changed +- Parsing of heuristic solutions found prior to nodelog (but not found by NoRel) (#51) ### Removed ## 3.0.0 - 2023-10-11 diff --git a/gurobi-logtools.ipynb b/gurobi-logtools.ipynb index 2ed8c26..0ff402c 100644 --- a/gurobi-logtools.ipynb +++ b/gurobi-logtools.ipynb @@ -285,7 +285,13 @@ } }, "source": [ - "This simply creates two more dataframes, `timelines[\"nodelog\"]` and `timelines[\"rootlp\"]`, which contain the information about the LP relaxation and node log. Let's have a look at them:" + "This simply creates four more dataframes:\n", + "- `timelines[\"nodelog\"]` : information related to the node log, i.e. branch and bound tree\n", + "- `timelines[\"norel\"]` : information related to solutions produced by the No Relaxation heuristic (\"NoRel\")\n", + "- `timelines[\"pretreesols\"]` : information related to heuristic solutions found prior to the branch and bound tree (but not including those found by NoRel)\n", + "- `timelines[\"rootlp\"]` : information related to the root linear relaxation solve\n", + "\n", + "Let's have a look at them:" ] }, { @@ -302,6 +308,27 @@ "results.progress(\"nodelog\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae623641", + "metadata": {}, + "outputs": [], + "source": [ + "# empty dataframe, since the No Relaxation heuristic was not used\n", + "results.progress(\"norel\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab3c3af4", + "metadata": {}, + "outputs": [], + "source": [ + "results.progress(\"pretreesols\")" + ] + }, { "cell_type": "code", "execution_count": null, From 30ba41ef22290e5356b3290d414311ed2b75835d Mon Sep 17 00:00:00 2001 From: venaturum Date: Mon, 8 Jul 2024 23:07:44 +1000 Subject: [PATCH 5/5] linting --- src/gurobi_logtools/parsers/pretree_solutions.py | 12 +++++------- tests/parsers/test_pretreesols.py | 15 ++++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/gurobi_logtools/parsers/pretree_solutions.py b/src/gurobi_logtools/parsers/pretree_solutions.py index 046e614..34a5a7b 100644 --- a/src/gurobi_logtools/parsers/pretree_solutions.py +++ b/src/gurobi_logtools/parsers/pretree_solutions.py @@ -2,8 +2,8 @@ from gurobi_logtools.parsers.util import typeconvert_groupdict -class PretreeSolutionParser: +class PretreeSolutionParser: pretree_solution_regex = re.compile( r"Found heuristic solution:\sobjective\s(?P[^\s]+)" ) @@ -17,7 +17,7 @@ def __init__(self): """ self._progress = [] self._summary = {} - #self._started = False + # self._started = False def parse(self, line: str) -> bool: """Parse the given log line to populate summary data. @@ -36,10 +36,8 @@ def parse(self, line: str) -> bool: def get_summary(self) -> dict: """Return the current parsed summary.""" - return { - "PreTreeSolutions": len(self._progress) - } - + return {"PreTreeSolutions": len(self._progress)} + def get_progress(self) -> list: """Return the progress of the search tree.""" - return self._progress \ No newline at end of file + return self._progress diff --git a/tests/parsers/test_pretreesols.py b/tests/parsers/test_pretreesols.py index 2fc415e..1b8c781 100644 --- a/tests/parsers/test_pretreesols.py +++ b/tests/parsers/test_pretreesols.py @@ -1,7 +1,7 @@ from unittest import TestCase, main -from gurobi_logtools.parsers.presolve import PresolveParser from gurobi_logtools.parsers.continuous import ContinuousParser +from gurobi_logtools.parsers.presolve import PresolveParser from gurobi_logtools.parsers.pretree_solutions import PretreeSolutionParser from gurobi_logtools.parsers.util import parse_lines @@ -25,10 +25,10 @@ expected_summary = {"PreTreeSolutions": 3} -expected_progress = [ - {'Incumbent': 1133356000.0}, - {'Incumbent': 2133356000.0}, - {'Incumbent': 3133356000.0}, +expected_progress = [ + {"Incumbent": 1133356000.0}, + {"Incumbent": 2133356000.0}, + {"Incumbent": 3133356000.0}, ] example_log_1 = """ @@ -63,9 +63,10 @@ Optimal objective 2.174035714e+02 """ -example_logs = (example_log_0,example_log_1) +example_logs = (example_log_0, example_log_1) parsers = (PresolveParser, ContinuousParser) + class TestPresolve(TestCase): def setUp(self): pass @@ -91,4 +92,4 @@ def test_get_progress_presolve(self): if __name__ == "__main__": - main() \ No newline at end of file + main()