Coverage for app/main.py: 100%

338 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2025-08-28 18:30 +0000

1"""Graphical user interface of Pears 

2Fit results and carrier accumulation are reset if 

3- The app mode is changed. 

4- New data are uploaded. 

5- The file delimiter is changed. 

6- The quantity is changed. 

7- The pre-processing is toggled. 

8Carrier accumulation is reset if 

9- The period is changed. 

10Changing any other value does not re-run the fit but displays a message about it""" 

11 

12import copy 

13import os 

14 

15import numpy as np 

16import pandas as pd 

17import streamlit as st 

18from streamlit.runtime.runtime_util import MessageSizeError 

19 

20import resources 

21from models import models, Model 

22from plot import plot_decays, plot_carrier_concentrations, parallel_plot 

23from fitting import FitFailedException 

24from utility.data import load_data, matrix_to_string, process_data, render_image, read_txt_file, generate_html_table 

25from utility.dict import merge_dicts, list_to_dict 

26from utility.numbers import get_concentrations_html, to_scientific 

27from utility.project import get_last_commit_date_from_github, get_pyproject_info 

28 

29__version__ = get_pyproject_info("project", "version") 

30__name__ = get_pyproject_info("project", "name") 

31__description__ = get_pyproject_info("project", "description") 

32__github__ = get_pyproject_info("project", "urls", "repository") 

33__date__ = get_last_commit_date_from_github(__github__) 

34__author__ = get_pyproject_info("project", "authors")[0]["name"] 

35 

36dirname = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 

37 

38# ---------------------------------------------------------------------------------------------------------------------- 

39# ------------------------------------------------------- SET UP ------------------------------------------------------- 

40# ---------------------------------------------------------------------------------------------------------------------- 

41 

42# General setup & layout 

43st.set_page_config(__name__.upper() + " - " + __description__, resources.ICON_PATH, layout="wide") 

44st.logo(resources.LOGO_TEXT_PATH, icon_image=resources.LOGO_PATH) 

45 

46if "results" not in st.session_state: 

47 st.session_state.results = [] # list of all the results 

48if "carrier_accumulation" not in st.session_state: 

49 st.session_state.carrier_accumulation = None # carrier accumulation 

50if "models" not in st.session_state: 

51 st.session_state.models = copy.deepcopy(models) 

52 

53 

54def reset_carrier_accumulation() -> None: 

55 """Reset the stored carrier accumulation""" 

56 print("Resetting carrier accumulation") 

57 st.session_state.carrier_accumulation = None 

58 

59 

60def reset_results() -> None: 

61 """Reset the stored results and carrier accumulation""" 

62 print("Resetting results") 

63 st.session_state.results = [] 

64 reset_carrier_accumulation() 

65 

66 

67# Change the default style 

68@st.cache_resource 

69def set_style() -> None: 

70 """Set the default style""" 

71 with open(resources.CSS_STYLE_PATH) as ofile: 

72 st.markdown(f"<style>{ofile.read()}</style>", unsafe_allow_html=True) 

73 

74 

75set_style() 

76 

77# ---------------------------------------------------------------------------------------------------------------------- 

78# -------------------------------------------------------- INPUT ------------------------------------------------------- 

79# ---------------------------------------------------------------------------------------------------------------------- 

80 

81logo_placeholder = st.container() 

82info_message = st.empty() 

83 

84# -------------------------------------------------------- MODES ------------------------------------------------------- 

85 

86fit_mode = st.sidebar.selectbox("Mode", resources.APP_MODES, on_change=reset_results, key="fit_mode_") 

87 

88# -------------------------------------------------------- DATA -------------------------------------------------------- 

89 

90# File uploader 

91input_filename = st.sidebar.file_uploader( 

92 label="Data file", 

93 key="input_filename_", 

94 on_change=reset_results, 

95) 

96 

97# Data format 

98data_format_help = """Select the data format of your file between:  

99* *X/Y1/Y2/Y3...*: first column is the time (in ns) followed by the signal intensity or  

100* *X1/Y1/X2/Y2...*: the time and intensity columns are alternated.""" 

101data_format = st.sidebar.radio( 

102 label="Data format", 

103 options=["X/Y1/Y2/Y3...", "X1/Y1/X2/Y2..."], 

104 help=data_format_help, 

105 key="data_format_", 

106 horizontal=True, 

107 on_change=reset_results, 

108) 

109 

110# Data delimiter 

111data_delimiter_help = """Data delimiter. tab/space cannot be used if the file has missing data.""" 

112data_delimiter = st.sidebar.radio( 

113 label="Data delimiter", 

114 options=[",", None, ";"], 

115 help=data_delimiter_help, 

116 key="data_delimiter", 

117 horizontal=True, 

118 format_func=lambda v: {None: "tab/space", ",": "comma", ";": "semicolon"}[v], 

119 on_change=reset_results, 

120) 

121 

122# Quantity 

123quantity_input = st.sidebar.radio( 

124 label="Quantity", 

125 options=["TRPL", "TRMC"], 

126 help="Quantity associated with the data", 

127 key="quantity_input_", 

128 horizontal=True, 

129 on_change=reset_results, 

130) 

131 

132# Processing data 

133if quantity_input == "TRPL": 

134 preprocess_help = r"""Shift the decay maximum intensity to $\mathsf{t=0}$ and normalise the intensity.""" 

135else: 

136 preprocess_help = r"""Shift the decay maximum intensity to $\mathsf{t=0}$.""" 

137process_input = st.sidebar.checkbox( 

138 label="Pre-process data", 

139 help=preprocess_help, 

140 key="preprocess_", 

141 on_change=reset_results, 

142) 

143 

144# Load the data 

145data_message = st.empty() 

146xs_data, ys_data = [None], [None] 

147if input_filename is not None: 

148 try: 

149 try: 

150 xs_data, ys_data = load_data(input_filename.getvalue(), data_delimiter, data_format) 

151 except: 

152 raise ValueError("Unknown error, Check that the correct delimiter has been selected.") 

153 

154 # Check number of arrays consistency 

155 if len(xs_data) != len(ys_data): 

156 raise ValueError("Mismatch: x data and y data must have the same number of columns.") 

157 

158 # Check length arrays consistency 

159 for i, (x, y) in enumerate(zip(xs_data, ys_data)): 

160 if len(x) != len(y): 

161 raise ValueError(f"Mismatch at index {i + 1}: x and y columns must have the same length.") 

162 

163 # Process the data 

164 if process_input: 

165 xs_data, ys_data = process_data(xs_data, ys_data, quantity_input == "TRPL") 

166 

167 except Exception as e: # if an error occurs during the file reading 

168 xs_data, ys_data = [None], [None] 

169 data_message.error(f"Uh-oh! The data could not be loaded. Error: {e}") 

170 

171 

172if xs_data[0] is None: 

173 logo_placeholder.html(render_image(resources.LOGO_PATH, 100)) # main logo 

174 title_string = """<div style="text-align: center; font-family: sans-serif; font-size: 45px; line-height: 1.3; 

175 color: rgb(230, 204, 0)">PEARS</div> 

176 <div style="text-align: center; font-family: sans-serif; font-size: 45px; line-height: 1.3;color: rgb(230, 204, 0)"> 

177 <strong>Pe</strong>rorvskite C<strong>a</strong>rrier <strong>R</strong>ecombination <strong>S</strong>imulator</div>""" 

178 logo_placeholder.html(title_string) 

179 

180 

181# ------------------------------------------------------ FLUENCES ------------------------------------------------------ 

182 

183 

184fluence_message = st.empty() 

185N0s = None 

186if xs_data[0] is not None: 

187 n0_help = r"""Photoexcited carrier concentration(s separated by commas), calculated as  

188 $\mathsf{N_0=A\frac{I_0}{E_{photon}D}}$ where $\mathsf{I_0}$ is the excitation pulse fluence  

189 (in $\mathsf{J/cm^2}$), $\mathsf{D}$ is the film thickness (in $\mathsf{cm}$), $\mathsf{E_{photon}}$  

190 is the the photon energy (in $\mathsf{J}$) and $\mathsf{A}$ is the sample absorptance.""" 

191 st.sidebar.markdown(r"Photoexcited carrier concentrations ($\mathsf{cm^{-3}}$)", help=n0_help) 

192 N0_inputs = [] 

193 for i in range(len(xs_data)): 

194 columns = st.sidebar.columns([1, 2], vertical_alignment="center") 

195 columns[0].markdown(f"Decay {i + 1}") 

196 N0_inputs.append(columns[1].text_input("label", label_visibility="collapsed", key=f"fluence_{i}")) 

197 

198 # Check validity of N0s 

199 if all(N0_inputs): 

200 try: 

201 N0s = [float(n) for n in N0_inputs] 

202 except ValueError: 

203 fluence_message.error("Uh-oh! The initial carrier concentrations input is not valid") 

204 

205 

206# --------------------------------------------------- MODEL SELECTION -------------------------------------------------- 

207 

208 

209model, model_name = None, "" 

210if N0s is not None: 

211 model_help = """Choose the model to use between the Bimolecular-Trapping-Auger and the Bimolecular-Trapping- 

212 Detrapping models.""" 

213 model_name = st.sidebar.selectbox( 

214 "Model", 

215 list(st.session_state.models), 

216 help=model_help, 

217 key="model_name_", 

218 format_func=lambda v: {"BTD": "Bimolecular-Trapping-Detrapping", "BTA": "Bimolecular-Trapping-Auger"}[v], 

219 ) 

220 model: Model | None = st.session_state.models[model_name][quantity_input] 

221 

222 

223# ------------------------------------------------ FIXED & GUESS VALUES ------------------------------------------------ 

224 

225 

226if N0s is not None: 

227 param_key = st.sidebar.selectbox( 

228 "Parameters", 

229 model.param_ids, 

230 format_func=model.get_parameter_label, 

231 key="param_key_", 

232 ) 

233 

234 # -------------------------------------------------- FIXED VALUES -------------------------------------------------- 

235 

236 key = model_name + param_key + "fixed" 

237 

238 if key not in st.session_state: 

239 st.session_state[key] = to_scientific(model.fvalues[param_key]) 

240 

241 # Display the fixed value 

242 fvalue_help = "Fixed values for the selected parameter. If not a number, use the guess value below for the fitting." 

243 fvalue = st.sidebar.text_input( 

244 "Fixed value", 

245 help=fvalue_help, 

246 key=key, 

247 ) 

248 

249 # Store the new value 

250 if fvalue: 

251 try: 

252 model.fvalues[param_key] = float(fvalue) 

253 except: 

254 pass 

255 else: 

256 model.fvalues[param_key] = None 

257 

258 # -------------------------------------------------- GUESS VALUES -------------------------------------------------- 

259 

260 # If no fixed value, display and set the guess value 

261 if model.fvalues[param_key] is None: 

262 

263 # Fitting mode 

264 if fit_mode == resources.FITTING_MODE: 

265 

266 key = model_name + param_key + "guess" 

267 if key not in st.session_state: 

268 st.session_state[key] = to_scientific(model.gvalues[param_key]) 

269 

270 # Display the guess value 

271 gvalue = st.sidebar.text_input( 

272 "Guess value", 

273 help="Parameter initial guess value for the fitting.", 

274 key=key, 

275 ) 

276 

277 # Store the guess value 

278 try: 

279 model.gvalues[param_key] = float(gvalue) 

280 except (ValueError, TypeError): 

281 pass 

282 

283 # Grid fitting mode 

284 else: 

285 

286 key = model_name + param_key + "guesses" 

287 if key not in st.session_state: 

288 st.session_state[key] = to_scientific(model.gvalues_range[param_key]) 

289 

290 # Display the guess values 

291 gvalues = st.sidebar.text_input( 

292 "Guess values", 

293 help="Enter multiple initial guess values, separated with commas.", 

294 key=key, 

295 ) 

296 

297 # Store the guess values 

298 try: 

299 model.gvalues_range[param_key] = [float(v) for v in gvalues.split(",")] 

300 except (ValueError, TypeError): 

301 pass 

302 

303# ------------------------------------------------- REPETITION PERIOD -------------------------------------------------- 

304 

305 

306period_input = None 

307period = 0.0 

308if N0s is not None: 

309 period_help = """Excitation repetition period. Used to calculate possible carrier accumulation between  

310 consecutive excitation pulses.""" 

311 period_input = st.sidebar.text_input( 

312 "Excitation repetition period (ns)", 

313 help=period_help, 

314 key="period_input_", 

315 on_change=reset_carrier_accumulation, 

316 ) 

317 

318 try: 

319 period = float(period_input) 

320 except ValueError: 

321 pass 

322 

323 

324# ----------------------------------------------------- RUN BUTTON ----------------------------------------------------- 

325 

326 

327run_button = False # when pressed, run the fit 

328if N0s is not None: 

329 run_button = st.sidebar.button("Run", use_container_width=True, key="run_button") 

330 

331# ---------------------------------------------------------------------------------------------------------------------- 

332# --------------------------------------------------- RESULTS DISPLAY -------------------------------------------------- 

333# ---------------------------------------------------------------------------------------------------------------------- 

334 

335# display the results if results have been previously stored 

336try: 

337 if st.session_state.results or run_button: 

338 

339 # Remove the data and fluence messages 

340 data_message.empty() 

341 fluence_message.empty() 

342 

343 # --------------------------------------------------- FITTING -------------------------------------------------- 

344 

345 # If the run button has been clicked... 

346 if run_button: 

347 reset_results() # reset the previously stored results 

348 info_message.info("Processing") 

349 

350 # Run the fitting or grid fitting and store the output in the session state 

351 

352 # Fitting 

353 if fit_mode == resources.FITTING_MODE: 

354 try: 

355 print("Fitting the data") 

356 output = model.fit(xs_data, ys_data, N0s) 

357 st.session_state.results = copy.deepcopy([output, model, N0s]) 

358 except ValueError: 

359 raise FitFailedException() 

360 

361 # Grid Fitting 

362 else: 

363 progressbar = st.sidebar.progress(0) # create the progress bar 

364 output = model.grid_fitting(progressbar, N0s, xs_data=xs_data, ys_data=ys_data) 

365 st.session_state.results = copy.deepcopy([output, model, N0s]) 

366 if len(output) == 0: 

367 raise FitFailedException() 

368 

369 info_message.empty() 

370 

371 # Check if the model settings or the carrier concentrations have changed 

372 elif st.session_state.results[1] != model or st.session_state.results[2] != N0s: 

373 info_message.warning("You have changed some of the input settings. Press 'Run' to apply these changes.") 

374 

375 # Retrieve the stored results from the session state 

376 fit_output, fit_model, fit_N0s = st.session_state.results 

377 

378 # -------------------------------------------- CARRIER ACCUMULATION -------------------------------------------- 

379 

380 carrier_accumulation = None 

381 if period: 

382 if st.session_state.carrier_accumulation is None: # if carrier accumulation has not been calculated 

383 print("Calculating CA") 

384 if isinstance(fit_output, list): 

385 try: 

386 carrier_accumulation = [ 

387 fit_model.get_carrier_accumulation(fit["popts"], period) for fit in fit_output 

388 ] 

389 except AssertionError: # if carrier accumulation cannot be calculated 

390 carrier_accumulation = [] 

391 except MessageSizeError: # pragma: no cover # if too many numbers (should not be possible) 

392 carrier_accumulation = [] 

393 else: 

394 try: 

395 carrier_accumulation = fit_model.get_carrier_accumulation(fit_output["popts"], period) 

396 except AssertionError: 

397 carrier_accumulation = dict() 

398 st.session_state.carrier_accumulation = carrier_accumulation # store the carrier accumulation value 

399 

400 print("Getting stored CA") 

401 carrier_accumulation = st.session_state.carrier_accumulation 

402 

403 # -------------------------------------------------- PARALLEL PLOT ------------------------------------------------- 

404 

405 if isinstance(fit_output, dict): 

406 fit_displayed = fit_output 

407 selected = 0 

408 message = "## Fitting results" 

409 else: 

410 st.markdown("## Parallel plot") 

411 

412 # Add the carrier accumulation to the values 

413 popts = [] 

414 for i, fit_popt in enumerate(fit_output): 

415 popt = fit_popt["all_values"].copy() 

416 if ( 

417 carrier_accumulation is not None # carrier accumulation was calculated 

418 and len(carrier_accumulation) > 0 # calculation did not fail 

419 ): 

420 popt["Max. CA (%)"] = np.max(carrier_accumulation[i]["CA"]) 

421 popts.append(popt) 

422 

423 # Display the parallel plot 

424 xp = parallel_plot(popts, fit_output[0]["hidden_keys"]) 

425 selected = xp.to_streamlit("hip", "selected_uids").display() 

426 fit_displayed = fit_output[int(selected[0])] 

427 message = f"### Displaying results of fit #{int(selected[0]) + 1}" 

428 

429 # -------------------------------------------------------------------------------------------------------------- 

430 # ------------------------------------------------ DISPLAY FIT ------------------------------------------------ 

431 # -------------------------------------------------------------------------------------------------------------- 

432 

433 st.markdown(message) 

434 col1, col2 = st.columns(2) 

435 

436 with col1: 

437 

438 # -------------------------------------------- FITTING PLOT & EXPORT ------------------------------------------- 

439 

440 st.markdown("#### Data Fit", help="Displays the raw and fitted data.") 

441 

442 # Plot 

443 figure = plot_decays( 

444 xs_data=fit_displayed["xs_data"], 

445 ys_data=fit_displayed["ys_data"], 

446 quantity=fit_model.QUANTITY, 

447 ys_data2=fit_displayed["fit_ydata"], 

448 labels=fit_displayed["N0s_labels"], 

449 ) 

450 st.plotly_chart(figure, use_container_width=True) 

451 

452 # Export 

453 header = np.concatenate([["Time (ns)", "Intensity %i" % i] for i in range(1, len(ys_data) + 1)]) 

454 data = [val for pair in zip(fit_displayed["xs_data"], fit_displayed["fit_ydata"]) for val in pair] 

455 export_data = matrix_to_string(data, header) 

456 st.download_button("Download Fits", export_data, "pears_fit_data.csv", use_container_width=True) 

457 

458 # -------------------------------------- OPTIMISED PARAMETERS DISPLAY -------------------------------------- 

459 

460 st.markdown("#### Parameters", help="Parameters obtained from the fit.") 

461 table = generate_html_table(fit_displayed["popt_df"]) 

462 st.html(table) 

463 

464 # Export 

465 df = pd.DataFrame(list_to_dict(fit_displayed["popts"]), index=fit_displayed["N0s"]) 

466 df.index.name = "Carrier concentration" 

467 st.download_button("Download Parameters", df.to_csv(), "Parameters.csv", use_container_width=True) 

468 

469 # --------------------------------------------- CONTRIBUTIONS ---------------------------------------------- 

470 

471 help_str = f"Contribution of each recombination process to the variation of the {fit_model.QUANTITY}." 

472 st.markdown("#### Process Contributions", help=help_str) 

473 table = generate_html_table(fit_displayed["contributions_df"]) 

474 st.html(table) 

475 

476 # Contribution analysis 

477 for s in fit_model.get_contribution_recommendations(fit_displayed["contributions"]): 

478 st.warning(s) 

479 

480 # ------------------------------------------ CARRIER ACCUMULATION ----------------------------------------- 

481 

482 if period: 

483 help_str = f"""The carrier accumulation is calculated as the maximum difference between the simulated  

484 {fit_model.QUANTITY} after 1 excitation pulse (as used during fitting) and after multiple pulses (as  

485 during experimental measurements).""" 

486 st.markdown("#### Carrier Accumulation", help=help_str) 

487 

488 # If carrier accumulation could be calculated 

489 if st.session_state.carrier_accumulation: 

490 

491 # Select the selected fit carrier accumulation if grid analysis mode 

492 if isinstance(carrier_accumulation, list): 

493 carrier_accumulation = carrier_accumulation[int(selected[0])] 

494 

495 # Display the carrier accumulation table 

496 table = generate_html_table(carrier_accumulation["CA_df"]) 

497 st.html(table) 

498 

499 # Display the decays 

500 figure = plot_decays( 

501 xs_data=carrier_accumulation["t"], 

502 ys_data=carrier_accumulation["Pulse 1"], 

503 quantity=fit_model.QUANTITY, 

504 ys_data2=carrier_accumulation["Pulse S"], 

505 labels=fit_displayed["N0s_labels"], 

506 label2=" (stabilised pulse)", 

507 ) 

508 st.plotly_chart(figure) 

509 

510 # Analysis 

511 max_ca = np.max(carrier_accumulation["CA"]) 

512 if max_ca > 5.0: 

513 ca_warning = f"""This fit predicts significant carrier accumulation leading to a maximum  

514 {max_ca:.1f} % difference between the single pulse and multiple pulse  

515 {quantity_input} decays. You might need to increase your excitation repetition  

516 period to prevent potential carrier accumulation.""" 

517 st.warning(ca_warning) 

518 else: 

519 st.success( 

520 "This fit does not predict significant carrier accumulation and is therefore " 

521 "self-consistent." 

522 ) 

523 

524 else: 

525 warn = "Carrier accumulation could not be calculated due to excessive computational requirements." 

526 st.warning(warn) 

527 

528 with col2: 

529 

530 # -------------------------------------------- CONCENTRATIONS PLOT --------------------------------------------- 

531 

532 help_str = """Carrier concentrations are calculated from the fitted parameters. If a period is specified,  

533 the concentration's evolution after multiple pulses is shown until stabilisation.""" 

534 st.markdown("#### Carrier Concentrations", help=help_str) 

535 

536 # Calculate the concentrations 

537 concentrations = fit_model.get_carrier_concentrations( 

538 fit_displayed["xs_data"], 

539 fit_displayed["popts"], 

540 period if st.session_state.carrier_accumulation else 0.0, 

541 ) 

542 

543 # Plot the concentrations 

544 figure = plot_carrier_concentrations( 

545 xs_data=concentrations[0], 

546 ys_data=concentrations[2], 

547 N0s=fit_displayed["N0s"], 

548 titles=fit_displayed["N0s_labels"], 

549 xlabel=concentrations[1], 

550 model=fit_model, 

551 ) 

552 st.plotly_chart(figure, use_container_width=True) 

553 

554 # -------------------------------------------- MATCHING QUANTITY ------------------------------------------- 

555 

556 # Determine the matching quantity and model 

557 matching_quantity = {"TRPL": "TRMC", "TRMC": "TRPL"}[fit_model.QUANTITY] 

558 matching_model = st.session_state.models[fit_model.MODEL][matching_quantity] 

559 

560 help_str = f"""The corresponding {matching_model.QUANTITY} decays are calculated from the fitting  

561 parameters. You can adjust the unknown parameters below.""" 

562 st.markdown(f"### Matching {matching_quantity}", help=help_str) 

563 

564 # Display the inputs for the matching model 

565 mu, mu_e, mu_h = 0.0, 0.0, 0.0 

566 if matching_model.QUANTITY == "TRMC": 

567 if matching_model.MODEL == "BTD": 

568 columns = st.columns(2) 

569 mu_e = columns[0].text_input( 

570 label=matching_model.get_parameter_label("mu_e"), 

571 value="10", 

572 key="matching_input1", 

573 ) 

574 mu_h = columns[1].text_input( 

575 label=matching_model.get_parameter_label("mu_h"), 

576 value="20", 

577 key="matching_input2", 

578 ) 

579 else: 

580 mu = st.text_input( 

581 label=matching_model.get_parameter_label("mu"), 

582 value="10", 

583 key="matching_input", 

584 ) 

585 

586 # Try to calculate the matching quantity 

587 try: 

588 mu, mu_e, mu_h = float(mu), float(mu_e), float(mu_h) 

589 matching_data = [] 

590 for popt in fit_displayed["popts"]: 

591 popt_ = merge_dicts(dict(mu_e=mu_e, mu_h=mu_h, mu=mu, I=1.0, y_0=0.0), popt) 

592 matching_data.append(matching_model.calculate_fit_quantity(t=fit_displayed["xs_data"][0], **popt_)) 

593 

594 # Display the decays 

595 figure = plot_decays( 

596 xs_data=fit_displayed["xs_data"], 

597 ys_data=matching_data, 

598 quantity=matching_model.QUANTITY, 

599 labels=fit_displayed["N0s_labels"], 

600 ) 

601 st.plotly_chart(figure) 

602 

603 except: 

604 st.warning("Please input correct values.") 

605 

606 # ---------------------------------------------------------------------------------------------------------------------- 

607 # ---------------------------------------------------- DATA DISPLAY ---------------------------------------------------- 

608 # ---------------------------------------------------------------------------------------------------------------------- 

609 

610 elif xs_data[0] is not None: 

611 

612 st.markdown("#### Input data") 

613 if N0s is not None: 

614 labels = get_concentrations_html(N0s) 

615 else: 

616 labels = None 

617 figure = plot_decays( 

618 xs_data, 

619 ys_data, 

620 quantity_input, 

621 labels=labels, 

622 ) 

623 st.plotly_chart(figure, use_container_width=True) 

624 

625except FitFailedException as exception: 

626 bad_fit_message = "The data could not be fitted. Try changing the parameter guess or fixed values." 

627 info_message.error(bad_fit_message) 

628 

629except Exception as exception: # pragma: no cover 

630 st.error(f"An unknown exception has happened {exception}") 

631 

632 

633# ---------------------------------------------------------------------------------------------------------------------- 

634# ------------------------------------------------- GENERAL INFORMATION ------------------------------------------------ 

635# ---------------------------------------------------------------------------------------------------------------------- 

636 

637# --------------------------------------------------- APP DESCRIPTION -------------------------------------------------- 

638 

639with st.expander("About", xs_data[0] is None): 

640 st.info( 

641 f"""*Pears* is a user-friendly web app designed to fit time-resolved photoluminescence (TRPL) and time-resolved  

642microwave photoconductivity (TRMC) data of perovskite materials using state-of-the-art charge carrier  

643recombination models (extensively discussed for TRPL in https://doi.org/10.1039/D0CP04950F): 

644* The **Bimolecular-Trapping-Auger** (BTA) accounts for bimolecular band-to-band recombination, monomolecular trapping and  

645trimolecular Auger recombination. Because Auger recombination are usually non-negligible only at very high excitation fluences,  

646Auger recombination are usually ignored within this model (the Auger recombination rate constant $k_A$ is fixed to 0 by default). 

647This model assumes that the trap states remain mostly empty and that doping is negligible. As a result, this model is  

648relatively simple and less likely to lead to over-parameterisation. 

649* The **Bimolecular-Trapping-Detrapping** (BTD) model accounts for bimolecular band-to-band recombination, bimolecular  

650trapping, and bimolecular detrapping. Contrary to the BTA model, the BTD model accounts for the population and depopulation  

651of the trap states, as well as the presence of doping. However, the increased complexity of this model can lead  

652to over-parameterisation and ambiguous results.\n 

653 

654*Pears* offers two operational modes: 

655* The **{resources.FITTING_MODE}** mode is the primary mode for fitting experimental TRPL and TRMC data. 

656* The **{resources.ANALYSIS_MODE}** mode allows to run the fitting optimisation across a range of guess parameters, helping to  

657identify whether multiple sets of parameters yield fitting solutions, as discussed in https://doi.org/10.1002/smtd.202400818.  

658If the optimisations do not converge to the same values, the fitting may be inaccurate due to the presence of multiple  

659possible solutions.\n 

660App created and maintained by [Emmanuel V. Péan](https://emmanuelpean.me).  

661[Version {__version__}](https://github.com/Emmanuelpean/pears) (last updated: {__date__}).  

662If you use *Pears* to fit your TRPL or TRMC data, please cite https://doi.org/10.1021/acs.jcim.3c00217.""" 

663 ) 

664 

665# -------------------------------------------------- MODEL DESCRIPTION ------------------------------------------------- 

666 

667with st.expander("Model & Computational Details"): 

668 st.markdown( 

669 r"""The following information can be found with more details [here](https://doi.org/10.1039/D0CP04950F) 

670 (Note that for simplicity purpose, the $\Delta$ notation for the photoexcited carriers was dropped here for  

671 simplicity purposes *e.g.* $\Delta n_e$ in the paper is $n_e$ here).""" 

672 ) 

673 st.markdown("""#### Models""") 

674 st.markdown( 

675 """Both the Bimolecular-Trapping-Auger and Bimolecular-Trapping-Detrapping models can be used which rate equations  

676 for the different carrier concentrations are given below.""" 

677 ) 

678 

679 col1, col2 = st.columns(2) 

680 

681 with col1: 

682 st.markdown("<h3 style='text-align: center; '>Bimolecular-Trapping-Auger model</h3>", unsafe_allow_html=True) 

683 st.markdown(render_image(resources.BT_MODEL_PATH, 350), unsafe_allow_html=True) 

684 st.latex(r"n_e(t)=n_h(t)=n(t)") 

685 st.latex(r"\frac{dn}{dt}=-k_Tn-k_Bn^2-k_An^3,\ \ \ n^p(t=0)=n^{p-1}(T)+N_0") 

686 st.latex(r"I_{TRPL} \propto n^2") 

687 st.latex(r"I_{TRMC}=2\mu n / N_0") 

688 st.markdown( 

689 r"""where: 

690* $n$ is the photoexcited carrier concentration (in $cm^{-3}$) 

691* $k_T$ is the trapping rate constant (in $ns^{-1}$) 

692* $k_B$ is the bimolecular recombination rate constant (in $cm^3/ns$) 

693* $k_A$ is the Auger recombination rate constant (in $cm^6/ns$) 

694* $\mu$ is the carrier mobility (in $cm^2/(Vs)$)""" 

695 ) 

696 with col2: 

697 st.markdown( 

698 "<h3 style='text-align: center; '>Bimolecular-Trapping-Detrapping model</h3>", unsafe_allow_html=True 

699 ) 

700 st.markdown(render_image(resources.BTD_MODEL_PATH, 350), unsafe_allow_html=True) 

701 st.latex(r"\frac{dn_e}{dt}=-k_B n_e (n_h+p_0 )-k_T n_e [N_T-n_t ],\ \ \ n_e^p(t=0)=n_e^{p-1}(T)+N_0") 

702 st.latex(r"\frac{dn_t}{dt}=k_T n_e [N_T-n_t]-k_D n_t (n_h+p_0 ),\ \ \ n_t^p(t=0)=n_t^{p-1}(T)") 

703 st.latex(r"\frac{dn_h}{dt}=-k_B n_e (n_h+p_0 )-k_D n_t (n_h+p_0 ),\ \ \ n_h^p(t=0)=n_h^{p-1}(T)+N_0") 

704 st.latex(r"I_{TRPL} \propto n_e(n_h+p_0)") 

705 st.latex(r"I_{TRMC} = \left(\mu_e n_e + \mu_h n_h \right)/N_0") 

706 st.markdown( 

707 r"""where: 

708* $n_e$ is the photoexcited electron concentration (in $cm^{-3}$) 

709* $n_h$ is the photoexcited hole concentration (in $cm^{-3}$) 

710* $n_t$ is the trapped electron concentration (in $cm^{-3}$) 

711* $k_B$ is the bimolecular recombination rate constant (in $cm^3/ns$) 

712* $k_T$ is the trapping rate constant (in $cm^3/ns$) 

713* $k_D$ is the detrapping rate constant (in $cm^3/ns$) 

714* $N_T$ is the trap state concentration (in $cm^{-3}$) 

715* $p_0$ is the dark hole concentration (in $cm^{-3}$) 

716* $\mu_e$ is the electron mobility (in $cm^2/(Vs)$) 

717* $\mu_h$ is the hole mobility (in $cm^2/(Vs)$)""" 

718 ) 

719 st.markdown( 

720 """For both models, the photoexcited charge carrier concentration $N_0$ is the concentration of carriers 

721 excited by a single excitation pulse. The initial condition of a electron and hole concentrations after excitation pulse $p$ is  

722 given by the sum of any remaining carriers $n_X^{p-1}(T)$ ($T$ is the excitation repetition period) just before  

723 excitation plus the concentration of carrier generated by the pulse $N_0$ (except for the trapped electrons).""" 

724 ) 

725 

726 st.markdown("""#### Fitting""") 

727 st.markdown( 

728 """Fitting is done using a least square optimisation. For a dataset containing $M$ curves, each containing  

729 $N_i$ data points, the residue $SS_{res}$ is:""" 

730 ) 

731 st.latex(r"""SS_{res}=\sum_i^M\sum_j^{N_i}\left(y_{i,j}-F(t_{i,j},A_{i})\right)^2""") 

732 st.markdown( 

733 """where $y_{i,j}$ is the intensity associated with time $t_{i,j}$ of point $j$ of curve $i$. 

734 $A_i$ are the model parameters associated with curve $i$ and $F$ is the fitting model given by for the TRPL and TRMC respectively:""" 

735 ) 

736 st.latex(r"F(t,I_0, y_0, k_B,...)=I_0 \frac{I_{TRPL}(t,k_B,...)}{I_{TRPL}(0, k_B,...)} + y_0") 

737 st.latex(r"F(t, y_0, k_B,...)=I_{TRMC}(t,k_B,...) + y_0") 

738 st.markdown( 

739 """ where $I_0$ is an intensity factor and $y_0$ is an intensity offset. Contrary to the other parameters of  

740 the models (e.g. $k_B$), $I_0$ and $y_0$ are not kept the same between the different TRPL curves *i.e.* the fitting  

741 models for curves $A$, $B$,... are:""" 

742 ) 

743 st.latex(r"F_A(t,I_0^A, y_0^A, k_B,...)=I_0^A \frac{I_{TRPL}(t,k_B,...)}{I_{TRPL}(0, k_B,...)} + y_0^A") 

744 st.latex(r"F_B(t,I_0^B, y_0^B, k_B,...)=I_0^B \frac{I_{TRPL}(t,k_B,...)}{I_{TRPL}(0, k_B,...)} + y_0^B") 

745 st.latex("...") 

746 st.markdown( 

747 """**By default, $I_0$ and $y_0$ are respectively fixed at 1 and 0 (assuming no background noise and  

748 normalised intensity**. The quality of the fit is estimated with the coefficient of determination $R^2$:""" 

749 ) 

750 st.latex(r"R^2=1-\frac{SS_{res}}{SS_{total}}") 

751 st.markdown( 

752 r"""where $SS_{total}$ is defined as the sum of the squared difference between each point and the  

753 average of all curves $\bar{y}$:""" 

754 ) 

755 st.latex(r"""SS_{total}=\sum_i^M\sum_j^{N_i}\left(y_{i,j}-\bar{y}\right)^2""") 

756 st.markdown( 

757 """For fitting, it is assumed that there is no carrier accumulation between excitation pulses due to the 

758 presence of non-recombined carriers from previous excitation pulses. This requires the TRPL decays to be measured 

759 with long enough excitation repetition periods such that all carriers can recombine.""" 

760 ) 

761 

762 st.markdown("""#### Carrier Accumulation""") 

763 st.markdown( 

764 """*Pears* can calculate the expected effect of carrier accumulation on the TRPL and TRMC from the  

765 parameters retrieved from the fits if the repetition period is provided. The carrier accumulation ($CA$) is  

766 calculated as the maximum difference between the simulated TRPL/TRMC after the first ($p=1$) and stabilised ($p=s$) pulses:""" 

767 ) 

768 st.latex( 

769 r"{CA}_{TRPL}=\max\left({\frac{I_{TRPL}^{p=1}(t)}{I_{TRPL}^{p=1}(0)}-\frac{I_{TRPL}^{p=s}(t)}{I_{TRPL}^{p=s}(0)}}\right)" 

770 ) 

771 st.latex(r"{CA}_{TRMC}=\max\left(I_{TRMC}^{p=1}(t)-I_{TRMC}^{p=s}(t)\right)") 

772 st.markdown( 

773 """The stabilised pulse is defined as when the electron and hole concentrations vary by less than  

774 10<sup>-3</sup> % of the photoexcited concentration between two consecutive pulses:""", 

775 unsafe_allow_html=True, 

776 ) 

777 st.latex(r"|n_e^p(t)-n_e^{p+1}(t)|<10^{-5} N_0") 

778 st.latex(r"|n_h^p(t)-n_h^{p+1}(t)|<10^{-5} N_0") 

779 

780 st.markdown("""#### Process Contributions""") 

781 st.markdown( 

782 """The contribution of each process to the TRPL/TRMC intensity variations over time is calculated from the  

783 fitted values (see Equations 22 to 25 in https://doi.org/10.1039/D0CP04950F). It is important to ensure that the  

784 contribution of a process (*e.g.*, trapping) is non-negligible so that the associated parameters (*e.g.*, $k_T$  

785 in the case of the Bimolecular-Trapping-Auger model) are accurately retrieved.""", 

786 unsafe_allow_html=True, 

787 ) 

788 

789 st.markdown("""#### Grid Fitting""") 

790 st.markdown( 

791 f"""This mode runs the fitting process for a grid of guess values. The grid is generated from every  

792 possible combinations of guess values supplied (*e.g.*, $k_B$: 10<sup>-20</sup>, 10<sup>-19</sup> and  

793 $k_T$: 10<sup>-3</sup>, 10<sup>-2</sup> yields 4 sets of guess values: (10<sup>-20</sup>, 10<sup>-3</sup>), 

794 (10<sup>-20</sup>, 10<sup>-2</sup>), (10<sup>-19</sup>, 10<sup>-3</sup>) and (10<sup>-19</sup>, 10<sup>-2</sup>)).  

795 Note that in the case of the bimolecular-trapping-detrapping model, only set of guess values satisfying $k_T>k_B$  

796 and $k_T>k_D$ are considered to keep the computational time reasonable. Fitting is then carried using each set of  

797 guess values as schematically represented below: {render_image(resources.OPT_GUESS_PATH, 600)} 

798 In the case where all the optimisations converge towards a similar solution, it can be assumed that only 1 solution 

799 exist and that therefore the parameter values obtained accurately describe the system measured. However, if the fits  

800 converge toward multiple solutions, it is not possible to ascertain which solution represents the system accurately. 

801 In this case, measuring the TRMC (if the TRPL was fitted) or the TRPL (if the TRMC was measured) can allow to determine  

802 which solution represents the system best (see https://doi.org/10.1002/smtd.202400818)""", 

803 unsafe_allow_html=True, 

804 ) 

805 

806# ------------------------------------------------------- HOW TO ------------------------------------------------------- 

807 

808with st.expander("Getting started"): 

809 st.markdown("""#### How To""") 

810 st.markdown("""Follow these steps to fit TRPL/TRMC decays.""") 

811 st.markdown( 

812 f"""1. Upload your data and select the correct data format and delimiter. The data should be displayed. 

813 Select the appropriate quantity (TRPL or TRMC). Check the "Pre-process data" if required, to shift the data  

814 and normalise them (for TRPL only). The following datasets are provided for testing purpose: 

815 * {resources.BT_TRPL_LINK},  

816 * {resources.BTD_TRPL_LINK}, 

817 * {resources.BT_TRMC_LINK}, 

818 * {resources.BTD_TRMC_LINK}""", 

819 unsafe_allow_html=True, 

820 ) 

821 

822 st.markdown( 

823 f"""2. Input the photoexcited carrier concentrations (in cm<sup>-3</sup>) for each decay. Use the values below  

824 if you are using the example datasets: 

825 * _e.g._: {', '.join([('%.2e' % f).replace('+', '') for f in resources.BT_TRPL_DATA[-1]])} (BT model datasets) 

826 * _e.g._: {', '.join([('%.2e' % f).replace('+', '') for f in resources.BTD_TRPL_DATA[-1]])} (BTD model datasets)""", 

827 unsafe_allow_html=True, 

828 ) 

829 

830 st.markdown("""3. Choose the desired fitting model.""") 

831 

832 st.markdown("""4. (Optional) Set the value of known parameters.""") 

833 

834 if fit_mode == "Fitting": 

835 st.markdown("""5. (Optional) For non fixed parameters, choose the guess value for the optimisation.""") 

836 else: 

837 st.markdown("""5. (Optional) For non fixed parameters, choose multiple guess values for each parameter.""") 

838 

839 st.markdown( 

840 """6. (Optional) Enter the excitation repetition period (in ns) to calculate the carrier accumulation effect on  

841 the TRPL/TRMC intensity as well as show the evolution of the carrier concentrations after multiple consecutive  

842 excitation pulses until stabilisation. Note that this can be done after fitting.""" 

843 ) 

844 

845 st.markdown("""7. Press "Run" """) 

846 

847 st.markdown("""#### Video tutorial""") 

848 st.video(resources.TUTORIAL_PATH) 

849 

850# ------------------------------------------------------ CHANGELOG ----------------------------------------------------- 

851 

852with st.expander("Changelog"): 

853 st.markdown(read_txt_file(os.path.join(dirname, "CHANGELOG.md"))) 

854 

855# ----------------------------------------------------- DISCLAIMER ----------------------------------------------------- 

856 

857with st.expander("License & Disclaimer"): 

858 st.markdown(read_txt_file(os.path.join(dirname, "LICENSE.txt")))