Tables

On this page you can find all tables and code to produce the tables of the manuscript.

Results

Table 1

Code
import os, pickle, sys
import numpy as np
import pandas as pd
from great_tables import GT, style, md, loc

cwd = os.getcwd()
baseDir = os.path.join(cwd,'..')
dataDir = os.path.join(baseDir,'data')
funcDir = os.path.join(baseDir,'analysis', 'functions')
sys.path.append(funcDir)

from gt_tex import make_latex, delete_rows, insert_rows

# Custom function
def compute_pdiffs(oPar, nPar, keys):
    """Compute percentage differences for all PARAM_KEYS."""
    return [((nPar[k]-oPar[k])/oPar[k])*100 for k in keys]

def format_single(values,form='{:2.0f}'):
    """Format list of diffs for UM/CM cases."""
    return [form.format(val) for val in values]

def format_mean_std(values,form='{:2.0f}'):
    """Format mean ± std for MC case."""
    values_mean, values_std = np.mean(values, axis=0), np.std(values, axis=0)
    return [f"{form.format(m)} ± {form.format(s)}" for m, s in zip(values_mean, values_std)]

#%% Extract parameters
rows = np.array([
    ['a',           '$a$',],
    ['b',           '$b$',],
    ['fmax',        '$F_{CE}^{max}$'],
    ['kpee',        '$k_{PEE}$'],
    ['ksee',        '$k_{SEE}$'],
    ['lce_opt',     '$L_{CE}^{opt}$'],
    ['lpee0',       '$L_{PEE}^0$'],
    ['lsee0',       '$L_{SEE}^0$'],
    ['tact',        '$\\tau_{act}$'],
    ['tdeact',      '$\\tau_{deact}$'],
])
row_keys, row_labels = map(list, zip(*rows))

records = {}
for vPar in ['TM', 'IM', 'MC']:         
    for mus in ['GMs1', 'GMs2', 'GMs3']:  
        # Actual parameters
        parFile = os.path.join(dataDir,mus,'parameters',mus+'_OR.pkl')
        orPar = pickle.load(open(parFile, 'rb'))

        if vPar != "MC":
            parFile = os.path.join(dataDir,mus,'parameters',mus+'_'+vPar+'.pkl')
            estPar = pickle.load(open(parFile, 'rb'))[0] 
            diffs = format_single(compute_pdiffs(orPar, estPar, row_keys),'{:2.0f}')
        else:
            A = []
            for iMC in range(1, 51):
                parFile = os.path.join(dataDir,mus,'parameters','mc',mus+f'_MC{iMC:02d}'+'.pkl')
                estPar = pickle.load(open(parFile, 'rb'))[0]
                A.append(compute_pdiffs(orPar, estPar, row_keys))
            diffs = format_mean_std(np.array(A),'{:2.0f}')

        # Store with a flat column name
        col_name = f'{vPar}_{mus}'
        records[col_name] = diffs

#%% Create pandas dataframe
df = pd.DataFrame(records, index=row_labels)

# %% TeX table
df_tex = df.copy().reset_index()
df_tex.index = row_labels

tex_table = (GT(df_tex)
    .cols_align(align='center') 
    .cols_align(align='left', columns="index")
    .cols_label(index='', TM_GMs1='GM1',TM_GMs2='GM2',TM_GMs3='GM3', IM_GMs1='GM1',IM_GMs2='GM2',IM_GMs3='GM3', MC_GMs1='GM1',MC_GMs2='GM2',MC_GMs3='GM3')
    .tab_spanner(label='Traditional method', columns=['TM_GMs1', 'TM_GMs2', 'TM_GMs3'])
    .tab_spanner(label='Improved method', columns=['IM_GMs1', 'IM_GMs2', 'IM_GMs3'])
    .tab_spanner(label='Monte Carlo', columns=['MC_GMs1', 'MC_GMs2', 'MC_GMs3'])
    .tab_style(
    style=style.text(weight="bold"),
    locations=loc.body(columns="GM_GMs1", rows=[1, 2]))
)

latex_str = make_latex(tex_table.as_latex())
latex_str = delete_rows(latex_str, row_numbers=[0,1])
add_rows = {
    0: r" & \multicolumn{3}{c|}{\itshape Traditional method} & \multicolumn{3}{c|}{\itshape Improved method} & \multicolumn{3}{c|}{\itshape Monte Carlo} \\ \hline",
    1: r" & \bfseries GM1 & \bfseries GM2 & \bfseries GM3 & \bfseries GM1 & \bfseries GM2 & \bfseries GM3 & \bfseries GM1 & \bfseries GM2 & \bfseries GM3 \\ \hline",
}
latex_str = insert_rows(latex_str, add_rows)

# Write to a .tex file
with open("tbl-r-pdiff.tex", "w", encoding="utf-8") as f:
    f.write(latex_str)

# %% Create GT
df_gt = df.reset_index()

tm_w = '6%'
mc_w = '12%'

gt_table = (GT(df_gt)
    .cols_align(align='center') 
    .cols_align(align='left', columns="index")
    .cols_label(index='', TM_GMs1='GM1',TM_GMs2='GM2',TM_GMs3='GM3', IM_GMs1='GM1',IM_GMs2='GM2',IM_GMs3='GM3', MC_GMs1='GM1',MC_GMs2='GM2',MC_GMs3='GM3')
    .tab_spanner(label='Traditional method', columns=['TM_GMs1', 'TM_GMs2', 'TM_GMs3'])
    .tab_spanner(label='Improved method', columns=['IM_GMs1', 'IM_GMs2', 'IM_GMs3'])
    .tab_spanner(label='Monte Carlo', columns=['MC_GMs1', 'MC_GMs2', 'MC_GMs3'])
    .cols_width(cases={'index': '6%', 'TM_GMs1':tm_w,'TM_GMs2':tm_w,'TM_GMs3':tm_w, 'IM_GMs1':tm_w,'IM_GMs2':tm_w,'IM_GMs3':tm_w, 'MC_GMs1':mc_w,'MC_GMs2':mc_w,'MC_GMs3':mc_w})
)
#gt_table.tab_options(column_labels_font_weight="bold") # Bold headers
gt_table
Table 1: Percentage differences between estimated and actual MTC parameter values.
Traditional method Improved method Monte Carlo
GM1 GM2 GM3 GM1 GM2 GM3 GM1 GM2 GM3
$a$ 1 4 2 1 3 1 1 ± 2 6 ± 2 1 ± 2
$b$ -0 2 -1 1 2 1 1 ± 2 5 ± 2 1 ± 2
$F_{CE}^{max}$ 0 -1 0 -0 -1 -0 -0 ± 1 -2 ± 1 -0 ± 1
$k_{PEE}$ 3 -9 9 -0 0 -0 3 ± 17 3 ± 16 6 ± 31
$k_{SEE}$ -39 -27 -37 0 -1 0 -37 ± 6 -36 ± 6 -36 ± 6
$L_{CE}^{opt}$ -1 -2 -3 -0 0 -0 0 ± 3 0 ± 2 0 ± 3
$L_{PEE}^0$ 0 -1 -0 -0 0 -0 0 ± 3 1 ± 2 0 ± 2
$L_{SEE}^0$ -1 -0 -0 0 -0 0 -0 ± 1 -0 ± 1 -0 ± 1
$\tau_{act}$ -21 -16 -21 -0 -1 -0 1 ± 3 -0 ± 4 1 ± 4
$\tau_{deact}$ -5 -4 -5 0 0 0 0 ± 1 0 ± 1 0 ± 1

Table 2

Code
# %% Imports
import os
import sys
import pickle
import numpy as np
import pandas as pd
from great_tables import GT, style, loc

# Paths
cwd = os.getcwd()
baseDir = os.path.join(cwd,'..')
dataDir = os.path.join(baseDir,'data')
funcDir = os.path.join(baseDir,'analysis','functions')
sys.path.append(funcDir)

# Custom import
from gt_tex import make_latex, delete_rows, insert_rows

def format_mean_std(values):
    return f"{form.format(np.mean(values))} ± {form.format(np.std(values))}"

#%% Extract parameters
form = "{:0.1f}"

# Cols: changed parameters
cols = [
    ['a',             '$a$'], 
    ['b',             '$b$'], 
    ['fmax',          '$F_{CE}^{max}$'], 
    ['ksee',          '$k_{SEE}$'],
    ['lce_opt',       '$L_{CE}^{opt}$'],
    ['lsee0',         '$L_{SEE}^0$']]
col_keys, col_labels = map(list, zip(*cols))

# Rows: affected parameters
rows = [
    ['a',             '$a$'], 
    ['b',             '$b$'], 
    ['fmax',          '$F_{CE}^{max}$'], 
    ['kpee',          '$k_{PEE}$'],
    ['ksee',          '$k_{SEE}$'],
    ['lce_opt',       '$L_{CE}^{opt}$'],
    ['lpee0',         '$L_{PEE}^0$'], 
    ['lsee0',         '$L_{SEE}^0$'],
    ['tact',          '$\\tau_{act}$'],
    ['tdeact',        '$\\tau_{deact}$']]
row_keys, row_labels = map(list, zip(*rows))

records = {}
for sPar in col_keys:   # outer loop: sensitivity parameter (columns)
    all_ratios = {k: [] for k in row_keys}
    for mus in ['GMs1', 'GMs2', 'GMs3']:
        tmPar = pickle.load(open(os.path.join(dataDir,mus,'parameters',f"{mus}_TM.pkl"), 'rb'))[0]
        
        for fChange in [0.95, 1.05]:
            saPar = pickle.load(open(os.path.join(dataDir, mus, "parameters",'interdep', f"{mus}_{sPar}_{fChange*100:03.0f}.pkl"), 'rb'))[0]
            for k in row_keys:
                all_ratios[k].append(saPar[k] / tmPar[k])
    
    # signed % diff from 1
    diffs_signed = {}
    for k, vals in all_ratios.items():
        vals = np.array(vals)
        sign = np.sign(vals - 1)[1]  
        diffs_signed[k] = sign * np.abs(vals - 1) * 100
    
    # format mean ± std
    col_vals = [format_mean_std(diffs_signed[k]) for k in row_keys]
    records[sPar] = col_vals

# DataFrame with PARAM_KEYS as rows and sensitivity param as columns
df = pd.DataFrame(records, index=row_keys)

# Apply dash overrides
for label in col_keys:
    df.loc[label, label] = "-"

# %% TeX table
gt_df = df.copy()
gt_df.index = row_labels
gt_df = gt_df.reset_index()
label_dict = {'index': '', **dict(zip(col_keys, col_labels))}

gt_table = (GT(gt_df)
    .cols_align(align='center') 
    .cols_align(align='left', columns="index")
    .cols_label(**label_dict)
)

latex_str = make_latex(gt_table.as_latex())
latex_str = delete_rows(latex_str, row_numbers=[0])
add_rows = {
    0: r" & $\mathbold{a}$ & $\mathbold{b}$ & $\mathbold{F_{CE}^{max}}$ & $\mathbold{k_{SEE}}$ & $\mathbold{L_{CE}^{opt}}$ & $\mathbold{L_{SEE}^{0}}$ \\ \hline",
}
latex_str = insert_rows(latex_str, add_rows)

# Write to a .tex file
with open("tbl-r-interdep.tex", "w", encoding="utf-8") as f:
    f.write(latex_str)

# %% GT
gt_df = df.copy()
gt_df.index = row_labels
gt_df = gt_df.reset_index()
label_dict = {'index': '', **dict(zip(col_keys, col_labels))}

colw = "14.28%"
gt_table = (
    GT(gt_df)
    .cols_align(align='center') 
    .cols_align(align='left', columns="index")
    .cols_label(**label_dict)
    .tab_style(
        style=style.text(weight="bold"),    
        locations=loc.column_labels()        
    )
    .cols_width(cases={
        'index': colw, 
        'a': colw, 
        'b': colw, 
        'fmax': colw, 
        'ksee': colw, 
        'lce_opt': colw, 
        'lsee0': colw
    })
).tab_options(column_labels_font_weight="bold") # Bold headers

gt_table
Table 2: Interdependency of the estimated MTC parameter values. Each entry shows the percentage change in the row parameter resulting from a 5% change in the column parameter. All values are expressed as percentage changes.
$a$ $b$ $F_{CE}^{max}$ $k_{SEE}$ $L_{CE}^{opt}$ $L_{SEE}^0$
$a$ - 10.1 ± 1.3 -14.0 ± 1.0 -0.6 ± 0.2 3.4 ± 1.0 -10.3 ± 2.0
$b$ 2.3 ± 1.2 - -14.3 ± 3.1 1.1 ± 0.3 3.9 ± 1.1 -10.8 ± 1.6
$F_{CE}^{max}$ -0.6 ± 0.1 -0.6 ± 0.1 - -0.6 ± 0.2 -1.2 ± 0.5 3.5 ± 0.5
$k_{PEE}$ -6.8 ± 2.9 -6.8 ± 2.9 -6.9 ± 3.5 -6.8 ± 3.2 -6.8 ± 2.9 -6.8 ± 3.3
$k_{SEE}$ 52.4 ± 12.6 52.4 ± 12.8 54.3 ± 17.0 - 52.4 ± 12.8 52.5 ± 14.3
$L_{CE}^{opt}$ 1.9 ± 0.6 1.9 ± 0.6 -5.4 ± 2.2 1.9 ± 0.6 - -14.2 ± 3.0
$L_{PEE}^0$ -0.6 ± 0.7 -0.6 ± 0.7 -0.6 ± 0.7 -0.6 ± 0.7 -0.6 ± 0.7 -10.6 ± 1.2
$L_{SEE}^0$ 0.5 ± 0.2 0.5 ± 0.2 1.8 ± 0.6 0.5 ± 0.2 -1.7 ± 0.6 -
$\tau_{act}$ 23.4 ± 4.2 23.4 ± 4.6 24.2 ± 5.9 23.3 ± 5.0 23.7 ± 4.3 20.9 ± 6.3
$\tau_{deact}$ 4.8 ± 0.7 4.9 ± 0.8 5.0 ± 3.1 4.8 ± 0.9 4.9 ± 1.4 5.1 ± 3.0

Supplementary material

Table S1

Code
# %% Imports
import os, pickle, sys
import numpy as np
import pandas as pd
from great_tables import GT, style, loc

# Paths
cwd = os.getcwd()
baseDir = os.path.join(cwd,'..')
dataDir = os.path.join(baseDir,'data')
funcDir = os.path.join(baseDir,'functions')
sys.path.append(funcDir)

# Custom import
from gt_tex import make_latex, insert_rows, fix_reference, delete_rows, replace_latex_table_cell, insert_multicolumn

#%% Extract parameters
# to-do bshape
params = np.array([
    ['a',           '$a$',                    'N',        'Contraction dynamics',     '@van_zandwijk_evaluation_1997'],
    ['b',           '$b$',                    'mm',       'Contraction dynamics',     '@van_zandwijk_evaluation_1997'],
    ['vfactmin',    '$b_{scale}^{min}$',      '-',        'Contraction dynamics',     '@van_soest_contribution_1993'],
    ['bshape',      '$b_{shape}$',            '-',        'Contraction dynamics',     '@van_soest_contribution_1993'],
    ['fasymp',      '$F_{asymp}$',            '-',        'Contraction dynamics',     '@rijkelijkhuizen_forcevelocity_2003'],
    ['fmax',        '$F_{CE}^{max}$',         'N',        'Contraction dynamics',     '@van_zandwijk_evaluation_1997'],
    ['kpee',        '$k_{PEE}$',              'N/mm<sup>2</sup>',  'Contraction dynamics',     '@van_zandwijk_evaluation_1997'],
    ['ksee',        '$k_{SEE}$',              'N/mm<sup>2</sup>',  'Contraction dynamics',     '@van_zandwijk_evaluation_1997'],
    ['lce_opt',     '$L_{CE}^{opt}$',         'mm',       'Contraction dynamics',     '@van_zandwijk_evaluation_1997'],
    ['lpee0',       '$L_{PEE}^0$',            'mm',       'Contraction dynamics',     '@van_zandwijk_evaluation_1997'],
    ['lsee0',       '$L_{SEE}^0$',            'mm',       'Contraction dynamics',     '@van_zandwijk_evaluation_1997'],
    ['r_as',        '$r_{as}$',               '-',        'Contraction dynamics',     'Arbitrary small value'],
    ['slopfac',     '$r_{slope}$',            'mm',       'Contraction dynamics',     '@katz_relation_1939'],
    ['w',           '$w$',                    '-',        'Contraction dynamics',     '@burkholder_sarcomere_2001'],
    ['a_act',       '$a_{act}$',              '?',        'Excitation dynamics',      '@bortolotto_mhc_2000'],
    ['b_act1',      '$b_{act,1}$',            '?',        'Excitation dynamics',      '@bortolotto_mhc_2000'],
    ['b_act2',      '$b_{act,2}$',            '?',        'Excitation dynamics',      '@stephenson_effects_1982'],
    ['b_act3',      '$b_{act,3}$',            '?',        'Excitation dynamics',      '@stephenson_effects_1982'],
    ['kCa',         '$kCa$',                  'mol/L',    'Excitation dynamics',      '@kistemaker_length-dependent_2005'],
    ['gamma_0',     '$\\gamma_0$',            '-',        'Excitation dynamics',      'Arbitrary small value'],
    ['q0',          '$q_0$',                  '-',        'Excitation dynamics',      '@hatze_myocybernetic_1981'],
    ['tact',        '$\\tau_{act}$',          'ms',       'Excitation dynamics',      '@van_zandwijk_evaluation_1997'],
    ['tdeact',      '$\\tau_{deact}$',        'ms',       'Excitation dynamics',      '@van_zandwijk_evaluation_1997'],
])

citation_map = {
    "van_zandwijk_evaluation_1997": "van Zandwijk et al.",
    "van_soest_contribution_1993": "van Soest and Bobbert",
    "rijkelijkhuizen_forcevelocity_2003": "Rijkelhuizen et al.",
    "katz_relation_1939": "Katz (1939)",
    "burkholder_sarcomere_2001": "Burkholder and Lieber",
    "bortolotto_mhc_2000": "Bortolotto et al.",
    "stephenson_effects_1982": "Stephenson and Williams",
    "kistemaker_length-dependent_2005": "Kistemaker et al.",
    "hatze_myocybernetic_1981": "Hatze"
}

def replace_citation(x):
    if x.startswith('@'):
        key = x[1:]
        display = citation_map.get(key, key)

        # extract the year = last underscore-separated part
        parts = key.split('_')
        year = parts[-1] if parts[-1].isdigit() else "year"

        return (
            f'<span class="citation" data-cites="{key}">'
            f'{display} (<a href="#ref-{key}" role="doc-biblioref" aria-expanded="false">{year}</a>)'
            f'</span>'
        )
    return x

params = np.array([[replace_citation(x) for x in row] for row in params]) 

orParms = []
for mus in ['GMs1','GMs2','GMs3']:   
    orPar = pickle.load(open(os.path.join(dataDir,mus,'parameters',mus+'_OR.pkl'), 'rb'))
    
    # b_act is an array, extract the values
    orPar['b_act1'], orPar['b_act2'], orPar['b_act3'] = orPar['b_act']
    
    # This is a magic number in the functin, well anyway add it here!
    orPar['bshape'] = 22
    
    # Compute r_as, this is the the increase in relative CE force (Fce/Fmax) per unit of relative CE velocity (vce/lce_opt)
    orPar['r_as'] = (orPar['slopfac']*0.005*0.0975*(1+orPar['a']/orPar['fmax'])) / (orPar['vfactmin']*orPar['b']/orPar['lce_opt'])
        
    parms = [orPar[k] for k in params[:,0]]
    orParms.append(parms)

#%% Create pandas dataframe
df = pd.DataFrame(list(zip(*orParms)), columns=['GM1', 'GM2', 'GM3'], index=params[:,0])

# Some are in other units so..
for parm in ['b', 'lce_opt', 'lpee0', 'lsee0', 'tact', 'tdeact']:
    df.loc[parm] = df.loc[parm]*1e3
for parm in ['kpee', 'ksee']:
    df.loc[parm] = df.loc[parm]/1e3

# Add partype
par_type = ['Contraction dynamics']*8 + 2*['Excitation dynamics']

df.insert(0, "Parameter", params[:,1]) 
df.insert(1, "Unit", params[:,2]) 
df.insert(2, "Partype", params[:,3]) 
df.insert(6, "Reference", params[:,4]) 

# %% TeX table
df_tex = df.drop('Partype', axis=1) # Remove Partype for LateX table
gt_table = (GT(df_tex)
    #.tab_stub(rowname_col="Parameter", groupname_col="Partype")
    .cols_align(align='left', columns="Parameter")
    .cols_align(align='center', columns=["GM1", "GM2", "GM3"]) 
    .fmt_number(columns=["GM1", "GM2", "GM3"], n_sigfig=3, sep_mark='',)
    .fmt_scientific(columns=["GM1", "GM2", "GM3"], n_sigfig=3, rows=['$k_{PEE}$, $k_{SEE}$, $r_{as}$', '$\\gamma_0$', '$kCa$', '$q_0$'])
    .cols_width(cases={"GM1": "100px", "GM2": "100px", "GM3": "100px"})
)

# Transform to LateX table
latex_str = make_latex(gt_table.as_latex())
latex_str = delete_rows(latex_str, row_numbers=[0])
add_rows = {
    0: r" \bfseries Parameter & \bfseries Unit & \bfseries GM1 & \bfseries GM2 & \bfseries GM3 & \bfseries Reference \\ \hline",
    1: r"\multicolumn{6}{|l|}{\itshape Contraction dynamics} \\ \hline",
    14: r"\multicolumn{6}{|l|}{\itshape Excitation dynamics} \\ \hline"
}
latex_str = insert_rows(latex_str, add_rows)

citation_map = {
    "van_zandwijk_evaluation_1997": "van Zandwijk et al. (1996)",
    "van_soest_contribution_1993": "van Soest and Bobbert (1993)",
    "rijkelijkhuizen_forcevelocity_2003": "Rijkelhuizen et al. (2003)",
    "katz_relation_1939": "Katz (1939)",
    "burkholder_sarcomere_2001": "Burkholder and Lieber (2001)",
    "bortolotto_mhc_2000": "Bortolotto et al. (2000)",
    "stephenson_effects_1982": "Stephenson and Williams (1982)",
    "kistemaker_length-dependent_2005": "Kistemaker et al. (2005)",
    "hatze_myocybernetic_1981": "Hatze (1981)"
}
latex_str = fix_reference(latex_str, citation_map)
latex_str = replace_latex_table_cell(latex_str, row=7, col=1, new_text=r'N/mm\textsuperscript{2}')
latex_str = replace_latex_table_cell(latex_str, row=8, col=1, new_text=r'N/mm\textsuperscript{2}')
latex_str = replace_latex_table_cell(latex_str, row=12, col=2, new_text=r'$3.71 \cdot 10^{-3}$')
latex_str = replace_latex_table_cell(latex_str, row=12, col=3, new_text=r'$5.45 \cdot 10^{-3}$')
latex_str = replace_latex_table_cell(latex_str, row=12, col=4, new_text=r'$3.16 \cdot 10^{-3}$')
latex_str = insert_multicolumn(latex_str=latex_str, row=3, col_start=2, col_span=3, text="0.100", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=4, col_start=2, col_span=3, text="22.0", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=5, col_start=2, col_span=3, text="1.50", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=13, col_start=2, col_span=3, text="2.00", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=14, col_start=2, col_span=3, text="0.50", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=15, col_start=2, col_span=3, text="-7.37", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=16, col_start=2, col_span=3, text="5.17", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=17, col_start=2, col_span=3, text="0.596", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=18, col_start=2, col_span=3, text="0.00", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=19, col_start=2, col_span=3, text=r"$8.00 \cdot 10^{-6}$", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=20, col_start=2, col_span=3, text=r"$1.00 \cdot 10^{-5}$", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=21, col_start=2, col_span=3, text=r"$5.00 \cdot 10^{-3}$", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=22, col_start=2, col_span=3, text="27.0", align='c|')
latex_str = insert_multicolumn(latex_str=latex_str, row=23, col_start=2, col_span=3, text="27.0", align='c|')

# Write to a .tex file
with open("supptbl-overview.tex", "w", encoding="utf-8") as f:
    f.write(latex_str)

# %% GT
df_gt = df.copy()

gt_table = (GT(df_gt)
    .tab_stub(rowname_col="Parameter", groupname_col="Partype")
    .cols_align(align='left', columns="Parameter")
    .cols_align(align='center', columns=["GM1", "GM2", "GM3"]) 
    .tab_style(style = style.text(style = "italic"), locations = loc.row_groups())
    .tab_style(style = style.borders(sides="right", color="#D3D3D3", weight = "0px", style = "solid"), locations = loc.body(columns="Parameter"))
    .tab_style(style = style.borders(sides="right", color="#D3D3D3", weight = "2px", style = "solid"), locations = loc.body(columns=["Unit", "GM1", "GM2", "GM3"]))
    .fmt_number(columns=["GM1", "GM2", "GM3"], n_sigfig=3)
    .fmt_scientific(columns=["GM1", "GM2", "GM3"], n_sigfig=3, rows=['$k_{PEE}$, $k_{SEE}$, $r_{as}$', '$\\gamma_0$', '$kCa$', '$q_0$'])
    #.fmt_scientific(columns=["GM1", "GM2", "GM3"], n_sigfig=3)
    .cols_width(cases={"GM1": "100px", "GM2": "100px", "GM3": "100px"})
)

#print(gt_table.as_raw_html())
gt_table
Table S1
MTC parameter values obtained from literature to simulate data.
Unit GM1 GM2 GM3 Reference
Contraction dynamics
$a$ N 2.68 1.80 2.58 van Zandwijk et al. ()
$b$ mm 41.6 24.8 41.8 van Zandwijk et al. ()
$b_{scale}^{min}$ - 0.100 0.100 0.100 van Soest and Bobbert ()
$b_{shape}$ - 22.0 22.0 22.0 van Soest and Bobbert ()
$F_{asymp}$ - 1.50 1.50 1.50 Rijkelhuizen et al. ()
$F_{CE}^{max}$ N 13.4 13.8 12.3 van Zandwijk et al. ()
$k_{PEE}$ N/mm2 213 165 511 van Zandwijk et al. ()
$k_{SEE}$ N/mm2 4,220 3,640 3,470 van Zandwijk et al. ()
$L_{CE}^{opt}$ mm 13.2 12.3 11.2 van Zandwijk et al. ()
$L_{PEE}^0$ mm 13.9 13.2 13.4 van Zandwijk et al. ()
$L_{SEE}^0$ mm 28.3 30.3 26.5 van Zandwijk et al. ()
$r_{as}$ - 0.00371 0.00545 0.00316 Arbitrary small value
$r_{slope}$ mm 2.00 2.00 2.00 Katz (1939) ()
$w$ - 0.500 0.500 0.500 Burkholder and Lieber ()
Excitation dynamics
$a_{act}$ ? −7.37 −7.37 −7.37 Bortolotto et al. ()
$b_{act,1}$ ? 5.17 5.17 5.17 Bortolotto et al. ()
$b_{act,2}$ ? 0.596 0.596 0.596 Stephenson and Williams ()
$b_{act,3}$ ? 0.00 0.00 0.00 Stephenson and Williams ()
$kCa$ mol/L 8.00 × 10−6 8.00 × 10−6 8.00 × 10−6 Kistemaker et al. ()
$\gamma_0$ - 1.00 × 10−5 1.00 × 10−5 1.00 × 10−5 Arbitrary small value
$q_0$ - 5.00 × 10−3 5.00 × 10−3 5.00 × 10−3 Hatze ()
$\tau_{act}$ ms 27.0 27.0 27.0 van Zandwijk et al. ()
$\tau_{deact}$ ms 27.0 27.0 27.0 van Zandwijk et al. ()

Table S2

Code
# %% Imports
import os, pickle, sys
import numpy as np
import pandas as pd
from great_tables import GT, style, loc

# Paths
cwd = os.getcwd()
baseDir = os.path.join(cwd,'..')
dataDir = os.path.join(baseDir,'data')
funcDir = os.path.join(baseDir,'functions')
sys.path.append(funcDir)

# Custom imports
from gt_tex import make_latex, insert_rows, delete_rows, replace_latex_table_cell

# %% Extract parameters
rows = [
    ['a',           '$a$',              'N'],
    ['b',           '$b$',              'mm'],
    ['fmax',        '$F_{CE}^{max}$',   'N'],
    ['kpee',        '$k_{PEE}$',        'N/mm<sup>2</sup>'],
    ['ksee',        '$k_{SEE}$',        'N/mm<sup>2</sup>'],
    ['lce_opt',     '$L_{CE}^{opt}$',   'mm'],
    ['lpee0',       '$L_{PEE}^0$',      'mm'],
    ['lsee0',       '$L_{SEE}^0$',      'mm'],
    ['tact',        '$\\tau_{act}$',    'ms'],
    ['tdeact',      '$\\tau_{deact}$',  'ms'],
]
param_keys, row_labels, unit = map(list, zip(*rows))

cols = [
    ['TM_GMe1',     'Rat 1'],
    ['TM_GMe2',     'Rat 2'],
    ['TM_GMe3',     'Rat 3'],
    ['IM_GMe1',     'Rat 1'],
    ['IM_GMe2',     'Rat 2'],
    ['IM_GMe3',     'Rat 3'],
]
col_keys, col_labels = map(list, zip(*cols))

tmParms,imParms = [],[]
for mus in ['GMe1','GMe2','GMe3']:   
    tmPar = pickle.load(open(os.path.join(dataDir,mus,'parameters',mus+'_TM.pkl'), 'rb'))[0]
    imPar = pickle.load(open(os.path.join(dataDir,mus,'parameters',mus+'_IM.pkl'), 'rb'))[0]
    
    parms = [tmPar[k] for k in param_keys]
    tmParms.append(parms)
    parms = [imPar[k] for k in param_keys]
    imParms.append(parms)
    
#%% Create pandas dataframe
df = pd.DataFrame(list(zip(*tmParms,*imParms)), columns=col_keys, index=param_keys)

# Some are in other units so..
for parm in ['b', 'lce_opt', 'lpee0', 'lsee0', 'tact', 'tdeact']:
    df.loc[parm] = df.loc[parm]*1e3
for parm in ['kpee', 'ksee']:
    df.loc[parm] = df.loc[parm]/1e3

# Add unit as column
df.insert(0, "Unit", unit)

#%% TeX table
df_gt = df.copy()
df_gt.index = row_labels
df_gt = df_gt.reset_index()
df_tex = df_gt

label_dict = {'index': 'Parameter', **dict(zip(col_keys, col_labels))}

gt_table = (GT(df_tex)
    .cols_align(align='center') 
    .cols_align(align='left', columns="index")
    .tab_spanner(label='Traditional method', columns=['TM_GMe1', 'TM_GMe2', 'TM_GMe3'])
    .tab_spanner(label='Improved method', columns=['IM_GMe1', 'IM_GMe2', 'IM_GMe3'])
    .cols_label(**label_dict)
    .fmt_number(columns=col_keys, n_sigfig=3, sep_mark='')
)

# Transform to LateX table
latex_str = make_latex(gt_table.as_latex())
latex_str = delete_rows(latex_str, row_numbers=[0,1])
add_rows = {
    0: r"  & & \multicolumn{3}{c|}{\itshape Traditional method} & \multicolumn{3}{c|}{\itshape Improved method} \\ \hline",
    1: r"  \bfseries Parameter & \bfseries Unit & \bfseries Rat 1 & \bfseries Rat 2 & \bfseries Rat 3 & \bfseries Rat 1 & \bfseries Rat 2 & \bfseries Rat 3 \\ \hline"
}
latex_str = insert_rows(latex_str, add_rows)
#latex_str = replace_latex_table_cell(latex_str, row=5, col=1, new_text=r'N/mm\textsuperscript{2}')
#latex_str = replace_latex_table_cell(latex_str, row=6, col=1, new_text=r'N/mm\textsuperscript{2}')

# Write to a .tex file
with open("supptbl-insitu.tex", "w", encoding="utf-8") as f:
    f.write(latex_str)

# %% GT
gt_df = df.copy()
gt_df.index = row_labels
gt_df = gt_df.reset_index()
label_dict = {'index': '', **dict(zip(col_keys, col_labels))}

colw = '12.5%'
gt_table = (GT(gt_df)
    .cols_align(align='center') 
    .cols_align(align='left', columns="index")
    .tab_spanner(label='Traditional method', columns=['TM_GMe1', 'TM_GMe2', 'TM_GMe3'])
    .tab_spanner(label='Improved method', columns=['IM_GMe1', 'IM_GMe2', 'IM_GMe3'])
    .cols_label(**label_dict)
    .tab_style(
        style=style.borders(sides=["top", "bottom"], weight='2px', color="red"),
        locations=loc.body(rows=[4])
    )
    .fmt_number(columns=col_keys, n_sigfig=3, sep_mark='')
    .cols_width(cases={'index': colw, 'Unit': colw, 
        'TM_GMe1': colw, 'TM_GMe2': colw, 'TM_GMe3': colw,
        'IM_GMe1': colw, 'IM_GMe2': colw, 'IM_GMe3': colw})
)

gt_table
Table S2
Estimated parameter values of the experimentally measured in situ data using both the traditional as well as the improved method.
Unit Traditional method Improved method
Rat 1 Rat 2 Rat 3 Rat 1 Rat 2 Rat 3
$a$ N 8.90 11.3 10.5 8.82 10.0 11.0
$b$ mm 76.7 108 84.6 81.9 105 90.6
$F_{CE}^{max}$ N 15.6 14.1 17.1 15.5 14.1 17.0
$k_{PEE}$ N/mm2 9.83 7.25 21.9 28.7 24.1 34.8
$k_{SEE}$ N/mm2 608 688 654 952 1260 1050
$L_{CE}^{opt}$ mm 12.4 12.9 12.3 13.7 14.7 13.3
$L_{PEE}^0$ mm 12.4 12.4 13.3 15.1 15.6 14.5
$L_{SEE}^0$ mm 29.0 29.7 25.1 28.8 29.3 25.2
$\tau_{act}$ ms 43.0 32.4 23.6 55.2 57.7 41.6
$\tau_{deact}$ ms 23.8 20.9 19.7 27.1 25.3 22.4

Table S3

Code
# %% Imports
import pickle, os, sys, glob
import numpy as np
import pandas as pd
from pathlib import Path
from great_tables import GT

# Set directories
cwd = Path.cwd()
baseDir = cwd.parent
dataDir = baseDir / 'data'
funcDir = baseDir / 'analysis' / 'functions'
sys.path.append(str(funcDir))

from stats import rmse
from helpers import get_stim
from gt_tex import make_latex, insert_rows, delete_rows

#%% Parameters
par_models = ['TM', 'IM']
experiments = ['QR', 'SR', 'ISOM', 'SSC']
muscles = ['GMe1', 'GMe2', 'GMe3']

#%% Create DataFrame: rows = experiments, columns = GMe1_UM, GMe1_CM, ..., Average_UM, etc.
columns = [f'{model}_{exp}' for model in par_models for exp in experiments]
rows = muscles + ['Avg ± Std']
df = pd.DataFrame(index=rows, columns=columns, dtype=str)

#%% Loop through experiments
for exp in experiments:
    for model in par_models:
        all_rmsd = []
        for mus in muscles:
            # Load parameter
            parFile = os.path.join(dataDir, mus, 'parameters', f'{mus}_{model}.pkl')
            muspar = pickle.load(open(parFile, 'rb'))[0]

            dataDirExp = os.path.join(dataDir, mus, 'dataExp', exp)
            rrunDirExp = os.path.join(dataDir, mus, 'simsExp', model, exp)

            dataFiles = sorted(glob.glob(os.path.join(dataDirExp, '*')))
            rrunFiles = sorted(glob.glob(os.path.join(rrunDirExp, '*')))

            rms_list = []
            for dataFile, rrunFile in zip(dataFiles, rrunFiles):
                dataFilename = os.path.basename(dataFile)[:-4]
                rrunFilename = os.path.basename(rrunFile)[:-4]
                if dataFilename[:-3] != rrunFilename[:-3]:
                    raise ValueError(f"File mismatch: {dataFilename} vs {rrunFilename}")

                dataData = pd.read_csv(dataFile).T.to_numpy()[0:4]
                rrunData = pd.read_csv(rrunFile).T.to_numpy()

                time1, _, stim1, fsee1 = dataData
                time2, _, _, fsee2 = rrunData

                tStimOn, tStimOff = get_stim(time1, stim1)[1:]
                iStart = int(np.argmin(abs(time1 - tStimOn[0])))

                if exp == 'ISOM':
                    iStop = int(np.argmin(abs(time1 - 0.1 - tStimOff[0])))
                elif exp in ['QR', 'SR']:
                    iStop = int(np.argmin(abs(time1 - tStimOff[0])))
                else:  # SSC
                    iStop = int(np.argmin(abs(time1 - tStimOff[-1])))

                rms = rmse(fsee1[iStart:iStop], fsee2[iStart:iStop]) / muspar['fmax'] * 100
                rms_list.append(rms)

            M = np.mean(rms_list)
            S = np.std(rms_list)

            df.loc[mus, f'{model}_{exp}'] = f'{M:.1f} ± {S:.1f}'
            all_rmsd += rms_list  # just mean, for averaging across muscles

        # Average over muscles for this (exp, model)
        avg_M = np.mean(all_rmsd)
        avg_S = np.std(all_rmsd)
        df.loc['Avg ± Std', f'{model}_{exp}'] = f'{avg_M:.1f} ± {avg_S:.1f}'

#%% Mapping of rows and columns
# Rows: affected parameters
rows = [
    ['GMe1',        'Rat 1'], 
    ['GMe2',        'Rat 2'], 
    ['GMe3',        'Rat 3'], 
    ['Avg ± Std',   'Avg ± Std']]
row_keys, row_labels = map(list, zip(*rows))

cols = [
    ['TM_QR',     'QR'],
    ['TM_SR',     'SR'],
    ['TM_ISOM',   'ISOM'],
    ['TM_SSC',    'SSC'],
    ['IM_QR',     'QR'],
    ['IM_SR',     'SR'],
    ['IM_ISOM',   'ISOM'],
    ['IM_SSC',    'SSC'],
]
col_keys, col_labels = map(list, zip(*cols))

label_dict = {'index': '', **dict(zip(col_keys, col_labels))}

# %% TeX table
df_tex = df.copy()
df_tex.index = row_labels
df_tex = df_tex.reset_index()

gt_table = (GT(df_tex)
    .cols_align(align='center') 
    .cols_align(align='left', columns="index")
    .tab_spanner(label='Traditional method', columns=col_keys[:4])
    .tab_spanner(label='Improved method', columns=col_keys[4:])
    .cols_label(**label_dict)
)

# Transform to LateX table
latex_str = make_latex(gt_table.as_latex())
latex_str = delete_rows(latex_str, row_numbers=[0,1])
add_rows = {
    0: r" & \multicolumn{4}{c|}{\itshape Traditional method} & \multicolumn{4}{c|}{\itshape Improved method} \\ \hline",
    1: r" & \bfseries QR & \bfseries SR & \bfseries ISOM & \bfseries SSC & \bfseries QR & \bfseries SR & \bfseries ISOM & \bfseries SSC \\ \hline"
}
latex_str = insert_rows(latex_str, add_rows)
latex_str += r"\break\hfill\footnotesize{Root mean squared differences are expressed as a percentage of the maximal isometric CE force ($F_{CE}^{max}$). Root mean squared differences were computed over the interval in which CE stimulation was maximal for the quick-release and step-ramp experiments and was computed over the interval from CE stimulation onset to 0.1 s after CE stimulation offset for the isometric experiments and the stretch-shortening cycles.}"
print(latex_str)

# Write to a .tex file
with open("supptbl-rmsd.tex", "w", encoding="utf-8") as f:
    f.write(latex_str)

# %% GT Table
df_gt = df.copy()
df_gt.index = row_labels
df_gt = df_gt.reset_index()

gt_table = (GT(df_gt)
    .cols_align(align='center') 
    .cols_align(align='left', columns="index")
    .tab_spanner(label='Traditional method', columns=col_keys[:4])
    .tab_spanner(label='Improved method', columns=col_keys[4:])
    .cols_label(**label_dict)
)
Table S3
Root mean squared differences between experimentally measured SEE force histories and those predicted by Hill-type MTC model after estimating all contraction and excitation dynamics parameter values.
Traditional method Improved method
QR SR ISOM SSC QR SR ISOM SSC
Rat 1 5.6 ± 3.4 5.6 ± 0.8 6.8 ± 3.2 6.2 ± 3.0 3.3 ± 1.5 2.8 ± 0.9 5.4 ± 3.0 5.4 ± 3.1
Rat 2 4.2 ± 1.5 5.0 ± 0.6 4.3 ± 1.6 4.6 ± 2.2 2.7 ± 1.6 2.1 ± 0.5 4.2 ± 2.6 4.2 ± 2.0
Rat 3 6.4 ± 2.3 6.8 ± 1.0 6.1 ± 2.1 5.5 ± 2.7 4.2 ± 1.8 2.6 ± 0.5 4.9 ± 2.3 5.0 ± 2.7
Avg ± Std 5.4 ± 2.6 5.8 ± 1.1 5.7 ± 2.7 5.4 ± 2.7 3.4 ± 1.8 2.5 ± 0.7 4.8 ± 2.7 4.9 ± 2.7