├── README.md └── TCP_NTCP.py /README.md: -------------------------------------------------------------------------------- 1 | # TCP/NTCP Radiobiological model 2 | Python code used to model the variation in TCP and NTCP with dose fraction specific dose variations. 3 | There is huge flexibility in variation of the delivered dose per fraction. 4 | Doses can be generated within the model (as used heavily within the thesis) or can be supplied as a list of doses (which must be done for the NTCP model; a list of doses can easily be gneerated from the TCP model and then supplied for NTCP calcualtions). 5 | 6 | ## Example usage 7 | This example generates a set of TCP reuslts for 1000 patients and then plots the individual results and the population mean. 8 | There are 1000 patients with alpha/beta mean of 10 and standard deviation of 20% of the mean. 9 | Nominal dose is 2Gy/#. There is no offset of dose from the mean, but delivered dose per fraction has a standard deviation of 1.5%. 10 | A 5% drift in beam output per year is included and specific results are returned at 74Gy. 11 | Usage of the NTCP model is similar. 12 | 13 | ```python 14 | # Import the modules 15 | import TCP_NTCP as model 16 | import matplotlib.pyplot as plt 17 | 18 | 19 | # Generate a set TCP of results 20 | results = model.completeTCPcalc(n=1000, # number of patients in population to model 21 | alphabeta_use=10, # mean alpha/beta 22 | alphabeta_sd_use=20, # SD of alpha/beta (%) 23 | d=2, # nominal dose (Gy/fraction) 24 | d_shift=0, # initial dose difference (%) 25 | d_sd=1.5, # standard deviation of delivered dose (%) 26 | d_trend=5/365, # dose drift (%/day) 27 | max_d=100, # maximum dose for which TCP is calcualted (Gy) 28 | dose_of_interest=74, # results of TCP at this dose are returned seperately for simpler analysis. 29 | n0 = 74) # Specified N0 value (can be determined by fitting to a defined population) 30 | 31 | 32 | # Produce a plot the results 33 | 34 | # plot the first 100 individual curves 35 | for i in range(100): 36 | plt.plot(results['nom_doses'],results['TCPs'][i],c='C1',alpha=0.3,lw=1) 37 | 38 | # plot population mean 39 | plt.plot(results['nom_doses'],results['TCP_pop'],ls='-',c='C0',marker='o',ms=3) 40 | plt.show() 41 | 42 | # Generate set of NTCP results 43 | 44 | ntcp_results = model.complete_NTCP_calc(d_data=[64,74], # dose values on NTCP curve 45 | ntcp_data=[0.36,0.47], # NTCP values corresponding to the above doses 46 | irrad_perc = 100, # scaling factor 47 | frac_doses=all_d_array, # this will be a list of dose values to compute the NTCP for 48 | max_dose=100, # maximum calculation dose 49 | ntcp_params={'td50_1':(31.4,0), 50 | 'v': (0.72,0), ## v=1 for parotid (e.g. low for rectum (0.08,10)) 51 | 'm':(0.53,0), # m paremter in NTCP equation (may have ot be found through optimisation 52 | 'n':(1,0)}, # n parameter in NTCP equation 53 | fit_vals=False)['patient_ntcps'] #[0][29] ## this will reuturn the NTCP after 30# 54 | ``` 55 | -------------------------------------------------------------------------------- /TCP_NTCP.py: -------------------------------------------------------------------------------- 1 | 2 | # coding: utf-8 3 | 4 | # ### Created all parts as functions to allow to be run multiple times with varied parameters 5 | 6 | # ### Import Required Modules 7 | 8 | # In[1]: 9 | 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | import scipy as sp 13 | import scipy.optimize as opt 14 | from scipy import stats 15 | import pandas as pd 16 | import random 17 | 18 | ## hide some numpy warnings 19 | import warnings 20 | warnings.filterwarnings("ignore", category=np.VisibleDeprecationWarning) 21 | 22 | #%matplotlib qt 23 | # qt if plot in seperate window 24 | # inline if plot on page 25 | 26 | ## Set the number of d.p. displayed in numpy arrays 27 | np.set_printoptions(precision=3) 28 | 29 | ## Progress bar widget 30 | #from IPython.html.widgets import FloatProgress 31 | from IPython.display import display 32 | 33 | ## allow timing of events 34 | import time 35 | 36 | ## allow export of results as csv 37 | import csv 38 | 39 | ## allow set of values to iterate over to be created 40 | import itertools 41 | 42 | ## allow to get current workign directory for info. 43 | import os 44 | 45 | import datetime as dt 46 | 47 | ## Removed depreciation warning due to ints vs floats. Will not change results 48 | import warnings 49 | warnings.filterwarnings("ignore", category=DeprecationWarning) 50 | 51 | 52 | # In[2]: 53 | 54 | # ## For rounding to nearest nuber with specified precision use round_to(n,10) for nearest 10 for example. 55 | # def round_to(n, precision): 56 | # correction = 0.5 if n >= 0 else -0.5 57 | # return int( n/precision+correction ) * precision 58 | 59 | # def round_to_05(n): 60 | # return round_to(n, 0.05) 61 | 62 | 63 | # In[3]: 64 | 65 | ### Alpha beta calculator - normal dist 66 | def alphacalc_normal(alphabeta, sd): 67 | """Return alphabetanew and alpha from normal distribution as specified by sd. 68 | Default is beta = 0.03 69 | 'alphabeta' is the alphabeta ratio 70 | If a negative value is returned it is resampled until positive""" 71 | 72 | beta = 0.03 # fixed beta in function 73 | 74 | ## get alpha beta to use from normal distribution 75 | if sd == 0: 76 | alphabetanew = alphabeta 77 | else: 78 | alphabetanew=np.random.normal(loc = alphabeta, scale = sd) 79 | 80 | ## make sure a positive value is returned 81 | while alphabetanew <= 0: 82 | alphabetanew=np.random.normal(loc = alphabeta, scale = sd) 83 | 84 | alpha = beta*alphabetanew 85 | 86 | return alpha, beta 87 | ## alpha/beta can be calced form the returned alpha and beta values 88 | 89 | 90 | # In[4]: 91 | 92 | ### Alpha beta calculator - log normal dist 93 | 94 | def alphacalc_lognormal(alphabeta, sd_perc,set_beta=None): 95 | """Return alphabetanew and alpha from normal distribution as specified by sd. 96 | Default is beta = 0.03 97 | 'alphabeta' is the alphabeta ratio mean 98 | sd supplied as percentage 99 | """ 100 | if set_beta==None: 101 | beta = 0.03 # fixed beta in function 102 | else: 103 | beta = set_beta 104 | #print('beta was set to:',beta) 105 | 106 | ## convert sd from percentage to absolute 107 | sd = alphabeta*sd_perc/100 108 | 109 | alphabeta_lognormal = np.log((alphabeta**2)/(np.sqrt((sd**2)+(alphabeta**2)))) 110 | sd_lognormal = np.sqrt(np.log(((sd**2)/(alphabeta**2))+1)) 111 | 112 | ## get alpha beta to use from normal distribution 113 | if sd == 0: 114 | alphabetanew = alphabeta 115 | else: 116 | alphabetanew=np.random.lognormal(mean = alphabeta_lognormal, sigma = sd_lognormal) 117 | 118 | alpha = beta*alphabetanew 119 | 120 | return alpha, beta 121 | ## alpha/beta can be calced form the returned alpha and beta values 122 | 123 | # ###N0 varies depending on the a/b Std Dev!!! 124 | 125 | #%% 126 | 127 | ## function to calculate the difference between input TCP and calculated TCP. only N0 is varied as want to estimate this. 128 | def calc_dif_sq(x,TCP, n, alphabeta_use, alphabeta_sd_use,d,d_shift,d_sd,d_trend,max_d,dose_of_interest,dose_input,TCP_input,weights_input=None): 129 | 130 | ## tcp/dose_input must be provided as a list or this will not work (even if single valued). 131 | 132 | if weights_input == None: 133 | weights = [1]*len(TCP_input) 134 | else: 135 | weights = weights_input 136 | #print(weights) 137 | 138 | TCP_in = TCP_input 139 | #print(len(TCP_input)) 140 | 141 | TCP_Dif_sq_sum = 0 142 | for i in range(len(TCP_input)): 143 | #print('TCP list val: ' + str(i)) 144 | TCP_calc_all = completeTCPcalc(n=n, 145 | alphabeta_use=alphabeta_use, 146 | alphabeta_sd_use=alphabeta_sd_use, 147 | d=d, 148 | d_shift=d_shift, 149 | d_sd=d_sd, 150 | d_trend=d_trend, 151 | n0=x, # this needs to be able to vary to allow optimisation of the value 152 | max_d=max_d, 153 | dose_of_interest=dose_input[i], 154 | dose_input=dose_input[i], 155 | TCP_input=TCP_input[i]) 156 | TCP_result = TCP_calc_all['TCP_cure_percent'] ## Get only the result of interest (TCP at d_int is in posn. 10 in the returned tuple) 157 | #print(weights[i]/sum(weights)) 158 | TCP_Dif_sq = (weights[i]/sum(weights))*((TCP_in[i] - TCP_result)**2) ## difference in squares to minimise 159 | TCP_Dif_sq_sum = TCP_Dif_sq_sum + TCP_Dif_sq 160 | #print(TCP_Dif_sq_sum) 161 | return TCP_Dif_sq_sum 162 | 163 | #%% 164 | 165 | ## function to calculate the difference between input TCP and calculated TCP. only N0 is varied as want to estimate this. 166 | def calc_dif_sq_ab_sd(x, 167 | TCP, 168 | n, 169 | alphabeta_use, 170 | d, 171 | d_shift, 172 | d_sd, 173 | d_trend, 174 | n0, 175 | max_d, 176 | dose_of_interest, 177 | dose_input, 178 | TCP_input, 179 | weights_input=None, 180 | set_beta=None): 181 | 182 | ## tcp/dose_input must be provided as a list or this will not work (even if single valued). 183 | 184 | if weights_input == None: 185 | weights = [1]*len(TCP_input) 186 | else: 187 | weights = weights_input 188 | #print(weights) 189 | 190 | TCP_in = TCP_input 191 | #print(len(TCP_input)) 192 | #print('tcp input',TCP_input) 193 | 194 | TCP_Dif_sq_sum = 0 195 | for i in range(len(TCP_input)): 196 | #print('TCP list val: ' + str(i)) 197 | TCP_calc_all = completeTCPcalc(n=n, 198 | alphabeta_use=alphabeta_use, 199 | alphabeta_sd_use=x,# this needs to be able to vary to allow optimisation of the value 200 | d=d, 201 | d_shift=d_shift, 202 | d_sd=d_sd, 203 | d_trend=d_trend, 204 | n0=n0, 205 | max_d=max_d, 206 | dose_of_interest=dose_input[i], 207 | dose_input=dose_input[i], 208 | TCP_input=TCP_input[i], 209 | set_beta=set_beta) 210 | TCP_result = TCP_calc_all['TCP_cure_percent'] ## Get only the result of interest (TCP at d_int is in posn. 10 in the returned tuple) 211 | #print(weights[i]/sum(weights)) 212 | #print(TCP_in,TCP_result) 213 | TCP_Dif_sq = (weights[i]/sum(weights))*((TCP_in[i] - TCP_result)**2) ## difference in squares to minimise 214 | TCP_Dif_sq_sum = TCP_Dif_sq_sum + TCP_Dif_sq 215 | #print('dif sqare',TCP_Dif_sq_sum) 216 | return TCP_Dif_sq_sum 217 | 218 | #%% 219 | 220 | ### 221 | ## N0 and ab_sd shoudl be allowed to vary for fitting as these determine 222 | ## the steepness/position of the curve. 223 | ## However if values are apssed to the function, then these should be abel to be used. 224 | ## the fit shoudl vary both parameters within its optimisation function 225 | ## minimize_scalar does now allow this? 226 | ## will need to use more generic minimisation funciton? 227 | ### 228 | 229 | def n0_determination(TCP_input, 230 | n, 231 | n0, 232 | alphabeta_use, 233 | alphabeta_sd_use, 234 | d, 235 | d_shift, 236 | d_sd, 237 | d_trend, 238 | max_d, 239 | dose_of_interest, 240 | dose_input, 241 | weights_input = None, 242 | repeats = 5): 243 | 244 | ## TCP to fit N0 to. This will come from the literature. 245 | 246 | ## IF value of N0 is supplied, then do not need to fit it... 247 | #print('d_input: ' + str(dose_input)) 248 | #print('dose_of_interest: ' +str(dose_of_interest)) 249 | #print(weights_input) 250 | 251 | ## Run optimisation multiple times to ensure accuracy (as based on random data) 252 | 253 | repeats_min = 5 254 | if repeats < repeats_min: 255 | repeats = repeats_min 256 | print('Number of repeats for fitting N0 has been set to the minimum reccomended value of ' + str(repeats_min)) 257 | else: 258 | repeats = repeats 259 | n=n#100 # set value of n for reliable fitting # use 1000 for final results? 260 | n0=n0 261 | alphabeta_use=alphabeta_use 262 | alphabeta_sd_use=alphabeta_sd_use 263 | d=d 264 | d_shift=d_shift 265 | d_sd=d_sd 266 | d_trend=d_trend 267 | max_d=max_d 268 | dose_of_interest=dose_of_interest 269 | TCP_input = TCP_input 270 | dose_input = dose_input 271 | 272 | ## store the fit results in this list 273 | fit_results=[] 274 | 275 | #TCP, n, alphabeta_use, alphabeta_sd_use,d,d_shift,d_sd,max_d,dose_of_interest 276 | 277 | ### This is minimising the difference of squares returned by the function. 278 | ### This could probably be made to return the value if multiple TCP/Dose points are passed to it? 279 | 280 | print('Fitting N0 value') 281 | 282 | for i in range(0,repeats): 283 | print('\r' + 'Fitting N0: Stage ' + str(i+1) + ' of ' + str(repeats), end="") 284 | n0_result = opt.minimize_scalar(calc_dif_sq,method='brent', 285 | args=(TCP_input, 286 | n, 287 | alphabeta_use, 288 | alphabeta_sd_use, 289 | d, 290 | d_shift, 291 | d_sd, 292 | d_trend, 293 | max_d, 294 | dose_of_interest, 295 | dose_input, 296 | TCP_input, 297 | weights_input)) 298 | #print(n0_result) 299 | fit_results.append(n0_result.x) 300 | fit_results.sort() # sort the repeated results to eliminate outliers 301 | #print(fit_results) 302 | #print(np.mean(fit_results)) 303 | num_outliers = 0 #1 **************************** set this. 304 | if num_outliers==0: 305 | fit_results_trim = fit_results 306 | else: 307 | fit_results_trim = fit_results[num_outliers:-num_outliers] 308 | 309 | n0_mean_fit = sum(fit_results_trim)/len(fit_results_trim) 310 | print('N0 fit:',n0_mean_fit) 311 | print('') 312 | print('Fitting Completed') 313 | 314 | return n0_mean_fit#, TCP_Calc_min[10] 315 | 316 | 317 | #%% 318 | 319 | ## fit a/b SD instead of N0. Leave both as an option in the model? i.e. if specify a/b fit then do this, if not can still do N0 fit. 320 | def ab_sd_determination(TCP_input, 321 | n, 322 | alphabeta_use, 323 | d, 324 | d_shift, 325 | d_sd, 326 | d_trend, 327 | max_d, 328 | dose_of_interest, 329 | dose_input, 330 | n0, 331 | weights_input = None, 332 | set_beta=None, 333 | ab_n=None): 334 | 335 | 336 | #n=1000#100 # set value of n for reliable fitting # use 1000 for final results? 337 | if ab_n==None: 338 | n=1000 ## default n for fitting 339 | else: 340 | n=ab_n 341 | print('ab_n set to:',n) 342 | n0=n0 343 | alphabeta_use=alphabeta_use 344 | d=d 345 | d_shift=d_shift 346 | d_sd=d_sd 347 | d_trend=d_trend 348 | max_d=max_d 349 | dose_of_interest=dose_of_interest 350 | TCP_input = TCP_input 351 | dose_input = dose_input 352 | set_beta=set_beta 353 | #print('tcp_input',TCP_input) 354 | #TCP, n, alphabeta_use, alphabeta_sd_use,d,d_shift,d_sd,max_d,dose_of_interest 355 | 356 | ### This is minimising the difference of squares returned by the function. 357 | ### This could probably be made to return the value if multiple TCP/Dose points are passed to it? 358 | 359 | print('Fitting a/b SD value...') 360 | 361 | ab_sd_result = opt.minimize_scalar(calc_dif_sq_ab_sd,method='Bounded', #'brent' 362 | bounds=(0,100), 363 | args=(TCP_input, 364 | n, 365 | alphabeta_use, 366 | d, 367 | d_shift, 368 | d_sd, 369 | d_trend, 370 | n0, 371 | max_d, 372 | dose_of_interest, 373 | dose_input, 374 | TCP_input, 375 | weights_input, 376 | set_beta)) 377 | 378 | 379 | #print('ab sd fit:',ab_sd_result) 380 | # print('') 381 | print('Fitting Completed') 382 | 383 | ab_sd_result_perc = ab_sd_result.x 384 | #print(ab_sd_result_perc) 385 | 386 | return ab_sd_result_perc 387 | ## need to optimise the dif in squares of the TCP at point of interest as for N0. 388 | ## but now set a/bSD as the parameter to optimise... 389 | ## return the ab_sd_mean_fit 390 | 391 | 392 | # In[9]: 393 | 394 | ###calculate dose for a given fraction based on normal distribution around dose shift 395 | ### Log normal distribution not necessary as SD small wrt mean so v unlikely to get negative values returned. 396 | ### Could also set a limit on the range (say to +/- 5%)? But wont make much difference with a small SD anyway. 397 | 398 | def fracdose(dose, shift, sd): 399 | """Return dose_actual from normal distribution around dose (Gy) as specified by sd (%) and shift (%). 400 | Default is dose = 2Gy, shift = 0%, and sd of 0% 401 | If a negative value is returned it is resampled until positive (use log-normal?) 402 | The standard deviation is of the nominal dose""" 403 | 404 | ## get actual dose to use from normal distribution based on shift 405 | 406 | dose_shift = dose + (dose*shift/100) 407 | 408 | ## if sd is zero, then no change to dose 409 | if sd == 0: 410 | dose_actual = dose_shift 411 | return dose_actual 412 | 413 | dose_actual=np.random.normal(loc = dose_shift, scale = (dose*sd/100)) 414 | 415 | ## make sure a positive value is returned 416 | while dose_actual <= 0: 417 | dose_actual=np.random.normal(loc = dose_shift, scale = (dose*sd/100)) 418 | 419 | return dose_actual 420 | 421 | 422 | # In[10]: 423 | 424 | ## Survival Fraction Calculation 425 | def SFcalc(alpha, beta, dose): 426 | """Return the SF with input values. 427 | Note this is for a single dose delivery. 428 | The product of multiple fractions shoudld be taken 429 | to give overall SF""" 430 | 431 | SF = np.exp(-(alpha*dose) - (beta*(dose**2))) 432 | 433 | return SF 434 | 435 | 436 | # In[11]: 437 | 438 | ## TCP Calculation absed on cumulative SF 439 | def TCPcalc(sf, n0): 440 | """Return the TCP with input values. 441 | Based on cumulative SF and N0""" 442 | 443 | TCP = np.exp(-n0*sf) 444 | 445 | return TCP 446 | 447 | 448 | # In[12]: 449 | 450 | ## Calc Number of fractions to get to max dose (note: round up as always want an integer) 451 | 452 | def no_frac_nom_doses_array(max_d, d): 453 | n_frac = np.ceil(max_d/d) 454 | 455 | fractions = np.arange(1,n_frac+1) 456 | #print(fractions) 457 | nom_doses = np.arange(d,(d*n_frac)+d, step = d) 458 | #print(nom_doses) 459 | return fractions, nom_doses, n_frac 460 | 461 | 462 | # In[13]: 463 | 464 | ## This gives a column with the patient number and makes it easier to check values as going 465 | 466 | def create_patients(n): 467 | 468 | if n<1: 469 | n=1 470 | patients = np.arange(0,n)+1 471 | patients.shape=(n,1) 472 | #print(patients) 473 | return patients 474 | 475 | 476 | # In[14]: 477 | 478 | ## empty array to store alpha values in (log normal distribution to be used) 479 | 480 | def create_alpha_beta_array(n, alphabeta_use, alphabeta_sd_use,set_beta=None): 481 | alpha_and_beta =[] 482 | 483 | for p in range(0,n): 484 | #alpha_and_beta = np.append(alpha_and_beta,[alphacalc_normal(alphabeta = alphabeta_use, sd=alphabeta_sd_use)]) 485 | alpha_and_beta.append([alphacalc_lognormal(alphabeta = alphabeta_use, sd_perc=alphabeta_sd_use,set_beta=set_beta)]) 486 | 487 | ## reshape to get a row per patient 488 | alpha_and_beta_np = np.array(alpha_and_beta) 489 | alpha_and_beta_np = np.reshape(alpha_and_beta_np,(n,2)) 490 | #print(alpha_and_beta) 491 | return alpha_and_beta_np 492 | 493 | 494 | # In[15]: 495 | 496 | ## Calculate Doses for all patients and all fractions and put in an array 497 | ## Added in a linear output trend. Specify in percent per day/treatment. Default = 0 498 | 499 | def doses_array(n, n_frac, d, d_shift, d_sd, d_trend=0): 500 | doses = [] 501 | for i in range(0,int(n*n_frac)): 502 | doses.append(fracdose(dose = d, shift=d_shift, sd=d_sd)) 503 | doses_np = np.array(doses) 504 | doses_np = np.reshape(doses_np,(n,n_frac)) 505 | 506 | ## Make an array of the trend to multiply the doses array with 507 | trend_array = [] 508 | for i in range(int(n_frac)): 509 | trend_array.append(1+(i*d_trend/100)) 510 | 511 | ## multiply array of doses by trend value for each fraction 512 | doses_np_trend = doses_np*trend_array 513 | 514 | #print(doses) 515 | return doses_np_trend 516 | 517 | 518 | # In[18]: 519 | 520 | ## Combine all results into single array which may be easier to work with for analysis 521 | 522 | def combine_results(patients, alpha_and_beta, doses): 523 | results_wanted = (patients,alpha_and_beta, doses) 524 | all_results = np.concatenate(results_wanted, axis=1) 525 | #print(all_results) 526 | return all_results 527 | 528 | 529 | # In[19]: 530 | 531 | ## Loop through the doses of the first patient (first row [0] of array) 532 | def calc_all_SFs(patients, n, n_frac, alpha_and_beta, doses): 533 | SFs = [] 534 | 535 | for i in range(0,len(patients)): # loop through each patient (row) 536 | for j in range(0,int(n_frac)): # loop through each fraction for each patient (col) 537 | SFs.append(SFcalc(alpha_and_beta[i][0],alpha_and_beta[i][1],doses[i,j])) 538 | 539 | SFs_np = np.array(SFs) 540 | SFs_np = np.reshape(SFs_np,(n,n_frac)) 541 | 542 | ## GEt cumulative SF for each patient 543 | SF_cum = np.cumprod(SFs_np, axis=1) 544 | return SFs_np, SF_cum 545 | 546 | 547 | # In[20]: 548 | 549 | ## append results to text file. 550 | 551 | def saveasCSV(filename, array): 552 | 553 | fl = open(filename, 'a', newline='\n') 554 | 555 | writer = csv.writer(fl) 556 | writer.writerow(array) 557 | 558 | fl.close() 559 | 560 | 561 | #%% 562 | def d_list_sort(d_list,n_frac,n): 563 | d_list_pad = d_list + [0] * (n_frac - len(d_list))# pad with zero doses 564 | #print(d_list_pad) 565 | doses = [d_list_pad for i in range(n)] # set the provided list as the doses 566 | doses = np.array(doses) # convert to numpy array for use to with other data types 567 | return doses 568 | 569 | 570 | # In[21]: 571 | 572 | ## Calc Number of fractions and nominal dose per fraction to get to max dose 573 | 574 | def completeTCPcalc(n, 575 | alphabeta_use, 576 | alphabeta_sd_use, 577 | d, 578 | d_shift, 579 | d_sd, 580 | d_trend, 581 | max_d, 582 | dose_of_interest, 583 | dose_input = None, 584 | TCP_input=None, 585 | d_list=None, 586 | n0=None, 587 | repeats = None, 588 | weights_input=None, 589 | set_beta=None, 590 | ab_n=None): 591 | 592 | #print('tcp input',TCP_input) 593 | 594 | fractions, nom_doses, n_frac = no_frac_nom_doses_array(max_d,d) 595 | 596 | if d_list is not None: 597 | doses = d_list_sort(d_list,n_frac,n) 598 | #d_list_pad = d_list + [0] * (n_frac - len(d_list))# pad with zero doses 599 | #print(d_list_pad) 600 | #doses = [d_list_pad for i in range(n)] # set the provided list as the doses 601 | #doses = np.array(doses) # convert to numpy array for use to with other data types 602 | else: 603 | doses = doses_array(n, n_frac, d, d_shift, d_sd, d_trend) 604 | #print(doses) 605 | #** 606 | ## array of doses after each fraction for each patient 607 | 608 | ## create array containing number of patients in population 609 | patients = create_patients(n) 610 | #print(patients) 611 | 612 | 613 | ## need to determine ab_sd prior to creating array. 614 | tcp_ab_fit = None 615 | if alphabeta_sd_use=='fit': 616 | alphabeta_sd_use_fit = ab_sd_determination([100*i for i in TCP_input], 617 | n, 618 | alphabeta_use, 619 | d, 620 | d_shift, 621 | d_sd, 622 | d_trend, 623 | max_d, 624 | dose_of_interest, 625 | dose_input, 626 | n0, 627 | weights_input, 628 | set_beta, 629 | ab_n) 630 | tcp_ab_fit = True 631 | #print('AB SD fitted: ',alphabeta_sd_use) 632 | else: 633 | alphabeta_sd_use_fit=alphabeta_sd_use 634 | tcp_ab_fit = False 635 | 636 | #print('AB SD fitted: ',alphabeta_sd_use_fit) 637 | ## Creat array of alpha and beta values for each patient 638 | alpha_and_beta = create_alpha_beta_array(n, alphabeta_use, alphabeta_sd_use_fit,set_beta) 639 | 640 | ## put all results in an array with a patient on each row 641 | all_results = combine_results(patients, alpha_and_beta, doses) 642 | 643 | ## Calc cumulative SF for all patients (also return individual fraction SFs) 644 | SFs, SF_cum = calc_all_SFs(patients, n, n_frac, alpha_and_beta, doses) 645 | 646 | ## determine N0 value to use if none provided 647 | if (n0 is None) and (TCP_input is not None) and (dose_input is not None): 648 | print('N0 not provided - will calculate N0') 649 | #print(weights_input) 650 | n0_use = n0_determination([100*i for i in TCP_input], 651 | n, 652 | n0, 653 | alphabeta_use, 654 | alphabeta_sd_use, 655 | d, 656 | d_shift, 657 | d_sd, 658 | d_trend, 659 | max_d, 660 | dose_of_interest, 661 | dose_input, 662 | weights_input) 663 | tpc_fit=True 664 | else: 665 | n0_use = n0 666 | tpc_fit=False 667 | 668 | ## Calculate TCP for all individual patients and fractions 669 | TCPs = TCPcalc(sf = SF_cum, n0=n0_use) 670 | 671 | ## Calculate population TCP by averaging down the columns 672 | TCP_pop = np.mean(TCPs, axis = 0) 673 | 674 | frac_of_interest = dose_of_interest/d #reinstate this 675 | #frac_of_interest = 74/d 676 | 677 | TCP_at_dose_of_interest = TCP_pop[frac_of_interest] 678 | 679 | #t_end = time.time() 680 | 681 | #t_total = t_end-t_start 682 | #print(str(round(t_total,2)) + ' secs for ' + str(n) + ' patients') 683 | 684 | TCPs_of_interest = TCPs[:,frac_of_interest-1] 685 | 686 | TCP_cure = (TCPs_of_interest).sum() 687 | TCP_cure_percent = 100*TCP_cure/n 688 | #print(TCP_cure_percent) 689 | #print(TCP_pop) 690 | 691 | return {'n':n, 692 | 'alphabeta_use':alphabeta_use, 693 | 'alphabeta_sd_use':alphabeta_sd_use, 694 | 'alphabeta_sd_use_fit':alphabeta_sd_use_fit, 695 | 'd':d, 696 | 'd_shift':d_shift, 697 | 'd_sd':d_sd, 698 | 'n0_use':n0_use, 699 | 'max_d':max_d, 700 | 'dose_of_interest':dose_of_interest, 701 | 'frac_of_interest':frac_of_interest, 702 | 'TCP_cure_percent':TCP_cure_percent, 703 | 'TCPs':TCPs, 704 | 'TCP_pop':TCP_pop, 705 | 'nom_doses':nom_doses, 706 | 'd_trend':d_trend, 707 | 'doses':doses, 708 | 'dose_input':dose_input, 709 | 'TCP_input':TCP_input, 710 | 'd_list':d_list, 711 | 'n0':n0, 712 | 'weights_input':weights_input, 713 | 'tcp_fit':tpc_fit, 714 | 'tcp_ab_fit':tcp_ab_fit, 715 | 'alpha_and_beta':alpha_and_beta 716 | } 717 | 718 | 719 | # In[23]: 720 | 721 | ## Return sum of squares [calc = calculation of TCP from TCP calc, ref = TCP_input] 722 | def sq_dif(calc, ref): 723 | sq_dif = (calc - ref)**2 724 | return sq_dif 725 | 726 | 727 | # In[28]: 728 | 729 | ## Round to nearest n (e.g. nearest 50 n=50) 730 | def round_n(x,n): 731 | return int(round(x / n)) * n 732 | 733 | 734 | # In[29]: 735 | 736 | ######***** Change this to calcualte a given percentage variation in dose? or allow both.....? 737 | 738 | ## This allows simple building of a tuple containing all possible combinations of values 739 | 740 | ## should probably not end up in the main TCP-NTCP file, but made by the user if required. 741 | 742 | def dose_iter(dose_var=0.5, 743 | dose_max=3, 744 | ab_var=1, 745 | ab_max=4, 746 | ab_min=2, 747 | ab_sd_var=0.5, 748 | ab_sd_max=1, 749 | ab_sd_min=0.5, 750 | di_var=0, 751 | di_max=74, 752 | di_min=74, 753 | n0_nominal=160, 754 | n0_var=5, 755 | n0_range=10): 756 | 757 | ## dose shifts to test 758 | dose_var = dose_var # dose step size 759 | dose_max = dose_max 760 | dose_min = -dose_max 761 | dose_number = (dose_max-dose_min)/dose_var+1 # number of points 762 | 763 | dose_vals = np.linspace(dose_min,dose_max,dose_number) # dose values to test 764 | 765 | ## alphabeta values to test 766 | ab_var = ab_var # step size 767 | ab_max = ab_max 768 | ab_min = ab_min 769 | ab_number = (ab_max-ab_min)/ab_var+1 # number of points 770 | ab_vals = np.linspace(ab_min,ab_max,ab_number) # different a/b values to test 771 | 772 | ## alphabeta_SD values to test 773 | ab_sd_var = ab_sd_var # step size 774 | ab_sd_max = ab_sd_max 775 | ab_sd_min = ab_sd_min 776 | ab_sd_number = (ab_sd_max-ab_sd_min)/ab_sd_var+1 # number of points 777 | ab_sd_vals = np.linspace(ab_sd_min,ab_sd_max,ab_sd_number) # different a/b values to test 778 | 779 | 780 | ## dose of interest to test 781 | di_var = di_var # dose step size 782 | di_max = di_max 783 | di_min = di_min 784 | di_number = (di_max-di_min)/di_var+1 # number of points 785 | di_vals = np.linspace(di_min,di_max,di_number) 786 | 787 | ## N0 values - determined from TCP model paramters input (from clinical data - coudl get form text file etc?) 788 | #n0_nominal = n0_determination(TCP=0.88,d=2,D=74,ab=3,b=0.02) 789 | n0_nominal = n0_nominal 790 | n0_var = n0_var 791 | n0_range = n0_range #percentage variation 792 | n0_min = round_n(n0_nominal * (1-n0_range/100),n0_var) 793 | n0_max = round_n(n0_nominal * (1+n0_range/100),n0_var) 794 | n0_number = (n0_max-n0_min)/n0_var+1 795 | n0_vals = np.linspace(n0_min,n0_max,n0_number) 796 | 797 | total_tests = len(dose_vals)*len(ab_vals)*len(di_vals)*len(n0_vals)*len(ab_sd_vals) 798 | test_val_iterator = itertools.product(dose_vals,ab_vals,di_vals,n0_vals,ab_sd_vals) 799 | 800 | test_vals = list(test_val_iterator) #list of all combinations of parameters 801 | num_its = len(test_vals) #number of iterations 802 | 803 | return test_vals,num_its 804 | 805 | #print(num_its) 806 | #print(n0_mean_fit) 807 | #print(n0_vals) 808 | 809 | 810 | # In[38]: 811 | 812 | ## vary multiple values through use of constructer iterator list above. 813 | 814 | #### This needs the N0 to be calculated before calculation starts. 815 | ### The values for the iterator above shouls also be calculated within this? 816 | #t1 = datetime.now() 817 | 818 | def TCP_full(k=10, 819 | TCP_input=80, 820 | repeats=20, 821 | n=1000, 822 | n0=169, 823 | alphabeta_use=3, 824 | alphabeta_sd_use=1, 825 | d=2, 826 | d_shift=0, 827 | d_sd=1, 828 | d_trend=0, 829 | max_d=100, 830 | dose_of_interest=74, 831 | save_name="TCPresultsProst-all_results-std.csv"): 832 | 833 | # gat variable values (does this really need doing???) 834 | TCP_input=TCP_input 835 | repeats=repeats 836 | n=n 837 | n0=n0 838 | alphabeta_use=alphabeta_use 839 | alphabeta_sd_use=alphabeta_sd_use 840 | d=d 841 | d_shift=d_shift 842 | d_sd=d_sd 843 | d_trend=d_trend 844 | max_d=max_d 845 | dose_of_interest=dose_of_interest 846 | save_name=save_name+".csv" 847 | 848 | #Print some values 849 | print("TCP Point at dose of interest: " + str(TCP_input)) 850 | print("Dose of interest: " + str(dose_of_interest)) 851 | 852 | print("Starting TCP Simulation") 853 | 854 | #N0 determination should use nominal values of dose to assume average? 855 | ###orig has n0_mean_fit, fit_results, trimmed = n0_determ.... 856 | if n0 is None: 857 | n0_mean_fit = n0_determination(TCP_input=TCP_input, 858 | repeats=repeats, 859 | n=n, 860 | n0=n0, 861 | alphabeta_use=alphabeta_use, 862 | alphabeta_sd_use=alphabeta_sd_use, 863 | d=d, 864 | d_shift=0, 865 | d_sd=0, 866 | d_trend=0, 867 | max_d=max_d, 868 | dose_of_interest=dose_of_interest) 869 | else: 870 | n0_mean_fit = n0 871 | 872 | set_precision = 5 #round to nearest 5 873 | n0_mean_fit_set_precision = round_n(n0_mean_fit,set_precision) 874 | 875 | ab_range = 1 876 | 877 | iter_list = dose_iter(dose_var=0.5, 878 | dose_max=4, 879 | ab_var=1, 880 | ab_max=alphabeta_use + ab_range, 881 | ab_min=alphabeta_use - ab_range, 882 | di_var=2, 883 | di_max=80, 884 | di_min=64, 885 | n0_nominal=n0_mean_fit_set_precision, 886 | n0_var=5, 887 | n0_range=10) 888 | 889 | print("N0 mean fit (nearest " + str(set_precision) + "): " + str(n0_mean_fit_set_precision)) 890 | 891 | test_vals = iter_list[0] 892 | num_its = len(test_vals) 893 | print("Total simulations to run: " + str(num_its)) 894 | 895 | k = k # number of repeats 896 | 897 | #f = FloatProgress(min=0, max=num_its) 898 | #display(f) 899 | 900 | #f1 = FloatProgress(min=0, max=k-1) 901 | #display(f1) 902 | 903 | #barpos = 1 904 | 905 | all_test_results_array = np.array([]) # array to store all results in before saving as excel csv file 906 | all_test_results_array = [] 907 | 908 | #print(test_vals) 909 | start_time = dt.datetime.now() 910 | progress_perc = 0 911 | for j in test_vals: 912 | 913 | current_time = dt.datetime.now() 914 | 915 | progress_perc = progress_perc + (100/len(test_vals)) 916 | no_completed = progress_perc/100 * len(test_vals) 917 | 918 | dif_secs = (current_time-start_time).seconds 919 | 920 | remaining_secs = (num_its-no_completed)*(dif_secs/no_completed) 921 | remaining_mins = remaining_secs/60 922 | #print(remaining_secs) 923 | 924 | #print('\r' + "TCP Calculation: " + str(int(progress_perc)) + "% completed", end='') 925 | print('\r' + "Running TCP Simulation: " + str(round(no_completed,0)) + " of " + str(len(test_vals)) + " (" + str(round(progress_perc,1)) + "% completed)" + " (" + str(round(remaining_mins,1)) + " mins remaining)", end='') 926 | 927 | results_array = [] 928 | 929 | n = n 930 | 931 | for i in range(0,k): 932 | t = completeTCPcalc(n, 933 | alphabeta_use = j[1], 934 | alphabeta_sd_use = j[4], 935 | d = d, 936 | d_shift = j[0], 937 | d_sd = d_sd, 938 | d_trend=d_trend, 939 | n0 = j[3], 940 | max_d = max_d, 941 | dose_of_interest = j[2]) 942 | 943 | #f1.value = i 944 | 945 | n=t[0] 946 | alphabeta_use = t[1] 947 | alphabeta_sd_use = t[2] 948 | d = t[3] 949 | d_shift = t[4] 950 | d_sd = t[5] 951 | d_trend = t[-1] 952 | n0 = t[6] 953 | TCP_pop = t[-3] 954 | TCPs = t[-4] 955 | nom_doses = t[-2] 956 | d_interest = t[8] 957 | frac_interest = t[9] 958 | TCP_cure_at_d_interest = t[10] 959 | max_d = max_d 960 | 961 | results_array.append(TCP_cure_at_d_interest) 962 | 963 | param_array = [] 964 | param_array.append(n) 965 | param_array.append(k) 966 | param_array.append(alphabeta_use) 967 | param_array.append(alphabeta_sd_use) 968 | param_array.append(d) 969 | param_array.append(d_shift) 970 | param_array.append(d_sd) 971 | param_array.append(d_trend) 972 | param_array.append(n0) 973 | param_array.append(max_d) 974 | param_array.append(d_interest) 975 | 976 | header_array = [] 977 | header_array.append("n") 978 | header_array.append("k") 979 | header_array.append("ab") 980 | header_array.append("ab_sd") 981 | header_array.append("d") 982 | header_array.append("d_shift") 983 | header_array.append("d_sd") 984 | header_array.append("d_trend") 985 | header_array.append("n0") 986 | header_array.append("max_d") 987 | header_array.append("d_interest") 988 | for i in range(k): 989 | header_array.append("k_"+str(i+1)) 990 | 991 | 992 | ## Array containing results form a single set of parameters (this will update each iteration) 993 | param_array.extend(results_array) 994 | param_results_array = param_array 995 | 996 | ## Create an array containing all the results for all sets of parameters 997 | all_test_results_array.extend(param_results_array) 998 | 999 | # for updating progress bar 1000 | #barpos = barpos+1 1001 | #f.value = barpos 1002 | 1003 | #print(param_results_array[-1]) 1004 | 1005 | ## Covnert to Numpy array and reshape so a single set of parameters is on one row 1006 | all_test_results_array_np = np.array(all_test_results_array) 1007 | all_test_results_array_np = np.reshape(all_test_results_array_np,(num_its,len(all_test_results_array_np)/num_its)) 1008 | 1009 | ## Save results to file 1010 | #np.savetxt(save_name, all_test_results_array_np, delimiter=",",fmt='%10.3f') 1011 | 1012 | with open(save_name, "w") as f: 1013 | writer = csv.writer(f) 1014 | writer.writerows([header_array]) 1015 | writer.writerows(all_test_results_array_np) 1016 | 1017 | print('\r' + "TCP Simulation: 100% Completed") 1018 | 1019 | print("Results saved in file: " + os.getcwd()+ save_name) 1020 | #print(header_array) 1021 | 1022 | return save_name, n0_mean_fit 1023 | 1024 | #t2 = datetime.now() 1025 | #t3 = (t2-t1).total_seconds() 1026 | 1027 | #print(t3) 1028 | 1029 | #%% 1030 | 1031 | ######## NTCP bits ############ 1032 | 1033 | 1034 | def td50_calc(td50_1,v,n): 1035 | ## calcualte TD50(V) which takes into account the volume effect 1036 | return td50_1/(v**n) 1037 | 1038 | def u_calc(d,td50,m): 1039 | ## calculation of the u value which is the limit for the NTCP integration 1040 | return (d-td50)/(m*td50) 1041 | 1042 | def ntcp_integrand(x): 1043 | ## This is the part of the NTCP which is within the integral 1044 | return sp.exp(-0.5*x**2) 1045 | 1046 | def ntcp_calc(d,td50_1,v,m,n): 1047 | """ 1048 | NTCP calculation based on LKB model. 1049 | Parameters required are: 1050 | d = total dose 1051 | td50_1 = dose at which 50% of patients see effect for partial volume 1052 | v = partial volume 1053 | m = describes SD of TD50 SD~m.TD50(V). Inversly related to curve steepness. 1054 | n = describes steepness of curve. n = 0 indicates a serial structure. 1055 | """ 1056 | 1057 | ## calcualte TD50(V) 1058 | td50 = td50_calc(td50_1,v,n) 1059 | #print(td50) 1060 | 1061 | ## calculate u for the integration limit 1062 | u = u_calc(d,td50,m) 1063 | #print(u) 1064 | 1065 | ## calculate NTCP value from input parameters 1066 | ntcp = (1/(sp.sqrt(2*sp.pi)))*sp.integrate.quad(ntcp_integrand,-sp.inf,u)[0] 1067 | #print(ntcp) 1068 | 1069 | return ntcp 1070 | 1071 | def ntcp_fit_calc(dose_data, td50_1, v, m, n): 1072 | ## calculate the NTCP at supplied dose points 1073 | ntcp_fitted = [] 1074 | for dose in dose_data: 1075 | ntcp_fitted.append(ntcp_calc(dose, td50_1, v, m, n)) 1076 | return ntcp_fitted 1077 | 1078 | def ntcp_curve_calc(dose_range,irrad_perc,td50_1, v, m, n): 1079 | ## calcualte the ntcp curve up to a maximum dose 1080 | ntcp_curve = [] 1081 | step = 0.1 1082 | doses = np.arange(0,dose_range+step,step) 1083 | for dose in doses: 1084 | #doses.append(dose) 1085 | ntcp_curve.append(ntcp_calc(dose*irrad_perc/100, td50_1, v, m, n)) 1086 | return doses,ntcp_curve 1087 | 1088 | def ntcp_patient_calc(cum_doses, td50_1, v, m, n): 1089 | ## calcualte the ntcp curve with a set of given cumulative doses 1090 | ## could adapt to create cumulative dose from the supplied doses (slower?) 1091 | patient_ntcp_curve = [] 1092 | for cum_dose in cum_doses: 1093 | patient_ntcp_curve.append(ntcp_calc(cum_dose, td50_1, v, m, n)) 1094 | return patient_ntcp_curve 1095 | 1096 | ## fit the TD50_1,m and n parameters. 1097 | 1098 | ## dif in squares to minimise 1099 | def sum_square_difs(vals): 1100 | ## must provide a list of tuples contianing mathcing pairs of (calc,ref) 1101 | ## can combine lists like vals = list(zip(calcs,refs)) 1102 | ## this will be used in determining the differnce between fitted curve 1103 | ## and data points 1104 | 1105 | return sum((vals[i][0] - vals[i][1])**2 for i in range(len(vals))) 1106 | 1107 | ## produce the effective volume calculation which might be useful. 1108 | 1109 | ##veff = sum(Di/D)^1/n*deltaVi 1110 | 1111 | def veff_calc(cdvh_data,n): 1112 | """ 1113 | Must provide: 1114 | cdvh_data = cdvh as a list of tuples (dose,volume (%)) 1115 | n = describes parallel (n=1) or serial structures (n=0) 1116 | 1117 | Calculates on its way to veff: 1118 | di = dose for each bin in differential dvh 1119 | dmax = max dose to organ 1120 | dvi = volume in specific dose bin i 1121 | 1122 | Returns: 1123 | ddvh = tuple containing ddvh (dose, volume (%)) 1124 | veff = effective volume (%) 1125 | """ 1126 | ## extract dose (di) and cdvh from input 1127 | ## n cant be zero or calc fails due to divide by zero 1128 | if n==0: 1129 | n=0.0001 1130 | di, cdvh = zip(*cdvh_data) 1131 | 1132 | ## calc ddvh 1133 | ddvh = -np.gradient(cdvh) # note the negative sign 1134 | 1135 | ## find max dose = find where cdvh is first equal to 0 1136 | dmax = di[cdvh.index(0)] 1137 | 1138 | ## dvi is equal to the ddvh at the point i. use to calc the bin values 1139 | veff = sum([(ddvh[i]*((di[i]/dmax)**(1/n))) for i in range(len(di))]) 1140 | 1141 | ## combine the dose and calced ddvh into single list of tuples 1142 | ddvh_data = list(zip(di,ddvh)) 1143 | 1144 | return veff,ddvh_data ## veff is a percentage value 1145 | 1146 | 1147 | def range_list(m, perc=None, dif=None,n=None, spacing=None): 1148 | """ 1149 | A function to create a list of values of length 2n+1, or set spacing. 1150 | n is the number of values either side of the mean to return 1151 | The values are centred around the mean, m and 1152 | have a range extending from +/- perc of m. 1153 | values returned will not exceed the m+/-perc specified 1154 | """ 1155 | ## ensure required parameters are passed 1156 | 1157 | if perc==None and dif==None: 1158 | raise Exception('Need to specify a range with perc or dif') 1159 | if n==None and spacing==None: 1160 | raise Exception('Need to specify number or spacing of output') 1161 | if n!=None and spacing!=None: 1162 | raise Exception('Ambiguous input as both n and spacing were supplied') 1163 | 1164 | ## convert percentage dif to absolute range 1165 | if perc == None: 1166 | abs_dif = dif 1167 | if dif == None: 1168 | abs_dif = m/100*perc 1169 | #print(abs_dif) 1170 | 1171 | if spacing == None: 1172 | if n < 1: 1173 | if n == 0: 1174 | results = [m] 1175 | else: 1176 | raise Exception('need at least 1 value either side of mean for n') 1177 | else: 1178 | n = np.floor(n) # wnat whole numbers either side 1179 | results = np.linspace(m-abs_dif,m+abs_dif,2*n+1) 1180 | 1181 | if n==None: 1182 | if spacing==0: 1183 | results = [m] 1184 | else: 1185 | vals = [] 1186 | val = m 1187 | ## add lower vlaues 1188 | while val >= m-abs_dif: 1189 | vals.append(val) 1190 | val = val - spacing 1191 | 1192 | val = (m+spacing) 1193 | ## add upper values 1194 | while val <= m+abs_dif: 1195 | vals.append(val) 1196 | val = val + spacing 1197 | 1198 | results = sorted(vals) 1199 | 1200 | return list(results) 1201 | 1202 | def closest_val(mylist,match): 1203 | """ 1204 | Returns (index,value) of closest match in list of values 1205 | Useful for getting values for a specified dose. 1206 | i.e. look up the index of the closest dose to that supplied 1207 | """ 1208 | return min(enumerate(mylist), key=lambda x:abs(x[1]-match)) 1209 | 1210 | def norm_trunc(lim_low,lim_high,mean,std,size): 1211 | """ 1212 | This is used for getting a set of data which is normally distributed 1213 | but is truncated between the range [lim_low,lim_high]. 1214 | This is useful if I want to use a normal distribution but limit its values. 1215 | This wrapper function is simpler to use for my purpose than the scipy 1216 | function directly. 1217 | """ 1218 | results = sp.stats.truncnorm.rvs((lim_low-mean)/std, 1219 | (lim_high-mean)/std, 1220 | loc=mean, 1221 | scale=std, 1222 | size=size) 1223 | return results 1224 | 1225 | #%% 1226 | 1227 | def ntcp_data_fit(dose_data,ntcp_data,initial_params,ntcp_params): 1228 | """ 1229 | fucntion to fit the NTCP model to supplied data and return the parameters. 1230 | At somepoint in the process, if parameter values are not supplied 1231 | this function will need calling to detemien them. 1232 | i.e. if data is supplied, then fit the values, if not then use supplied vals. 1233 | Funciton should only return fitted params, not do any plotting etc. 1234 | """ 1235 | 1236 | #plt.close() # close any open plots 1237 | ## some example data to fit to and plot 1238 | dose_data = dose_data#[55,60, 62, 67, 72, 65] 1239 | ntcp_data = ntcp_data#[0.1,0.15,0.1,0.2,0.3, 0.19] 1240 | 1241 | ## specify some initial starting values 1242 | initial_params = initial_params # supply inital params as a list to the function 1243 | ## can supply all at once using *initial_params (must be in correct order) 1244 | 1245 | ## calculate NTCP for initial params 1246 | ntcp_fit = ntcp_fit_calc(dose_data,*initial_params) 1247 | 1248 | ## calc dif of squares (for use in optimisation) 1249 | ntcp_dif_squares = sum_square_difs(list(zip(ntcp_data,ntcp_fit))) 1250 | #print(ntcp_dif_squares) 1251 | 1252 | ## fit the parameters TD50_1, m, n using scipy 1253 | ## note v_would be specified on a patient by patient basis in reality? 1254 | ## but for my purposes could use fixed values to see the effect of changes? 1255 | 1256 | ## at this point want to set bounds on all of the parameters which are provided 1257 | 1258 | ntcp_params={'td50_1':(58.2,1.92), 1259 | 'v': None,#(0.08,10), 1260 | 'm':(0.28,37.3), 1261 | 'n':(0.14,16.43)} 1262 | 1263 | ## set the mean and bounds for each supplied parameter. 1264 | ## set appropriate range if None supplied 1265 | 1266 | if ntcp_params['td50_1']==None: ## if None given then set range 1267 | td50_1_val_lower = 0 1268 | td50_1_val_upper = 200 1269 | else: 1270 | td50_1_val_lower = ntcp_params['td50_1'][0]*0.999 1271 | td50_1_val_upper = ntcp_params['td50_1'][0]*1.001 1272 | 1273 | if ntcp_params['v']==None: ## if None given then set range 1274 | v_val_lower = -100 1275 | v_val_upper = 100 1276 | else: 1277 | v_val_lower = ntcp_params['v'][0]*0.999 1278 | v_val_upper = ntcp_params['v'][0]*1.001 1279 | 1280 | if ntcp_params['m']==None: ## if None given then set range 1281 | m_val_lower = 0 1282 | m_val_upper = 1 1283 | else: 1284 | m_val_lower = ntcp_params['m'][0]*0.999 1285 | m_val_upper = ntcp_params['m'][0]*1.001 1286 | 1287 | if ntcp_params['n']==None: ## if None given then set range 1288 | n_val_lower = 0 1289 | n_val_upper = 1 1290 | else: 1291 | n_val_lower = ntcp_params['n'][0]*0.999 1292 | n_val_upper = ntcp_params['n'][0]*1.001 1293 | 1294 | 1295 | set_bounds = ([td50_1_val_lower,v_val_lower,m_val_lower,n_val_lower], 1296 | [td50_1_val_upper,v_val_upper,m_val_upper,n_val_upper]) 1297 | 1298 | #set_bounds = ([0,v_val_lower,0,0], 1299 | # [200,v_val_upper,1,1]) 1300 | 1301 | #[td50,v,m,n)] 1302 | 1303 | #methods = ['dogbox','trf'] 1304 | ## could hold parameters fixed by specifying a very small range? 1305 | 1306 | #all_results_list = [] 1307 | 1308 | #for i in range(len(methods)): 1309 | #print(methods[i]) 1310 | popt,pcov = sp.optimize.curve_fit(f = ntcp_fit_calc, 1311 | xdata = dose_data, 1312 | ydata = ntcp_data, 1313 | p0 = initial_params, 1314 | bounds = set_bounds, 1315 | method='trf') #method : {‘lm’, ‘trf’, ‘dogbox’} 1316 | 1317 | perr = np.sqrt(np.diag(pcov)) 1318 | 1319 | ## calculate complete NTCP curve (using fitted params) 1320 | #fitted_params = [param*1 for param in initial_params] 1321 | fitted_params = [param for param in popt] 1322 | fitted_params[1]=1 1323 | 1324 | return popt # return the fitted params 1325 | 1326 | #%% 1327 | 1328 | def complete_NTCP_calc(d_data=None, 1329 | ntcp_data=None, 1330 | frac_doses = None, 1331 | irrad_perc = 100, 1332 | initial_params_ntcp=None, 1333 | max_dose=100, 1334 | ntcp_params=None, 1335 | fit_vals = True, 1336 | confirmation=False 1337 | ): 1338 | 1339 | if initial_params_ntcp==None: ## 1340 | ## initial params (use these defaults, but allow list to be suppled) 1341 | if ntcp_params['td50_1']==None: 1342 | intial_ntcp_td50_1 = 70 1343 | else: 1344 | intial_ntcp_td50_1 = ntcp_params['td50_1'][0] 1345 | 1346 | if ntcp_params['v']==None: 1347 | intial_ntcp_v = 0.05 1348 | else: 1349 | intial_ntcp_v = ntcp_params['v'][0] 1350 | 1351 | if ntcp_params['m']==None: 1352 | intial_ntcp_m = 0.1 1353 | else: 1354 | intial_ntcp_m = ntcp_params['m'][0] 1355 | 1356 | if ntcp_params['n']==None: 1357 | intial_ntcp_n = 0.1 1358 | else: 1359 | intial_ntcp_n = ntcp_params['n'][0] 1360 | 1361 | #intial_ntcp_v = 0.05 1362 | #intial_ntcp_m = 0.1 1363 | #intial_ntcp_n = 0.1 1364 | #[td50,v,m,n)] 1365 | initial_params_ntcp = [intial_ntcp_td50_1,intial_ntcp_v,intial_ntcp_m,intial_ntcp_n] 1366 | 1367 | ## optimise fitting parameters [td50,v,m,n] is returned 1368 | if fit_vals==True: ## only do complete fitting if no params supplied. 1369 | print("Fitting NTCP data") 1370 | pop_fit = ntcp_data_fit(dose_data = d_data, 1371 | ntcp_data = ntcp_data, 1372 | initial_params = initial_params_ntcp, 1373 | ntcp_params = ntcp_params) 1374 | ntcp_fit=True 1375 | #print('new fit:',pop_fit) 1376 | else: 1377 | pop_fit=[ntcp_params['td50_1'][0], 1378 | ntcp_params['v'][0], 1379 | ntcp_params['m'][0], 1380 | ntcp_params['n'][0] 1381 | ] 1382 | ntcp_fit = False 1383 | 1384 | perc_scale = irrad_perc/100 1385 | 1386 | ## calculate population NTCP curve 1387 | #max_oar_dose = max_dose * perc_scale 1388 | fit_x,fit_y = ntcp_curve_calc(max_dose,irrad_perc,*pop_fit) 1389 | 1390 | ## calc cumulative dose for use in calculating individual patient NTCP 1391 | ## the input is a list of doses for each fraction. 1392 | 1393 | if frac_doses is not None: 1394 | cum_doses = np.cumsum(frac_doses,axis=1) 1395 | 1396 | ## scale by the supplied oar dose percentage 1397 | cum_doses = cum_doses * perc_scale 1398 | 1399 | ## calculate a set of NTCP parameters for each patient 1400 | ## variation is supplied in the ntcp_params[1] values of the dict 1401 | 1402 | ## 1 set the sd vals to use 1403 | #td50_1_sd = ntcp_params['td50_1'][1] 1404 | #v_sd = ntcp_params['v'][1] 1405 | #m_sd = ntcp_params['m'][1] 1406 | #n_sd = ntcp_params['n'][1] 1407 | 1408 | ## if SD is set to zero then cannot get from normal distribution, so set explicitely 1409 | ## need to limit these values to between their set limits ([0,1] etc...) 1410 | #print("Calculating Individual Patient NTCP curves") 1411 | if ntcp_params['td50_1'][1] == 0: 1412 | td50_1_use = np.full(len(cum_doses),pop_fit[0],dtype=float) 1413 | else: 1414 | td50_1_use = np.random.normal(loc = pop_fit[0], scale = (pop_fit[0]*ntcp_params['td50_1'][1]/100),size=len(cum_doses)) 1415 | if ntcp_params['v']==None: 1416 | v_lim_low = pop_fit[1]*0.999 1417 | v_lim_high = pop_fit[1]*1.000 1418 | v_std = 1 1419 | v_use = norm_trunc(lim_low=v_lim_low,lim_high=v_lim_high,mean=pop_fit[1],std=(pop_fit[1]*v_std/100),size=len(cum_doses)) 1420 | elif ntcp_params['v'][1] == 0: 1421 | v_use = np.full(len(cum_doses),pop_fit[1],dtype=float) 1422 | else: 1423 | #v_use = np.random.normal(loc = pop_fit[1], scale = (pop_fit[1]*ntcp_params['v'][1]/100),size=len(cum_doses)) 1424 | v_lim_low = 0 1425 | v_lim_high = 1 1426 | v_use = norm_trunc(lim_low=v_lim_low,lim_high=v_lim_high,mean=pop_fit[1],std=(pop_fit[1]*ntcp_params['v'][1]/100),size=len(cum_doses)) 1427 | if ntcp_params['m'][1] == 0: 1428 | m_use = np.full(len(cum_doses),pop_fit[2],dtype=float) 1429 | else: 1430 | #m_use = np.random.normal(loc = pop_fit[2], scale = (pop_fit[2]*ntcp_params['m'][1]/100),size=len(cum_doses)) 1431 | m_lim_low = 0 1432 | m_lim_high = 1 1433 | m_use = norm_trunc(lim_low=m_lim_low,lim_high=m_lim_high,mean=pop_fit[2],std=(pop_fit[2]*ntcp_params['m'][1]/100),size=len(cum_doses)) 1434 | 1435 | if ntcp_params['n'][1] == 0: 1436 | n_use = np.full(len(cum_doses),pop_fit[3],dtype=float) 1437 | else: 1438 | #n_use = np.random.normal(loc = pop_fit[3], scale = (pop_fit[3]*ntcp_params['n'][1]/100),size=len(cum_doses)) 1439 | n_lim_low = 0 1440 | n_lim_high = 1 1441 | n_use = norm_trunc(lim_low=n_lim_low,lim_high=n_lim_high,mean=pop_fit[3],std=(pop_fit[3]*ntcp_params['n'][1]/100),size=len(cum_doses)) 1442 | 1443 | ## calcualte the NTCP curve for each patient based on their cumulative doses 1444 | patient_ntcps = [] 1445 | for i in range(len(cum_doses)): 1446 | patient_ntcps.append(ntcp_patient_calc(cum_doses[i], td50_1_use[i], v_use[i], m_use[i], n_use[i])) 1447 | patient_ntcps = np.array(patient_ntcps) 1448 | else: 1449 | print('Fraction doses should be supplied in format: 1 row for each patient, with each column containing dose/# for that patient') 1450 | 1451 | if confirmation==True: 1452 | print('*** NTCP Simulation Completed ***') 1453 | 1454 | return {'pop_fit': pop_fit, 1455 | 'fit_x':fit_x, 1456 | 'fit_y':fit_y, 1457 | 'd_data':d_data, 1458 | 'ntcp_data':ntcp_data, 1459 | 'cum_doses':cum_doses, 1460 | 'patient_ntcps':patient_ntcps, 1461 | 'ntcp_fit':ntcp_fit, 1462 | 'td50_1_use':td50_1_use, 1463 | } 1464 | 1465 | 1466 | 1467 | #%% 1468 | ## include a function to plot the results 1469 | 1470 | def plot_TCP_NTCP(resultsTCP=None, resultsNTCP=None, TCP=True, NTCP=True, 1471 | n=100, colors={'TCP':'green','NTCP':'red'},dark_color=True, 1472 | pop_plot=True, xlabel='Nominal Dose (Gy)', ylabel='TCP / NTCP', 1473 | alpha=0.03, plot_points=True, 1474 | plot_percentiles=(5,95), show_percentiles=True, 1475 | show_legend=True, legend_label = None): 1476 | """ 1477 | Plot the TCP/NTCP curves. 1478 | Select n random curves to plot. 1479 | Can also plot the population with pop_plot=True (default) 1480 | Can select colour with e.g. colors={'TCP':'blue','NTCP':'orange'}. 1481 | Set the x and y labels with xlabel='' and ylabel='' 1482 | Set the alpha of the patient plots with e.g. alpha=0.1 1483 | Can supply error positions as [[y1lower, y2lower], [y1upper, y2upper]]. 1484 | ** Currently have to supply a TCP set of results as values for the plot are gathered from this set of data. 1485 | """ 1486 | 1487 | # if given n is larger than sample, then set equal to sampel size 1488 | if n > resultsTCP['n']: 1489 | n = resultsTCP['n'] 1490 | 1491 | ## pick n numbers within the range len(results['TCPs']) 1492 | ns = random.sample(range(resultsTCP['n']), n) 1493 | 1494 | if TCP==True: 1495 | for i in ns: 1496 | plt.plot(resultsTCP['nom_doses'], resultsTCP['TCPs'][i], 1497 | color=colors['TCP'], alpha=alpha) 1498 | plt.xlabel(xlabel) 1499 | plt.ylabel(ylabel) 1500 | 1501 | ## plot the population mean 1502 | if pop_plot == True: 1503 | ## set the color for plotting the population curve 1504 | if legend_label == None: 1505 | the_label = 'TCP' 1506 | else: 1507 | the_label = legend_label 1508 | if dark_color ==True: 1509 | darkcolorTCP = 'dark' + colors['TCP'] 1510 | else: 1511 | darkcolorTCP = colors['TCP'] 1512 | plt.plot(resultsTCP['nom_doses'], np.mean(resultsTCP['TCPs'],axis=0), 1513 | color=darkcolorTCP, alpha=1, label=the_label) 1514 | 1515 | ## plot the points which were fitted to 1516 | if plot_points==True: 1517 | plt.plot(resultsTCP['dose_input'], resultsTCP['TCP_input'], 1518 | color=colors['TCP'], markeredgecolor='black', marker='o', ls='', 1519 | alpha=0.7, ms=4) 1520 | 1521 | ## add percentile plots 1522 | if show_percentiles == True: 1523 | for percentile in plot_percentiles: 1524 | plt.plot(resultsTCP['nom_doses'], 1525 | np.percentile(resultsTCP['TCPs'], percentile, axis=0), 1526 | color=darkcolorTCP, alpha=1, ls=':') 1527 | if NTCP==True: 1528 | for i in ns: 1529 | plt.plot(resultsTCP['nom_doses'], resultsNTCP['patient_ntcps'][i], 1530 | color=colors['NTCP'], alpha=alpha) 1531 | 1532 | ## plot the population mean 1533 | if pop_plot == True: 1534 | ## set the color for plotting the population curve 1535 | if dark_color ==True: 1536 | darkcolorNTCP = 'dark' + colors['NTCP'] 1537 | else: 1538 | darkcolorNTCP = colors['TCP'] 1539 | if legend_label == None: 1540 | the_label = 'NTCP' 1541 | else: 1542 | the_label = legend_label 1543 | plt.plot(resultsTCP['nom_doses'], np.mean(resultsNTCP['patient_ntcps'],axis=0), 1544 | color=darkcolorNTCP, alpha=1, label=the_label) 1545 | 1546 | ## plot the points which were fitted to 1547 | if plot_points==True: 1548 | plt.plot(resultsNTCP['d_data'], resultsNTCP['ntcp_data'], 1549 | color=colors['NTCP'], markeredgecolor='black', marker='o', ls='', 1550 | alpha=0.7, ms=4) 1551 | 1552 | ## add percentile plots 1553 | if show_percentiles == True: 1554 | for percentile in plot_percentiles: 1555 | plt.plot(resultsTCP['nom_doses'], 1556 | np.percentile(resultsNTCP['patient_ntcps'], percentile, axis=0), 1557 | color=darkcolorNTCP, alpha=1, ls=':') 1558 | if show_legend==True: 1559 | plt.legend(loc='upper left') 1560 | 1561 | --------------------------------------------------------------------------------