diff --git a/README.md b/README.md index bc35891..b61cb31 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ [![Open in GitHub Codespaces]( https://img.shields.io/badge/Open%20in%20GitHub%20Codespaces-333?logo=github)]( - https://codespaces.new/dwave-examples/flow-shop-scheduling-nl?quickstart=1) + https://codespaces.new/dwave-examples/flow-shop-scheduling?quickstart=1) # Flow Shop Scheduling -[Job shop scheduling](https://en.wikipedia.org/wiki/Job-shop_scheduling) (JSS) is an optimization problem with the goal of scheduling, on a number of machines, jobs with diverse orderings of processing on the machines. The objective is to minimize the schedule length, also called "make-span," or completion time of the last task of all jobs. [Flow shop scheduling](https://en.wikipedia.org/wiki/Flow-shop_scheduling) (FSS) is a constrained case of JSS where every job uses every machine in the same order. The machines in FSS problems can often be seen as sequential operations to be executed on each job, as is the case in this particular demo. +[Job shop scheduling](https://en.wikipedia.org/wiki/Job-shop_scheduling) (JSS) is an optimization problem with the goal of scheduling, on a number of machines, jobs with diverse orderings of processing on the machines. The objective is to minimize the schedule length, also called "makespan," or completion time of the last task of all jobs. [Flow shop scheduling](https://en.wikipedia.org/wiki/Flow-shop_scheduling) (FSS) is a constrained case of JSS where every job uses every machine in the same order. The machines in FSS problems can often be seen as sequential operations to be executed on each job, as is the case in this particular demo. ![Demo Screenshot](_static/screenshot.png) @@ -65,11 +65,11 @@ These are the parameters of the problem: - `M_(j,t)`: the machine that processes task `t` of job `j` - `T_(j,i)`: the task that is processed by machine `i` for job `j` - `D_(j,t)`: the processing duration that task `t` needs for job `j` -- `V`: maximum possible make-span +- `V`: maximum possible makespan ### Variables -- `w`: a positive integer variable that defines the completion time (make-span) +- `w`: a positive integer variable that defines the completion time (makespan) of the JSS - `x_(j_i)`: positive integer variables used to model start of each job `j` on machine `i` @@ -77,7 +77,7 @@ of the JSS ### Objective -The objective is to minimize the make-span (`w`) of the given FSS problem. +The objective is to minimize the makespan (`w`) of the given FSS problem. ### Nonlinear Model @@ -102,7 +102,7 @@ Above: a solution to a flow shop scheduling problem with 3 jobs on 3 machines. ![fss_example1](_static/fss_example1.png) Above: an improved solution to the same problem with a permutated job order. -### Constraint Quadratic Model +### Constrained Quadratic Model The constrained quadratic model (CQM) requires adding a set of constraints to ensure that tasks are executed in order and that no single machine is used by different jobs at the same time. @@ -134,7 +134,7 @@ There are two cases: ![equation2_2](_static/eq2_2.png) Since these equations are applied to every pair of jobs, they guarantee that the jobs don't overlap on a machine. -#### Make-Span Constraint +#### Makespan Constraint In this demonstration, the maximum makespan can be defined by the user or it will be determined using a greedy heuristic. Placing an upper bound on the makespan improves the performance of the D-Wave sampler; however, if the upper bound is too low then the sampler may fail to find a feasible solution. diff --git a/app.py b/app.py index 4066a77..b88a0b3 100644 --- a/app.py +++ b/app.py @@ -32,16 +32,16 @@ import pathlib import time -from enum import Enum +from typing import NamedTuple import dash import diskcache import plotly.graph_objs as go -from dash import DiskcacheManager, ctx, MATCH +from dash import MATCH, DiskcacheManager, ctx from dash.dependencies import ClientsideFunction, Input, Output, State from dash.exceptions import PreventUpdate -from dash_html import set_html +from dash_html import generate_problem_details_table, set_html cache = diskcache.Cache("./cache") background_callback_manager = DiskcacheManager(cache) @@ -54,8 +54,9 @@ # the `multiprocessing` library, but its fork, `multiprocess` still hasn't caught up. # (see docs: https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods) import multiprocess + if multiprocess.get_start_method(allow_none=True) is None: - multiprocess.set_start_method('spawn') + multiprocess.set_start_method("spawn") from app_configs import ( APP_TITLE, @@ -63,11 +64,12 @@ DEBUG, DWAVE_TAB_LABEL, SCENARIOS, + SHOW_CQM, THEME_COLOR, THEME_COLOR_SECONDARY, ) from src.generate_charts import generate_gantt_chart, get_empty_figure, get_minimum_task_times -from src.job_shop_scheduler import run_shop_scheduler +from src.job_shop_scheduler import HybridSamplerType, SamplerType, run_shop_scheduler from src.model_data import JobShopData app = dash.Dash( @@ -95,12 +97,6 @@ f.write(css) -class SamplerType(Enum): - CQM = 0 - NL = 1 - HIGHS = 2 - - @app.callback( Output({"type": "to-collapse-class", "index": MATCH}, "className"), inputs=[ @@ -127,53 +123,40 @@ def toggle_left_column(collapse_trigger: int, to_collapse_class: str) -> str: @app.callback( - Output("solver-select", "options"), + Output("hybrid-select-wrapper", "className"), inputs=[ Input("solver-select", "value"), - State("solver-select", "options"), ], prevent_initial_call=True, ) def update_solvers_selected( selected_solvers: list[int], - solver_options: list[dict] -) -> list[dict]: - """Disable NL/CQM solver checkboxes when the other one is selected. - - Note, can be removed if CQM solver is not in use. +) -> str: + """Hide NL/CQM selector when Hybrid is unselected. Not applicable when SHOW_CQM is False. Args: selected_solvers (list[int]): Currently selected solvers. - solver_options (list[dict]): List of solver checkbox options. Returns: - list: Updated list of solver checkbox options. + str: Class name for hybrid select wrapper. """ + if SHOW_CQM: + return "" if SamplerType.HYBRID.value in selected_solvers else "display-none" - if SamplerType.CQM.value in selected_solvers: - solver_options[1]["disabled"] = True - return solver_options - - elif SamplerType.NL.value in selected_solvers: - solver_options[0]["disabled"] = True - return solver_options - - solver_options[0]["disabled"] = False - solver_options[1]["disabled"] = False - return solver_options + raise PreventUpdate @app.callback( Output("dwave-tab", "label", allow_duplicate=True), - Output("highs-tab", "label", allow_duplicate=True), Output("dwave-tab", "disabled", allow_duplicate=True), - Output("highs-tab", "disabled", allow_duplicate=True), Output("dwave-tab", "className", allow_duplicate=True), + Output("running-dwave", "data", allow_duplicate=True), + Output("highs-tab", "label", allow_duplicate=True), + Output("highs-tab", "disabled", allow_duplicate=True), Output("highs-tab", "className", allow_duplicate=True), + Output("running-classical", "data", allow_duplicate=True), Output("run-button", "className", allow_duplicate=True), Output("cancel-button", "className", allow_duplicate=True), - Output("running-dwave", "data", allow_duplicate=True), - Output("running-classical", "data", allow_duplicate=True), Output("tabs", "value"), [ Input("run-button", "n_clicks"), @@ -183,7 +166,7 @@ def update_solvers_selected( ) def update_tab_loading_state( run_click: int, cancel_click: int, solvers: list[str] -) -> tuple[str, str, bool, bool, str, str, str, str, bool, bool, str]: +) -> tuple[str, bool, str, bool, str, bool, str, bool, str, str, str]: """Updates the tab loading state after the run button or cancel button has been clicked. @@ -194,47 +177,37 @@ def update_tab_loading_state( Returns: str: The label for the D-Wave tab. - str: The label for the Classical tab. bool: True if D-Wave tab should be disabled, False otherwise. - bool: True if Classical tab should be disabled, False otherwise. str: Class name for the D-Wave tab. + bool: Whether Hybrid is running. + str: The label for the Classical tab. + bool: True if Classical tab should be disabled, False otherwise. str: Class name for the Classical tab. + bool: Whether HiGHS is running. str: Run button class. str: Cancel button class. - bool: Whether Hybrid is running. - bool: Whether HiGHS is running. str: The value of the tab that should be active. """ if ctx.triggered_id == "run-button" and run_click > 0: - run_hybrid = SamplerType.CQM.value in solvers or SamplerType.NL.value in solvers - run_highs = SamplerType.HIGHS.value in solvers - + running = ("Loading...", True, "tab", True) return ( - "Loading..." if run_hybrid else dash.no_update, - "Loading..." if run_highs else dash.no_update, - True if run_hybrid else dash.no_update, - True if run_highs else dash.no_update, - "tab", - "tab", + *(running if SamplerType.HYBRID.value in solvers else [dash.no_update] * 4), + *(running if SamplerType.HIGHS.value in solvers else [dash.no_update] * 4), "display-none", "", - run_hybrid, - run_highs, "input-tab", ) + if ctx.triggered_id == "cancel-button" and cancel_click > 0: + not_running = (dash.no_update, dash.no_update, False) return ( DWAVE_TAB_LABEL, + *not_running, CLASSICAL_TAB_LABEL, - dash.no_update, - dash.no_update, - dash.no_update, - dash.no_update, + *not_running, "", "display-none", - False, - False, dash.no_update, ) raise PreventUpdate @@ -268,50 +241,71 @@ def update_button_visibility(running_dwave: bool, running_classical: bool) -> tu @app.callback( - Output({"type": "gantt-chart-jobsort", "index": MATCH}, "className"), - Output({"type": "gantt-chart-startsort", "index": MATCH}, "className"), - Output({"type": "sort-button", "index": MATCH}, "children"), + Output({"type": "gantt-chart-visible-wrapper", "index": MATCH}, "children"), + Output({"type": "gantt-chart-hidden-wrapper", "index": MATCH}, "children"), + Output({"type": "gantt-heading-button", "index": MATCH}, "children"), inputs=[ - Input({"type": "sort-button", "index": MATCH}, "n_clicks"), - State({"type": "sort-button", "index": MATCH}, "children"), + Input({"type": "gantt-heading-button", "index": MATCH}, "n_clicks"), + State({"type": "gantt-heading-button", "index": MATCH}, "children"), + State({"type": "gantt-chart-visible-wrapper", "index": MATCH}, "children"), + State({"type": "gantt-chart-hidden-wrapper", "index": MATCH}, "children"), ], prevent_initial_call=True, ) -def switch_gantt_chart(new_click: int, sort_button_text: str) -> tuple[str, str, str]: +def switch_gantt_chart( + new_click: int, sort_button_text: str, visibleChart: list, hiddenChart: list +) -> tuple[str, str, str]: """Switch between the results plot sorted by job or by start time. Args: new_click (int): The number of times the sort button has been clicked. - sort_button_text: The text in the sort button (indicating how to sort the plot). + sort_button_text (str): The text of the sort button (indicating how to sort the plot). + visibleChart (list): The children of the currently visible graph. + hiddenChart (list): The children of the currently hidden graph. Return: - str: The results class name sorted by job (whether hidden or displayed). - str: The results class name sorted by start time (whether hidden or displayed). + list: The new graph that should be visible. + list: The new graph that should be hidden. str: The new text of the sort button. """ - if sort_button_text == "Sort by start time": - return "display-none", "gantt-div", "Sort by job" - return "gantt-div", "display-none", "Sort by start time" + if ctx.triggered_id["index"] == 0: + button_text = "Show Conflicts" if sort_button_text == "Hide Conflicts" else "Hide Conflicts" + else: + button_text = ( + "Sort by job" if sort_button_text == "Sort by start time" else "Sort by start time" + ) + return hiddenChart, visibleChart, button_text + + +class RunOptimizationHybridReturn(NamedTuple): + """Return type for the ``run_optimization_hybrid`` callback function.""" + + gantt_chart_jobsort: go.Figure = dash.no_update + gantt_chart_startsort: go.Figure = dash.no_update + dwave_makespan: str = dash.no_update + dwave_solution_stats_table: list = dash.no_update + dwave_tab_disabled: bool = dash.no_update + dwave_gantt_title_span: str = dash.no_update + dwave_tab_class: str = dash.no_update + dwave_tab_label: str = dash.no_update + running_dwave: bool = dash.no_update @app.callback( - Output({"type": "gantt-chart-jobsort", "index": 0}, "figure"), - Output({"type": "gantt-chart-startsort", "index": 0}, "figure"), - Output("dwave-stats-make-span", "children"), - Output("dwave-stats-time-limit", "children"), - Output("dwave-stats-wall-clock-time", "children"), - Output("dwave-stats-scenario", "children"), - Output("dwave-stats-solver", "children"), - Output("dwave-stats-jobs", "children"), - Output("dwave-stats-resources", "children"), + Output({"type": "gantt-chart-jobsort", "index": 1}, "figure"), + Output({"type": "gantt-chart-startsort", "index": 1}, "figure"), + Output("dwave-stats-makespan", "children"), + Output("dwave-solution-stats-table", "children"), + Output("dwave-tab", "disabled"), + Output("dwave-gantt-title-span", "children"), Output("dwave-tab", "className"), Output("dwave-tab", "label"), - Output("dwave-tab", "disabled"), Output("running-dwave", "data"), background=True, inputs=[ Input("run-button", "n_clicks"), State("solver-select", "value"), + State("hybrid-select", "value"), State("scenario-select", "value"), State("solver-time-limit", "value"), ], @@ -319,36 +313,39 @@ def switch_gantt_chart(new_click: int, sort_button_text: str) -> tuple[str, str, prevent_initial_call=True, ) def run_optimization_hybrid( - run_click: int, solvers: list[int], scenario: str, time_limit: int -) -> tuple[go.Figure, go.Figure, str, str, str, str, str, str, str, str, str, bool, bool]: + run_click: int, solvers: list[int], hybrid_solver: int, scenario: str, time_limit: int +) -> RunOptimizationHybridReturn: """Runs optimization using the D-Wave hybrid solver. Args: run_click (int): The number of times the run button has been clicked. solvers (list[int]): The solvers that have been selected. + hybrid_solver (int): The hybrid solver that have been selected. scenario (str): The scenario to use for the optimization. time_limit (int): The time limit for the optimization. Returns: - go.Figure: Gantt chart for the D-Wave hybrid solver sorted by job. - go.Figure: Gantt chart for the D-Wave hybrid solver sorted by start time. - str: Final make-span for the D-Wave tab. - str: Set time limit for the D-Wave tab. - str: Wall clock time for the D-Wave tab. - str: Scenario for the D-Wave tab. - str: Solver for the D-Wave tab. - str: Number of jobs for the D-Wave tab. - str: Number of resources the D-Wave tab. - str: Class name for the D-Wave tab. - str: The label for the D-Wave tab. - bool: True if D-Wave tab should be disabled, False otherwise. - bool: Whether D-Wave solver is running. + A NamedTuple (RunOptimizationHybridReturn) containing all outputs to be used when updating the HTML + template (in ``dash_html.py``). These are: + go.Figure: Gantt chart for the D-Wave hybrid solver sorted by job. + go.Figure: Gantt chart for the D-Wave hybrid solver sorted by start time. + str: Final makespan for the D-Wave tab. + list: Solution stats table for problem details. + bool: True if D-Wave tab should be disabled, False otherwise. + str: Graph title span to add the solver type to. + str: Class name for the D-Wave tab. + str: The label for the D-Wave tab. + bool: Whether D-Wave solver is running. """ if ctx.triggered_id != "run-button" or run_click == 0: raise PreventUpdate - if SamplerType.CQM.value not in solvers and SamplerType.NL.value not in solvers: - return (*([dash.no_update] * 9), "tab", DWAVE_TAB_LABEL, dash.no_update, False) + if SamplerType.HYBRID.value not in solvers: + return RunOptimizationHybridReturn( + dwave_tab_class="tab", + dwave_tab_label=DWAVE_TAB_LABEL, + running_dwave=False + ) start = time.perf_counter() model_data = JobShopData() @@ -356,42 +353,63 @@ def run_optimization_hybrid( model_data.load_from_file(DATA_PATH.joinpath(filename)) + running_nl = not SHOW_CQM or hybrid_solver is HybridSamplerType.NL.value + results = run_shop_scheduler( model_data, use_scipy_solver=False, - use_nl_solver=SamplerType.NL.value in solvers, + use_nl_solver=running_nl, solver_time_limit=time_limit, ) fig_jobsort = generate_gantt_chart(results, sort_by="JobInt") fig_startsort = generate_gantt_chart(results, sort_by="Start") - table = ( - f"Make-span: {int(results['Finish'].max())}", - time_limit, - round(time.perf_counter() - start, 2), + solution_stats_table = generate_problem_details_table( scenario, - "NL Solver" if SamplerType.NL.value in solvers else "CQM Solver", + "NL Solver" if running_nl else "CQM Solver", model_data.get_job_count(), + time_limit, model_data.get_resource_count(), - ) + time.perf_counter() - start, + ) - return (fig_jobsort, fig_startsort, *table, "tab-success", DWAVE_TAB_LABEL, False, False) + return RunOptimizationHybridReturn( + gantt_chart_jobsort=fig_jobsort, + gantt_chart_startsort=fig_startsort, + dwave_makespan=f"Makespan: {int(results['Finish'].max())}", + dwave_solution_stats_table=solution_stats_table, + dwave_tab_disabled=False, + dwave_gantt_title_span=" (NL)" if running_nl else " (CQM)", + dwave_tab_class="tab-success", + dwave_tab_label=DWAVE_TAB_LABEL, + running_dwave=False, + ) + + +class RunOptimizationScipyReturn(NamedTuple): + """Return type for the ``run_optimization_scipy`` callback function.""" + + gantt_chart_jobsort: go.Figure = dash.no_update + gantt_chart_startsort: go.Figure = dash.no_update + highs_makespan: str = dash.no_update + highs_solution_stats_table: list = dash.no_update + highs_tab_disabled: bool = dash.no_update + sort_button_style: dict = dash.no_update + highs_tab_class: str = dash.no_update + highs_tab_label: str = dash.no_update + running_classical: bool = dash.no_update @app.callback( - Output({"type": "gantt-chart-jobsort", "index": 1}, "figure"), - Output({"type": "gantt-chart-startsort", "index": 1}, "figure"), - Output("highs-stats-make-span", "children"), - Output("highs-stats-time-limit", "children"), - Output("highs-stats-wall-clock-time", "children"), - Output("highs-stats-scenario", "children"), - Output("highs-stats-solver", "children"), - Output("highs-stats-jobs", "children"), - Output("highs-stats-resources", "children"), + Output({"type": "gantt-chart-jobsort", "index": 2}, "figure"), + Output({"type": "gantt-chart-startsort", "index": 2}, "figure"), + Output("highs-stats-makespan", "children"), + Output("highs-solution-stats-table", "children"), + Output("highs-tab", "disabled"), + Output({"type": "gantt-heading-button", "index": 2}, "style"), Output("highs-tab", "className"), Output("highs-tab", "label"), - Output("highs-tab", "disabled"), Output("running-classical", "data"), background=True, inputs=[ @@ -405,7 +423,7 @@ def run_optimization_hybrid( ) def run_optimization_scipy( run_click: int, solvers: list[int], scenario: str, time_limit: int -) -> tuple[go.Figure, go.Figure, str, str, str, str, str, str, str, str, str, bool, bool]: +) -> RunOptimizationScipyReturn: """Runs optimization using the HiGHS solver. Args: @@ -416,25 +434,27 @@ def run_optimization_scipy( time_limit (int): The time limit for the optimization. Returns: - go.Figure: Gantt chart for the Classical solver sorted by job. - go.Figure: Gantt chart for the Classical solver sorted by start time. - str: Final make-span for the Classical tab. - str: Set time limit for the Classical tab. - str: Wall clock time for the Classical tab. - str: Scenario for the Classical tab. - str: Solver for the Classical tab. - str: Number of jobs for the Classical tab. - str: Number of resources the Classical tab. - str: Class name for the Classical tab. - str: The label for the Classical tab. - bool: True if Classical tab should be disabled, False otherwise. - bool: Whether Classical solver is running. + A NamedTuple (RunOptimizationScipyReturn) containing all outputs to be used when updating the HTML + template (in ``dash_html.py``). These are: + go.Figure: Gantt chart for the Classical solver sorted by job. + go.Figure: Gantt chart for the Classical solver sorted by start time. + str: Final makespan for the Classical tab. + list: Solution stats table for problem details. + bool: True if Classical tab should be disabled, False otherwise. + dict: Sort button style, either display none or nothing. + str: Class name for the Classical tab. + str: The label for the Classical tab. + bool: Whether Classical solver is running. """ if ctx.triggered_id != "run-button" or run_click == 0: raise PreventUpdate if SamplerType.HIGHS.value not in solvers: - return (*([dash.no_update] * 9), "tab", CLASSICAL_TAB_LABEL, dash.no_update, False) + return RunOptimizationScipyReturn( + highs_tab_class="tab", + highs_tab_label=CLASSICAL_TAB_LABEL, + running_classical=False + ) start = time.perf_counter() model_data = JobShopData() @@ -448,36 +468,49 @@ def run_optimization_scipy( solver_time_limit=time_limit, ) + solution_stats_table = generate_problem_details_table( + scenario, + "HiGHS", + model_data.get_job_count(), + time_limit, + model_data.get_resource_count(), + time.perf_counter() - start + ) + makespan = f"Makespan: {0 if results.empty else int(results['Finish'].max())}" + if results.empty: fig = get_empty_figure("No solution found for Classical solver") - table = ( - "Make-span: 0", - time_limit, - round(time.perf_counter() - start, 2), - scenario, - "HiGHS", - model_data.get_job_count(), - model_data.get_resource_count(), - ) - return (fig, fig, *table, "tab-fail", CLASSICAL_TAB_LABEL, False, False) + return RunOptimizationScipyReturn( + gantt_chart_jobsort=fig, + gantt_chart_startsort=fig, + highs_makespan=makespan, + highs_solution_stats_table=solution_stats_table, + highs_tab_disabled=False, + sort_button_style={"display": "none"}, + highs_tab_class="tab-fail", + highs_tab_label=CLASSICAL_TAB_LABEL, + running_classical=False, + ) fig_jobsort = generate_gantt_chart(results, sort_by="JobInt") fig_startsort = generate_gantt_chart(results, sort_by="Start") - table = ( - f"Make-span: {int(results['Finish'].max())}", - time_limit, - round(time.perf_counter() - start, 2), - scenario, - "HiGHS", - model_data.get_job_count(), - model_data.get_resource_count(), - ) - return (fig_jobsort, fig_startsort, *table, "tab-success", CLASSICAL_TAB_LABEL, False, False) + return RunOptimizationScipyReturn( + gantt_chart_jobsort=fig_jobsort, + gantt_chart_startsort=fig_startsort, + highs_makespan=makespan, + highs_solution_stats_table=solution_stats_table, + highs_tab_disabled=False, + sort_button_style={}, + highs_tab_class="tab-success", + highs_tab_label=CLASSICAL_TAB_LABEL, + running_classical=False, + ) @app.callback( - Output("unscheduled-gantt-chart", "figure"), + Output({"type": "gantt-chart-unscheduled", "index": 0}, "figure"), + Output({"type": "gantt-chart-conflicts", "index": 0}, "figure"), [ Input("scenario-select", "value"), ], @@ -496,7 +529,8 @@ def generate_unscheduled_gantt_chart(scenario: str) -> go.Figure: model_data.load_from_file(DATA_PATH.joinpath(SCENARIOS[scenario])) df = get_minimum_task_times(model_data) fig = generate_gantt_chart(df) - return fig + fig_conflicts = generate_gantt_chart(df, show_conflicts=True) + return fig, fig_conflicts # import the html code and sets it in the app diff --git a/app_configs.py b/app_configs.py index 604f039..e64ef87 100644 --- a/app_configs.py +++ b/app_configs.py @@ -38,7 +38,7 @@ """ CLASSICAL_TAB_LABEL = "Classical Results" -DWAVE_TAB_LABEL = "Hybrid Solver Results" +DWAVE_TAB_LABEL = "Quantum Hybrid Results" SHOW_CQM = True @@ -58,10 +58,10 @@ "Taillard 20x5": "tai20_5.txt", "Heller 20x10": "hel2", "Taillard 20x10": "tai20_10.txt", - "Reeves 20x10": "reC07", #reC09, reC11 - "Reeves 20x15": "reC13", #reC15, reC17 + "Reeves 20x10": "reC07", # reC09, reC11 + "Reeves 20x15": "reC13", # reC15, reC17 "Taillard 20x20": "tai20_20.txt", - "Reeves 30x10": "reC19", #reC21, reC23 + "Reeves 30x10": "reC19", # reC21, reC23 # "Reeves 30x15": "reC25", #reC27, reC29 # "Taillard 50x5": "tai50_5.txt", # "Reeves 50x10": "reC31", #reC33, reC35 diff --git a/assets/job_shop.css b/assets/job_shop.css index a5c8726..2a13600 100644 --- a/assets/job_shop.css +++ b/assets/job_shop.css @@ -81,7 +81,6 @@ body { margin-left: 2rem; } - h1, h2, h3, h4, h5, h6, td, th, span, a, p, label { font-family: "Helvetica Neue", sans-serif; } @@ -103,7 +102,6 @@ h3 { h3.gantt-title { margin-bottom: 0; - margin-left: 4.6rem; } h4 { @@ -114,7 +112,7 @@ h4 { h5 { font-size: 1.8rem; - font-weight: 600; + font-weight: 500; } label { @@ -166,11 +164,6 @@ th:last-child, td:last-child { flex: 1; } -#columns { - display: flex; - flex: 1; -} - .left-column { display: flex; } @@ -196,10 +189,9 @@ th:last-child, td:last-child { width: 100%; } -.table-row { - height: 63px; - vertical-align: middle; - font-size: 13px; +div.dash-sk-circle { + height: 6rem; + width: 6rem; } .wait_time_graph > div, .patient_score_graph > div { @@ -215,46 +207,31 @@ th:last-child, td:last-child { font-weight: bold; } - -.gantt-chart-card { +.solution-card { height: 100%; display: flex; - flex-flow: column; -} - -.gantt-div { - height: 100%; + flex-direction: column; + justify-content: space-between; } -.gantt-chart-card > div { +.gantt-chart-card { height: 100%; + display: flex; + flex-direction: column; } -.gantt-chart-card-parent { +.graph-wrapper, +.graph, +.dash-graph { height: 100%; - flex: 1 1 auto; } -.gantt-heading-button { +.gantt-heading { display: flex; justify-content: space-between; align-items: end; margin-right: 20rem; - flex: 0 1 content; -} - -.table-row:nth-child(even) { - background-color: #f2f2f2; -} - - -#header_wait_time_min, #header_care_score { - display: table; -} - -#header_wait_time_min > b, #header_care_score > b { - display: table-cell; - vertical-align: middle; + margin-left: 4.3rem; } .tab-container { @@ -298,19 +275,6 @@ div.tab.tab--disabled { padding: 0 1.5rem 3rem; } -#run-button, -#cancel-button { - width: 100%; - font-size: 1.4rem; - line-height: 1.4rem; - padding: 1.8rem; - height: auto; - color: white; - transition: all 0.2s ease-in-out; - margin-bottom: 4rem; - border: none; -} - .left-column-collapse, .left-column-collapse:hover, .left-column-collapse:focus { @@ -331,11 +295,28 @@ div.tab.tab--disabled { box-shadow: none; } -#run-button { +#run-button, +#cancel-button, +.gantt-heading-button { + width: 100%; + font-size: 1.4rem; + line-height: 1.4rem; + padding: 1.8rem; + height: auto; + color: white; + transition: all 0.2s ease-in-out; + margin-bottom: 4rem; + border: none; +} + +#run-button, +.gantt-heading-button, +.gantt-heading-button:focus { background-color: var(--theme); } -#run-button:hover { +#run-button:hover, +.gantt-heading-button:hover { filter: brightness(80%); } @@ -347,6 +328,13 @@ div.tab.tab--disabled { background-color: var(--red-dark); } +.gantt-heading-button { + width: auto; + margin: 0; + font-size: 1.2rem; + padding: 1.2rem 2.4rem; +} + .tab-fail { border-top: 3px solid var(--red-light) !important; } @@ -401,10 +389,14 @@ div.tab.tab--disabled { } .details-collapse-wrapper { - margin-bottom: 2rem; overflow: hidden; } +.details-collapse-title { + display: flex; + justify-content: space-between; +} + .problem-details-parent { flex: 0 1 content; z-index: 1; @@ -418,27 +410,26 @@ div.tab.tab--disabled { margin-right: 4rem; } -#highs-stats-make-span, -#dwave-stats-make-span { - font-size: 2rem; +.stats-makespan { font-weight: 400; } .collapse-arrow { - border-right: 3px solid var(--grey-light); - border-bottom: 3px solid var(--grey-light); + border-right: 4px solid var(--grey-light); + border-bottom: 4px solid var(--grey-light); transform: rotate(135deg) skew(165deg, 165deg); height: 2rem; width: 2rem; margin-right: -0.3rem; transition: border-color 0.25s ease-in-out; } + .details-collapse .collapse-arrow { transform: rotate(225deg) skew(165deg, 165deg); margin: 1.5rem 0 0 1.5rem; border-color: var(--theme); - height: 1.2rem; - width: 1.2rem; + height: 1rem; + width: 1rem; } .left-column-collapse:hover .collapse-arrow { diff --git a/dash_html.py b/dash_html.py index 8fb9600..d092688 100644 --- a/dash_html.py +++ b/dash_html.py @@ -38,14 +38,23 @@ CLASSICAL_TAB_LABEL, DESCRIPTION, DWAVE_TAB_LABEL, - SHOW_CQM, MAIN_HEADER, SCENARIOS, + SHOW_CQM, SOLVER_TIME, - THUMBNAIL + THEME_COLOR_SECONDARY, + THUMBNAIL, ) +from src.job_shop_scheduler import HybridSamplerType, SamplerType -SOLVER_OPTIONS = ["Quantum Hybrid (CQM)", "Quantum Hybrid (NL)", "Classical (HiGHS)"] +SAMPLER_TYPES = { + SamplerType.HYBRID: "Quantum Hybrid" if SHOW_CQM else "Quantum Hybrid (NL)", + SamplerType.HIGHS: "Classical (HiGHS)", +} +HYBRID_SAMPLER_TYPES = { + HybridSamplerType.NL: "Nonlinear (NL)", + HybridSamplerType.CQM: "Constrained Quadratic Model (CQM)" +} def description_card(): @@ -56,9 +65,13 @@ def description_card(): ) -def dropdown(label: str, id: str, options: list) -> html.Div: +def dropdown( + label: str, id: str, options: list, wrapper_id: str = "", wrapper_class_name: str = "" +) -> html.Div: """Slider element for value selection.""" return html.Div( + id=wrapper_id, + className=wrapper_class_name, children=[ html.Label(label), dcc.Dropdown( @@ -72,6 +85,93 @@ def dropdown(label: str, id: str, options: list) -> html.Div: ) +def checklist(label: str, id: str, options: list) -> html.Div: + """Slider element for value selection.""" + return html.Div([ + html.Label(label), + dcc.Checklist( + id=id, + options=options, + value=[options[0]["value"]], + ) + ]) + + +def generate_graph(visible: bool, type: str, index: int) -> html.Div: + """Generates graph either hidden or visible.""" + return html.Div( + id={ + "type": f"gantt-chart-{'visible' if visible else 'hidden'}-wrapper", + "index": index, + }, + className="graph" if visible else "display-none", + children=[ + dcc.Graph( + id={"type": f"gantt-chart-{type}", "index": index}, + responsive=True, + config={"displayModeBar": False}, + ), + ], + ) + + +def generate_solution_tab(label: str, title: str, tab: str, index: int) -> dcc.Tab: + """Generates solution tab containing, solution graphs, sort functionality, and + problem details dropdown. + + Returns: + dcc.Tab: A Tab containing the solution graph and problem details. + """ + return dcc.Tab( + label=label, + id=f"{tab}-tab", + className="tab", + value=f"{tab}-tab", + disabled=True, + children=[ + html.Div( + className="solution-card", + children=[ + html.Div( + className="gantt-chart-card", + children=[ + html.Div( + className="gantt-heading", + children=[ + html.H3( + [title, html.Span(id=f"{tab}-gantt-title-span")], + className="gantt-title", + ), + html.Button( + id={"type": "gantt-heading-button", "index": index}, + className="gantt-heading-button", + children="Sort by start time", + n_clicks=0, + ), + ], + ), + html.Div( + className="graph-wrapper", + children=[ + generate_graph(True, "jobsort", index), + generate_graph(False, "startsort", index), + ], + ), + ], + ), + html.Div( + [ + html.Hr(), + html.Div(problem_details(tab, index), className="problem-details"), + ], + className="problem-details-parent", + ), + ], + ), + ], + ) + + def generate_control_card() -> html.Div: """Generates the control card for the dashboard. @@ -83,8 +183,13 @@ def generate_control_card() -> html.Div: """ scenario_options = [{"label": scenario, "value": scenario} for scenario in SCENARIOS] - solver_options = [ - {"label": solver_value, "value": i} for i, solver_value in enumerate(SOLVER_OPTIONS) + sampler_options = [ + {"label": label, "value": sampler_type.value} + for sampler_type, label in SAMPLER_TYPES.items() + ] + hybrid_sampler_options = [ + {"label": label, "value": hybrid_sampler_type.value} + for hybrid_sampler_type, label in HYBRID_SAMPLER_TYPES.items() ] return html.Div( @@ -95,12 +200,17 @@ def generate_control_card() -> html.Div: "scenario-select", scenario_options, ), - html.Label("Solver (hybrid and/or classical)"), - dcc.Checklist( - id="solver-select", - options=solver_options, - value=[solver_options[2]["value"]], - className="" if SHOW_CQM else "hide-cqm", + checklist( + "Solver (hybrid and/or classical)", + "solver-select", + sorted(sampler_options, key=lambda op: op["value"]), + ), + dropdown( + "Quantum Hybrid Solver", + "hybrid-select", + sorted(hybrid_sampler_options, key=lambda op: op["value"]), + "hybrid-select-wrapper", + "" if SHOW_CQM else "display-none", ), html.Label("Solver Time Limit (seconds)"), dcc.Input( @@ -124,11 +234,38 @@ def generate_control_card() -> html.Div: ) -def problem_details(solver: str) -> html.Div: - """generate the problem details section. +def generate_problem_details_table( + scenario: str, solver: str, num_jobs: int, time_limit: int, num_operations: int, wall_clock_time: float +) -> html.Tbody: + """Generate the problem details table. + + Args: + scenario: The scenario that was optimized. + solver: The solver used for optimization. + num_jobs: The number of jobs in the scenario. + time_limit: The solver time limit. + num_operations: The number of operations in the scenario. + wall_clock_time: The overall time to optimize the scenario. + + Returns: + html.Tbody: Tbody containing table rows for problem details. + """ + + table_rows = ( + ("Scenario", scenario, "Solver", solver), + ("Number of Jobs", num_jobs, "Solver Time Limit", f"{time_limit}s"), + ("Number of Operations", num_operations, "Wall Clock Time", f"{round(wall_clock_time, 2)}s") + ) + + return html.Tbody([html.Tr([html.Td(cell) for cell in row]) for row in table_rows]) + + +def problem_details(solver: str, index: int) -> html.Div: + """Generate the problem details section. Args: solver: Which solver tab to generate the section for. Either "dwave" or "highs" + index: Unique element id to differentiate matching elements. Returns: html.Div: Div containing a collapsable table. @@ -136,15 +273,21 @@ def problem_details(solver: str) -> html.Div: return html.Div( [ html.Div( - id={"type": "to-collapse-class", "index": 1 if solver == "dwave" else 2}, + id={"type": "to-collapse-class", "index": index}, className="details-collapse-wrapper collapsed", children=[ - html.Button( - id={"type": "collapse-trigger", "index": 1 if solver == "dwave" else 2}, - className="details-collapse", + html.Div( + className="details-collapse-title", children=[ - html.H5("Problem Details"), - html.Div(className="collapse-arrow"), + html.H5(id=f"{solver}-stats-makespan", className="stats-makespan"), + html.Button( + id={"type": "collapse-trigger", "index": index}, + className="details-collapse", + children=[ + html.H5("Problem Details"), + html.Div(className="collapse-arrow"), + ], + ), ], ), html.Div( @@ -152,36 +295,7 @@ def problem_details(solver: str) -> html.Div: children=[ html.Table( className="solution-stats-table", - children=[ - html.Tbody( - children=[ - html.Tr( - [ - html.Td("Scenario"), - html.Td(id=f"{solver}-stats-scenario"), - html.Td("Solver"), - html.Td(id=f"{solver}-stats-solver"), - ] - ), - html.Tr( - [ - html.Td("Number of Jobs"), - html.Td(id=f"{solver}-stats-jobs"), - html.Td("Solver Time Limit [s]"), - html.Td(id=f"{solver}-stats-time-limit"), - ] - ), - html.Tr( - [ - html.Td("Number of Operations"), - html.Td(id=f"{solver}-stats-resources"), - html.Td("Wall Clock Time [s]"), - html.Td(id=f"{solver}-stats-wall-clock-time"), - ] - ), - ], - ), - ], + id=f"{solver}-solution-stats-table", ), ], ), @@ -190,6 +304,7 @@ def problem_details(solver: str) -> html.Div: ], ) + # set the application HTML def set_html(app): app.layout = html.Div( @@ -213,11 +328,6 @@ def set_html(app): [ # Padding and content wrapper description_card(), generate_control_card(), - html.Div( - ["initial child"], - id="output-clientside", - style={"display": "none"}, - ), ] ) ] @@ -245,144 +355,46 @@ def set_html(app): className="tab", children=[ html.Div( - html.Div( - id="unscheduled-gantt-chart-card", - className="gantt-chart-card", - children=[ - html.H3( - "Unscheduled Jobs and Operations", - className="gantt-title", - ), - dcc.Loading( - id="loading-icon-input", - children=[ - dcc.Graph( - className="gantt-div", - id="unscheduled-gantt-chart", - responsive=True, - config={"displayModeBar": False}, - ), - ], - ), - ], - ), - className="gantt-chart-card-parent", - ) - ], - ), - dcc.Tab( - label=DWAVE_TAB_LABEL, - value="dwave-tab", - id="dwave-tab", - className="tab", - disabled=True, - children=[ - html.Div( - html.Div( - id="optimized-gantt-chart-card", - className="gantt-chart-card", - children=[ - html.Div( - [ - html.H3( - "Leap Hybrid Solver", - className="gantt-title", - ), - html.Button(id={"type": "sort-button", "index": 0}, children="Sort by start time", n_clicks=0), - ], - className="gantt-heading-button", - ), - html.Div( - [ - dcc.Graph( - id={"type": "gantt-chart-jobsort", "index": 0}, - responsive=True, - className="gantt-div", - config={"displayModeBar": False}, - ), - dcc.Graph( - id={"type": "gantt-chart-startsort", "index": 0}, - responsive=True, - className="display-none", - config={"displayModeBar": False}, - ), - ], - ), - html.Div( - [ - html.Hr(), - html.Div( - [ - html.H5(id="dwave-stats-make-span"), - problem_details("dwave"), - ], - className="problem-details" - ), - ], - className="problem-details-parent", - ), - ], - ), - className="gantt-chart-card-parent", + className="gantt-chart-card", + children=[ + html.Div( + className="gantt-heading", + children=[ + html.H3( + "Unscheduled Jobs and Operations", + className="gantt-title", + ), + html.Button( + id={ + "type": "gantt-heading-button", + "index": 0, + }, + className="gantt-heading-button", + children="Show Conflicts", + n_clicks=0, + ), + ], + ), + dcc.Loading( + id="loading-icon-input", + parent_className="graph-wrapper", + type="circle", + delay_show=300, + color=THEME_COLOR_SECONDARY, + children=[ + generate_graph(True, "unscheduled", 0), + generate_graph(False, "conflicts", 0), + ], + ), + ], ), ], ), - dcc.Tab( - label=CLASSICAL_TAB_LABEL, - id="highs-tab", - className="tab", - value="highs-tab", - disabled=True, - children=[ - html.Div( - html.Div( - id="highs-gantt-chart-card", - className="gantt-chart-card", - children=[ - html.Div( - [ - html.H3( - "HiGHS Classical Solver", - className="gantt-title", - ), - html.Button(id={"type": "sort-button", "index": 1}, children="Sort by start time", n_clicks=0), - ], - className="gantt-heading-button", - ), - html.Div( - [ - dcc.Graph( - id={"type": "gantt-chart-jobsort", "index": 1}, - responsive=True, - className="gantt-div", - config={"displayModeBar": False}, - ), - dcc.Graph( - id={"type": "gantt-chart-startsort", "index": 1}, - responsive=True, - className="display-none", - config={"displayModeBar": False}, - ), - ] - ), - html.Div( - [ - html.Hr(), - html.Div( - [ - html.H5(id="highs-stats-make-span"), - problem_details("highs"), - ], - className="problem-details" - ), - ], - className="problem-details-parent", - ), - ], - ), - className="gantt-chart-card-parent", - ), - ], + generate_solution_tab( + DWAVE_TAB_LABEL, "Quantum Hybrid Solver", "dwave", 1 + ), + generate_solution_tab( + CLASSICAL_TAB_LABEL, "Classical Solver (HiGHS)", "highs", 2 ), ], ) diff --git a/requirements.txt b/requirements.txt index 9050a98..5954037 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -dash==2.14.1 +dash==2.17.1 diskcache==5.6.3 dash[diskcache] dwave-ocean-sdk>=7.0.0 diff --git a/src/generate_charts.py b/src/generate_charts.py index f57c1dc..91ae12d 100644 --- a/src/generate_charts.py +++ b/src/generate_charts.py @@ -50,9 +50,24 @@ def get_minimum_task_times(job_shop_data: JobShopData) -> pd.DataFrame: "Start": start_time, "Finish": end_time, COLOR_LABEL: task.resource, + "Conflicts": "No", } ) start_time = end_time + + def tasks_overlap(task_i, task_j): + """Returns True if tasks a and b overlap""" + interval_a, interval_b = sorted( + ((task_i["Start"], task_i["Finish"]), (task_j["Start"], task_j["Finish"])) + ) + return interval_b[0] < interval_a[1] + + # Calculate scheduling conflicts + for i, task_i in enumerate(task_data): + for j, task_j in enumerate(task_data[i + 1 :]): + if task_i[COLOR_LABEL] == task_j[COLOR_LABEL] and tasks_overlap(task_i, task_j): + task_data[i]["Conflicts"] = task_data[i + 1 + j]["Conflicts"] = "Yes" + df = pd.DataFrame(task_data) df["delta"] = df.Finish - df.Start df[Y_AXIS_LABEL] = df[Y_AXIS_LABEL].astype(str) @@ -91,7 +106,9 @@ def get_empty_figure(message: str) -> go.Figure: return fig -def generate_gantt_chart(df: pd.DataFrame = None, sort_by: str = "JobInt",) -> go.Figure: +def generate_gantt_chart( + df: pd.DataFrame = None, sort_by: str = "JobInt", show_conflicts: bool = False +) -> go.Figure: """Generates a Gantt chart of the unscheduled tasks for the given scenario. Args: @@ -130,14 +147,29 @@ def generate_gantt_chart(df: pd.DataFrame = None, sort_by: str = "JobInt",) -> g x_end="Finish", y=Y_AXIS_LABEL, color=COLOR_LABEL, + pattern_shape="Conflicts" if show_conflicts else None, + pattern_shape_map={"Yes": "/", "No": ""}, color_discrete_sequence=[color_map[label] for label in color_labels], - category_orders={COLOR_LABEL: color_labels} + category_orders={COLOR_LABEL: color_labels}, ) + # Update legend to remove conflict text and duplicates. + unique_labels = set() + for i, trace in enumerate(fig.select_traces()): + label = trace.legendgroup.split(",")[0] + if label not in unique_labels: + trace.update({"name": label, "legendrank": i}) + unique_labels.add(label) + else: + trace.update({"showlegend": False}) + + fig.update_legends({"title": {"text": COLOR_LABEL}}) # Update legend title. + for index, data in enumerate(fig.data): - resource = data.name + resource = data.name.split(",")[0] fig.data[index].x = [ - df[(df[Y_AXIS_LABEL] == job) & (df[COLOR_LABEL] == resource)].delta.tolist()[0] for job in data.y + df[(df[Y_AXIS_LABEL] == job) & (df[COLOR_LABEL] == resource)].delta.tolist()[0] + for job in data.y ] fig.layout.xaxis.type = "linear" diff --git a/src/job_shop_scheduler.py b/src/job_shop_scheduler.py index 42ccc63..6218aec 100644 --- a/src/job_shop_scheduler.py +++ b/src/job_shop_scheduler.py @@ -3,24 +3,36 @@ solve a Job Shop Scheduling problem using CQM. """ + from __future__ import annotations import argparse import sys import warnings +from enum import Enum import pandas as pd from dimod import Binary, ConstrainedQuadraticModel, Integer from dwave.system import LeapHybridCQMSampler, LeapHybridNLSampler sys.path.append("./src") -import utils.scipy_solver as scipy_solver +from dwave.optimization.generators import flow_shop_scheduling + import utils.plot_schedule as job_plotter +import utils.scipy_solver as scipy_solver from model_data import JobShopData from utils.greedy import GreedyJobShop from utils.utils import print_cqm_stats, write_solution_to_file -from dwave.optimization.generators import flow_shop_scheduling + +class SamplerType(Enum): + HYBRID = 0 + HIGHS = 1 + + +class HybridSamplerType(Enum): + NL = 0 + CQM = 1 def generate_greedy_makespan(job_data: JobShopData, num_samples: int = 100) -> int: @@ -94,7 +106,7 @@ def create_nl_model(self) -> None: def define_cqm_variables(self) -> None: """Define CQM variables.""" - # Define make span as an integer variable + # Define makespan as an integer variable self._makespan_var = Integer("makespan", lower_bound=0, upper_bound=self.max_makespan) # Define integer variable for start time of using machine i for job j @@ -141,7 +153,8 @@ def add_precedence_constraints(self) -> None: machine_curr = curr_task.resource machine_prev = prev_task.resource self.cqm.add_constraint( - self._x[(job, machine_curr)] - self._x[(job, machine_prev)] >= prev_task.duration, + self._x[(job, machine_curr)] - self._x[(job, machine_prev)] + >= prev_task.duration, label="pj{}_m{}".format(job, machine_curr), ) diff --git a/src/model_data.py b/src/model_data.py index aba7008..62fb340 100644 --- a/src/model_data.py +++ b/src/model_data.py @@ -1,8 +1,8 @@ from __future__ import annotations import sys -from pathlib import Path from collections.abc import Iterable +from pathlib import Path from typing import TYPE_CHECKING, Union sys.path.append("./src") @@ -94,7 +94,6 @@ def resource_names(self) -> Iterable[str, int]: """ return self._resource_names or list(range(len(self.processing_times))) - @property def job_tasks(self) -> dict: """Returns the tasks in the data, grouped by job. @@ -139,7 +138,9 @@ def add_resource(self, resource: str) -> None: """ self._resources.add(resource) - def add_task_from_data(self, resource: str, job: str, duration: int, position: int = None) -> None: + def add_task_from_data( + self, resource: str, job: str, duration: int, position: int = None + ) -> None: """Adds a task to the dataset. Args: @@ -334,4 +335,6 @@ def load_from_file(self, filename: Union[Path, str]) -> None: for machine, machine_times in enumerate(self.processing_times): for job, duration in enumerate(machine_times): - self.add_task(Task(str(job), duration=duration, resource=self._resource_names[machine])) + self.add_task( + Task(str(job), duration=duration, resource=self._resource_names[machine]) + ) diff --git a/src/utils/scipy_solver.py b/src/utils/scipy_solver.py index 6bfc275..ed92c87 100644 --- a/src/utils/scipy_solver.py +++ b/src/utils/scipy_solver.py @@ -25,15 +25,16 @@ class SciPyCQMSolver: See :func:`scipy.optimize.milp()` """ + @staticmethod def iter_constraints( - cqm: dimod.ConstrainedQuadraticModel, - ) -> typing.Iterator[scipy.optimize.LinearConstraint]: + cqm: dimod.ConstrainedQuadraticModel, + ) -> typing.Iterator[scipy.optimize.LinearConstraint]: num_variables = cqm.num_variables() for comp in cqm.constraints.values(): if comp.sense is dimod.sym.Sense.Eq: - lb = ub = comp.rhs - comp.lhs.offset # move offset (if not 0) to rhs of constraint + lb = ub = comp.rhs - comp.lhs.offset # move offset (if not 0) to rhs of constraint elif comp.sense is dimod.sym.Sense.Ge: lb = comp.rhs - comp.lhs.offset ub = +float("inf") @@ -49,13 +50,13 @@ def iter_constraints( # Create the LinearConstraint. # We save A as a csr matrix to save on a bit of memory - yield scipy.optimize.LinearConstraint( - scipy.sparse.csr_array([A]), lb=lb, ub=ub) + yield scipy.optimize.LinearConstraint(scipy.sparse.csr_array([A]), lb=lb, ub=ub) @staticmethod - def sample_cqm(cqm: dimod.ConstrainedQuadraticModel, - time_limit: float = float('inf'), - ) -> dimod.SampleSet: + def sample_cqm( + cqm: dimod.ConstrainedQuadraticModel, + time_limit: float = float("inf"), + ) -> dimod.SampleSet: """Use HiGHS via SciPy to solve a constrained quadratic model. Note that HiGHS requires the objective and constraints to be @@ -83,11 +84,13 @@ def sample_cqm(cqm: dimod.ConstrainedQuadraticModel, # Check that we're a linear model if not cqm.objective.is_linear(): - raise ValueError("scipy.optimize.milp() does not support objectives " - "with quadratic interactions") + raise ValueError( + "scipy.optimize.milp() does not support objectives " "with quadratic interactions" + ) if not all(comp.lhs.is_linear() for comp in cqm.constraints.values()): - raise ValueError("scipy.optimize.milp() does not support constraints " - "with quadratic interactions") + raise ValueError( + "scipy.optimize.milp() does not support constraints " "with quadratic interactions" + ) num_variables = cqm.num_variables() @@ -118,12 +121,13 @@ def sample_cqm(cqm: dimod.ConstrainedQuadraticModel, constraints = list(SciPyCQMSolver.iter_constraints(cqm)) t = time.perf_counter() - solution = scipy.optimize.milp(c, - integrality=integrality, - bounds=scipy.optimize.Bounds(lb=lb, ub=ub), - options=dict(time_limit=time_limit), - constraints=constraints, - ) + solution = scipy.optimize.milp( + c, + integrality=integrality, + bounds=scipy.optimize.Bounds(lb=lb, ub=ub), + options=dict(time_limit=time_limit), + constraints=constraints, + ) run_time = time.perf_counter() - t # If we're infeasible, return an empty solution @@ -133,6 +137,7 @@ def sample_cqm(cqm: dimod.ConstrainedQuadraticModel, # Otherwise we can just read the solution out and convert it into a # dimod sampleset sampleset = dimod.SampleSet.from_samples_cqm( - (solution.x, cqm.variables), cqm, info=dict(run_time=run_time)) + (solution.x, cqm.variables), cqm, info=dict(run_time=run_time) + ) return sampleset diff --git a/src/utils/utils.py b/src/utils/utils.py index a2a85b6..6ed0030 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -1,13 +1,12 @@ from __future__ import annotations -from functools import cache import os from collections import defaultdict +from functools import cache from pathlib import Path from typing import TYPE_CHECKING, Union import numpy as np - from dimod import BINARY, INTEGER, ConstrainedQuadraticModel, sym from tabulate import tabulate @@ -200,6 +199,7 @@ def read_or_library_instance(instance_path: Union[Path, str]) -> array_like: return load_or_library_instances(instance_path)[instance_label] + @cache def load_or_library_instances(instance_path: Union[Path, str]) -> list[dict]: """Read the OR library formatted Flow Shop Schedule problems file. @@ -216,7 +216,6 @@ def load_or_library_instances(instance_path: Union[Path, str]) -> list[dict]: num_jobs = num_machines = None expect_problem = expect_label = False - def _store_instance(processing_times): """Store processing_times as instance.""" # stored as num_jobs x num_machines @@ -227,7 +226,6 @@ def _store_instance(processing_times): instances[label] = processing_times_array processing_times.clear() - with open(instance_path) as f: for line in f: if line.isspace(): diff --git a/tests/test_scipy_solver.py b/tests/test_scipy_solver.py index 6f93cee..c51a23f 100644 --- a/tests/test_scipy_solver.py +++ b/tests/test_scipy_solver.py @@ -28,7 +28,7 @@ def test_empty(self): def test_infease(self): cqm = dimod.ConstrainedQuadraticModel() - i, j = dimod.Integers('ij') + i, j = dimod.Integers("ij") cqm.add_constraint(i - j <= -1) cqm.add_constraint(i - j >= +1) @@ -39,32 +39,32 @@ def test_infease(self): def test_bounds(self): cqm = dimod.ConstrainedQuadraticModel() - i = dimod.Integer('i', lower_bound=-5, upper_bound=5) + i = dimod.Integer("i", lower_bound=-5, upper_bound=5) cqm.set_objective(i) sampleset = SciPyCQMSolver().sample_cqm(cqm) - self.assertEqual(sampleset.first.sample['i'], -5) + self.assertEqual(sampleset.first.sample["i"], -5) cqm.set_objective(-i) sampleset = SciPyCQMSolver().sample_cqm(cqm) - self.assertEqual(sampleset.first.sample['i'], 5) + self.assertEqual(sampleset.first.sample["i"], 5) def test_offset(self): cqm = dimod.ConstrainedQuadraticModel() - i = dimod.Integer('i', lower_bound=-10, upper_bound=10) + i = dimod.Integer("i", lower_bound=-10, upper_bound=10) cqm.add_constraint(i + 5 <= 0) sampleset = SciPyCQMSolver().sample_cqm(cqm) - self.assertEqual(sampleset.first.sample['i'], -5) + self.assertEqual(sampleset.first.sample["i"], -5) def test_quadratic(self): cqm = dimod.ConstrainedQuadraticModel() - i, j = dimod.Integers('ij') + i, j = dimod.Integers("ij") - cqm.add_constraint(i*j <= 5) + cqm.add_constraint(i * j <= 5) with self.assertRaises(ValueError): SciPyCQMSolver().sample_cqm(cqm) @@ -73,10 +73,10 @@ def test_vartypes(self): cqm = dimod.ConstrainedQuadraticModel() - i = dimod.Integer('i') - a = dimod.Real('a') - x = dimod.Binary('x') - s = dimod.Spin('s') + i = dimod.Integer("i") + a = dimod.Real("a") + x = dimod.Binary("x") + s = dimod.Spin("s") cqm.set_objective(-i - a - x) cqm.add_constraint(i <= 5.5) @@ -85,7 +85,7 @@ def test_vartypes(self): sampleset = SciPyCQMSolver().sample_cqm(cqm) - self.assertEqual(sampleset.first.sample, {'i': 5, 'a': 6.5, 'x': 1}) + self.assertEqual(sampleset.first.sample, {"i": 5, "a": 6.5, "x": 1}) cqm.add_constraint(s <= 5) with self.assertRaises(ValueError):