diff --git a/app.py b/app.py index 4b33417..9eb3fb1 100644 --- a/app.py +++ b/app.py @@ -52,7 +52,7 @@ app.config.suppress_callback_exceptions = True -app.index_string = ''' +app.index_string = """ @@ -71,7 +71,7 @@ -''' +""" # Parse debug argument parser = argparse.ArgumentParser(description="Dash debug setting.") diff --git a/assets/__demo_variables.css b/assets/__demo_variables.css index bea577d..f0c185a 100644 --- a/assets/__demo_variables.css +++ b/assets/__demo_variables.css @@ -26,21 +26,24 @@ Dash reads all css files contained in `/assets/` so no imports are necessary. --blue-bright: #03B8FF; --blue-light: #2A7DE1; --blue-dark: #074C91; - --red-light: #C70039; - --red-dark: #900C3F; + --blue-darker: #2d4376;; + --blue-darkest: #202239; + --red-light: #f57677; + --red-dark: #aa3a3c; --orange: #FF7006; - --teal-light: #06ECDC; + --teal-light: #06ecdc; + --teal: #17bebb; --teal-dark: #17BEBB; --teal-darker: #008C82; - --grey-lighter: #EEEEEE; - --grey-light: #DDDDDD; + --grey-lighter: #f5f7fb; + --grey-light: #DBDBDB; --grey: #AAAAAA; --grey-medium: #737373; --grey-dark: #222222; --box-shadow: 0 0 1rem rgba(66, 82, 121, 0.2); --font: "proxima-nova", "Helvetica Neue", sans-serif; - --banner-height: 3.5rem; + --banner-height: 6.5rem; --left-col-width: 26.25rem; --problem-details-height: 11.25rem; - /*** Add new variables here ***/ + --title-section-height: 13.5rem; } diff --git a/assets/__style_guide.css b/assets/__style_guide.css index 7065afb..2223d06 100644 --- a/assets/__style_guide.css +++ b/assets/__style_guide.css @@ -21,8 +21,7 @@ limitations under the License. } body { - background-color: #f9f9f9; - color: var(--grey-dark); + color: var(--grey-darkest); font-size: 1.125rem; line-height: 1.5rem; margin: 0; @@ -31,17 +30,19 @@ body { h1, h2, h3, h4, h5, h6, td, th, span, a, p, label, button, input { font-family: var(--font); + color: var(--blue-darkest); } h1, h2, h3, h4, h5, h6 { - color: var(--blue-dark); - font-weight: 400; + font-weight: 600; + margin-top: 0; } h1 { font-size: 2.5rem; line-height: 3rem; margin-bottom: 1rem; + font-weight: 400; } h2 { @@ -62,6 +63,8 @@ h5 { font-size: 1.125rem; margin: 1rem 0; font-weight: 600; + margin-top: 1rem; + margin-bottom: 1rem; } label { @@ -79,19 +82,29 @@ hr { table { margin-bottom: 1.25rem; font-size: 1rem; - border: 1px solid var(--grey-light); border-collapse: collapse; + border: 1px solid var(--grey-light); +} + +thead { + border-bottom: 1px solid var(--grey-light); + font-weight: 600; + background-color: var(--blue-darker); +} + +tfoot { + border-top: 1px solid var(--grey-light); + font-weight: 600; } th { font-weight: 600; + color: white; + padding: 0.5rem; } -th, td { - color: var(--grey-dark); +td { padding: 0.25rem 0.5rem; - border-right: 1px solid var(--grey-light); - border-bottom: 1px solid var(--grey-light); } th:first-child, td:first-child { @@ -104,8 +117,8 @@ th:last-child, td:last-child { input[type="checkbox"], input[type="radio"] { - accent-color: var(--blue-dark); - margin: 0 0.6rem 0 0; + accent-color: var(--blue-darker); + margin: 0 0.5rem 0 0; } .display-none { @@ -113,7 +126,6 @@ input[type="radio"] { } #app-container { - min-width: 62.5rem; height: 100vh; display: flex; flex-direction: column; @@ -121,9 +133,9 @@ input[type="radio"] { .banner { height: var(--banner-height); - box-sizing: border-box; - background-color: var(--blue-dark); - padding: 1rem 2rem; + background-color: #12131f; + padding: 0 2rem; + border-bottom: 2px solid var(--teal); display: flex; align-items: center; @@ -131,7 +143,7 @@ input[type="radio"] { } .banner img { - height: 100%; + height: 1rem; } .columns-main { @@ -141,17 +153,45 @@ input[type="radio"] { .left-column { display: flex; - height: calc(100vh - var(--banner-height)); + height: 100%; + background-color: var(--grey-lighter); + box-shadow: var(--box-shadow); } .right-column { - background-color: var(--grey-lighter); - padding: 1.25rem 0 0; width: 100%; + min-height: 40rem; +} + +.title-section { + padding: 2rem 1rem 1.5rem; + background-color: var(--blue-darkest); + background-image: url("background.svg"); + background-position: 30% 0; + height: var(--title-section-height); +} + +.title-section h1 { + color: white; +} + +.title-section p { + margin-bottom: 0; + color: white; } .settings { - margin: 1rem 0 1.5rem; + margin: 1rem 0 2rem; +} + +.form-section { + display: flex; + flex: 1; +} + +.settings-and-buttons-wrapper { + padding: 0 1rem 2rem; + flex: 1; } .radio label, @@ -171,7 +211,7 @@ input[type="radio"] { } .is-focused:not(.is-open) > .Select-control { - border-color: var(--orange); + border-color: var(--teal); box-shadow: none; } @@ -195,7 +235,7 @@ div.dash-sk-circle { /* Make the skip link visible when it receives keyboard focus */ .skip-link:focus { - color: var(--orange); + color: var(--teal); position: static; width: auto; height: auto; @@ -204,3 +244,13 @@ div.dash-sk-circle { padding: 1rem; z-index: 1000; } + +@media screen and (max-width: 1000px) { + .columns-main { + flex-direction: column; + } + + .form-section { + flex-direction: column; + } +} diff --git a/assets/_buttons.css b/assets/_buttons.css index e4d2a54..93354d2 100644 --- a/assets/_buttons.css +++ b/assets/_buttons.css @@ -17,24 +17,34 @@ limitations under the License. /* Style rules for buttons */ button { - font-size: 1rem; - line-height: 1rem; - padding: 1rem 2rem; - height: auto; - color: white; - transition: all 0.2s ease-in-out; border: none; - background-color: var(--blue-dark); + font-size: 0.875rem; font-weight: 600; - text-transform: uppercase; - border-radius: 0.25rem; + line-height: 1.75; + border-radius: 4px; + padding: 0.75rem 1.5rem; cursor: pointer; - letter-spacing: .1rem; + transition: all 0.2s ease-in-out; } -button:hover { - filter: brightness(80%); +.button { + font-size: 0.875rem; + font-weight: 600; + line-height: 1.75; + border-radius: 4px; + padding: 0.75rem 1.5rem; color: white; + text-transform: uppercase; + background: radial-gradient(61.22% 95.86% at 26.96% 100%, #4c71c6 0%, var(--blue-darker) 100%); + transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1), + border-color 250ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: rgba(29, 30, 36, 0.19) 0px 5px 14px 0px; + border: 1px solid rgba(76, 113, 198, 0.4); +} + +.button:hover { + background: radial-gradient(61.22% 95.86% at 26.96% 100%, var(--blue-darker) 0%, var(--blue-darker) 100%); } #run-button, @@ -43,11 +53,12 @@ button:hover { } #cancel-button { - background-color: var(--red-light); + background: radial-gradient(61.22% 95.86% at 26.96% 100%, var(--red-light) 0%, var(--red-dark) 100%); + border: 1px solid rgb(245, 118, 119, 0.4); } #cancel-button:hover { - background-color: var(--red-dark); + background: radial-gradient(61.22% 95.86% at 26.96% 100%, var(--red-dark) 0%, var(--red-dark) 100%); } button:disabled { @@ -57,5 +68,5 @@ button:disabled { } button:focus-visible { - box-shadow: 0 0 0 4px var(--orange); + box-shadow: 0 0 0 4px var(--teal); } diff --git a/assets/_collapse.css b/assets/_collapse.css index 18c2782..f75b40a 100644 --- a/assets/_collapse.css +++ b/assets/_collapse.css @@ -16,25 +16,63 @@ limitations under the License. /* Style rules for collapsible dropdowns like left-column-collapse and problem-details-collapse */ +.collapse-arrow { + height: 1.5rem; + position: relative; + width: 1.4rem; + transition: border-color 0.25s ease-in-out; +} + +.collapse-arrow:before, +.collapse-arrow:after { + content: ""; + display: block; + position: absolute; + height: 0.25rem; + width: 1.5rem; + background: var(--blue-darker); + border-radius: 0.125rem; +} + +.collapse-arrow:before { + rotate: -70deg; + top: 0; +} + +.collapse-arrow:after { + rotate: 70deg; + bottom: 0; +} + +.settings-and-buttons-wrapper { + height: calc(100vh - var(--title-section-height)); + overflow-y: auto; + direction: rtl; +} + +.settings-and-buttons { + direction: ltr; +} + .left-column .left-column-layer-1 { width: var(--left-col-width); - transition: width 0.6s ease-in-out; - overflow-x: hidden; - overflow-y: auto; + transition: width 0.5s ease-in-out; direction: rtl; } .details-to-collapse { height: var(--problem-details-height); - transition: height 0.6s ease-in-out; + transition: height 0.5s ease-in-out; overflow: hidden; } .left-column .left-column-layer-2 { - padding: 0 1rem 2.5rem; width: var(--left-col-width); box-sizing: border-box; direction: ltr; + display: flex; + flex-direction: column; + height: 100%; } .details-collapse-wrapper { @@ -42,43 +80,37 @@ limitations under the License. overflow: hidden; } -.left-column-collapse, -.left-column-collapse:hover, -.left-column-collapse:focus { +.left-column-collapse { background: white; - border-right: 1px solid var(--grey-lighter); + border: none; height: 100%; border-radius: 0; - padding: 0 0 0 0.25rem; - filter: none; + padding: 0; display: block; } +.left-column-collapse:hover, +.left-column-collapse:focus { + background: var(--grey-lighter); +} + .details-collapse, .details-collapse:hover, .details-collapse:focus { background: none; display: flex; + align-items: center; padding: 0 1.25rem 0 0; text-transform: none; -} - -.collapse-arrow { - border-right: 4px solid var(--grey-medium); - border-bottom: 4px solid var(--grey-medium); - transform: rotate(135deg) skew(165deg, 165deg); - height: 1.25rem; - width: 1.25rem; - margin-right: -3px; - transition: border-color 0.25s ease-in-out; + color: var(--blue-dark); } .details-collapse .collapse-arrow { - transform: rotate(225deg) skew(165deg, 165deg); - margin: 1rem 0 0 1rem; - border-color: var(--blue-dark); - height: 0.75rem; - width: 0.75rem; + transform: rotate(90deg); + width: 3rem; + margin-top: 1.5rem; + margin-left: 0.5rem; + color: var(--blue-darkest); } .left-column-collapse:hover .collapse-arrow { @@ -90,14 +122,24 @@ limitations under the License. } .collapsed .left-column-collapse .collapse-arrow { - margin-left: -3px; - margin-right: 0; - transform: rotate(315deg) skew(165deg, 165deg); + margin-right: -4px; + transform: rotate(180deg); +} + +.title-section { + transition: all 0.5s ease-in-out; +} + +.collapsed .title-section { + height: 0; + padding: 0; + overflow: hidden; } .collapsed .details-collapse .collapse-arrow { - margin-top: 1rem; - transform: rotate(45deg) skew(165deg, 165deg); + margin-bottom: 1.5rem; + margin-top: 0; + transform: rotate(-90deg); } .collapsed .details-to-collapse { @@ -105,5 +147,40 @@ limitations under the License. } .collapsed .left-column-layer-1 { - width: 0; + width: 1.5rem; +} + +@media screen and (max-width: 1000px) { + .left-column { + height: auto; + width: 100%; + } + + .left-column .left-column-layer-1, + .left-column .left-column-layer-2, + .left-column .left-column-collapse { + width: 100%; + } + + .left-column-collapse .collapse-arrow { + transform: rotate(90deg); + margin: auto; + } + + .collapsed .title-section { + height: 0; + overflow-y: hidden; + } + + .collapsed .settings-and-buttons-wrapper { + height: 0; + flex: unset; + padding: 0; + overflow-y: hidden; + } + + .collapsed .left-column-collapse .collapse-arrow { + margin-right: auto; + transform: rotate(-90deg); + } } diff --git a/assets/_inputs.css b/assets/_inputs.css index 53f0213..623b929 100644 --- a/assets/_inputs.css +++ b/assets/_inputs.css @@ -65,10 +65,10 @@ limitations under the License. /* Style rules for the dmc.NumberInput element */ .mantine-NumberInput-input:focus-visible { - box-shadow: 0 0 0 2px var(--orange); + box-shadow: 0 0 0 2px var(--teal); } /* Style rules for the dmc.Select element */ .mantine-Select-input:focus-visible { - box-shadow: 0 0 0 2px var(--orange); + box-shadow: 0 0 0 2px var(--teal); } diff --git a/assets/_tabs.css b/assets/_tabs.css index 26ccbb6..db07e2a 100644 --- a/assets/_tabs.css +++ b/assets/_tabs.css @@ -16,48 +16,63 @@ limitations under the License. /* Style rules for tabs like the input and results tabs */ -.tab-container { - border-bottom: 3px solid var(--blue-light); - flex-direction: row; +nav { + height: 100%; +} + +.mantine-Tabs-root { + height: 100%; +} + +.mantine-Tabs-panel { + height: calc(100% - var(--banner-height)); } -.tab-parent .tab { - margin: 0 1.25rem; - border-top-left-radius: 0.5rem; - border-top-right-radius: 0.5rem; +.mantine-Tabs-list { + --tabs-list-gap: 1.5rem; + height: 100%; +} + +.mantine-Tabs-list::before { border: none; } -div.tab.tab--selected { - border: 3px solid var(--blue-light) !important; - border-bottom: none !important; - cursor: default; - box-shadow: 0 6px 0 -3px white; +.mantine-Tabs-tab { + font-size: 1.5rem; + font-weight: 400; + border-radius: 0; + border-bottom-width: 0; + border-top-width: 5px; + height: 100%; } -div.tab.tab--disabled { - cursor: not-allowed !important; - color: var(--grey-medium) !important; +button.mantine-Tabs-tab:disabled { + filter: unset; + opacity: 1; + color: #7E7F86; + border: transparent; } -.tab:first-child { - margin-right: 0.625rem; +.mantine-Tabs-tab span { + color: #A0A1A5; + opacity: 1; + transition: color 0.2s ease-in-out; } -.tab:last-child { - margin-left: 0.625rem; +.mantine-Tabs-tab:where([data-active]) span { + opacity: 1; + color: white; } -.tab-parent { - display: flex; - flex-direction: column; +.mantine-Tabs-tab:hover { + background-color: unset; } -.tab-content, -.tab-parent { - height: 100%; +.mantine-Tabs-tab:hover:where(:not([data-active])) { + border-color: unset; } -.tab-content { - background-color: white; +.mantine-Tabs-tab:hover:where(:not(:disabled)) span { + opacity: 1; + color: white; } diff --git a/assets/background.svg b/assets/background.svg new file mode 100644 index 0000000..d8ad4ad --- /dev/null +++ b/assets/background.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/demo.css b/assets/demo.css index 517c24d..b93f289 100644 --- a/assets/demo.css +++ b/assets/demo.css @@ -69,10 +69,12 @@ h5.no-results { #solution-stats-table td:nth-child(2n+1) { text-align: left; + font-weight: 600; } #solution-stats-table td:nth-child(2n) { border-right: 1px solid var(--grey-light); + text-align: right; } .result-table-div { @@ -84,13 +86,6 @@ h5.no-results { width: 100%; } -.result-table { - font-size: 1rem; - border: 3px var(--blue-dark); - border-style: solid solid none; - background-color: white; -} - #solution-cost-table-div { margin-right: 1.625rem; } @@ -100,25 +95,16 @@ h5.no-results { display: none; } -.result-table th { - color: white; - background-color: var(--blue-dark); -} - -th, td { +.result-table-div th, .result-table-div td { text-align: right; } -.total-cost-row td { - font-weight: 600; -} - .VirtualizedSelectFocusedOption { background-color: var(--grey-lighter); } .is-focused:not(.is-open)>.Select-control { - border-color: var(--blue-light); + border-color: var(--teal); box-shadow: none; } @@ -131,7 +117,7 @@ th, td { } #solution-map:focus-visible { - box-shadow: 0 0 0 2px var(--orange); + box-shadow: 0 0 0 2px var(--teal); } .map-wrapper[style="visibility: visible;"] { diff --git a/demo_callbacks.py b/demo_callbacks.py index 273b93e..837f24a 100644 --- a/demo_callbacks.py +++ b/demo_callbacks.py @@ -37,29 +37,31 @@ @dash.callback( Output({"type": "to-collapse-class", "index": MATCH}, "className"), + Output({"type": "collapse-trigger", "index": MATCH}, "aria-expanded"), inputs=[ Input({"type": "collapse-trigger", "index": MATCH}, "n_clicks"), State({"type": "to-collapse-class", "index": MATCH}, "className"), ], prevent_initial_call=True, ) -def toggle_left_column(collapse_trigger: int, to_collapse_class: str) -> str: +def toggle_left_column(collapse_trigger: int, to_collapse_class: str) -> tuple[str, str]: """Toggles a 'collapsed' class that hides and shows some aspect of the UI. Args: collapse_trigger (int): The (total) number of times a collapse button has been clicked. to_collapse_class (str): Current class name of the thing to collapse, 'collapsed' if not - visible, empty string if visible + visible, empty string if visible. Returns: str: The new class name of the thing to collapse. + str: The aria-expanded value. """ classes = to_collapse_class.split(" ") if to_collapse_class else [] if "collapsed" in classes: classes.remove("collapsed") - return " ".join(classes) - return to_collapse_class + " collapsed" if to_collapse_class else "collapsed" + return " ".join(classes), "true" + return to_collapse_class + " collapsed" if to_collapse_class else "collapsed", "false" def generate_initial_map(num_clients: int) -> folium.Map: @@ -118,9 +120,7 @@ def render_initial_map(num_clients: int, _) -> str: ], prevent_initial_call=True, ) -def update_tables( - run_in_progress, stored_results, reset_results, solver_type -) -> tuple[list, list]: +def update_tables(run_in_progress, stored_results, reset_results, solver_type) -> tuple[list, list]: """Update the results tables each time a run is made. Args: @@ -217,6 +217,7 @@ def get_updated_wall_clock_times( class RunOptimizationReturn(NamedTuple): """Return type for the ``run_optimization`` callback function.""" + solution_map: str cost_table: tuple hybrid_table_label: str @@ -232,6 +233,7 @@ class RunOptimizationReturn(NamedTuple): num_locations: int vehicles_deployed: int + @dash.callback( # update map and results Output("solution-map", "srcDoc", allow_duplicate=True), @@ -265,8 +267,8 @@ class RunOptimizationReturn(NamedTuple): ], running=[ # show cancel button and hide run button, and disable and animate results tab - (Output("cancel-button", "className"), "", "display-none"), - (Output("run-button", "className"), "display-none", ""), + (Output("cancel-button", "style"), {}, {"display": "none"}), + (Output("run-button", "style"), {"display": "none"}, {}), (Output("results-tab", "disabled"), True, False), (Output("results-tab", "label"), "Loading...", "Results"), # switch to map tab while running @@ -334,9 +336,7 @@ def run_optimization( solver_type = SolverType(int(solver_type)) map_network, depot_id, client_subset, map_bounds = generate_mapping_information(num_clients) - initial_map = show_locations_on_initial_map( - map_network, depot_id, client_subset, map_bounds - ) + initial_map = show_locations_on_initial_map(map_network, depot_id, client_subset, map_bounds) routing_problem_parameters = RoutingProblemParameters( map_network=map_network, @@ -382,25 +382,23 @@ def run_optimization( wall_clock_time, solver_type, reset_results ) - hybrid_table_label = ( - dash.no_update if solver_type is SolverType.KMEANS else solver_type.label - ) + hybrid_table_label = dash.no_update if solver_type is SolverType.KMEANS else solver_type.label return RunOptimizationReturn( - solution_map = open("src/maps/solution_map.html", "r").read(), - cost_table = cost_table, - hybrid_table_label = hybrid_table_label, - solver_type = "classical" if solver_type is SolverType.KMEANS else "quantum", - reset_results = reset_results, - parameter_hash = str(parameter_hash), - performance_improvement_quantum = performance_improvement_quantum, - cost_comparison = cost_comparison, - problem_size = problem_size, - search_space = search_space, - wall_clock_time_classical = wall_clock_time_kmeans, - wall_clock_time_quantum = wall_clock_time_quantum, - num_locations = num_clients, - vehicles_deployed = num_vehicles, + solution_map=open("src/maps/solution_map.html", "r").read(), + cost_table=cost_table, + hybrid_table_label=hybrid_table_label, + solver_type="classical" if solver_type is SolverType.KMEANS else "quantum", + reset_results=reset_results, + parameter_hash=str(parameter_hash), + performance_improvement_quantum=performance_improvement_quantum, + cost_comparison=cost_comparison, + problem_size=problem_size, + search_space=search_space, + wall_clock_time_classical=wall_clock_time_kmeans, + wall_clock_time_quantum=wall_clock_time_quantum, + num_locations=num_clients, + vehicles_deployed=num_vehicles, ) diff --git a/demo_interface.py b/demo_interface.py index 7ba67a0..9ec9082 100644 --- a/demo_interface.py +++ b/demo_interface.py @@ -15,8 +15,8 @@ """This file stores the HTML layout for the app.""" from __future__ import annotations -from dash import dcc, html import dash_mantine_components as dmc +from dash import dcc, html from demo_configs import ( COST_LABEL, @@ -44,7 +44,7 @@ def slider(label: str, id: str, config: dict) -> html.Div: Args: label: The title that goes above the slider. id: A unique selector for this element. - config: A dictionary of slider configerations, see dcc.Slider Dash docs. + config: A dictionary of slider configurations, see dcc.Slider Dash docs. """ return html.Div( className="slider-wrapper", @@ -55,8 +55,8 @@ def slider(label: str, id: str, config: dict) -> html.Div: className="slider", **config, marks=[ - {"value": config["min"], "label": f"{config["min"]}"}, - {"value": config["max"], "label": f"{config["max"]}"}, + {"value": config["min"], "label": f'{config["min"]}'}, + {"value": config["max"], "label": f'{config["max"]}'}, ], labelAlwaysOn=True, thumbLabel=f"{label} slider", @@ -96,10 +96,11 @@ def generate_settings_form() -> html.Div: """ # calculate drop-down options vehicle_options = [ - {"label": vehicle_type.label, "value": f"{vehicle_type.value}"} for vehicle_type in VehicleType + {"label": vehicle_type.label, "value": f"{vehicle_type.value}"} + for vehicle_type in VehicleType ] - solver_options =[] + solver_options = [] for solver_type in SolverType: if solver_type is not SolverType.DQM or SHOW_DQM: solver_options.append({"label": solver_type.label, "value": f"{solver_type.value}"}) @@ -141,12 +142,15 @@ def generate_run_buttons() -> html.Div: return html.Div( id="button-group", children=[ - html.Button(id="run-button", children="Run Optimization", n_clicks=0, disabled=False), html.Button( + "Run Optimization", id="run-button", className="button", n_clicks=0, disabled=False + ), + html.Button( + "Cancel Optimization", id="cancel-button", - children="Cancel Optimization", + className="button", n_clicks=0, - className="display-none", + style={"display": "none"}, ), ], ) @@ -154,10 +158,7 @@ def generate_run_buttons() -> html.Div: def create_row_cells(values: list) -> list[html.Td]: """List required to execute loop, unpack after to maintain required structure.""" - return [ - html.Td(round(value, 3 if UNITS_IMPERIAL else 0)) - for value in values - ] + return [html.Td(round(value, 3 if UNITS_IMPERIAL else 0)) for value in values] def create_table(values_dicts: dict[int, dict], values_totals: list) -> html.Table: @@ -228,6 +229,7 @@ def problem_details(index: int) -> html.Div: html.H5("Problem Details"), html.Div(className="collapse-arrow"), ], + **{"aria-expanded": "true"}, ), html.Div( className="details-to-collapse", @@ -242,15 +244,11 @@ def problem_details(index: int) -> html.Div: [ html.Th( colSpan=2, - children=[ - "Problem Specifics" - ], + children=["Problem Specifics"], ), html.Th( colSpan=2, - children=[ - "Wall Clock Time" - ], + children=["Wall Clock Time"], ), ] ) @@ -261,54 +259,30 @@ def problem_details(index: int) -> html.Div: children=[ html.Tr( [ - html.Td( - LOCATIONS_LABEL - ), - html.Td( - id="num-locations" - ), - html.Td( - "Quantum Hybrid" - ), - html.Td( - id="wall-clock-time-quantum" - ), + html.Td(LOCATIONS_LABEL), + html.Td(id="num-locations"), + html.Td("Quantum Hybrid"), + html.Td(id="wall-clock-time-quantum"), ] ), html.Tr( [ - html.Td( - "Vehicles Deployed" - ), - html.Td( - id="vehicles-deployed" - ), - html.Td( - "Classical" - ), - html.Td( - id="wall-clock-time-classical" - ), + html.Td("Vehicles Deployed"), + html.Td(id="vehicles-deployed"), + html.Td("Classical"), + html.Td(id="wall-clock-time-classical"), ] ), html.Tr( [ - html.Td( - "Problem Size" - ), - html.Td( - id="problem-size" - ), + html.Td("Problem Size"), + html.Td(id="problem-size"), ] ), html.Tr( [ - html.Td( - "Search Space" - ), - html.Td( - id="search-space" - ), + html.Td("Search Space"), + html.Td(id="search-space"), ] ), ], @@ -331,6 +305,7 @@ def create_interface(): href="#main-content", id="skip-to-main", className="skip-link", + tabIndex=1, ), # below are any temporary storage items, e.g., for sharing data between callbacks dcc.Store(id="stored-results"), # temporarily stored results table @@ -343,8 +318,6 @@ def create_interface(): ), # callback blocker to signal that the run is complete dcc.Store(id="parameter-hash"), # hash string to detect changed parameters dcc.Store(id="cost-comparison"), # dictionary with solver keys and run values - # Banner - html.Header(className="banner", children=[html.Img(src=THUMBNAIL, alt="D-Wave logo")]), html.Main( className="columns-main", id="main-content", @@ -365,39 +338,77 @@ def create_interface(): html.H1(MAIN_HEADER), html.P(DESCRIPTION), ], - className="title-card" + className="title-section", + ), + html.Div( + [ + html.Div( + html.Div( + [ + generate_settings_form(), + generate_run_buttons(), + ], + className="settings-and-buttons", + ), + className="settings-and-buttons-wrapper", + ), + # Left column collapse button + html.Div( + html.Button( + id={ + "type": "collapse-trigger", + "index": 0, + }, + className="left-column-collapse", + title="Collapse sidebar", + children=[ + html.Div(className="collapse-arrow") + ], + **{"aria-expanded": "true"}, + ), + ), + ], + className="form-section", ), - generate_settings_form(), - generate_run_buttons(), ], ) ], ), - # Left column collapse button - html.Div( - html.Button( - id={"type": "collapse-trigger", "index": 0}, - className="left-column-collapse", - title="Collapse sidebar", - children=[html.Div(className="collapse-arrow")], - ), - ), ], ), # Right column html.Div( className="right-column", children=[ - dcc.Tabs( + dmc.Tabs( id="tabs", value="map-tab", - mobile_breakpoint=0, + color="white", children=[ - dcc.Tab( - label="Map", - id="map-tab", - value="map-tab", # used for switching to programatically - className="tab", + html.Header( + className="banner", + children=[ + html.Nav( + [ + dmc.TabsList( + [ + dmc.TabsTab("Map", value="map-tab"), + dmc.TabsTab( + "Results", + value="results-tab", + id="results-tab", + disabled=True, + ), + ] + ), + ] + ), + html.Img(src=THUMBNAIL, alt="D-Wave logo"), + ], + ), + dmc.TabsPanel( + value="map-tab", + tabIndex="12", children=[ dcc.Loading( id="loading", @@ -405,15 +416,15 @@ def create_interface(): color=THEME_COLOR, parent_className="map-wrapper", overlay_style={"visibility": "visible"}, - children=html.Iframe(id="solution-map", title="Map of locations"), + children=html.Iframe( + id="solution-map", title="Map of locations" + ), ), ], ), - dcc.Tab( - label="Results", - id="results-tab", - className="tab", - disabled=True, + dmc.TabsPanel( + value="results-tab", + tabIndex="13", children=[ html.Div( className="tab-content-wrapper", diff --git a/map.py b/map.py index 6a0658e..8f9f58d 100644 --- a/map.py +++ b/map.py @@ -238,10 +238,7 @@ def plot_solution_routes_on_map( locations[node], tooltip=folium.map.Tooltip( text="
".join( - [ - f"{resource}: {nodes[i]}" - for i, resource in enumerate(RESOURCES) - ] + [f"{resource}: {nodes[i]}" for i, resource in enumerate(RESOURCES)] ) + f"
Vehicle ID: {vehicle_id}
Stop: #{stop_number} of {len(route_network.nodes)-1}", style="font-size: 1.4rem;", diff --git a/src/ckmeans.py b/src/ckmeans.py index fd0f5db..cc5f11f 100644 --- a/src/ckmeans.py +++ b/src/ckmeans.py @@ -160,7 +160,7 @@ def predict( X: 2-d numpy.array (each row is a sample, each column is feature/coordinate. demand: The demand of each sample. capacities: Capacity of each cluster (must be same length as the number of clusters). - time_limit: Maximum time in seconds the stochastic K-Means lgorithm + time_limit: Maximum time in seconds the stochastic K-Means algorithm can be repeated before returning solution. Returns: diff --git a/src/cvrp.py b/src/cvrp.py index 1cd3a9d..29ce6ae 100644 --- a/src/cvrp.py +++ b/src/cvrp.py @@ -189,10 +189,7 @@ def cluster_dqm( time_limit = None res = sampler.sample_dqm( - self._optimization["dqm"], - time_limit=time_limit, - label="Example - MVRP", - **kwargs + self._optimization["dqm"], time_limit=time_limit, label="Example - MVRP", **kwargs ) res.resolve() @@ -242,7 +239,7 @@ def cluster_kmeans(self, time_limit=None) -> None: self._optimization["capacity_violation"] = assignments def solve_tsp_heuristic(self) -> None: - """Solve the travelling salesman problem for each cluster.""" + """Solve the traveling salesman problem for each cluster.""" clusters = {vehicle_id: list(self.depots) for vehicle_id, _ in enumerate(self._vehicles)} # invert self.assignments dictionary to dict[vehicle_id, location_id] @@ -340,7 +337,7 @@ def generate_nl_model(self) -> Model: Model: The NL Model. """ - # Take maxium vehicle capacity. Vehicle capacity should be updated to only allow + # Take maximum vehicle capacity. Vehicle capacity should be updated to only allow # one value for all vehicles or update NL solution to allow multiple capacities. max_capacity = max(self._vehicle_capacity.values()) num_vehicles = len(self._vehicles) @@ -379,16 +376,14 @@ def _recompute_objective(self, solution): for index, location in enumerate([0, *r[:-1]]): total_cost += self._costs[all_locations[location], all_locations[r[index]]] - total_cost += self._costs[ - all_locations[r[-1]], all_locations[0] # Go back to depot - ] + total_cost += self._costs[all_locations[r[-1]], all_locations[0]] # Go back to depot return total_cost def _check_feasibility(self, solution): """Check whether the given solution is feasible""" - # Take maxium vehicle capacity. Vehicle capacity should be updated to only allow + # Take maximum vehicle capacity. Vehicle capacity should be updated to only allow # one value for all vehicles or update NL solution to allow multiple capacities. max_capacity = max(self._vehicle_capacity.values()) num_vehicles = len(self._vehicle_capacity) @@ -415,12 +410,17 @@ def _get_solution(self, tolerance=1e-6): for i in range(num_states): # extract the solution decision = next(model.iter_decisions()) - solution_candidate = [[int(v) + 1 for v in route.state(i)] for route in decision.iter_successors()] + solution_candidate = [ + [int(v) + 1 for v in route.state(i)] for route in decision.iter_successors() + ] if not solution_candidate: continue solver_objective = model.objective.state(i) - assert abs(solver_objective - self._recompute_objective(solution=solution_candidate)) < tolerance + assert ( + abs(solver_objective - self._recompute_objective(solution=solution_candidate)) + < tolerance + ) solver_feasibility = True for c in model.iter_constraints(): @@ -436,8 +436,7 @@ def _get_solution(self, tolerance=1e-6): raise ValueError("No feasible solution found.") def parse_solution_nl(self) -> None: - """Checks the solutions from the NL solver (attached to the model) and outputs the parsed ones. - """ + """Checks the solutions from the NL solver (attached to the model) and outputs the parsed ones.""" all_locations = [*self._depots, *self._clients] diff --git a/static/demo.png b/static/demo.png index 877f2a6..4289af1 100644 Binary files a/static/demo.png and b/static/demo.png differ