Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
app_image = html.Div(html.Img(src=logo, style={'width': '5%', 'height': '5%'}), style={'textAlign': 'center'})
app_title = html.Div([html.H1(children='obsidian'),
html.H5(children='Algorithmic Process Optimization and Experiment Design',
style={'font-style': 'italic', 'color': '#AAAAAA'})],
className="fst-italic",
style={'color': '#AAAAAA'})],
style={'textAlign': 'center'})
app_infobar = html.Div(id='root-infobar')
app_tabs = dbc.Tabs(children=[], id="root-tabs")
Expand Down
58 changes: 58 additions & 0 deletions assets/css/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
:root {
--obsd-label-color: var(--bs-primary);
--obsd-text-color: var(--bs-body-color);
--obsd-form-font-size: 0.7em;
--obsd-form-lg-font-size: 0.8em;
--obsd-form-xl-font-size: 1em;
--obsd-info-color: var(--bs-info, #0dcaf0);
}

.obsd-dashed-box {
height: 60px;
line-height: 60px;
border: 1px dashed;
border-radius: 5px;
box-sizing: border-box;
}

/* Base Class */
.obsd-form-text {
font-size: var(--obsd-form-font-size);
color: var(--obsd-text-color);
}

/* Utility Classes */
.obsd-text-lg {
font-size: var(--obsd-form-lg-font-size);
}

.obsd-text-xl {
font-size: var(--obsd-form-xl-font-size);
}

.obsd-text-normal-style {
font-style: var(--obsd-help-font-style);
}

.obsd-input-row {
margin-bottom: 1rem;
}

.obsd-form-label {
font-weight: bold;
color: var(--obsd-label-color);
font-size: var(--obsd-form-lg-font-size);
}

.text-transform-none {
text-transform: none !important;
}

.d-inline-block {
display: inline-block;
}

.overflow-x-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
11 changes: 4 additions & 7 deletions obsidian/dash/infobar.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,9 @@ def setup_infobar(app, app_infobar):
app_infobar.children = dbc.Container([
html.Br(),
dbc.Row([
dbc.Col(dbc.Button('Help', outline='True', color='warning', className='me-1', id='button-help'),
style={'textAlign': 'left'}, width={'size': 2}),
dbc.Col(html.Div(dbc.Badge(f'v{obsidian.__version__}', color='primary', className='me-1'), style={'textAlign': 'center'}),
width={'size': 2}),
dbc.Col(dbc.Button('Contact', outline='True', color='secondary', className='me-1', id='button-contact'),
style={'textAlign': 'right'}, width={'size': 2}),
dbc.Col(dbc.Button('Help', outline='True', color='warning', className='me-1', id='button-help'), className="text-start", width={'size': 2}),
dbc.Col(html.Div(dbc.Badge(f'v{obsidian.__version__}', color='primary', className='me-1 text-center')), className="text-center", width={'size': 2}),
dbc.Col(dbc.Button('Contact', outline='True', color='secondary', className='me-1', id='button-contact'), className="text-end", width={'size': 2}),
], justify='center'),
# Pop-up for Contact Us
dbc.Modal([
Expand All @@ -31,7 +28,7 @@ def setup_infobar(app, app_infobar):
color='primary', className='me-1'),
dbc.Button('Visit Our Site', href='https://msdllcpapers.github.io/obsidian/', target='_blank', external_link=True,
color='primary', className='me-1')
], style={'textAlign': 'center'}),
], className="text-center"),
dbc.ModalFooter([])
], id='modal-contact', is_open=False, size='xl', centered=True),
# Pop-up for Help
Expand Down
11 changes: 6 additions & 5 deletions obsidian/dash/inputs_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def make_acquisition(index, delete=True):
className='me-2', color='danger', n_clicks=0))

columns.append(dbc.Col([dbc.InputGroup(hyper_input),
dbc.FormText('Input a hyperparameter (optional)', style={'font-size': '0.7em'})]))
dbc.FormText('Input a hyperparameter (optional)',
className="obsd-form-text")]))

return dbc.Row(id={'type': 'div-acquisition', 'index': index}, children=columns, align='center')

Expand Down Expand Up @@ -62,19 +63,19 @@ def setup_config(app, app_tabs):
acquisitions = dbc.Container(dbc.Card(id='aq_inputs', children=[
dbc.CardHeader(['Acquisition Functions', html.Div(dbc.FormText('Objective function of the experiment\
search/selection',
style={'font-size': '0.7em'}))]),
className="obsd-form-text"))]),
dbc.CardBody(html.Div(id='div-acquisition_all', children=[make_acquisition(index=0, delete=False)])),
dbc.CardFooter(dbc.Button('Add', id='button-acquisition_add', className='me-2', color='secondary', n_clicks=0))
], style={'margin-top': '15px'}))
], className="mt-3"))

general_options = dbc.Container(dbc.Card(dbc.CardBody(
children=[input_surrogate, input_m_batch, input_optim_sequential])),
style={'margin-top': '15px'})
className="mt-3")

advanced_options = dbc.Container(make_collapse('adv_options',
[input_optimizer_seed, input_f_transform, input_optim_restarts],
'Advanced Options'),
style={'margin-top': '15px'})
className="mt-3")

# Config store
config = dcc.Store(id='store-config')
Expand Down
116 changes: 84 additions & 32 deletions obsidian/dash/inputs_data.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .utils import add_tab, make_input, make_dropdown, make_switch, make_slider, make_knob, make_table, make_collapse
from .utils import center
from .utils import center, error_message_handling, is_input_empty
import dash_bootstrap_components as dbc
from dash import dcc, html, Dash, dash_table, callback, Output, Input, State, ALL, MATCH
from dash import dcc, html, Dash, dash_table, callback, Output, Input, State, ALL, MATCH, no_update
import pandas as pd
import base64
import io
Expand All @@ -16,12 +16,7 @@ def setup_data(app, app_tabs, default_data, default_Xspace):
uploader = dcc.Upload(id='uploader-X0',
children=html.Div(['Upload Data: Drag and Drop or ',
html.A('Select Files')]),
style={
'width': '100%', 'height': '60px',
'lineHeight': '60px', 'borderWidth': '1px',
'borderStyle': 'dashed', 'borderRadius': '5px',
'textAlign': 'center', 'margin': '10px'
},
className="w-100 m-2 text-center obsd-dashed-box",
multiple=False,
filename='Example Data'
)
Expand All @@ -30,7 +25,7 @@ def setup_data(app, app_tabs, default_data, default_Xspace):
template_downloader = html.Div(children=[dbc.Button('Download Template Data', id='button-download_data',
className='me-2', color='primary'),
dcc.Download(id='downloader-X0_template')],
style={'textAlign': 'center'})
className="text-center")

# Data preview
preview = html.Div(id='table-X0', children=dbc.Card(
Expand All @@ -41,16 +36,18 @@ def setup_data(app, app_tabs, default_data, default_Xspace):
dbc.Tooltip('Input data must be a CSV file and must include column headers for the input\
parameters and response variable(s), with one row per observation. Download\
template data (left) for example.',
target='info-data', placement='top', style={'text-transform': 'none'}),
dbc.FormText('Example Data', color='info', style={'font-size': '1em', 'font-style': 'italic'},
target='info-data', placement='top', className="text-transform-none"),
dbc.FormText('Example Data',
color="info",
className="obsd-form-text obsd-text-xl fst-italic",
id='table-X0-footer')
],
style={'textAlign': 'center'}))
className="text-center"))
]))

preview_uploader = dbc.Row([dbc.Col([uploader, template_downloader], width=4),
dbc.Col(preview, width=8)],
style={'margin-top': '15px'})
className="mt-3")

# Data store
storage_X0 = dcc.Store(id='store-X0', data=default_data.to_dict())
Expand All @@ -68,7 +65,7 @@ def setup_data(app, app_tabs, default_data, default_Xspace):
it is the target of the objective function before\
including optional transformations.',
target='info-response_col', placement='top',
style={'text-transform': 'none'}),
className="text-transform-none"),
'Response Selection']),
dbc.CardBody(html.Div(id='div-response_name',
children=[make_dropdown('Data Column',
Expand All @@ -78,18 +75,43 @@ def setup_data(app, app_tabs, default_data, default_Xspace):
kwargs={'value': default_data.columns[-1]})]))
]), width=4), justify='center')

input_container = dbc.Row(
dbc.Col(
dbc.Card(
[
dbc.CardHeader(
[
html.I(
id="search-space-block",
className="bi bi-info-circle-fill me-2",
),
dbc.Tooltip(
"The search space defines the range and types of values for each parameter that the optimizer will explore. These parameter types and ranges are initially inferred from the uploaded data. Please verify they are appropriate for your use case. While you can update the parameter space during optimization, do so only if you understand the implications.",
target="search-space-block",
placement="top",
className="text-transform-none",
),
"Search Space Configuration",
]
),
dbc.CardBody(xspace),
]
),
),
justify="center",
className="m-3",
)
# Extra div for printing outputs, for troubleshooting
troubleshoot = html.Div(id='debug-print-data')

# Add all of these elements to the app
elements = [html.Br(), preview_uploader, html.Hr(), ycol, xspace, storage_X0,
elements = [html.Br(), preview_uploader, html.Hr(), ycol, input_container, storage_X0,
storage_X0_template, storage_Xspace, html.Hr(), troubleshoot]
add_tab(app_tabs, elements, 'tab-data', 'Data')
setup_data_callbacks(app)

return


def setup_data_callbacks(app):

# Save the uploaded data into the data-store
Expand Down Expand Up @@ -180,7 +202,7 @@ def update_xspace_types(data, ycol, Xspace_save):
],
color='primary', outline=True), width=2))

return dbc.Container(dbc.Row(cols, style={'margin-top': '15px'},
return dbc.Container(dbc.Row(cols, className="mt-3",
justify='center'),
fluid=True)

Expand Down Expand Up @@ -218,7 +240,7 @@ def update_xspace_vals(param_type, param_id, data, ycol):
className='me-2', color='danger')]),
html.Hr(),
html.Div(id={'type': 'div-param_categories', 'index': x}, children=None,
style={'textAlign': 'center'})])),
className="text-center")])),
dcc.Store(id={'type': 'store-param_categories', 'index': x},
data=', '.join(list(ser_x.sort_values().astype('str').unique()))),
html.Div(id={'type': 'input-param_min', 'index': x}),
Expand All @@ -233,25 +255,55 @@ def update_xspace_vals(param_type, param_id, data, ycol):
Input({'type': 'input-param_min', 'index': MATCH}, 'value'),
Input({'type': 'input-param_max', 'index': MATCH}, 'value'),
Input({'type': 'store-param_categories', 'index': MATCH}, 'data'),
State('store-config', 'data'),
prevent_initial_call=True # It takes a second for these input matches to show up
)
def update_param_save(param_id, param_type, param_min, param_max, param_cat):

name = param_id['index']

def update_param_save(
param_id, param_type, param_min, param_max, param_cat, config
):
"""
Build and serialize a single-parameter ParamSpace for this control.
On exception, do not update the store (return no_update) to avoid triggering downstream aggregation.
"""

name = param_id["index"]
# Placeholder for verbosity setting from config
# It should be configurable in the future
verbosity = (config or {}).get('verbose', 1)
try:
if param_type == 'Numeric':
# Explicit conversion/validation
if param_type == "Numeric":
# Allow empty/incomplete during edit: keep previous store value
if not (param_min and param_max):
return no_update
param_min = float(param_min)
param_max = float(param_max)
param = Param_Continuous(name, param_min, param_max)
elif param_type == 'Categorical':
param = Param_Categorical(name, param_cat)
elif param_type == 'Ordinal':
param = Param_Ordinal(name, param_cat)


elif param_type in ("Categorical", "Ordinal"):
# Expect a comma-separated string or list; accept both
if is_input_empty(param_cat):
return no_update
if param_type == "Categorical":
param = Param_Categorical(name, param_cat)
else:
param = Param_Ordinal(name, param_cat)

else:
error_message_handling(name, f'unknown param_type "{param_type}"', verbosity)
return no_update

single_param = ParamSpace([param])
return single_param.save_state()

except TypeError:
return None

except Exception as e:
error_message_handling(
name,
f"please double check the inputs. Error: {e}",
verbosity,
tb="traceback",
)
return no_update

# Category management for categorical variables
@app.callback(
Expand Down Expand Up @@ -298,7 +350,7 @@ def preview_cats(current_cats):

preview = []
for cat in cat_list:
preview += [html.Div(cat, style={'font-size': '0.8em'})]
preview += [html.Div(cat, className="obsd-form-text obsd-text-lg")]
return preview

@app.callback(
Expand Down
15 changes: 7 additions & 8 deletions obsidian/dash/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def setup_optimize(app, app_tabs):
]),
html.Div(id='graph-parity', children=[])
],
style={'textAlign': 'center'}
className="text-center"
)

predict_div = dbc.Container([
Expand All @@ -41,11 +41,11 @@ def setup_optimize(app, app_tabs):
dbc.Card([
dbc.CardHeader('Optimal Experiments'),
dbc.CardBody([
html.Div(id='div-predict', children=[], style={})
html.Div(id="div-predict", children=[], className="overflow-x-scroll")
]),
]),
],
style={'textAlign': 'center'}
className="text-center"
)

storage_fit = dcc.Store(id='store-fit', data=None)
Expand All @@ -57,8 +57,7 @@ def setup_optimize(app, app_tabs):
candidates_downloader = html.Div(children=[
dbc.Button('Download Suggested Candidates', id='button-download_candidates',
className='me-2', color='primary'),
dcc.Download(id='downloader-candidates')],
style={'textAlign': 'center', 'margin-top': '15px'})
dcc.Download(id='downloader-candidates')], className="text-center mt-3")

# Add all of these elements to the app
columns = dbc.Row([dbc.Col(fit_div, width=6), dbc.Col([predict_div, candidates_downloader], width=6)])
Expand Down Expand Up @@ -142,7 +141,7 @@ def graph_parity_plot(opt_save, config):

@app.callback(
Output('div-predict', 'children'),
Output('div-predict', 'style'),
Output('div-predict', 'className'),
Output('button-predict', 'n_clicks'),
Output('store-candidates', 'data'),
Input('button-predict', 'n_clicks'),
Expand All @@ -157,15 +156,15 @@ def predict_optimizer(predict_clicked, config, opt_save):
alert_color = 'danger'
else:
alert_color = 'info'
return dbc.Alert('Model must be fit first', color=alert_color), {}, predict_clicked, {}
return dbc.Alert('Model must be fit first', color=alert_color), "", predict_clicked, {}

optimizer = load_optimizer(config, opt_save)
X_suggest, eval_suggest = optimizer.suggest(**config['aq_params'])
df_suggest = pd.concat([X_suggest, eval_suggest], axis=1)
df_suggest.insert(loc=0, column='CandidatesID', value=df_suggest.index)
tables = [center(make_table(df_suggest))]

return tables, {'overflow-x': 'scroll'}, 0, df_suggest.to_dict()
return tables, "overflow-x-scroll", 0, df_suggest.to_dict()

# Download Suggested Candidates
@app.callback(
Expand Down
Loading
Loading