Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed app based on new APIs for release #33

Merged
merged 7 commits into from
Aug 19, 2024
Merged
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
10 changes: 5 additions & 5 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dash import html, Dash
import dash_bootstrap_components as dbc
from obsidian.dash import setup_data, setup_config, setup_optimize, setup_plots, setup_predict, setup_help
from obsidian.dash import setup_data, setup_config, setup_optimize, setup_plots, setup_predict, setup_infobar
import pandas as pd
from PIL import Image

Expand All @@ -15,7 +15,7 @@
dbc_css = "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates/dbc.min.css" # For data tables
app.config.external_stylesheets = [dbc.themes.SANDSTONE, dbc_css, dbc.icons.BOOTSTRAP]

logo = Image.open('docs/figures/obsidian_logo.png')
logo = Image.open('docs/_static/obsidian_logo.png')

app_image = html.Div(html.Img(src=logo, style={'width': '5%', 'height': '5%'}), style={'textAlign': 'center'})
app_title = html.Div([html.H1(children='obsidian'),
Expand All @@ -35,7 +35,7 @@
Param_Continuous('Concentration', 10, 150),
Param_Continuous('Enzyme', 0.01, 0.30),
Param_Categorical('Variant', ['MRK001', 'MRK002', 'MRK003']),
Param_Ordinal('Stir Rate', ['Low', 'Medium', 'High']),
Param_Ordinal('Stir Rate', ['0 - Low', '1 - Medium', '2 - High']),
]
X_space = ParamSpace(params)
designer = ExpDesigner(X_space, seed=0)
Expand All @@ -45,8 +45,8 @@
default_data = pd.concat([X0, y0], axis=1)

# Set up each tab
setup_help(app, app_infobar)
setup_data(app, app_tabs, default_data)
setup_infobar(app, app_infobar)
setup_data(app, app_tabs, default_data, X_space)
setup_config(app, app_tabs)
setup_optimize(app, app_tabs)
setup_plots(app, app_tabs)
Expand Down
2 changes: 1 addition & 1 deletion obsidian/dash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
from .optimize import *
from .plots import *
from .predict import *
from .help import *
from .infobar import *
17 changes: 11 additions & 6 deletions obsidian/dash/help.py → obsidian/dash/infobar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,43 @@

import pandas as pd

import obsidian
from obsidian.experiment import ParamSpace
from obsidian.optimizer import BayesianOptimizer
from obsidian.plotting.plotly import parity_plot
from .utils import load_optimizer, center


def setup_help(app, app_infobar):

def setup_infobar(app, app_infobar):
# Infobar = 3 columns [Help, version, Contact]
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('v0.1.1', color='primary', className='me-1'), style={'textAlign': 'center'}),
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}),
], justify='center'),
# Pop-up for Contact Us
dbc.Modal([
dbc.ModalHeader(dbc.ModalTitle('Contact Us')),
dbc.ModalBody([
dbc.Button('Email Support', href='', external_link=True,
dbc.Button('Email Developers', href='mailto:[email protected]', external_link=True,
color='primary', className='me-1'),
dbc.Button('Visit Our Site', href='', target='_blank', external_link=True,
dbc.Button('Visit Our Site', href='https://msdllcpapers.github.io/obsidian/', target='_blank', external_link=True,
color='primary', className='me-1')
], style={'textAlign': 'center'}),
dbc.ModalFooter([])
], id='modal-contact', is_open=False, size='xl', centered=True),
# Pop-up for Help
dbc.Modal([
dbc.ModalHeader(dbc.ModalTitle('Help: APO with the obsidian Web App')),
dbc.ModalBody('Coming soon...'),
dbc.ModalFooter([])
], id='modal-help', is_open=False, size='xl', centered=True)
], id='modal-help', is_open=False, size='xl', centered=True),
html.Br()
], fluid=True)

setup_help_callbacks(app)
Expand Down
23 changes: 2 additions & 21 deletions obsidian/dash/inputs_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,6 @@ def toggle_collapse(n, is_open):
return not is_open
return is_open

@app.callback(
Output({'type': 'store-param_xspace', 'index': MATCH}, 'data'),
Input({'type': 'store-param_xspace', 'index': MATCH}, 'id'),
Input({'type': 'input-param_type', 'index': MATCH}, 'value'),
Input({'type': 'input-param_min', 'index': MATCH}, 'value'),
Input({'type': 'input-param_max', 'index': MATCH}, 'value'),
Input({'type': 'store-param_categories', 'index': MATCH}, 'data'),
prevent_initial_call=True # It takes a second for these input matches to show up
)
def update_param_xspace(param_id, param_type, param_min, param_max, param_cat):

param_xspace = {'Name': param_id['index'], 'Type': param_type,
'Low': param_min, 'High': param_max, 'Categories': param_cat}

return param_xspace

# Add acquisition function
@app.callback(
Output('div-acquisition_all', 'children'),
Expand Down Expand Up @@ -144,7 +128,6 @@ def func(del_clicked, aq_match):
# Store all of the input and settings into config
@app.callback(
Output('store-config', 'data'),
Input({'type': 'store-param_xspace', 'index': ALL}, 'data'),
Input('input-response_name', 'value'),
Input('input-optimizer_seed', 'value'),
Input('input-f_transform', 'value'),
Expand All @@ -156,20 +139,18 @@ def func(del_clicked, aq_match):
Input({'type': 'input-alpha', 'index': ALL}, 'value'),
prevent_initial_call=True
)
def compile_config(param_xspaces, response_name, optimizer_seed, f_transform, surrogate,
def compile_config(response_name, optimizer_seed, f_transform, surrogate,
m_batch, optim_sequential, optim_restarts, aq, alpha):

config = {}

config['response_name'] = response_name
config['optimizer_seed'] = int(optimizer_seed) if optimizer_seed is not None else None
config['surrogate_params'] = {'f_transform': f_transform, 'surrogate': surrogate}
#config['aq_params'] = {'optim_sequential': optim_sequential, 'optim_restarts': optim_restarts,
# 'm_batch': m_batch, 'acquisition': aq, 'alpha': alpha}
# TODO: Re-implement hyperparmeters based on selection in alpha
config['aq_params'] = {'optim_sequential': optim_sequential, 'optim_restarts': optim_restarts,
'm_batch': m_batch, 'acquisition': aq}
config['verbose'] = 0
config['xspace'] = param_xspaces

return config

Expand Down
87 changes: 75 additions & 12 deletions obsidian/dash/inputs_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import base64
import io
from dash.exceptions import PreventUpdate
from obsidian.parameters import ParamSpace, Param_Categorical, Param_Ordinal, Param_Continuous, Target, Parameter
from pandas.api.types import is_numeric_dtype


def setup_data(app, app_tabs, default_data):
def setup_data(app, app_tabs, default_data, default_Xspace):

# Data upload
uploader = dcc.Upload(id='uploader-X0',
Expand Down Expand Up @@ -53,6 +55,7 @@ def setup_data(app, app_tabs, default_data):
# Data store
storage_X0 = dcc.Store(id='store-X0', data=default_data.to_dict())
storage_X0_template = dcc.Store(id='store-X0_template', data=default_data.to_dict())
storage_Xspace = dcc.Store(id='store-Xspace', data=default_Xspace.save_state())

# Parameter space table
xspace = html.Div([html.Div(id='div-xspace', children=[])])
Expand Down Expand Up @@ -80,7 +83,7 @@ def setup_data(app, app_tabs, default_data):

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

Expand Down Expand Up @@ -111,7 +114,6 @@ def save_X0(contents):
)
def preview_X0(data, filename):
df = pd.DataFrame(data)

return make_table(df, fill_width=True), filename

# Download template data
Expand Down Expand Up @@ -139,25 +141,42 @@ def choose_col(data):
@app.callback(
Output('div-xspace', 'children'),
Input('store-X0', 'data'),
Input('input-response_name', 'value')
Input('input-response_name', 'value'),
State('store-Xspace', 'data')
)
def update_xspace_types(data, ycol):
def update_xspace_types(data, ycol, Xspace_save):

X_space = ParamSpace.load_state(Xspace_save)

df_xspace = pd.DataFrame(data)
xcols = [x for x in df_xspace.columns if x != ycol]
df_X0 = pd.DataFrame(data)
xcols = [x for x in df_X0.columns if x != ycol]

#param_types = ['Numeric', 'Categorical', 'Ordinal']
param_types = ['Numeric', 'Categorical'] # Note: the Ordinal var may cause fitting issues
param_types = ['Numeric', 'Categorical', 'Ordinal']

cols = []
for i, x in enumerate(xcols):
if x in X_space.X_names:
x_idx = X_space.X_names.index(x)
param = X_space.params[x_idx]
if isinstance(param, Param_Continuous):
param_type = 'Numeric'
elif isinstance(param, Param_Categorical):
param_type = 'Categorical'
elif isinstance(param, Param_Ordinal):
param_type = 'Ordinal'
else:
if not is_numeric_dtype(df_X0[x]):
param_type = 'Categorical'
else:
param_type = 'Numeric'

cols.append(dbc.Col(children=dbc.Card([
dbc.CardHeader(f'{x}'),
dbc.CardBody([make_dropdown('Type', f'Select parameter type for {x}', param_types,
id={'type': 'input-param_type', 'index': x},
kwargs={'value': param_types[0]}),
kwargs={'value': param_type}),
html.Div(id={'type': 'div-param_vals', 'index': x}, children=[]),
dcc.Store(id={'type': 'store-param_xspace', 'index': x}, data={})],)
dcc.Store(id={'type': 'store-param_save', 'index': x}, data={})],)
],
color='primary', outline=True), width=2))

Expand Down Expand Up @@ -207,7 +226,34 @@ def update_xspace_vals(param_type, param_id, data, ycol):
]
return choices

# ategory management for categorical variables
@app.callback(
Output({'type': 'store-param_save', 'index': MATCH}, 'data'),
Input({'type': 'store-param_save', 'index': MATCH}, 'id'),
Input({'type': 'input-param_type', 'index': MATCH}, 'value'),
Input({'type': 'input-param_min', 'index': MATCH}, 'value'),
Input({'type': 'input-param_max', 'index': MATCH}, 'value'),
Input({'type': 'store-param_categories', 'index': MATCH}, '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']

try:
if param_type == 'Numeric':
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)

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

except TypeError:
return None

# Category management for categorical variables
@app.callback(
Output({'type': 'store-param_categories', 'index': MATCH}, 'data'),
Output({'type': 'input-new_category', 'index': MATCH}, 'value'),
Expand Down Expand Up @@ -262,4 +308,21 @@ def preview_cats(current_cats):
def troubleshoot_config(data):
return

@app.callback(
Output('store-Xspace', 'data'),
Input({'type': 'store-param_save', 'index': ALL}, 'data'),
prevent_initial_call=True
)
def save_Xspace(param_saves):

param_list = []
for param_input in param_saves:
if param_input:
param_i = ParamSpace.load_state(param_input)
param_list += list(param_i.params)

X_space = ParamSpace(param_list)

return X_space.save_state()

return
45 changes: 21 additions & 24 deletions obsidian/dash/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pandas as pd

from obsidian.parameters import ParamSpace, Param_Categorical, Param_Ordinal, Param_Continuous
#from obsidian.optimizer import BayesianOptimizer
from obsidian.optimizer import BayesianOptimizer
from obsidian.campaign import Campaign
from obsidian.parameters import Target

Expand Down Expand Up @@ -54,10 +54,11 @@ def setup_optimize(app, app_tabs):
store_candidates = dcc.Store(id='store-candidates', data={})

# Suggested candidates download
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'})
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'})

# 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 All @@ -76,30 +77,25 @@ def setup_optimize_callbacks(app):
Input('button-fit', 'n_clicks'),
State('store-config', 'data'),
State('store-X0', 'data'),
State('store-Xspace', 'data'),
prevent_initial_call=True
)
def fit_optimizer(fit_clicked, config, X0):
def fit_optimizer(fit_clicked, config, X0, Xspace_save):

if config['xspace'] == {}:
if not Xspace_save:
return 0, None

xspace = []
for param_xspace in config['xspace']:
if param_xspace['Type']=='Numeric':
this_para = Param_Continuous(param_xspace['Name'], param_xspace['Low'], param_xspace['High'])
elif param_xspace['Type']=='Categorical':
this_para = Param_Categorical(param_xspace['Name'], param_xspace['Categories'])
else:
this_para = Param_Ordinal(param_xspace['Name'], param_xspace['Categories'])
xspace.append(this_para)
X_space = ParamSpace(xspace)
my_campaign = Campaign(X_space)
my_campaign.add_data(pd.DataFrame(X0))
X_space = ParamSpace.load_state(Xspace_save)
target = Target(config['response_name'], aim='max')
my_campaign.set_target(target)
my_campaign.fit()
campaign = Campaign(X_space, target)
campaign.add_data(pd.DataFrame(X0))

optimizer = BayesianOptimizer(X_space=campaign.X_space,
surrogate=config['surrogate_params']['surrogate'])
campaign.set_optimizer(optimizer)
campaign.fit()

return 0, my_campaign.optimizer.save_state()
return 0, campaign.optimizer.save_state()

@app.callback(
Output('div-fit', 'children'),
Expand All @@ -117,8 +113,9 @@ def fit_statistics(opt_save, config, filename):
[
dbc.ListGroupItem(['Model Type: ', f'{optimizer.surrogate_type}']),
dbc.ListGroupItem(['Data Name: ', filename]),
dbc.ListGroupItem(['R', html.Sup('2'), ' Score: ', f'{optimizer.surrogate[0].r2_score: .4g}']), # for SOO only
dbc.ListGroupItem(['Marginal Log Likelihood: ', f'{optimizer.surrogate[0].loss: .4g}']), # for SOO only
# TODO: Implement score for each surrogate, to support MOO
dbc.ListGroupItem(['R', html.Sup('2'), ' Score: ', f'{optimizer.surrogate[0].r2_score: .4g}']),
dbc.ListGroupItem(['Marginal Log Likelihood: ', f'{optimizer.surrogate[0].loss: .4g}']),
], flush=True
)

Expand Down
Loading
Loading