electrokitty 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 1.2
2
+ Name: electrokitty
3
+ Version: 1.0.2
4
+ Summary: A simulation and simple analysis tool for electrochemical data
5
+ Home-page: UNKNOWN
6
+ Author: Ožbej Vodeb
7
+ Author-email: ozbej.vodeb@gmail.com
8
+ License: BSD
9
+ Description: More information comming soon
10
+ Platform: UNKNOWN
11
+ Requires-Python: >=3.7
@@ -0,0 +1,5 @@
1
+ An Electrochemical simulator build in Python and C++
2
+
3
+ to install run pip install electrokitty
4
+
5
+ More information comming soon
@@ -0,0 +1,189 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Thu Aug 29 14:58:33 2024
4
+
5
+ @author: ozbejv
6
+ """
7
+
8
+ import numpy as np
9
+ import multiprocessing
10
+
11
+ class electrokitty_sampler():
12
+ """
13
+ Class containing the MCMC sampler used in parameter estimation
14
+ """
15
+ def __init__(self, n_samples, burn_in_per, num_chains,
16
+ multi_processing, bounds, I_data):
17
+
18
+ self.n_samples=n_samples
19
+ self.burn_in_per=burn_in_per
20
+ self.num_chains=num_chains
21
+ self.multi_processing=multi_processing
22
+ self.bounds=bounds
23
+ self.y_sim=None
24
+ self.I_data=I_data
25
+
26
+ def give_y_sim(self, ysim):
27
+ """
28
+ update the function used to generate the simulation
29
+ """
30
+ self.y_sim=ysim
31
+
32
+ def prior(self, theta, lower_bound, upper_bound):
33
+ """
34
+ Function that given the guess and the bounds calculates the prior,
35
+ either 0 or 1
36
+ """
37
+ p=[]
38
+
39
+ for i in range(len(theta)):
40
+
41
+ if theta[i] >= lower_bound[i] and theta[i] <= upper_bound[i]:
42
+ p.append(1)
43
+ else:
44
+ p.append(-np.inf)
45
+ return min(p)
46
+
47
+ def likelihood(self, theta):
48
+ """
49
+ Given the guess calculates the likelihood based on a guessian that the guess fits the data
50
+ """
51
+ i_sim=self.y_sim(theta)
52
+
53
+ N=len(self.I_data)
54
+
55
+ p=-N/2*np.log(2*np.pi*theta[-1]**2)-np.sum((self.I_data-i_sim)**2)/2/theta[-1]**2
56
+
57
+ return p
58
+
59
+ def propose(self, p, covar):
60
+ """
61
+ A function that given the means and their covariance matrix proposes a guess
62
+ """
63
+
64
+ p_new=np.random.multivariate_normal(p, covar)
65
+
66
+ return p_new
67
+
68
+ def accept(self, p, p_new, lower_bound, upper_bound, acc_rate):
69
+ """
70
+ Calculates the likelihood of the old and new guess and either accepts it or rejects the new guesss
71
+ returning either
72
+ """
73
+ theta=p
74
+ acc=0
75
+
76
+ numerator = self.likelihood(p) + (self.prior(p, lower_bound, upper_bound))
77
+
78
+ numerator_new = self.likelihood(p_new) + (self.prior(p_new, lower_bound, upper_bound))
79
+
80
+ if numerator_new >= numerator:
81
+ theta=p_new
82
+ acc_rate+=1
83
+ acc=1
84
+
85
+ elif np.log(np.random.rand()) < (numerator_new-numerator):
86
+ theta=p_new
87
+ acc_rate+=1
88
+ acc=1
89
+
90
+ return theta, acc_rate, acc
91
+
92
+ def update_moms(self, means, M2, current, log_lam, i, p):
93
+ """
94
+ updates the parameter mean and covariance matrix based on the new guess
95
+
96
+ using HB-MCMC it updates the mean and covariance based on the acceptance rate
97
+ """
98
+ nexti=i+1
99
+ w=nexti**-0.5
100
+ newme=(1-w)*means+w*current
101
+ db, da = (current-newme), (current-newme)
102
+ newM=(1-w)*M2+w*np.outer(db,da)
103
+ log_lam+=w*(p-0.225)
104
+ return newme, newM, log_lam
105
+
106
+ def single_chain(self, n_samples, means, lower_bound, upper_bound, chain_num):
107
+ """
108
+ Calculates a single MCMC chain using HBMCMC
109
+ """
110
+ divisor=max([1,int(0.01*n_samples)])
111
+ results=[]
112
+
113
+ dim=len(means)
114
+
115
+ cov0=1/np.sqrt(dim)*np.eye(dim)
116
+
117
+ results.append(means)
118
+
119
+ M2=cov0
120
+ means=means
121
+ acc=0
122
+ log_lam=0
123
+ acc_total=0
124
+
125
+ for i in range(n_samples):
126
+ x_cur=results[-1]
127
+
128
+ prop_theta=self.propose(x_cur, np.exp(log_lam)*M2)
129
+
130
+ theta, acc_total, acc = self.accept(x_cur, prop_theta, lower_bound, upper_bound, acc_total)
131
+ results.append(theta)
132
+
133
+ means, M2, log_lam = self.update_moms(means, M2, results[-1], log_lam, i, acc_total/(i+1))
134
+ if i%divisor==0:
135
+ print(f"chain num: {chain_num} iteration: {i} acceptance rate: {round(acc_total/(i+1),3)}")
136
+
137
+ return np.array(results)
138
+
139
+ def single_chain_wrap(self, args):
140
+ """
141
+ Call to execute a single chain
142
+ """
143
+ return self.single_chain(*args)
144
+
145
+ def start_sampler(self, initial_guess):
146
+ """
147
+ Call to start samplig from the initial guess
148
+
149
+ This will perform either on a single or multicore depending on what was chosen
150
+ """
151
+ lower_bound, upper_bound=self.bounds
152
+
153
+ initial_positions=[]
154
+ scatter=np.random.uniform(0.8, 1.2, self.num_chains)
155
+ for index in range(self.num_chains):
156
+ initial_positions.append(scatter[index]*initial_guess)
157
+
158
+ chains=[]
159
+
160
+ if self.multi_processing == False:
161
+ print("Starting MCMC sampling")
162
+ print()
163
+
164
+ for ch_num in range(self.num_chains):
165
+ print()
166
+ print(f"Resolving chain num: {ch_num}")
167
+ print()
168
+ single_chain=self.single_chain(self.n_samples,
169
+ initial_positions[ch_num],
170
+ lower_bound, upper_bound, ch_num)
171
+ chains.append(single_chain)
172
+
173
+
174
+ else:
175
+ stuff=[]
176
+ for i in range(self.num_chains):
177
+ stuff.append([self.n_samples, initial_positions[i],
178
+ lower_bound, upper_bound, i])
179
+
180
+ pool=multiprocessing.Pool(self.num_chains)
181
+ chains=pool.map(self.single_chain_wrap, stuff)
182
+ pool.close()
183
+ pool.join()
184
+
185
+
186
+ chains=np.array(chains)
187
+ return chains
188
+
189
+