diff --git a/dighosp-des/dighosp_des/api.py b/dighosp-des/dighosp_des/api.py index 7aa2543..e05ca6f 100644 --- a/dighosp-des/dighosp_des/api.py +++ b/dighosp-des/dighosp_des/api.py @@ -17,9 +17,9 @@ from pymongo import MongoClient from rq import Queue -from .conf import MONGO_CLIENT_ARGS, APP_VERSION +from .conf import APP_VERSION, MONGO_CLIENT_ARGS from .config import Config -from .kpis import lab_tats_fig, utilisation_fig, wips_fig +from .kpis import lab_tats_fig, lab_tats_table, utilisation_fig, utilisation_table, wips_fig from .model import Model from .redis_worker import RedisSingleton @@ -209,10 +209,12 @@ def save_dash_objs(job_id: ObjectIdStr): data = [get_result(job_id, idx) for idx in range(n)] kpi_objs = { 'utilisation': utilisation_fig(data), + 'utilisation_table': utilisation_table(data), 'wip': wips_fig(data, wip='Total WIP'), - 'tat': lab_tats_fig(data) + 'tat': lab_tats_fig(data), + 'tat_table': lab_tats_table(data) } - + with MongoClient(**MONGO_CLIENT_ARGS) as client: coll = client['sim']['sim_jobs'] obj = coll.find_one({'_id': ObjectId(job_id)}) @@ -221,18 +223,19 @@ def save_dash_objs(job_id: ObjectIdStr): status_code=status.HTTP_404_NOT_FOUND, detail=f"No simulation job with ObjectId {job_id}." ) - + output_bytes = orjson.dumps(kpi_objs) fs = GridFS(client['sim']) obj_id = fs.put(output_bytes) - + coll.find_one_and_update( filter={'_id': ObjectId(job_id)}, update={'$set': {'results_kpi_obj_id': str(obj_id)}} ) return obj_id + @app.get( '/jobs/{job_id}/results/dash_objs', summary='Get Plotly Dash objects' @@ -251,11 +254,12 @@ def get_dash_objs(job_id: ObjectIdStr): status_code=status.HTTP_404_NOT_FOUND, detail=f"No simulation job with ObjectId {job_id}." ) - + fs = GridFS(client['sim']) obj_id = ObjectId(obj['results_kpi_obj_id']) return json.load(fs.get(obj_id)) + @app.get( '/jobs', summary='List jobs' diff --git a/dighosp-des/dighosp_des/kpis.py b/dighosp-des/dighosp_des/kpis.py index e2e9aca..74979e9 100644 --- a/dighosp-des/dighosp_des/kpis.py +++ b/dighosp-des/dighosp_des/kpis.py @@ -60,6 +60,18 @@ def utilisation_fig(data): fig.update_layout(title='Resource utilisation') return json.loads(fig.to_json()) +def utilisation_table(data): + """Create a table of mean resource utilisation by resource.""" + res_names = list(data[0]['resources']['n_claimed'].keys()) + res_means = [ + np.mean([mean_claimed(dd, res)/mean_available(dd, res) for dd in data]) + for res in res_names + ] + return { + 'Resource': res_names, + 'Utilisation': [f'{x:.3%}' for x in res_means] + } + def wip_df(data, wip): """Get the hourly means for a given WIP counter, for a single simulation replication.""" @@ -143,3 +155,19 @@ def lab_tats_fig(data): fig.update_xaxes(title='Days') fig.update_yaxes(title='Probability') return json.loads(fig.to_json()) + + +def lab_tats_table(data): + """Create a table of lab turnaround times, showing the percentage of specimens completed within + 7, 10, 14, or 21 days.""" + lab_tats = [[ + (x['qc_end']-x['reception_start'])/24.0 + for x in data[n]['specimen_data'].values() + if 'reporting_end' in x + ] for n in range(len(data))] + lab_tats = np.array(list(chain(*lab_tats))) + days = [7, 10, 14, 21] + return { + 'Days': days, + 'Specimens completed': [f'{np.mean(lab_tats < x):.3%}' for x in days] + } diff --git a/dighosp-des/pyproject.toml b/dighosp-des/pyproject.toml index 6f09b5f..707931d 100644 --- a/dighosp-des/pyproject.toml +++ b/dighosp-des/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dighosp-des" -version = "0.1.1a0" +version = "0.1.1" description = "" authors = ["Yin-Chi Chan "] readme = "README.md" diff --git a/dighosp-docs/pyproject.toml b/dighosp-docs/pyproject.toml index 0ee86a8..c6c1697 100644 --- a/dighosp-docs/pyproject.toml +++ b/dighosp-docs/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dighosp-docs" -version = "0.1.1a0" +version = "0.1.1" description = "# Internal (developer) documentation" authors = ["Yin-Chi Chan "] readme = "README.md" diff --git a/dighosp-docs/source/changelog.md b/dighosp-docs/source/changelog.md index eaeea0c..f6ba546 100644 --- a/dighosp-docs/source/changelog.md +++ b/dighosp-docs/source/changelog.md @@ -2,6 +2,10 @@ ## 0.1 +### 0.1.1 +- Modify DES service to precompute KPI figure/table data for improved page loading speed +- Add version numbers to saved KPI data + ### 0.1.0 - Functioning frontend (based on existing [digital-hosp-frontend](https://github.com/cam-digital-hospitals/digital-hosp-frontend)) diff --git a/dighosp-docs/source/roadmap.md b/dighosp-docs/source/roadmap.md index 7febdb7..41c8c91 100644 --- a/dighosp-docs/source/roadmap.md +++ b/dighosp-docs/source/roadmap.md @@ -2,15 +2,6 @@ ## 0.1 -### 0.1.1 -- Modify DES service to precompute KPIs for improved page loading speed - - Actually, a entire Plotly Figure object is saved in `dict` form - - [x] Run automatically when all simulation replications finished, save results in database - - [x] New API endpoint for fetching precomputed KPI values from database; modify frontend accordingly - - Keep or remove full simulation results??? - - If kept, can recompute KPIs if changes are made (e.g. to add more KPIs to the output) - - [x] Add version numbers to saved KPI values - ### 0.1.2 - [ ] Containerise the DES workers - See: https://github.com/yinchi/container-queue diff --git a/dighosp-frontend/dighosp_frontend/app.py b/dighosp-frontend/dighosp_frontend/app.py index a67ec16..5d3754b 100644 --- a/dighosp-frontend/dighosp_frontend/app.py +++ b/dighosp-frontend/dighosp_frontend/app.py @@ -28,7 +28,7 @@ def app_main(): style={'max-width': '1600px'} ) as ret: with dbc.NavbarSimple( - brand='Digital Hospitals Demo', + brand=f'Digital Hospitals Demo v{conf.APP_VERSION.base_version}', brand_href=dash.get_relative_path('/#'), color='primary', dark=True, @@ -47,8 +47,10 @@ def app_main(): href=dash.get_relative_path(service_data['href']) ) with dbc.DropdownMenu(nav=True, in_navbar=True, label='Developer'): - yield dbc.DropdownMenuItem("Docs", href='/docs', external_link=True) - yield dbc.DropdownMenuItem("MongoDB admin", href='/mongoadmin', external_link=True) + yield dbc.DropdownMenuItem( + "Docs", href='/docs', external_link=True, target='_blank') + yield dbc.DropdownMenuItem( + "MongoDB admin", href='/mongoadmin', external_link=True, target='_blank') yield dash.page_container return ret diff --git a/dighosp-frontend/dighosp_frontend/conf.py b/dighosp-frontend/dighosp_frontend/conf.py index b554b5e..5cfe6b0 100644 --- a/dighosp-frontend/dighosp_frontend/conf.py +++ b/dighosp-frontend/dighosp_frontend/conf.py @@ -2,7 +2,9 @@ import os from pathlib import Path +import toml from dotenv import find_dotenv, load_dotenv +from packaging.version import Version env_get = os.environ.get @@ -23,3 +25,15 @@ if __name__ == "__main__": print(ASSETS_DIRNAME) print(ASSETS_DIRNAME.is_dir()) + + +# APP VERSION +APP_VERSION = None +try: + data = toml.load('/app/pyproject.toml') + if 'project' in data and 'version' in data['project']: + APP_VERSION = Version(data['project']['version']) + elif 'tool' in data and 'poetry' in data['tool'] and 'version' in data['tool']['poetry']: + APP_VERSION = Version(data['tool']['poetry']['version']) +except Exception: + pass # Keep default of None diff --git a/dighosp-frontend/dighosp_frontend/pages/des_result_single.py b/dighosp-frontend/dighosp_frontend/pages/des_result_single.py index 0d3f896..baf9fa9 100644 --- a/dighosp-frontend/dighosp_frontend/pages/des_result_single.py +++ b/dighosp-frontend/dighosp_frontend/pages/des_result_single.py @@ -46,15 +46,21 @@ def layout(job_id: str): @composition def populate_card(job_id: str): """Generate the Card layout showing KPIs for the simulation job.""" - fig_data = get_kpi_figs(job_id) + kpi_objs = get_kpi_objs(job_id) with dbc.Card(class_name='mb-3') as ret1: with dbc.CardBody(): - yield dcc.Graph(figure=fig_data['tat']) + yield html.H3('Lab Turnaround Times') + yield dbc.Table.from_dataframe( + pd.DataFrame(kpi_objs['tat_table']), + striped=True, bordered=True, hover=True + ) + yield dcc.Graph(figure=kpi_objs['tat']) with dbc.Card(class_name='mb-3') as ret2: with dbc.CardBody(): - yield dcc.Graph(figure=fig_data['wip']) + yield html.H3('Work in Progress') + yield dcc.Graph(figure=kpi_objs['wip']) n = get_num_reps(job_id) yield html.P(f"""\ Bands denote the lower and upper deciles (light blue) and quantiles (dark blue); the black line \ @@ -62,7 +68,12 @@ def populate_card(job_id: str): with dbc.Card(class_name='mb-3') as ret3: with dbc.CardBody(): - yield dcc.Graph(figure=fig_data['utilisation']) + yield html.H3('Resource Utilisation') + yield dbc.Table.from_dataframe( + pd.DataFrame(kpi_objs['utilisation_table']), + striped=True, bordered=True, hover=True + ) + yield dcc.Graph(figure=kpi_objs['utilisation']) return [ret1, ret2, ret3] @@ -78,7 +89,7 @@ def get_num_reps(job_id: str) -> dict: -def get_kpi_figs(job_id: str) -> dict: +def get_kpi_objs(job_id: str) -> dict: """Get the KPI-related figure objects for a given job.""" url = f'{DES_FASTAPI_URL}/jobs/{job_id}/results/dash_objs' response = requests.get(url, timeout=100) diff --git a/dighosp-frontend/pyproject.toml b/dighosp-frontend/pyproject.toml index 2e1e57b..7e457a9 100644 --- a/dighosp-frontend/pyproject.toml +++ b/dighosp-frontend/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dighosp-frontend" -version = "0.1.1a0" +version = "0.1.1" description = "" authors = ["Yin-Chi Chan "] readme = "README.md" @@ -24,6 +24,7 @@ gunicorn = "^22.0.0" [tool.poetry.group.dev.dependencies] pylint = "^3.2.5" +ipykernel = "^6.29.5" [build-system] requires = ["poetry-core"]