{'fraction_non_alphanumeric': 0.029135518497220625, 'fraction_numerical': 0.008370072199859434, 'mean_word_length': 4.913109180204004, 'pattern_counts': {'":': 0, '<': 0, '<?xml version=': 0, '>': 0, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
Bayesim: a tool for adaptive grid model fitting with Bayesian inference

Bayesian inference is a widely used and powerful analytical technique in fields such as astronomy and particle physics but has historically been underutilized in some other disciplines including semiconductor devices. In this work, we introduce Bayesim, a Python package that utilizes adaptive grid sampling to efficiently generate a probability distribution over multiple input parameters to a forward model using a collection of experimental measurements. We discuss the implementation choices made in the code, showcase two examples in photovoltaics, and discuss general prerequisites for the approach to apply to other systems.


INTRODUCTION
There are a plethora of examples across diverse scientific and engineering fields of mathematical models used to simulate the results of experimental observations. In many cases, there are input parameters to these models which are difficult to determine via direct measurement, and it is desirable to invert the numerical model -that is, use the experimental observations to determine values of the input parameters. Bayesian inference, or Bayesian parameter estimation (BPE) is a fruitful framework within which to do such fitting, since the resulting posterior probability distribution over the parameters of interest can give rich insights into not just the most likely values of the parameters, but also uncertainty about these values and the potentially complicated ways in which they can covary to give equally good fits to observations. It has been widely used for decades in fields such as cosmology [17], particle physics [3,18], and biology [6,20], with well-developed and widely used code packages specialized to these purposes. [5,16] However, the use of Bayesian inference in materials science and related contexts is much more recent, having only emerged in the past few years. [1, 19,21] We have previously demonstrated the value of a BPE approach in using automated high-throughput temperatureand illumination-dependent current-voltage measurements (JV T i) to fit material/interface properties and defect recombination parameters in photovoltaic (PV) absorbers [1,9], i.e. as a replacement for direct experimental characterization via probabilitistic inversion of a forward model. In cases such as these, when the data model is not a simple analytical equation but rather a computationally intensive numerical model, efficient, nonredundant sampling of the parameter space when computing likelihoods becomes critical to making the fit feasible.
In this work, we introduce bayesim, a Python-based code that utilizes adaptive grid sampling to perform BPE. It is designed to be easy to use by anyone with basic familiarity with Python. We discuss the structure of the code, its implementation, and provide several examples of its usage. While the authors' expertise is in the realm of semiconductor physics and thus the examples herein are drawn from that space, we also discuss the general characteristics of a problem amenable to this approach so that researchers from other fields might adopt it as well.

TECHNICAL BACKGROUND
Bayes' Theorem is a relationship between conditional probabilities. It states where the notation P (A|B) indicates the probability of A being true given that B is true. H is a hypothesis and E the observed evidence. P (H) is termed the prior, P (E|H) the likelihood, P (H|E) the posterior, and P (E) is a normalizing constant. If there are n pieces of evidence, this can generalize to an iterative process where i.e. as each new piece of evidence is fed in, the posterior probability distribution is updated to accommodate it and should in general become more concentrated into one region of parameter space. In a multidimensional parameter estimation problem, each hypothesis H is a tuple of possible values for the fitting parameters, i.e. a point in the parameter space, while the evidence E is an observed output of a measurement as a function of various experimental conditions.
In order to compute a likelihood, a model capable of simulating that observed output as a function of both the fitting parameters and the experimental conditions is required. In bayesim, likelihoods are calculated for each point in parameter space using a Gaussian where the argument is the difference between observed and simulated output at that point, and the standard deviation is the sum of experimental uncertainty and model uncertainty. The experimental uncertainty is a number provided by the user that quantifies any noise/irreproducibility inherent to the measurement, while the model uncertainty is calculated by bayesim and reflects the sparseness of the parameter space grid, i.e. how much simulated output changes from one grid point to another.
A high-level summary of what bayesim does is shown in Figure 1a. The flowchart shows that observed (as a function of experimental conditions {C} and simulated (as a function of fitting parameters {P } and experimental conditions {C}) outputs are compared to produce a probability distribution over {P }. Figure 1b schematically indicates the adaptive grid sampling approach for a hypothetical two-dimensional parameter space, wherein grid boxes exceeding some threshold probability are subdivided and lower-probability regions discarded, allowing attainment of a high fitting precision without needing to sample the entire parameter space at the same high density.

SOFTWARE ARCHITECTURE AND INTERFACE
The basic structure of bayesim is shown in Figure 2a. Detailed and up-to-date documentation of classes, functions, and example applications is maintained online. [11] The top-level object with which users interact is implemented in the Model class. The params module defines classes to store information about the various types of parameters (fitting parameters, experimental conditions, and measured output) while the Pmf class stores the probability distribution and implements the manipulations required for Bayesian updates. Figure 2b is a flowchart of using the code, with the main inference loop surrounded by a dotted line. One first needs to provide the observed and modeled data. Observed data is accepted in HDF5 format, while modeled data can be an HDF5 file (in which case bayesim will determine the fitting parameter space grid from this file), or if the fitting parameter space has been predefined, one can also attach a Python function that computes the model data (e.g. an interface to a numerical model). Observed data should have associated experimental uncertainties, or the user can indicate a fixed value of uncertainty to use for all observations upon attaching the data.
Once modeled data is attached, model uncertainty can be computed. At each point in the space of experimental conditions, a matrix of the simulated output variable as a function of the fitting parameters is constructed and at each point in this matrix, the largest difference along any index is computed. This is set as the model uncertainty for this set of experimental conditions at this point in parameter space.
At this point, the Bayesian inference loop can proceed. We start with a bounded uniform prior distribution over the entire parameter space grid and the observed data in a randomized order. We feed in one piece of evidence (i.e. one observed data point) and compute the likelihood using a Gaussian. The standard deviation for each point in the parameter space is the sum of the experimental uncertainy of that observation and the model uncertainty at that point. The posterior distribution is computed as a Bayesian update on the prior. Next, we check whether the threshold condition for concentration of probability (defined as at least some percentage (by default 90%) of the probability mass residing in at most some percentage (by default 5%) of the parameter space volume. If this threshold is satisfied, the current posterior is stored and the prior reset to a uniform distribution. If not, the next observation is fed in. This procedure is repeated until the requisite number (by default, 80%) of observed data points has been used, with the final posterior being the average of every stored posterior. The posterior is calculated in this way (rather than a Bayesian multiplication through every single observed point) to avoid numerical errors that can arise if very small numbers are multiplied together. This procedure of running the Bayesian loop multiple times until the desired number of pieces of evidence are used is encapsulated in the Model.run() function. Now the grid can be subdivided. The user defines a threshold probability (by default 0.001) for a grid box to possess, and all boxes with greater than this probability, as well as all their immediate neighbors, are divided in two along every parameter. bayesim saves a list of the new model calculations that need to be done for inference to proceed again, and the user provides these, again either as a callable or an HDF5 file. For the next round of inference, the user can also define a "relaxation" parameter from 0 to 1.0 (default 0) that will mix in some fraction of the posterior from the previous step with the uniform distribution over this new grid.
This overall procedure can be repeated as many times as the user wishes to get a more precise fit -eventually, the concentration of probability becomes limited by experimental rather than model uncertainty. There is also an option to define a minimum width for a box along any parameter direction, and if a box is already less than this width, bayesim will not subdivide in that direction.
The most flexible way to interact with bayesim is via Python scripting or through literate programming in a Jupyter notebook. There is also a command line interface (CLI) for users less familiar with coding in Python planned for future release.
bayesim relies on a variety of external open-source packages. These include numpy [15] and scipy [8] for a variety of mathematical functions and vectorized implementations, joblib [4] for simple parallelism, deepdish [13] for saving and loading HDF5 files, pandas [14] for data manipulation, and matplotlib [7] for visualization.

Ideal Diode Model
As a first example, we fit a simple two-parameter model of solar cell current density J (as a function of voltage V and temperature T ) known as the ideal diode model: where k = 8.61733 × 10 −5 eV/K is Boltzmann's constant, by convention J L (the light current) is negative and J 0 (the saturation current) is positive but strongly dependent on temperature, a dependence we can approximate as: where E g0 is the zero-temperature bandgap of the material. For this example, we will set this to 1.2 eV and J L to -0.03 mA/cm 2 . There are thus two parameters to be fit, the coefficient B (which can range over a wide variety of values) and the ideality factor n (which is by definition between 1 and 2).
As validation, we generate the "observations" using the same model we'll use for fitting, with parameter values of n = 1.36 and B = 258, and four temperature values of 150, 200, 250, and 300 K. These "observations" are shown in Figure 3a. When bayesim imports observed data, it can optionally discard data points that are very close by to each other in order to reduce computational load on modeling less informative points -the observed points that were actually imported (59 out of 305 total "observations") are shown as overlaid scatter dots. Figure 3b shows the posterior distributions after the first inference run and after each round of subdivision, demonstrating that bayesim gets progressively closer to the "true" values as the loop continues iterating. In addition, on the first inference run (prior to any subdivision), an average of 45% of grid points had larger model uncertainty than experimental uncertainty, while after the second subdivision, only 1.9% did, while the average absolute error of the highest-probability model decreased from 0.03 after the first run to 0.007 after the second subdivision.
In addition, the initial grid had 400 boxes, the singly subdivided grid 164, and the final grid (4x initial resolution) had 144, meaning the overall additional computational effort to quadruple the fit precision was less than producing the initial simulation set. In comparison, using a "brute-force" grid approach, reaching 4x precision would take 4 n times the computational load in n dimensions, i.e. 16x for this case. The exact factor of improvement is problem-dependent, but in all cases the load is significantly less than the exponential scaling of the prior approach.

SnS Solar Cell
This example demonstrates a more realistic application of bayesim, i.e. when the model being used to compute likelihoods is more computationally expensive. In this case, we will repeat the analysis from Ref.
We start on a significantly coarser grid than Ref.
[1] and show only one level of subdivision to make the example feasible to run on a personal computer. Figure 4 shows the results from this fit done in bayesim compared to the higher-resolution grid analysis done previously. Note that the inference step in full-resolution grid took a few hours, while the adaptive grid fit takes a few minutes. While the fit precision is of course lower at only one level of subdivision, the extracted values are generally in agreement and more importantly, the relationship between the mobility and lifetime (namely, that the product, i.e. the diffusion length), is constrained, emerges just as clearly. The  FIG. 3. a) "Observations" generated by ideal diode model, Equation 3, with overlaid scatter to show which points were actually imported and used by bayesim, b) PMF's after the first inference run, second (one subdivision), and third (two subdivision), in blue, orange, and green, respectively. Values used to generate data indicated in red.
FIG. 4. PMFs produced by bayesim, after initial run and after one subdivision (blue and green, respectively). In red, PMF from Reference [1]. Inset: device structure (illuminated from above) with bulk SnS (µ,τ fit) and SnS/Zn(O,S) interface (∆Ec, S eff fit) indicated. differences in fitted values stem from the fact that the prior published analysis didn't account for model uncertainty, while this feature is included in bayesim.

CONCLUSIONS
We have introduced bayesim, a Python-based adaptive grid sampled Bayesian inference code which is flexible to the model used to calculated likelihood as well as the parameter space over which to fit. We provided two examples of application of the code in the space of characterizing PV devices using JVT(i) measurements, as this is the authors' area of expertise. However, nothing about the code or the approach limits it to application in PV materials. Any situation where there is: • a forward model that captures the relevant physics (particularly one that is relatively expensive to evaluate), and • a set of measurements of the output of that model as a function of several experimental conditions would be amenable to bayesim. The online documentation page [11] maintains a list of examples which will grow as the authors become aware of/implement more. The latest version of the code can always be found in the Github repository [10], and the most recent stable and thoroughly-tested version is installable via PyPI [12]. The online documentation also hosts other useful information, such as detailed documentation of every class and function in bayesim, a more extensive technical background section than is included here, a list of planned future features, and an updated manual for usage of the code.