Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a7b8a49
disable engine specific error_categories and add LEMS_NML2_Ex9_FN bio…
stellaprins Jun 20, 2024
04d6535
rename .py file for more general purpose down the line
stellaprins Jun 26, 2024
fb36573
Add draft for test_compatibility_biosimulators.py which creates preli…
stellaprins Jun 27, 2024
dc0f56e
Remove redundant code and update biosimulators compatibility test script
stellaprins Jun 27, 2024
4ed2ea7
rename jupyter notebook used for testing
stellaprins Jun 27, 2024
21f0827
rename d1 plots and save them in d1_plots folder before deleting bios…
stellaprins Jun 27, 2024
88a8fc1
add links to d1 plots to table
stellaprins Jun 28, 2024
f506ad1
add updated table
stellaprins Jun 28, 2024
33d1a21
remove old results table
stellaprins Jun 28, 2024
38bd9af
rename results compatibility biosimulators markdown file to be more c…
stellaprins Jun 28, 2024
15cbb8c
correct OpenCOR engines_dict and type_dict (compatible with CellML no…
stellaprins Jul 3, 2024
477034b
add error_categories back in run_biosimulators_docker
stellaprins Jul 3, 2024
e5f9855
Merge pull request #35 from OpenSourceBrain/development
pgleeson Jul 3, 2024
d72ac7c
Merge branch 'development' into feature/31-create-lems_nml2_ex9_fn-do…
stellaprins Jul 3, 2024
b81dac0
Regenerated some files
pgleeson Jul 31, 2024
d200604
Regenerated sedml...
pgleeson Jul 31, 2024
0a24d0e
Merge pull request #43 from OpenSourceBrain/experimental
pgleeson Jul 31, 2024
1ffb4f9
Merge branch 'master' into feature/31-create-lems_nml2_ex9_fn-docker-…
stellaprins Jul 31, 2024
6cb531f
add test_compatibility_biosimulators.py to github actions
stellaprins Jul 31, 2024
75fdc26
deal with test_compatibility_biosimulators.py: Permission denied
stellaprins Aug 1, 2024
05fa69a
change d1_plots to temp_plots to see whether it solves github actions…
stellaprins Aug 1, 2024
64c2f64
Change plot directory from 'd1_plots' to 'temp_plots' to resolve GitH…
stellaprins Aug 1, 2024
e0c1fac
Revert "Change plot directory from 'd1_plots' to 'temp_plots' to reso…
stellaprins Aug 1, 2024
52f40ee
change tmp_plots plot_dir back to d1_plots
stellaprins Aug 1, 2024
276f62a
remove try/except from toplevel script, make sure biosimulators_core …
robertvi Aug 1, 2024
fe807fd
biosimulators_core now with default option to fix output file ownersh…
robertvi Aug 1, 2024
910e893
add option to put pdf outputs into a different folder
robertvi Aug 1, 2024
471c23c
trying to chown output files, still not working
robertvi Aug 1, 2024
8e19350
make sure the chown command always runs
robertvi Aug 2, 2024
7497737
make test output to tmp folder
robertvi Aug 2, 2024
2acc0c0
simplified move_d1_files
robertvi Aug 2, 2024
4645b60
print out contents of output_folder just before deletion
robertvi Aug 2, 2024
fd9d5ef
make sure output folder is removed from any previous test
robertvi Aug 2, 2024
cb273e6
remove printing of the output_folder contents before deletion
robertvi Aug 2, 2024
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
Binary file added SBML/d1_plots/amici_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/bionetgen_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/boolnet_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/cbmpy_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/cobrapy_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/copasi_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/gillespy2_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/ginsim_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/libsbmlsim_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/masspy_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/pysces_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/rbapy_d1.pdf
Binary file not shown.
Binary file added SBML/d1_plots/tellurium_d1.pdf
Binary file not shown.
23 changes: 23 additions & 0 deletions SBML/results_compatibility_biosimulators.md

Large diffs are not rendered by default.

217 changes: 217 additions & 0 deletions SBML/test_compatibility_biosimulators.ipynb

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions SBML/test_compatibility_biosimulators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'''
This script tests the compatibility of different biosimulation engines with a given SBML and SED-ML file.
It runs each engine and records the result (pass/fail) and any error messages encountered during the simulation.
The results are then displayed in a table and saved to a markdown file.
'''

import sys
sys.path.append("..")
import utils
import os
import pandas as pd
import shutil

sbml_filepath = 'LEMS_NML2_Ex9_FN.sbml'
sedml_filepath = 'LEMS_NML2_Ex9_FN_missing_xmlns.sedml' #xmlns:sbml missing (original file)


engines = utils.engines
types_dict = utils.types_dict

engine_dict = {}

output_folder = 'output'

for e in engines.keys():
print('Running ' + e)
output_dir = os.path.abspath(os.path.join(output_folder, e))
try:
record = utils.run_biosimulators_docker(e, sedml_filepath, sbml_filepath, output_dir=output_dir)
engine_dict[e] = record
except Exception as error:
error_message = str(error)
print(f"Error occurred while running {e}")
engine_dict[e] = error_message
continue

file_paths = utils.find_files(output_folder, '.pdf')
utils.move_d1_files(file_paths, 'd1_plots')
shutil.rmtree(output_folder)

# TODO: move part that creates table to utils
# Create a table of the results
results_table = pd.DataFrame.from_dict(engine_dict).T
results_table.columns = ['pass/FAIL', 'Error']
results_table.index.name = 'Engine'
results_table.reset_index(inplace=True)

results_table['Error'] = results_table.apply(lambda x: None if x['pass/FAIL'] == x['Error'] else x['Error'], axis=1)
results_table['pass/FAIL'] = results_table['pass/FAIL'].replace('other', 'FAIL')

results_table['Error'] = results_table['Error'].apply(lambda x: utils.parse_error_message(x))
results_table['Error'] = results_table['Error'].apply(lambda x: utils.collapsible_content(x))

results_table['Compatibility'] = results_table['Engine'].apply(lambda x: utils.check_file_compatibility_test(x, types_dict, sbml_filepath, sedml_filepath))
results_table['Compatibility'] = results_table['Compatibility'].apply(lambda x: utils.collapsible_content(x[1], title=x[0]))
results_table['pass/FAIL'] = results_table['pass/FAIL'].apply(lambda x: f'<span style="color:darkred;">{x}</span>' if x == 'FAIL' else x)
results_table['Compatibility'] = results_table['Compatibility'].apply(lambda x: f'<span style="color:darkred;">{x}</span>' if 'FAIL' in x else x)

# d1 plot clickable link
results_table['d1'] = results_table['Engine'].apply(lambda x: utils.d1_plots_dict(engines, 'd1_plots').get(x, None))
results_table['d1'] = results_table['d1'].apply(lambda x: utils.create_hyperlink(x))

results_table = results_table.to_markdown(index=False)

# save results_md_table
with open('results_compatibility_biosimulators.md', 'w', encoding='utf-8') as f:
f.write(results_table)
193 changes: 188 additions & 5 deletions utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,45 @@
import libsbml
import libsedml
import tempfile
import glob

#
engines = {
'amici': ('sbml', 'sedml'),\
'brian2': [('nml', 'sedml'),('lems', 'sedml'),('sbml', 'sedml')],\
'bionetgen': ('bngl', 'sedml'),\
'boolnet': ('sbmlqual', 'sedml'),\
'cbmpy': ('sbml', 'sedml'),\
'cobrapy': ('sbml', 'sedml'),\
'copasi': ('sbml', 'sedml'),\
'gillespy2': ('sbml', 'sedml'),\
'ginsim': ('sbmlqual', 'sedml'),\
'libsbmlsim': ('sbml', 'sedml'),\
'masspy': ('sbml', 'sedml'),\
'netpyne': ('sbml', 'sedml'),\
'neuron': [('nml', 'sedml'),('lems', 'sedml')],\
'opencor': ('cellml', 'sedml'),\
'pyneuroml': [('nml', 'sedml'),('lems', 'sedml')],\
'pysces': ('sbml', 'sedml'),\
'rbapy': ('rbapy', 'sedml'),\
'smoldyn':None ,\
'tellurium': ('sbml', 'sedml'),\
'vcell': None,\
'xpp': ('xpp', 'sedml')
}

types_dict = {
'sbml':'SBML',\
'sedml':'SED-ML',\
'nml':'NeuroML',\
'lems':'LEMS',\
'sbmlqual':'SBML-qual',\
'bngl':'BNGL',\
'rbapy':'RBApy',\
'xpp':'XPP',\
'smoldyn':'Smoldyn',\
'cellml':'CellML'\
}

#define error categories for detailed error counting per engine
# (currently only tellurium)
Expand Down Expand Up @@ -197,6 +236,149 @@ def read_log_yml(log_filepath):
ym = yaml.safe_load(f)
return ym['exception']['message']

def find_files(directory, extension):
files = glob.glob(f"{directory}/**/*{extension}", recursive=True)
return files

def move_d1_files(file_paths, plot_dir='d1_plots',engines=engines):
if not os.path.exists(plot_dir):
os.makedirs(plot_dir, exist_ok=True)

for i in range(len(file_paths)):
engine = [key for key in engines.keys() if key in file_paths[i]]
new_file_name = '_'.join(engine) + '_' + os.path.basename(file_paths[i])
new_file_path = os.path.join(plot_dir, new_file_name)
print(new_file_path)
if os.path.exists(new_file_path):
os.remove(new_file_path)
shutil.move(file_paths[i], new_file_path)

return find_files(plot_dir, '.pdf')

# write definition to create d1 plots dict
def d1_plots_dict(engines=engines, d1_plots_path='d1_plots'):
"""
Create a dictionary with engine names as keys and d1 plot paths as values.
"""
d1_plots = find_files(d1_plots_path, '.pdf')
d1_plots_dict = {e: d1_plot for e in engines.keys() for d1_plot in d1_plots if e in d1_plot}
return d1_plots_dict


def create_hyperlink(file_path):
"""
Create a hyperlink to a file or folder. If the path is None, return None.
Title is the basename of the path.
"""
if file_path:
title = os.path.basename(file_path)
return f'<a href="{file_path}">{title}</a>'
else:
return None


def parse_error_message(text):
if text != None:
text_message = re.findall(r'"([^"]*)"', text)
if len(text_message) > 0:
text = text_message
else:
text = text.replace('|', '')
return text
text = bytes(text[0], "utf-8").decode("unicode_escape")
text = text.replace('|', '')

# # for any text with "<*>" remove "<" as well as ">" but leave wildcard text *
text = re.sub(r'<([^>]*)>', r'\1', text)

# replace color codes with html color codes
text = text.replace("\x1b[33m",'<span style="color:darkorange;">')
text = text.replace("\x1b[31m",'<span style="color:red;">')

# # remove .\x1b[0m
text = text.replace("\x1b[0m", "")

# find first "." or ":" after "<span*" and add "</span>"after it
pattern = r'(<span style="[^"]*">[^.:]*)([.:])'
replacement = r'\1\2</span>'
text = re.sub(pattern, replacement, text, count=1)

# bullet points and new lines
text = text.replace('\r\n - ', '</li><li>')
text = text.replace('\r\n', '<br>')
text = text.replace('\n', '<br>')

# BioSimulatorsWarning: two <br> tags after
text = text.replace('BioSimulatorsWarning:', '<br><br>BioSimulatorsWarning:<br><br>')
text = text.replace('warnings.warn(termcolor.colored(message, Colors.warning.value), category)', '<br>')

# if text includes The COMBINE/OMEX did not execute successfully: make everyhting from that point red
text = text.replace('The COMBINE/OMEX did not execute successfully:', '<span style="color:red;">The COMBINE/OMEX did not execute successfully:')
return text

def display_error_message(error_message):
if error_message != None:
display_markdown(f'{error_message}', raw=True)
return error_message

def check_file_compatibility_test(engine, types_dict, model_filepath, experiment_filepath):
'''
Check if the file extensions suggest the file types are compatible with the engine
'''
input_filetypes = set(get_filetypes(model_filepath, experiment_filepath))
input_file_types_text = [types_dict[i] for i in input_filetypes]


engine_filetypes = engines[engine]
if engine_filetypes is not None:
# Flatten the list if the engine_filetypes is a list of tuples
if all(isinstance(i, tuple) for i in engine_filetypes):
engine_filetypes = {item for sublist in engine_filetypes for item in sublist}
engine_file_types_text = [types_dict[i] for i in engine_filetypes if i in types_dict]
if input_filetypes.issubset(engine_filetypes):
return 'pass', (f"The file extensions suggest the input file types are '{input_file_types_text}'. These are compatible with {engine}")
else:
return 'FAIL', (f"The file extensions suggest the input file types are '{input_file_types_text}'. Tese are not compatible with {engine}. The following file types will be compatible {engine_file_types_text}")
else:
return 'FAIL', (f"{engine} compatible file types unknown.")


def collapsible_content(content, title='Details'):
"""
Create a collapsible content section in markdown format

Input: content, title
"""
if content:
return f'<details><summary>{title}</summary>{content}</details>'
else:
return None

def get_filetypes(model_filepath, simulation_filepath):
"""
Get the filetypes of the model and simulation files

Input: model_filepath, simulation_filepath
Output: tuple of filetypes
"""
if model_filepath.endswith(".sbml") and simulation_filepath.endswith(".sedml"):
filetypes = ('sbml', 'sedml')
else:
filetypes = "other"
return filetypes

def delete_output_folder(output_dir):
'''
# Delete the output folder and its contents
'''
for file_name in os.listdir(output_dir):
file_path = os.path.join(output_dir, file_name)
if os.path.isfile(file_path):
os.remove(file_path)
elif os.path.isdir(file_path):
shutil.rmtree(file_path)


def run_biosimulators_docker(engine,sedml_filepath,sbml_filepath,output_dir=None,error_categories=error_categories):
'''
put the sedml and sbml file into an omex archive
Expand All @@ -212,13 +394,14 @@ def run_biosimulators_docker(engine,sedml_filepath,sbml_filepath,output_dir=None
return "pass" #no errors
except Exception as e:
#capture the error as a string which won't break markdown tables
error_str = safe_md_string(e)
# error_str = safe_md_string(e)
error_str = str(e)

#try to load the cleaner error message from the log.yml file
log_str = read_log_yml(os.path.join(os.path.dirname(omex_filepath),"log.yml"))
# #try to load the cleaner error message from the log.yml file
# log_str = read_log_yml(os.path.join(os.path.dirname(omex_filepath),"log.yml"))

if log_str:
error_str = safe_md_string(log_str)
# if log_str:
# error_str = safe_md_string(log_str)

#categorise the error string
if engine in error_categories:
Expand Down