TideGatesApp

Bases: BootstrapTemplate

The web application is based on the Bootstrap template provided by Panel. It displays a map (an instance of the TGMap class) in the sidebar. The main content area has a Tabs widget with five tabs: a welcome message, a help page, the main page (described below) and two tabs for displaying outputs.

The application also displays several small help buttons next to the main widgets. Clicking one of these buttons brings up a floating window with information about the widget.

The main tab (labeled "Start") displays the widgets that allow the user to specify optimization parameters: region names, budget levels, and restoration targets. It also has a Run button. When the user clicks this button the callback function makes sure the necessary parameters have been defined and then uses the template's modal dialog area. Clicking the "OK" button in that dialog invokes another callback, defined here, that runs the optimizer.

Parameters:
  • params

    runtime options passed to the parent class constructor

Source code in src/tidegates/widgets.py
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
def __init__(self, **params):
    """
    Initialize the application.

    Arguments:
      params:  runtime options passed to the parent class constructor
    """
    super(TideGatesApp, self).__init__(**params)

    self.bf = Project('static/workbook.csv', DataSet.TNC_OR)

    self.map = TGMap(self.bf)
    self.map_pane = pn.panel(self.map.graphic())

    self.budget_box = BudgetBox()
    self.region_boxes = RegionBox(self.bf, self.map, self.budget_box)
    self.target_boxes = TargetBox()
    self.climate_group = pn.widgets.RadioBoxGroup(name='Climate', options=self.bf.climates)

    self.optimize_button = pn.widgets.Button(name='Run Optimizer', stylesheets=[button_style_sheet])

    self.info = InfoBox(self, self.run_optimizer)

    self.map_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.map_help_button.on_click(self.map_help_cb)

    self.region_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.region_help_button.on_click(self.region_help_cb)

    self.budget_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.budget_help_button.on_click(self.budget_help_cb)

    self.target_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.target_help_button.on_click(self.target_help_cb)

    self.climate_help_button = pn.widgets.Button(name='ℹ️', stylesheets = [help_button_style_sheet])
    self.climate_help_button.on_click(self.climate_help_cb)

    welcome_tab = pn.Column(
        self.section_head('Welcome'),
        pn.pane.HTML(open('static/welcome.html').read()),
    )

    help_tab = pn.Column(
        self.section_head('Instructions'),
        pn.pane.HTML(open('static/help1.html').read()),
        pn.pane.PNG('static/ROI.png', width=400),
        pn.pane.HTML(open('static/help2.html').read()),
    )

    start_tab = pn.Column(
        self.section_head('Geographic Regions', self.region_help_button),
        pn.WidgetBox(self.region_boxes, width=600),

        self.section_head('Budget', self.budget_help_button),
        self.budget_box,

        self.section_head('Targets', self.target_help_button),
        pn.WidgetBox(
            pn.Row(
                self.target_boxes,
                pn.Column(
                    self.section_head('Climate', self.climate_help_button),
                    self.climate_group, 
                    margin=(0,0,0,20),
                ),
            ),
            width=600,
        ),

        self.optimize_button,
    )

    output_tab = pn.Column(
        self.section_head('Nothing to See Yet'),
        pn.pane.HTML('<p>After running the optimizer this tab will show the results.</p>')
    )

    download_tab = pn.Column(
        self.section_head('Nothing to Download Yet'),
        pn.pane.HTML('<p>After running the optimizer use this tab to save the results.</p>')        )

    self.tabs = pn.Tabs(
        ('Home', welcome_tab),
        ('Help', help_tab),
        ('Start', start_tab),
        ('Output', output_tab),
        ('Download', download_tab),
        sizing_mode = 'fixed',
        width=800,
        # height=700,
    )

    self.sidebar.append(pn.Row(self.map_pane, self.map_help_button))
    self.main.append(self.tabs)

    self.info = InfoBox(self, self.run_optimizer)
    self.modal.append(self.info)

    self.optimize_button.on_click(self.validate_settings)

section_head(s, b=None)

Create an HTML header for one of the sections in the Start tab.

src/tidegates/widgets.py
938
939
940
941
942
943
def section_head(self, s, b = None):
    """
    Create an HTML header for one of the sections in the Start tab.
    """
    header = pn.pane.HTML(f'<h3>{s}</h3>', styles=header_styles)
    return header if b is None else pn.Row(header, b)

validate_settings(_)

Callback function invoked when the user clicks the Run Optimizer button.

src/tidegates/widgets.py
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
def validate_settings(self, _):
    """
    Callback function invoked when the user clicks the Run Optimizer button.
    """
    regions = self.region_boxes.selection()
    budget_max, budget_delta = self.budget_box.values()
    targets = self.target_boxes.selection()

    if len(regions) == 0 or budget_max == 0 or len(targets) == 0:
        self.info.show_missing(regions, budget_max, targets)
        return

    if weights := self.target_boxes.weights():
        if not all([w.isdigit() and (1 <= int(w) <= 5) for w in weights]):
            self.info.show_invalid_weights(weights)
            return

    self.info.show_params(regions, budget_max, budget_delta, targets, weights, self.climate_group.value)

run_optimizer(_)

Callback function invoked when the user clicks the Continue button after verifying the parameter options.

Use the settings in the budget widgets to figure out the sequence of budget levels to use. Instantiate an OP object with the budget settings and values from the other parameter widgets, then use that widget to run OptiPass.

src/tidegates/widgets.py
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
def run_optimizer(self, _):
    """
    Callback function invoked when the user clicks the Continue button after verifying
    the parameter options.

    Use the settings in the budget widgets to figure out the sequence of budget levels
    to use.  Instantiate an OP object with the budget settings and values from the
    other parameter widgets, then use that widget to run OptiPass.
    """
    Logging.log('running optimizer')

    self.close_modal()
    self.main[0].loading = True

    budget_max, budget_delta = self.budget_box.values()
    num_budgets = budget_max // budget_delta

    self.op = OP(
        self.bf, 
        list(self.region_boxes.selection()),
        [self.bf.target_map[t] for t in self.target_boxes.selection()],
        self.target_boxes.weights(),
        self.climate_group.value,
    )
    self.op.generate_input_frame()
    self.op.run(self.budget_box.values(), False)

    self.main[0].loading = False

    # If OP ran successfully we expect to find one file for each budget level 
    # plus one more for the $0 budget

    try:
        Logging.log('runs complete')
        if self.op.outputs is None or len(self.op.outputs) != num_budgets+1:
            raise(RuntimeError('Missing output files'))
        self.op.collect_results(False)
        Logging.log('Output files:' + ','.join(self.op.outputs))
        self.info.show_success()
        self.add_output_pane()
    except RuntimeError as err:
        print(err)
        self.info.show_fail(err)

add_output_pane(op=None)

After running OptiPass call this method to add tabs to the main panel to show the results.

Parameters:
  • op

    an optional Project object used by integration tests (if no argument is passed use the Project option defined for the application)

src/tidegates/widgets.py
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
def add_output_pane(self, op=None):
    """
    After running OptiPass call this method to add tabs to the main
    panel to show the results.

    Arguments:
      op:  an optional Project object used by integration tests (if no argument
           is passed use the Project option defined for the application)
    """
    op = op or self.op

    output = OutputPane(op, self.bf)
    output.make_dots(self.map.graphic())
    self.region_boxes.add_external_callback(output.hide_dots)

    self.tabs[3] = ('Output', output)

    self.tabs[4] = ('Download', DownloadPane(output))

map_help_cb(_)

Callback function for the help button next to the map in the sidebar.

src/tidegates/widgets.py
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
def map_help_cb(self, _):
    """
    Callback function for the help button next to the map in the sidebar.
    """
    msg = pn.pane.HTML('''
    <p>When you move your mouse over the map the cursor will change to a "crosshairs" symbol and a set of buttons will appear below the map.
    Navigating with the map is similar to using Google maps or other online maps:</p>
    <ul>
        <li>Left-click and drag to pan (move left and right or up and down).</li>
        <li>If you want to zoom in and out, first click the magnifying glass button below the map; then you can zoom in and out using the scroll wheel on your mouse.</li>   
        <li>Click the refresh button to restore the map to its original size and location.</li>
    </ul>
    ''')
    self.tabs[0].append(pn.layout.FloatPanel(msg, name='Map Controls', contained=False, position='center', width=400))

region_help_cb(_)

Callback function for the help button next to the region box widget in the start tab.

src/tidegates/widgets.py
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
def region_help_cb(self, _):
    """
    Callback function for the help button next to the region box widget in the start tab.
    """
    msg = pn.pane.HTML('''
    <p>Select a region by clicking in the box to the left of an estuary name.</p>
    <p>Each time you click in a box the map will be updated to show the positions of the barriers that are in our database for the estuary.</p>
    <p>You must select at least one region before you run the optimizer.</p>
    ''')
    self.tabs[2].append(pn.layout.FloatPanel(msg, name='Geographic Regions', contained=False, position='center', width=400))

budget_help_cb(_)

Callback function for the help button next to the budget box widget in the start tab.

src/tidegates/widgets.py
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
def budget_help_cb(self, _):
    """
    Callback function for the help button next to the budget box widget in the start tab.
    """
    msg = pn.pane.HTML('''
    <p>There are three ways to specify the budgets used by the optimizer.</p>
    <H4>Basic</H4>
    <p>The simplest method is to specify an upper limit by moving the slider back and forth.  When you use this method, the optimizer will run 10 times, ending at the value you select with the slider.  For example, if you set the slider at $10M (the abbreviation for $10 million), the optimizer will make ROI curves based on budgets of $1M, $2M, <i>etc</i>, up to the maximum of $10M.</p>
    <p>Note that the slider is disabled until you select one or more regions.  That's because the maximum value depends on the costs of the gates in each region.
    For example, the total cost of all gates in the Coquille region is $11.8M.  Once you choose that region, you can move the budget slider
    left and right to pick a maximum budget for the optimizer to consider.
    <H4>Advanced</H4>
    <p>If you click on the Advanced tab in this section you will see ways to specify the budget interval and the number of budgets.</p>
    <p>You can use this method if you want more control over the layout of the ROI curves, for example you can include more points by increasing the number of budgets.</p>
    <H4>Fixed</H4>
    <p>If you know exactly how much money you have to spend you can enter that amount by clicking on the Fixed tab and entering the budget amount.</p>
    <p>The optimizer will run just once, using that budget.  The output will have tables showing the gates identified by the optimizer, but there will be no ROI curve.</p>
    <p>When entering values, you can write the full amount, with or without commas (<i>e.g.</i>11,500,000 or 11500000) or use the abbreviated form (11.5M).</p>
    ''')
    self.tabs[2].append(pn.layout.FloatPanel(msg, name='Budget Levels', contained=False, position='center', width=400))

target_help_cb(_)

Callback function for the help button next to the target box widget in the start tab.

src/tidegates/widgets.py
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
def target_help_cb(self, _):
    """
    Callback function for the help button next to the target box widget in the start tab.
    """
    msg = pn.pane.HTML('''
    <p>Click boxes next to one or more target names to have the optimizer include those targets in its calculations.</p>
    <p>The optimizer will create an ROI curve for each target selected. </p>
    <p>If more than one target is selected the optimizer will also generate an overall "net benefit" curve based on considering all targets at the same time.</p>
    ''')
    self.tabs[2].append(pn.layout.FloatPanel(msg, name='Targets', contained=False, position='center', width=400))

climate_help_cb(_)

Callback function for the help button next to the climate scenario checkbox in the start tab.

src/tidegates/widgets.py
1086
1087
1088
1089
1090
1091
1092
1093
1094
def climate_help_cb(self, _):
    """
    Callback function for the help button next to the climate scenario checkbox in the start tab.
    """
    msg = pn.pane.HTML('''
    <p>By default the optimizer uses current water levels when computing potential benefits.  Click the button next to <b>Future</b> to have it use water levels expected due to climate change.</p>
    <p>The future scenario uses two projected water levels, both for the period to 2100. For fish habitat targets, the future water level is based on projected sea level rise of 5.0 feet.  For agriculture and infrastructure targets, the future water level is projected to be 7.2 feet, which includes sea level rise and the probabilities of extreme water levels causing flooding events.</p>
    ''')
    self.tabs[2].append(pn.layout.FloatPanel(msg, name='Targets', contained=False, position='center', width=400))