Source code for myoconverter.utils.generate_pdf

import os
import glob
from fpdf import Template
from loguru import logger
import pickle

[docs]def generate_pdf(mjc_vlt1_path:str, mjc_vlt2_path:str, mjc_vlt3_path:str, model_name:str, save_path:str) -> Template: """ Generate pdf report with all the validation plots """ # define a template elements = [ {'name': 'main_title', 'type': 'T', 'x1': 20.0, 'y1': 17.0, 'x2': 280.0, 'y2': 28.0,\ 'font': 'helvetica', 'size': 28, 'bold': 1, 'italic': 0, 'text': '', 'multiline': False,}, {'name': 'plant_text_title', 'type': 'T', 'x1': 20.0, 'y1': 35.0, 'x2': 280.0, 'y2': 45.0,\ 'font': 'helvetica', 'size': 16, 'bold': 0, 'italic': 0, 'text': '', 'multiline': True,}, {'name': 'plant_text1', 'type': 'T', 'x1': 20.0, 'y1': 30.0, 'x2': 280.0, 'y2': 40.0,\ 'font': 'helvetica', 'size': 14, 'bold': 0, 'italic': 0, 'text': '', 'multiline': True,}, {'name': 'plant_text0', 'type': 'T', 'x1': 20.0, 'y1': 40.0, 'x2': 280.0, 'y2': 45.0,\ 'font': 'helvetica', 'size': 14, 'bold': 1, 'italic': 1, 'text': '', 'foreground': 0x110000,\ 'multiline': True,}, {'name': 'list_title1', 'type': 'T', 'x1': 40.0, 'y1': 60.0, 'x2': 280.0, 'y2': 70.0,\ 'font': 'helvetica', 'size': 20, 'bold': 1, 'italic': 0, 'text': '', 'multiline': False,}, {'name': 'list_content1', 'type': 'T', 'x1': 50.0, 'y1': 70.0, 'x2': 280.0, 'y2': 80.0,\ 'font': 'helvetica', 'size': 16, 'bold': 0, 'italic': 0, 'text': '', 'multiline': True,}, {'name': 'error_content1', 'type': 'T', 'x1': 50.0, 'y1': 90.0, 'x2': 280.0, 'y2': 100.0,\ 'font': 'helvetica', 'size': 16, 'bold': 0, 'italic': 1, 'text': '', 'foreground': 0x110000,\ 'multiline': False,}, {'name': 'list_title2', 'type': 'T', 'x1': 40.0, 'y1': 110.0, 'x2': 280.0, 'y2': 120.0,\ 'font': 'helvetica', 'size': 20, 'bold': 1, 'italic': 0, 'text': '', 'multiline': False,}, {'name': 'list_content2', 'type': 'T', 'x1': 50.0, 'y1': 120.0, 'x2': 280.0, 'y2': 130.0,\ 'font': 'helvetica', 'size': 16, 'bold': 0, 'italic': 0, 'text': '', 'multiline': True,}, {'name': 'error_content2', 'type': 'T', 'x1': 50.0, 'y1': 130.0, 'x2': 280.0, 'y2': 140.0,\ 'font': 'helvetica', 'size': 16, 'bold': 0, 'italic': 1, 'text': '', 'foreground': 0x110000,\ 'multiline': False,}, {'name': 'list_title3', 'type': 'T', 'x1': 40.0, 'y1': 150.0, 'x2': 280.0, 'y2': 160.0,\ 'font': 'helvetica', 'size': 20, 'bold': 1, 'italic': 0, 'text': '', 'multiline': False,}, {'name': 'list_content3', 'type': 'T', 'x1': 50.0, 'y1': 160.0, 'x2': 280.0, 'y2': 170.0,\ 'font': 'helvetica', 'size': 16, 'bold': 0, 'italic': 0, 'text': '', 'multiline': True,}, {'name': 'error_content3', 'type': 'T', 'x1': 50.0, 'y1': 180.0, 'x2': 280.0, 'y2': 190.0,\ 'font': 'helvetica', 'size': 16, 'bold': 0, 'italic': 1, 'text': '', 'foreground': 0x110000,\ 'multiline': False,}, {'name': 'step_title', 'type': 'T', 'x1': 20.0, 'y1': 17.0, 'x2': 280.0, 'y2': 27.0,\ 'font': 'helvetica', 'size': 24, 'bold': 1, 'italic': 0, 'text': '', 'multiline': False,}, {'name': 'plant_text2', 'type': 'T', 'x1': 20.0, 'y1': 75.0, 'x2': 280.0, 'y2': 85.0,\ 'font': 'helvetica', 'size': 14, 'bold': 0, 'italic': 0, 'text': '', 'multiline': True,}, {'name': 'plant_text3', 'type': 'T', 'x1': 20.0, 'y1': 95.0, 'x2': 280.0, 'y2': 105.0,\ 'font': 'helvetica', 'size': 14, 'bold': 0, 'italic': 0, 'text': '', 'multiline': True,}, {'name': 'plot_title', 'type': 'T', 'x1': 10.0, 'y1': 5.0, 'x2': 280.0, 'y2': 10.0,\ 'font': 'helvetica', 'size': 18, 'bold': 0, 'italic': 0, 'text': '', 'multiline': False,}, {'name': 'overall_image', 'type': 'I', 'x1': 20.0, 'y1': 15.0, 'x2': 280.0, 'y2': 210.0,}, {'name': 'xml_image', 'type': 'I', 'x1': 20.0, 'y1': 15.0, 'x2': 280.0, 'y2': 210.0,}, {'name': 'ma_image', 'type': 'I', 'x1': 10.0, 'y1': 20.0, 'x2': 300.0, 'y2': 200.0,}, {'name': 'mf_image', 'type': 'I', 'x1': 10.0, 'y1': 20.0, 'x2': 300.0, 'y2': 200.0,},] # if generate pdf report, then initilize the pdf file, Landscape A4 (297 by 210 mm) with element template pdf = Template(format = 'A4', elements = elements, orientation = 'L', unit = 'mm',\ title = 'MyoConverter Model Validation Report',\ author=' Huawei Wang & Aleksi Ikkala',\ keywords='Musculoskeletal Model, MuJoCo, OpenSim, Muscle Kinematics & Kinetics, Conversion pipeline') # Landscape A4 (210 by 297 mm) def add_main_page(pdf:Template, model_name:str, mjc_vlt1_path:str, mjc_vlt2_path:str,\ mjc_vlt3_path:str) -> Template: """ Add main page to the pdf """ # The main page contains the title, a summary of how good the model is, # and overview of the validation steps # Extract overall RMS and STD # First Page pdf.add_page() pdf['main_title'] = 'Validation of converted ' + model_name + ' model ' pdf['plant_text_title'] = "The converted MJC model has been tested under the three categories below" + \ " and with the accuracy of:" pdf['list_title1'] = '- Step 1: XML Conversion Validation' pdf['list_content1'] = 'Check multi-body forward kinematics (using endpoints), approximation of custom/coupling joints' + \ ' & conditional/moving path points' # load the end point data from the saved files if os.path.isfile(mjc_vlt1_path + '/end_points/end_point_error.pkl'): with open(mjc_vlt1_path + '/end_points/end_point_error.pkl', "rb+") as endpoint_error_file: endpoint_error = pickle.load(endpoint_error_file) pdf['error_content1'] = 'Mean error:' + str(round(endpoint_error['mean']*100, 4)) + " cm" + \ '; std: ' + str(round(endpoint_error['std']*100, 4)) + " cm" pdf['list_title2'] = '- Step 2: Muscle Kinematics Validation' pdf['list_content2'] = 'Check muscle moment arms as indication how muscle wrap over joints' # load the moment arm error data from the saved files if os.path.isfile(mjc_vlt2_path + '/overall_comp_momentarms.pkl'): with open(mjc_vlt2_path + '/overall_comp_momentarms.pkl', "rb+") as momentarm_error_file: momentarm_error = pickle.load(momentarm_error_file) pdf['error_content2'] = 'Mean error:' + str(round(momentarm_error['cost_opt_mat_mean']*100, 4)) + " cm" + \ '; std: ' + str(round(momentarm_error['cost_opt_mat_std']*100, 4)) + " cm" pdf['list_title3'] = '- Step 3: Muscle Kinetic Validation' pdf['list_content3'] = 'Check muscle force-length relationship as indication of how similar of them in generating forces' # load the muscle force error data from the saved files if os.path.isfile(mjc_vlt3_path + '/overall_comp_muscleforces.pkl'): with open(mjc_vlt3_path + '/overall_comp_muscleforces.pkl', "rb+") as muscleforce_error_file: muscleforce_error = pickle.load(muscleforce_error_file) pdf['error_content3'] = 'Mean error:' + str(round(muscleforce_error['rms_opt_mean'], 4)) + " Fmax" + \ '; std: ' + str(round(muscleforce_error['rms_opt_std'], 4)) + " Fmax" return pdf def add_step1_page(pdf:Template) -> Template: pdf.add_page() pdf['step_title'] = 'Step 1: xml Conversion Validation' pdf['plant_text1'] = 'Randomly pose the model with 10 confgurations within the joint limits. ' + \ 'In each posture, the endpoints(markers) global locations of Osim and Mjc models are extracted and compared. ' + \ 'Box plot of their mean-std errros are plotted together. Individual endpoint differences of these 10 postures' + \ ' are also ploted in the VLT folder, but not included inside this report.' pdf['plant_text2'] = 'Besides the endpoint check, the approximation of customer joints, coupling joints,' + \ ' conditional/moving path points are plotted and attached. In these plots, blue dots/lines represent ' + \ ' their setup in the OpenSim model. Yellow dots/lines represent the approximations in the MuJoCo model.' return pdf def add_step1_plots(pdf:Template, mjc_vlt1_path:str) -> Template: logger.info(" Adding vlt1 results.") # Scan the end_points folder to find all .svg files and add the overall comparison to the pdf report vlt1_plot_list = [os.path.split(f)[1][0:-4] for f in glob.glob(mjc_vlt1_path + "/end_points/*.svg")] if len(vlt1_plot_list) == 1: # only empty overall_comp plot generated, no end point plots pdf.add_page() pdf['step_title'] = "NO End Points found in the model." else: # first add the overall comparsion plots for vlt1_name in vlt1_plot_list: if "overall_comp" in vlt1_name: pdf.add_page() pdf['plot_title'] = "End points" pdf['overall_image'] = mjc_vlt1_path + '/end_points/' + vlt1_name + '.svg' # Scan the coordinate_coupler_constraints folder to find all .svg files and add the overall comparison to the pdf report if os.path.isdir(mjc_vlt1_path + "/coordinate_coupler_constraints"): vlt1_plot_list = [os.path.split(f)[1][0:-4] for f in glob.glob(mjc_vlt1_path + "/coordinate_coupler_constraints/*.svg")] # add all plots for vlt1_name in vlt1_plot_list: pdf.add_page() pdf['plot_title'] = "Approximation of coupling joints" pdf['xml_image'] = mjc_vlt1_path + '/coordinate_coupler_constraints/' + vlt1_name + '.svg' # Scan the custom joints folder to find all .svg files and add the overall comparison to the pdf report if os.path.isdir(mjc_vlt1_path + "/custom_joints"): vlt1_plot_list = [os.path.split(f)[1][0:-4] for f in glob.glob(mjc_vlt1_path + "/custom_joints/*.svg")] # add all plots for vlt1_name in vlt1_plot_list: pdf.add_page() pdf['plot_title'] = "Approximation of custom joints" pdf['xml_image'] = mjc_vlt1_path + '/custom_joints/' + vlt1_name + '.svg' # Scan the moving path folder to find all .svg files and add the overall comparison to the pdf report if os.path.isdir(mjc_vlt1_path + "/moving_path_points"): vlt1_plot_list = [os.path.split(f)[1][0:-4] for f in glob.glob(mjc_vlt1_path + "/moving_path_points/*.svg")] # add all plots for vlt1_name in vlt1_plot_list: pdf.add_page() pdf['plot_title'] = "Approximation of moving path points" pdf['xml_image'] = mjc_vlt1_path + '/moving_path_points/' + vlt1_name + '.svg' # Scan the moving path folder to find all .svg files and add the overall comparison to the pdf report if os.path.isdir(mjc_vlt1_path + "/conditional_path_points"): vlt1_plot_list = [os.path.split(f)[1][0:-4] for f in glob.glob(mjc_vlt1_path + "/conditional_path_points/*.svg")] # add all plots for vlt1_name in vlt1_plot_list: pdf.add_page() pdf['plot_title'] = "Approximation of conditional path points" pdf['xml_image'] = mjc_vlt1_path + '/conditional_path_points/' + vlt1_name + '.svg' return pdf def add_step2_page(pdf:Template) -> Template: pdf.add_page() pdf['step_title'] = 'Step 2: Muscle Kinematics Validation' pdf['plant_text1'] = 'Moment arm of each muscle at each joint are compared between Osim and converted MJC model. ' + \ 'A overall heatmap is included to indicate the overall moment arm errors before and after optimization. ' + \ 'Then detail moment arm curves are plotted for comparison. ' + \ 'For the muscles that wrap over multiple joints, moment arms with respect to one joint maybe affected by several other joints. ' + \ 'In this case, several mesh points were check of these affecting joints, when plotting moment arms at one joint. ' + \ 'This is why there are multiple lines (with different grey levels) plotted for one muscle on one joint. ' pdf['plant_text3'] = 'How to interpret the plot: \n' + \ 'Global title indicate the muscle and joints that affecting the moment arms in the plots. ' + \ 'X axis indicate the joint that moment arms were extracted. ' + \ 'Grey level of the lines indicate the mesh postures of other relevant joints (in the global tile, but not the x axis)' return pdf def add_step2_plots(pdf:Template, mjc_vlt2_path:str) -> Template: logger.info(" Adding vlt2 results.") # Scan the result folder to find all the <muscle_name>.png files and add all of them to the pdf report vlt2_plot_list = [os.path.split(f)[1][0:-4] for f in glob.glob(mjc_vlt2_path + "/moment_arms/*.svg")] # first add the overall comparsion plots for vlt2_name in vlt2_plot_list: if "overall_comp" in vlt2_name: pdf.add_page() pdf['plot_title'] = "Overall comparison of muscle moment arms before/after optimization" pdf['ma_image'] = mjc_vlt2_path + '/moment_arms/' + vlt2_name + '.svg' for vlt2_name in vlt2_plot_list: if not "overall_comp" in vlt2_name: pdf.add_page() pdf['plot_title'] = "Muscle specific moment arm comparison with Osim model after optimization" pdf['ma_image'] = mjc_vlt2_path + '/moment_arms/' + vlt2_name + '.svg' return pdf def add_step3_page(pdf:Template) -> Template: pdf.add_page() pdf['step_title'] = 'Step 3: Muscle Kinetic Validation' pdf['plant_text1'] = 'Muscle force-length property are compared between Osim and Mjc models. ' + \ 'This force-length property only depends on muscle-fiber-tendon unit lengths. ' + \ 'We made it isolated with the moment arm, so that the change in moment arms will not affect the muscle force properties. ' + \ 'The muscle-fiber-tendon unit lengths were roughly even extracted (from shorest to longest) with all possible body postures. ' + \ 'A bar plot of the froce errors of all muscle before and after optimization is included. ' + \ 'Then the detail force-length curve comparsion plot of each muscle is included. ' pdf['plant_text3'] = 'How to interpret the plot: \n' + \ 'Global title indicate the muscle name. ' + \ 'X axis indicate the muscle-fiber-tendon unit length. ' + \ 'Y axis is the muscle force (unnormalized)' return pdf def add_step3_plots(pdf:Template, mjc_vlt3_path:str) -> Template: logger.info(" Adding vlt3 results.") # Scan the result folder to find all the <muscle_name>.png files and add all of them to the pdf report vlt3_plot_list = [os.path.split(f)[1][0:-4] for f in glob.glob(mjc_vlt3_path + "/muscle_forces/*.svg")] # first add the overall comparsion plots for vlt3_name in vlt3_plot_list: if "overall_comp" in vlt3_name: pdf.add_page() pdf['plot_title'] = "Overall comparison of muscle force-length relationship before/after optimization" pdf['ma_image'] = mjc_vlt3_path + '/muscle_forces/' + vlt3_name + '.svg' for vlt3_name in vlt3_plot_list: if not "overall_comp" in vlt3_name: pdf.add_page() pdf['plot_title'] = "Muscle specific force comparison before/after optimization" pdf['ma_image'] = mjc_vlt3_path + '/muscle_forces/' + vlt3_name + '.svg' return pdf pdf = add_main_page(pdf, model_name, mjc_vlt1_path, mjc_vlt2_path, mjc_vlt3_path) pdf = add_step1_page(pdf) pdf = add_step1_plots(pdf, mjc_vlt1_path) pdf = add_step2_page(pdf) pdf = add_step2_plots(pdf, mjc_vlt2_path) pdf = add_step3_page(pdf) pdf = add_step3_plots(pdf, mjc_vlt3_path) # # generate pdf # pdf.render(save_path + '/' + model_name + '.pdf') return pdf