Coverage for app/main.py: 100%
338 statements
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-28 18:30 +0000
« 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"""
12import copy
13import os
15import numpy as np
16import pandas as pd
17import streamlit as st
18from streamlit.runtime.runtime_util import MessageSizeError
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
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"]
36dirname = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
38# ----------------------------------------------------------------------------------------------------------------------
39# ------------------------------------------------------- SET UP -------------------------------------------------------
40# ----------------------------------------------------------------------------------------------------------------------
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)
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)
54def reset_carrier_accumulation() -> None:
55 """Reset the stored carrier accumulation"""
56 print("Resetting carrier accumulation")
57 st.session_state.carrier_accumulation = None
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()
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)
75set_style()
77# ----------------------------------------------------------------------------------------------------------------------
78# -------------------------------------------------------- INPUT -------------------------------------------------------
79# ----------------------------------------------------------------------------------------------------------------------
81logo_placeholder = st.container()
82info_message = st.empty()
84# -------------------------------------------------------- MODES -------------------------------------------------------
86fit_mode = st.sidebar.selectbox("Mode", resources.APP_MODES, on_change=reset_results, key="fit_mode_")
88# -------------------------------------------------------- DATA --------------------------------------------------------
90# File uploader
91input_filename = st.sidebar.file_uploader(
92 label="Data file",
93 key="input_filename_",
94 on_change=reset_results,
95)
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)
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)
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)
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)
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.")
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.")
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.")
163 # Process the data
164 if process_input:
165 xs_data, ys_data = process_data(xs_data, ys_data, quantity_input == "TRPL")
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}")
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)
181# ------------------------------------------------------ FLUENCES ------------------------------------------------------
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}"))
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")
206# --------------------------------------------------- MODEL SELECTION --------------------------------------------------
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]
223# ------------------------------------------------ FIXED & GUESS VALUES ------------------------------------------------
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 )
234 # -------------------------------------------------- FIXED VALUES --------------------------------------------------
236 key = model_name + param_key + "fixed"
238 if key not in st.session_state:
239 st.session_state[key] = to_scientific(model.fvalues[param_key])
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 )
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
258 # -------------------------------------------------- GUESS VALUES --------------------------------------------------
260 # If no fixed value, display and set the guess value
261 if model.fvalues[param_key] is None:
263 # Fitting mode
264 if fit_mode == resources.FITTING_MODE:
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])
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 )
277 # Store the guess value
278 try:
279 model.gvalues[param_key] = float(gvalue)
280 except (ValueError, TypeError):
281 pass
283 # Grid fitting mode
284 else:
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])
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 )
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
303# ------------------------------------------------- REPETITION PERIOD --------------------------------------------------
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 )
318 try:
319 period = float(period_input)
320 except ValueError:
321 pass
324# ----------------------------------------------------- RUN BUTTON -----------------------------------------------------
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")
331# ----------------------------------------------------------------------------------------------------------------------
332# --------------------------------------------------- RESULTS DISPLAY --------------------------------------------------
333# ----------------------------------------------------------------------------------------------------------------------
335# display the results if results have been previously stored
336try:
337 if st.session_state.results or run_button:
339 # Remove the data and fluence messages
340 data_message.empty()
341 fluence_message.empty()
343 # --------------------------------------------------- FITTING --------------------------------------------------
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")
350 # Run the fitting or grid fitting and store the output in the session state
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()
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()
369 info_message.empty()
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.")
375 # Retrieve the stored results from the session state
376 fit_output, fit_model, fit_N0s = st.session_state.results
378 # -------------------------------------------- CARRIER ACCUMULATION --------------------------------------------
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
400 print("Getting stored CA")
401 carrier_accumulation = st.session_state.carrier_accumulation
403 # -------------------------------------------------- PARALLEL PLOT -------------------------------------------------
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")
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)
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}"
429 # --------------------------------------------------------------------------------------------------------------
430 # ------------------------------------------------ DISPLAY FIT ------------------------------------------------
431 # --------------------------------------------------------------------------------------------------------------
433 st.markdown(message)
434 col1, col2 = st.columns(2)
436 with col1:
438 # -------------------------------------------- FITTING PLOT & EXPORT -------------------------------------------
440 st.markdown("#### Data Fit", help="Displays the raw and fitted data.")
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)
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)
458 # -------------------------------------- OPTIMISED PARAMETERS DISPLAY --------------------------------------
460 st.markdown("#### Parameters", help="Parameters obtained from the fit.")
461 table = generate_html_table(fit_displayed["popt_df"])
462 st.html(table)
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)
469 # --------------------------------------------- CONTRIBUTIONS ----------------------------------------------
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)
476 # Contribution analysis
477 for s in fit_model.get_contribution_recommendations(fit_displayed["contributions"]):
478 st.warning(s)
480 # ------------------------------------------ CARRIER ACCUMULATION -----------------------------------------
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)
488 # If carrier accumulation could be calculated
489 if st.session_state.carrier_accumulation:
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])]
495 # Display the carrier accumulation table
496 table = generate_html_table(carrier_accumulation["CA_df"])
497 st.html(table)
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)
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 )
524 else:
525 warn = "Carrier accumulation could not be calculated due to excessive computational requirements."
526 st.warning(warn)
528 with col2:
530 # -------------------------------------------- CONCENTRATIONS PLOT ---------------------------------------------
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)
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 )
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)
554 # -------------------------------------------- MATCHING QUANTITY -------------------------------------------
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]
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)
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 )
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_))
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)
603 except:
604 st.warning("Please input correct values.")
606 # ----------------------------------------------------------------------------------------------------------------------
607 # ---------------------------------------------------- DATA DISPLAY ----------------------------------------------------
608 # ----------------------------------------------------------------------------------------------------------------------
610 elif xs_data[0] is not None:
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)
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)
629except Exception as exception: # pragma: no cover
630 st.error(f"An unknown exception has happened {exception}")
633# ----------------------------------------------------------------------------------------------------------------------
634# ------------------------------------------------- GENERAL INFORMATION ------------------------------------------------
635# ----------------------------------------------------------------------------------------------------------------------
637# --------------------------------------------------- APP DESCRIPTION --------------------------------------------------
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
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 )
665# -------------------------------------------------- MODEL DESCRIPTION -------------------------------------------------
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 )
679 col1, col2 = st.columns(2)
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 )
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 )
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")
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 )
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 )
806# ------------------------------------------------------- HOW TO -------------------------------------------------------
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 )
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 )
830 st.markdown("""3. Choose the desired fitting model.""")
832 st.markdown("""4. (Optional) Set the value of known parameters.""")
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.""")
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 )
845 st.markdown("""7. Press "Run" """)
847 st.markdown("""#### Video tutorial""")
848 st.video(resources.TUTORIAL_PATH)
850# ------------------------------------------------------ CHANGELOG -----------------------------------------------------
852with st.expander("Changelog"):
853 st.markdown(read_txt_file(os.path.join(dirname, "CHANGELOG.md")))
855# ----------------------------------------------------- DISCLAIMER -----------------------------------------------------
857with st.expander("License & Disclaimer"):
858 st.markdown(read_txt_file(os.path.join(dirname, "LICENSE.txt")))