-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcapillary_potential.py
More file actions
2113 lines (1637 loc) · 95.3 KB
/
capillary_potential.py
File metadata and controls
2113 lines (1637 loc) · 95.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python
#
# capillary_potential.py
# Written by: Lars Konermann, June 2025
#
# -------------------------------------------------------------------------
# MODELING THE ELECTROSTATICS OF AN ESI CAPILLARY AND ITS COUNTER ELECTRODE
# -------------------------------------------------------------------------
# PLEASE CITE:
#
# "A Simple Method for Determining Charge Distributions, Potentials, and Electric Fields
# Associated with an Electrospray Capillary"
# Lars Konermann
# J. Am. Soc. Mass Spectrom. 2025
#
#
# See below for
# - what this program does
# - input file info
# - output file info
#
# run this program by typing "python3 ./capillary_potential.py <input_file.txt>"
#
#
# WHAT THIS PROGRAM DOES:
# =======================
#
# This program builds a single layer ESI capillary consisting of concentric rings (cap_rings) of beads.
# Because of symmetry, all beads in a given cap_ring have the same charge.
# These charges are optimized, such that the surface potential is equal everywhere,
# as expected for an electrically conducting capillary (e.g. iron) connected to
# a high voltage power supply.
# The surface potential at each cap_probe_out site j is phi = SUM(qi/rij)
#
# The program can run without or with counter electrode (cel). The cel (if present) is modeled as a grounded
# planar metal disk
#
# Every capillary bead has the same size, and the spherical capillary diameter at every z-value assumes that
# beads are in direct contact, in a hexagonally packed pattern.
# Arbitrary capillary shapes can be generated by specifying the number of beads in each cap_ring.
#
# The program can operate in two modes:
#
# Mode 1: NO COUNTER ELECTRODE
# cel = "no" means that we are dealing with an isolated capillary,
# where the potential becomes zero at infinite distance.
#
# capillary layout and cap_ring numbering (*s represent cap_probe_out sites):
# (cap_probe_ins sites are not shown, they are just on the INSide of the capillary)
#
# CAPILLARY (pos charge)
# * * * * .
# 9+ 8+ 7+ 6+ * .
# 9+ 8+ 7+ 6+ 5+ * * * * * .
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ .
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ .
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ .
# 9+ 8+ 7+ 6+ 5+ .
# 9+ 8+ 7+ .
# .
# <---------negative---------------------------------0--------------------positive----> x-axis
# |
# -(x_cel_dist)
#
# (when used in this mode, x_cel_dist specifies the distance from ring0 to yz plane at x=0)
#
# Mode 2: WITH GROUNDED COUNTER ELECTRODE
# cel = "yes" means that the capillary is facing a grounded flat counter electrode
# The potential at the counter electrode is zero.
# The distance from capillary tip (cap_ring 0) to the counter electrode is x_cel_dist.
# We first deal with this situation using the image charge method, where every bead has an oppositely
# charged mirror image.
#
# capillary layout and cap_ring numbering (dots represent cap_probe_out sites):
#
#
#
# ACTUAL CAPILLARY (pos charge) | grounded IMAGE CHARGE CAPILLARY
# * * * * | counter
# 9+ 8+ 7+ 6+ * | electrode has pot = 0 6- 7- 8- 9-
# 9+ 8+ 7+ 6+ 5+ * * * * * | 5- 6- 7- 8- 9-
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ | 0- 1- 2- 3- 4- 5- 6- 7- 8- 9-
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ | 0- 1- 2- 3- 4- 5- 6- 7- 8- 9-
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ | 0- 1- 2- 3- 4- 5- 6- 7- 8- 9-
# 9+ 8+ 7+ 6+ 5+ | 5- 6- 7- 8- 9-
# 9+ 8+ 7+ | 6- 7- 8- 9-
#
# <---------negative---------------------------------0--------------------positive----> x-axis
# | |
# -(x_cel_dist) +(x_cel_dist)
#
# Once charges on the actual capillary are optimized in the presence of the image charge capillary,
# actual capillary charges are retained and an explicit counter electrode is added at x = 0.
# The (negative) charges on the explicit counter electrode are adjusted to ensure that the counter
# electrode has a zero potential.
#
#
#
# ACTUAL CAPILLARY (pos charge) -| grounded
# * * * * -| counter
# 9+ 8+ 7+ 6+ * -| electrode has pot = 0 and carries negative charges
# 9+ 8+ 7+ 6+ 5+ * * * * * -|
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ -| (image capillary is no longer present)
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ -|
# 9+ 8+ 7+ 6+ 5+ 4+ 3+ 2+ 1+ 0+ -|
# 9+ 8+ 7+ 6+ 5+ -|
# 9+ 8+ 7+ -|
#
# <---------negative---------------------------------0--------------------positive----> x-axis
# | |
# -(x_cel_dist) +(x_cel_dist)#
#
#
#
# CHANGING SYSTEM DIMENSIONS:
# 1. By default,
# potentials, distances, fields, charges are reported in output files assuming that 1 a.u. length = 1 nm:
# [V] [nm] [V/nm] [e]
#
# 2. Changing the ESI voltage: Could be done by running program with a specific cap_voltage value.
# BETTER: Always run with cap_voltage = 1 (ESI voltage = 1 V).
# For scaling to different ESI voltage = X V: multiply potentials by X
# multiply fields by X
# multiply charges by X
# leave distances unchanged
#
# 3. Changing length units:
# * Potentials [V] - no change needed (or multiply by X as explained below)
#
# * Field = -delta_potential / delta_distance; switch from length unit 1 a.u. = 1 nm to another unit
# for example 1 a.u. = 0.01 mm = 1E4 nm
# This means that fields will retain their numerical values, but units switch from V/nm to V/(1E4 nm) = V/(0.01 mm).
# In the example used here, 1 V/nm becomes V/(0.01 mm) = 100 V/mm
# BOTTOM LINE: SIZE INCREASE BY 1E4 LOWERS FIELD BY 1E4
#
# * Charges: Start from charges in e (generated for length in nm)
# switch from length unit 1 a.u. = 1 nm to another unit
# for example 1 a.u. = 0.01 mm
#
# potential = SUM (qj/rj); so for potential to stay constant, rj and qj must be scaled by the same factor
# In the example used here, 0.01 mm / 1 nm = 1E-5 m / 1E-9 m = 1E4, so charges have to be multiplied by 1E4 to retain the same potential
# The rescaled charges obtained in this way are still in e. To convert to C, multiply by 1.6E-19.
# BOTTOM LINE: SIZE INCREASE BY 1E4 REQUIRES CHARGE INCREASE BY 1E4, IF POTENTIAL IS TO BE CONSTANT
#
#
#
#
# INPUT FILE:
# ===========
#
# All the parameters required to perform a run are summarized in an input file
# Example (remove all # symbols when using this text as imput file):
#
#=== input file for capillary_potential.py === DO NOT CHANGE ORDER OF INPUT PARAMETERS ===
#=== output prefix; used for all files produced by the program >output_prefix
#test_with_cel
#=== mage capillary and/or counter electrode (cel) present (yes or no) >cel
#yes
#=== dist of cap tip from cel (or from x = 0 if cel = "no"; same units as bead_radius_cap) >x_cel_dist
#100.
#=== number determines counter electrode disk size >n_cel_grid_size
#10
#=== distance between back wall and cap exit (0. = no back wall; same units as bead_radius_cap) >bwa_dist
#10
#=== capillary voltage set by power supply (V) >cap_voltage
#1.
#=== # of capillary fitting iterations >n_capfit_iter
#20
#=== # of cel fit iterations (0 = no charge transfer from img cap to cel) >n_celfit_iter
#30
#=== radius of capillary beads (a.u. or nm) >bead_radius_cap
#1.
#=== radius of counter electrode beads (same units as bead_radius_cap) >bead_radius_cel
#6.
#=== radius of back wall beads (same units as bead_radius_cap) >bead_radius_bwa
#0.2
#=== number of beads in each cap_ring (comma delimited, start & end [] are optional, NO LINE BREAK ALLOWED)
#[10,10,10,10,10,10,10,10,10,10,10,10,10,10,12,14,16,18,20,22,24,26,28,30,30,30,30,30,30,30,30,30,30]
#
#
#
# OUTPUT FILES:
# =============
#
# The following output files can be expected (names preceded by output_prefix)
#
# ..._center_pot_V_field_Vnm_no_img_no_cel.txt
# potential and field for optimized charges, contribution from capillary only
#
# t..._center_pot_V_field_Vnm_with_cel.txt
# potential and field for optimized charges, contribution from capillary and cel
#
# ..._center_pot_V_field_Vnm_with_img.txt
# potential and field for optimized charges, contribution from capillary and img capillary
#
# ..._fit_summary_cap.txt
# summary of all fitting iterations for capillary charges: energies, pot on each ring, charges on each ring
#
# test_with_cel_fit_summary_cel.txt
# same as above, but for cel
#
# ..._inp_outp_summary.txt
# summary of all input and output parameters. Also: capillary outline coordinates
#
# ..._rad_cel_pot_V.txt
# radial positions of cel rings vs optimized potentials
#
# ..._rad_cel_pot_V_no_img_no_cel.txt
# as above, but only considering the contributions from the capillary
#
# ..._system_with_charges.gro
# xyz coordinates, charges in a.u., charges in e for all atoms
# Use this file to visualize the system, e.g. using Pymol or VMD
#
# ..._xy_cap_charge_e.txt
# 2D data of capillary charges
#
# ..._xy_elfield_Vnm_dist_1_with_cel.txt
# electric field at capillary surface, in the presence of cel
#
# ..._xy_elfield_Vnm_dist_1_with_img.txt
# same as above, but in the presence of image capillary
#
# ..._xy_pot_V_no_img_no_cel.txt
# 2D data of potential in the plance of the capillary centerline, img cap and cel contributions omitted
#
# ..._xy_pot_V_with_cel.txt
# as above, with cel included (no img cap)
#
# ..._xy_pot_V_with_img.txt
# as above, with img cap included (no cel)
import sys
import numpy as np
import os.path
from os import path
import random
#------------------------------------------------------------------------------------------
# Start of function read_input():
# CHANGE PARAMETERS HERE
def read_input(input_file_name):
global gro_out_name
global n_capfit_iter
global n_celfit_iter
global x_cel_dist
global n_bead_in_ring_cap
global cel
global n_cel_grid_size
global bwa_dist
global cap_voltage
global output_prefix
global bead_radius_cap
global bead_radius_cel
global bead_radius_bwa
# read all lines of input_file_name
input = open(input_file_name,"r")
input_content = input.readlines()
input.close()
# output prefix is used for all files produced by the program
output_prefix = input_content[2].rstrip()
print(" output_prefix : ",output_prefix)
# image capillary and/or counter electrode present ("yes") or not ("no")
cel = input_content[4].rstrip()
print(" counter electr / img cap present : ",cel)
# distance of capillary tip from counter-electrode (or from x = 0 if cel = "no")
x_cel_dist = float(input_content[6])
print(" cap tip dist from counter electr (or from x=0 if cel=no): ",x_cel_dist)
# number determines counter electrode disk size
n_cel_grid_size = int(input_content[8])
print(" n_cel_grid_size (determines size of the counter electr): ",n_cel_grid_size)
# distance between back wall and cap exit (0. = no back wall)
bwa_dist = float(input_content[10])
print(" dist between back wall and cap exit (0. = no back wall) : ",bwa_dist)
# capillary voltage set by power supply (V)
cap_voltage = float(input_content[12])
print(" capillary voltage (V) : ",cap_voltage)
# number of capillary fit iterations
n_capfit_iter = int(input_content[14])
print(" number of capillary fitting iterations : ",n_capfit_iter)
# number of counter electr fit iterations (0 = no charge transfer from img cap to counter electr)
n_celfit_iter = int(input_content[16])
print(" number of counter electrode fitting iterations : ",n_celfit_iter)
# radius of capillary beads; these beads are the building blocks of the capillary.
# The units (a.u.) assumed for this parameter determine the dimensions of the capillary (it is ok to assume 1 a.u. = 1 nm).
# It may be best to leave this at 1 or 0.1 (this facilitates subsequent changes to other length units, see program header)
bead_radius_cap = float(input_content[18])
print(" radius of capillary beads : ",bead_radius_cap)
# radius of counter electrode beads (same units as bead_radius_cap)
bead_radius_cel = float(input_content[20])
print(" radius of counter electrode beads : ",bead_radius_cel)
# radius of back wall beads (same units as bead_radius_cap; bead_radius_cap/5 might be a good value)
bead_radius_bwa = float(input_content[22])
print(" radius of back wall beads : ",bead_radius_bwa)
# number of beads in each cap_ring (comma delimited, square brackets mark beginning and end)
n_bead_in_ring_cap_string = input_content[24]
n_bead_in_ring_cap_string = n_bead_in_ring_cap_string.replace("[","")
n_bead_in_ring_cap_string = n_bead_in_ring_cap_string.replace("]","")
# n_bead_in_ring_cap = [int(number) for number in n_bead_in_ring_cap_string.split(",") if number.strip().isdigit()]
n_bead_in_ring_cap = [int(number) for number in n_bead_in_ring_cap_string.split(",")]
print(" number of beads in each ring : ",n_bead_in_ring_cap)
#------------------------------------------------------------------------------------------
# Calculating an xy - potential map at z = 0 (in the capillary center)
# for the optimized charge distribution.
# Also: writing data to output file
def calculate_xy_pot_map(rest_of_filename):
print(" xy mapping of potential ...")
xy_pot_name = output_prefix + rest_of_filename
n_x = 51 # number of x data points
n_y = 51 # number of y data points
xy_pot_content = []
# scanning from negative x (left of capillary inlet) to zero (counter electrode plane)
x_start = bead_xyzq[n_bead_cap-1,0] *1.1
delta_x = abs( x_start/ (n_x - 1) )
#scanning from negative to positive y
y_start = 0.5 * bead_xyzq[n_bead_cap-1,0] *1.1
delta_y = abs( 2 * y_start/ (n_y - 1) )
for i_x in range(0,n_x):
x = x_start + i_x * delta_x
for i_y in range(0,n_y):
y = y_start + i_y * delta_y
pot = 0.
for j_bead in range(0,2*n_bead_cap + n_bead_cel):
d_ij = np.sqrt( (x - bead_xyzq[j_bead,0])**2
+ (y - bead_xyzq[j_bead,1])**2
+ ( bead_xyzq[j_bead,2])**2 )
if (d_ij < 0.5*bead_radius_cap): d_ij = 0.5*bead_radius_cap
# pot = pot + bead_xyzq[j_bead,3] / d_ij # this is in a.u.
pot = pot + bead_xyzq[j_bead,3] / d_ij * volt_conv_factor # Potential in Volts
line_out =str( format(x,"12.5f") + format(y,"12.5f") + format(pot,"12.5f") + "\n" )
xy_pot_content.append(line_out)
xy_pot_out = open(xy_pot_name,"w+")
xy_pot_out.writelines(xy_pot_content)
xy_pot_out.close()
print(" ... saved as :",xy_pot_name)
print("")
#------------------------------------------------------------------------------------------
# Calculating xy - electric field map at z = 0 (in the capillary center plane)
# for the optimized charge distribution.
# One could use
# E_x = -(pot[x+dx,y] - pot[x,y])/dx
# E_y = -(pot[x,y+dy] - pot[x,y])/dy ... but this would yield the potential slightly next to point x/y.
# Better:
# E_x = -(pot[x+dx,y] - pot[x-dx,y])/(2.*dx)
# E_y = -(pot[x,y+dy] - pot[x,y-dy])/(2.*dy) ... to truly get the derivative AT point x/y.
#
# Also: writing data to output file: x y E_x [V/nm] E_y [V/nm] angle [degrees rel. to x-axis] field_strength [V/nm]
#
# dist_factor determines how far from the capillary the scan is performed
def calculate_xy_elfield_map(rest_of_filename,dist_factor):
print(" xy mapping of electric field ...")
xy_elfield_name = output_prefix + rest_of_filename
xy_elfield_content = []
# generating xy points that trace the capillary outline
#
# --> --> --> -->
# 4 3 2 1 0 *
# 4 3 2 1 0
# 4 3 2 1 0 *
# 4 3 2 1 0
# 4 3 2 1 0 *
# <-- <-- <-- <--
i_ring_cap = n_ring_cap
for i_xy_point in range(0,2*n_ring_cap+5):
if(i_xy_point < n_ring_cap ): i_ring_cap = i_ring_cap - 1
if(i_xy_point > n_ring_cap+5): i_ring_cap = i_ring_cap + 1
x = cap_ring_xcoord[i_ring_cap]
if(i_xy_point < n_ring_cap+5): y = cap_ring_radius[i_ring_cap] + dist_factor*bead_radius_cap
if(i_xy_point > n_ring_cap+5): y = -1.*cap_ring_radius[i_ring_cap] - dist_factor*bead_radius_cap
if(i_xy_point == n_ring_cap):
x = cap_ring_xcoord[0] + dist_factor*bead_radius_cap
y = cap_ring_radius[0] + dist_factor*bead_radius_cap
if(i_xy_point == n_ring_cap+1):
x = cap_ring_xcoord[0] + dist_factor*bead_radius_cap
y = 0.5*(cap_ring_radius[0] + dist_factor*bead_radius_cap)
if(i_xy_point == n_ring_cap+2):
x = cap_ring_xcoord[0] + dist_factor*bead_radius_cap
y = 0.
if(i_xy_point == n_ring_cap+3):
x = cap_ring_xcoord[0] + dist_factor*bead_radius_cap
y = -0.5*(cap_ring_radius[0] + dist_factor*bead_radius_cap)
if(i_xy_point == n_ring_cap+4):
x = cap_ring_xcoord[0] + dist_factor*bead_radius_cap
y = -1.*cap_ring_radius[0] - dist_factor*bead_radius_cap
dx = 0.05 * bead_radius_cap
dy = dx
pot_xmdx_y = 0. #potential at point x-dx,y
pot_xpdx_y = 0. #potential at point x+dx,y
pot_x_ymdy = 0. #potential at point x,y-dy
pot_x_ypdy = 0. #potential at point x,y+dy
for j_bead in range(0,2*n_bead_cap + n_bead_cel):
d_ij_xmdx_y = np.sqrt( (x-dx - bead_xyzq[j_bead,0])**2 #for calculating potential at point x-dx,y
+ (y - bead_xyzq[j_bead,1])**2
+ ( bead_xyzq[j_bead,2])**2 )
d_ij_xpdx_y = np.sqrt( (x+dx - bead_xyzq[j_bead,0])**2 #for calculating potential at point x+dx,y
+ (y - bead_xyzq[j_bead,1])**2
+ ( bead_xyzq[j_bead,2])**2 )
d_ij_x_ymdy = np.sqrt( (x - bead_xyzq[j_bead,0])**2 #for calculating potential at point x,y-dy
+ (y-dy - bead_xyzq[j_bead,1])**2
+ ( bead_xyzq[j_bead,2])**2 )
d_ij_x_ypdy = np.sqrt( (x - bead_xyzq[j_bead,0])**2 #for calculating potential at point x,y+dy
+ (y+dy - bead_xyzq[j_bead,1])**2
+ ( bead_xyzq[j_bead,2])**2 )
if (d_ij_xmdx_y < 0.5*bead_radius_cap): d_ij_xmdx_y = 0.5*bead_radius_cap
if (d_ij_xpdx_y < 0.5*bead_radius_cap): d_ij_xpdx_y = 0.5*bead_radius_cap
if (d_ij_x_ymdy < 0.5*bead_radius_cap): d_ij_x_ymdy = 0.5*bead_radius_cap
if (d_ij_x_ypdy < 0.5*bead_radius_cap): d_ij_x_ypdy = 0.5*bead_radius_cap
# these potentials are in a.u.
pot_xmdx_y = pot_xmdx_y + bead_xyzq[j_bead,3] / d_ij_xmdx_y
pot_xpdx_y = pot_xpdx_y + bead_xyzq[j_bead,3] / d_ij_xpdx_y
pot_x_ymdy = pot_x_ymdy + bead_xyzq[j_bead,3] / d_ij_x_ymdy
pot_x_ypdy = pot_x_ypdy + bead_xyzq[j_bead,3] / d_ij_x_ypdy
# potentials are converted to V (assuming distances are in nm), so that field is in V/nm
elfield_x = -1.*( pot_xpdx_y - pot_xmdx_y ) * volt_conv_factor / (2.*dx) #electric field component in x-direction
elfield_y = -1.*( pot_x_ypdy - pot_x_ymdy ) * volt_conv_factor / (2.*dy) #electric field component in y-direction
elfield_norm = np.sqrt(elfield_x**2 + elfield_y**2) #length of elfield vector
# angle of elfield vector relative to x-axis (converted to degrees)
# (parallel = 0 deg; up = 90 deg; antiparallel = -180)
if (elfield_y >= 0.) : # angles 0 to 180 deg
elfield_angle = np.arccos(elfield_x/elfield_norm)*360./2/3.14159
if (elfield_y < 0.) : # angles 0 to -180 deg
elfield_angle = -1.*np.arccos(elfield_x/elfield_norm)*360./2/3.14159
line_out =str( format(x,"12.5f") + format(y,"12.5f")
+ format(elfield_x,"12.5f") + format(elfield_y,"12.5f")
+ format(elfield_angle,"12.5f") + format(elfield_norm,"12.5f") + "\n" )
xy_elfield_content.append(line_out)
xy_elfield_out = open(xy_elfield_name,"w+")
xy_elfield_out.writelines(xy_elfield_content)
xy_elfield_out.close()
print(" ... saved as :",xy_elfield_name)
print("")
#------------------------------------------------------------------------------------------
# writing overall input and some results to output file
def write_input_output_summary():
print(" writing summary of input and output parameters ...")
input_output_summary_name = output_prefix + "_inp_outp_summary.txt"
input_output_summary_content = []
output_line = " === capillary_potential.py parameters === " + "\n"
input_output_summary_content.append(output_line)
output_line = " " + "\n"
input_output_summary_content.append(output_line)
output_line = " INPUT ****************" + "\n"
input_output_summary_content.append(output_line)
output_line = " output_prefix = " + output_prefix + "\n"
input_output_summary_content.append(output_line)
output_line = " n_bead_in_ring_cap= " + str(n_bead_in_ring_cap) + "\n"
input_output_summary_content.append(output_line)
output_line = " n_capfit_iter = " + str(n_capfit_iter) + "\n"
input_output_summary_content.append(output_line)
output_line = " n_celfit_iter = " + str(n_celfit_iter) + "\n"
input_output_summary_content.append(output_line)
output_line = " cel = " + cel + "\n"
input_output_summary_content.append(output_line)
output_line = " bwa_dist = " + str(bwa_dist) + "\n"
input_output_summary_content.append(output_line)
output_line = " x_cel_dist = " + str(x_cel_dist) + "\n"
input_output_summary_content.append(output_line)
output_line = " n_cel_grid_size = " + str(n_cel_grid_size) + "\n"
input_output_summary_content.append(output_line)
output_line = " cap_voltage = " + str(cap_voltage) + "\n"
input_output_summary_content.append(output_line)
output_line = " bead_radius_cap = " + str(bead_radius_cap) + "\n"
input_output_summary_content.append(output_line)
output_line = " bead_radius_cel = " + str(bead_radius_cel) + "\n"
input_output_summary_content.append(output_line)
output_line = " bead_radius_bwa = " + str(bead_radius_bwa) + "\n"
input_output_summary_content.append(output_line)
output_line = " " + "\n"
input_output_summary_content.append(output_line)
output_line = " OUTPUT **************** " + "\n"
input_output_summary_content.append(output_line)
output_line = " capillary inlet x-position [units] = " + format(bead_xyzq[n_bead_cap-1,0],"12.3f") + "\n"
input_output_summary_content.append(output_line)
cap_length = abs(bead_xyzq[n_bead_cap-1,0] + x_cel_dist)
output_line = " capillary length [units] = " + format(cap_length,"12.3f") + "\n"
input_output_summary_content.append(output_line)
output_line = " capillary outlet i.d. [units] = " + format(2*cap_ring_radius[0],"12.3f") + "\n"
input_output_summary_content.append(output_line)
output_line = " capillary inlet i.d. [units] = " + format(2*cap_ring_radius[n_ring_cap-1],"12.3f") + "\n"
input_output_summary_content.append(output_line)
# Determining maximum system dimensions (largest bead-bead distance, excl. image beads).
# Also determining counter electrode diameter.
max_sys_dist = 0.
cel_diam = 0.
for i_bead in range(0,2*n_bead_cap + n_bead_cel):
for j_bead in range(i_bead+1,2*n_bead_cap + n_bead_cel):
dist = np.sqrt( (bead_xyzq[i_bead,0] - bead_xyzq[j_bead,0])**2
+ (bead_xyzq[i_bead,1] - bead_xyzq[j_bead,1])**2
+ (bead_xyzq[i_bead,2] - bead_xyzq[j_bead,2])**2 )
if (dist > max_sys_dist):
if (cel == "yes"):
# by excluding positive x-values, image beads are excluded
if (bead_xyzq[i_bead,0] <= 0.) and (bead_xyzq[j_bead,0] <= 0.):max_sys_dist = dist
if (cel == "no"):
# by excluding positive x-values and x = 0, image and cel beads are excluded
# (this works properly only for x_cel_dist > 0, otherwise the first ring is not considered)
if (bead_xyzq[i_bead,0] < 0.) and (bead_xyzq[j_bead,0] < 0.):max_sys_dist = dist
if (dist > cel_diam):
if (cel == "yes"):
# by focusing on x = 0 values, only counter electrode beads are considered
if (bead_xyzq[i_bead,0] == 0.) and (bead_xyzq[j_bead,0] == 0.):cel_diam = dist
output_line = " maxdist. (cap and cel beads) [units] = " + format(max_sys_dist,"12.3f") + "\n"
input_output_summary_content.append(output_line)
output_line = " n_ring_cap = " + str(n_ring_cap) + "\n"
input_output_summary_content.append(output_line)
output_line = " n_ring_cel = " + str(n_ring_cel) + "\n"
input_output_summary_content.append(output_line)
output_line = " n_bead_cap = " + str(n_bead_cap) + "\n"
input_output_summary_content.append(output_line)
output_line = " n_bead_cel = " + str(n_bead_cel) + "\n"
input_output_summary_content.append(output_line)
output_line = " counter electrode diameter [units] = " + format(cel_diam,"12.3f") + "\n"
input_output_summary_content.append(output_line)
output_line = " charge recovery on counter electrode = " + format(charge_recov_on_cel,"12.3f") + "\n"
input_output_summary_content.append(output_line)
#save coordinates of capillary outline
output_line = "\n"
input_output_summary_content.append(output_line)
output_line = " capillary x/y outline [units]" + "\n"
input_output_summary_content.append(output_line)
for i_ring_cap in range (n_ring_cap-1,-1,-1):
output_line = format(cap_ring_xcoord[i_ring_cap],"12.3f") + format(cap_ring_radius[i_ring_cap],"12.3f") + "\n"
input_output_summary_content.append(output_line)
for i_ring_cap in range (0,n_ring_cap):
output_line = format(cap_ring_xcoord[i_ring_cap],"12.3f") + format(-1*cap_ring_radius[i_ring_cap],"12.3f") + "\n"
input_output_summary_content.append(output_line)
output_line = format(cap_ring_xcoord[n_ring_cap-1],"12.3f") + format(cap_ring_radius[n_ring_cap-1],"12.3f") + "\n"
input_output_summary_content.append(output_line)
line_out = "\n"
input_output_summary_content.append(line_out)
line_out = "CAPILLARY CHARGES" + "\n"
input_output_summary_content.append(line_out)
line_out = " ring# #beads x-coord (units) q per bead (a.u.) q per bead (e)" + "\n"
input_output_summary_content.append(line_out)
for i_ring_cap in range(0,n_ring_cap):
# charge values prior to unit conversion:
# These values were used internally during fitting, assuming that every capillary
# bead initially had a charge of 1 a.u.
charge_in_au = charge_per_bead_in_ring_cap[i_ring_cap]
#charge values after unit conversion
charge_in_e = charge_per_bead_in_ring_cap[i_ring_cap] / 1.44 * volt_conv_factor
line_out =str( format(i_ring_cap ,"6")
+ format(n_bead_in_ring_cap[i_ring_cap],"6")
+ format(cap_ring_xcoord[i_ring_cap] ,"16.7f")
+ format(charge_in_au ,"16.7f")
+ format(charge_in_e ,"20.7f") + "\n" )
input_output_summary_content.append(line_out)
line_out = " " + "\n"
input_output_summary_content.append(line_out)
line_out = "COUNTER ELECTRODE CHARGES (data are listed twice (first with neg. radial_dist) for plotting)" + "\n"
input_output_summary_content.append(line_out)
line_out = " ring# #beads rad dist (units) q per bead (a.u.) q per bead (e)" + "\n"
input_output_summary_content.append(line_out)
for i_ring_cel in range(n_ring_cel-1,0,-1): # printing in reverse order, from n_ring_cel downward
charge_in_au = charge_per_bead_in_ring_cel[i_ring_cel] # nalogous to cap charges in previous section
charge_in_e = charge_per_bead_in_ring_cel[i_ring_cel] / 1.44 * volt_conv_factor
line_out =str( format(i_ring_cel ,"6")
+ format(n_bead_in_ring_cel[i_ring_cel] ,"6")
+ format(-1.*cel_ring_radius[i_ring_cel] ,"16.7f")
+ format(charge_in_au ,"16.7f")
+ format(charge_in_e ,"20.7f") + "\n" )
input_output_summary_content.append(line_out)
for i_ring_cel in range(0,n_ring_cel): # printing again, in proper order from 0 upward
charge_in_au = charge_per_bead_in_ring_cel[i_ring_cel] # analogous to cap charges in previous section
charge_in_e = charge_per_bead_in_ring_cel[i_ring_cel] / 1.44 * volt_conv_factor
line_out =str( format(i_ring_cel ,"6")
+ format(n_bead_in_ring_cel[i_ring_cel] ,"6")
+ format(cel_ring_radius[i_ring_cel], "16.7f")
+ format(charge_in_au ,"16.7f")
+ format(charge_in_e ,"20.7f") + "\n" )
input_output_summary_content.append(line_out)
input_output_summary_out = open(input_output_summary_name,"w+")
input_output_summary_out.writelines(input_output_summary_content)
input_output_summary_out.close()
print(" ... saved as :",input_output_summary_name)
print("")
#------------------------------------------------------------------------------------------
# writing capillary fitting statistics to output file
def write_fit_summary_cap(rest_of_filename):
print(" writing capillary fitting summary ...")
fit_summary_cap_name = output_prefix + rest_of_filename
fit_summary_cap_content = []
for i_ring_cap in range(0,n_ring_cap+1):
output_line = str(format(i_ring_cap-1,"6"))
if (i_ring_cap == 0): output_line = " ring#"
for j_capfit_iter in range(0,n_capfit_iter+1):
output_line = output_line + str( format(fit_summary_cap[i_ring_cap,2*j_capfit_iter ],"11.2f")
+ format(fit_summary_cap[i_ring_cap,2*j_capfit_iter+1],"11.2f"))
output_line = output_line+ "\n"
fit_summary_cap_content.append(output_line)
fit_summary_cap_out = open(fit_summary_cap_name,"w+")
fit_summary_cap_out.writelines(fit_summary_cap_content)
fit_summary_cap_out.close()
print(" ... saved as :",fit_summary_cap_name)
print("")
#------------------------------------------------------------------------------------------
# writing counter electrode fitting summary to output file
def write_fit_summary_cel(rest_of_filename):
print(" writing counter electrode fitting summary ...")
fit_summary_cel_name = output_prefix + rest_of_filename
fit_summary_cel_content = []
for i_ring_cel in range(0,n_ring_cel+1):
output_line = str(format(i_ring_cel-1,"6"))
if (i_ring_cel == 0): output_line = " ring#"
for j_celfit_iter in range(0,n_celfit_iter+1):
output_line = output_line + str( format(fit_summary_cel[i_ring_cel,2*j_celfit_iter ],"11.2f")
+ format(fit_summary_cel[i_ring_cel,2*j_celfit_iter+1],"11.2f"))
output_line = output_line+ "\n"
fit_summary_cel_content.append(output_line)
fit_summary_cel_out = open(fit_summary_cel_name,"w+")
fit_summary_cel_out.writelines(fit_summary_cel_content)
fit_summary_cel_out.close()
print(" ... saved as :",fit_summary_cel_name)
print("")
#------------------------------------------------------------------------------------------
# Calculating an xy - charge map at z = 0 (in the capillary center)
# for the optimized capillary charges (no other charges are considered here).
def calculate_xy_cap_charge_map(rest_of_filename):
print(" xy mapping of capillary charge ...")
xy_cap_charge_name = output_prefix + rest_of_filename
n_x = n_ring_cap+10 # number of x data points
n_y = n_ring_cap+5 # number of y data points
xy_cap_charge_content = []
# finding minimum y value for grid sizing
y_min = 0.
for j_bead in range(0,n_bead_cap):
if (bead_xyzq[j_bead,1] < y_min): y_min = bead_xyzq[j_bead,1]
y_min = y_min * 1.1
# scanning from negative x (left of cap inlet) to just past the cap outlet
x_start = bead_xyzq[n_bead_cap-1,0] - 10. * 0.866 * bead_radius_cap
delta_x = 0.866 * 2. * bead_radius_cap
#scanning from negative to positive y
y_start = y_min - 2. * 0.866 * bead_radius_cap
delta_y = 2.* (abs(y_min - 2. * 0.866 * bead_radius_cap) / (n_y-1) )
for i_x in range(0,n_x):
x = x_start + i_x * delta_x
for i_y in range(0,n_y):
y = y_start + i_y * delta_y
charge = 0.
for j_bead in range(0,n_bead_cap):
# distances in xy projected capillary
d_ij = np.sqrt( (x - bead_xyzq[j_bead,0])**2
+ (y - bead_xyzq[j_bead,1])**2 )
# charges are converted to e, assuming distances are in nm
if (d_ij < bead_radius_cap): charge = bead_xyzq[j_bead,3] / 1.44 * volt_conv_factor
line_out =str( format(x,"12.5f") + format(y,"12.5f") + format(charge,"12.5f") + "\n" )
xy_cap_charge_content.append(line_out)
xy_cap_charge_out = open(xy_cap_charge_name,"w+")
xy_cap_charge_out.writelines(xy_cap_charge_content)
xy_cap_charge_out.close()
print(" ... saved as :",xy_cap_charge_name)
print("")
#------------------------------------------------------------------------------------------
# Calculating a radial potential map of the counter electrode at x = -0.5 * bead_radius_cel
# y = 0
# (all charges are considered here).
def calculate_rad_cel_pot_map(rest_of_filename):
print(" mapping radial counter electrode potential ...")
rad_cel_pot_name = output_prefix + rest_of_filename
n_z = 101 # number of z data points (radial scan is performed along z-axis)
rad_cel_pot_content = []
# finding minimum y value for grid sizing
z_min = 0.
for j_bead in range(2*n_bead_cap,2*n_bead_cap + n_bead_cel):
if (bead_xyzq[j_bead,2] < z_min): z_min = bead_xyzq[j_bead,2]
z_min = z_min * 1.1
delta_z = 2.* abs(z_min / (n_z-1) )
for i_z in range(0,n_z):
z = z_min + i_z * delta_z
pot = 0.
for j_bead in range(0,2*n_bead_cap + n_bead_cel):
# xyz distances in counter electrode
# Scanning is performed not IN the xy plane (x = 0), but with a slight x-offset to avoid singularities.
# <--probe coordinates--> <--bead coordinates-->
d_ij = np.sqrt( (-0.5 * bead_radius_cel - bead_xyzq[j_bead,0])**2
+ (0. - bead_xyzq[j_bead,1])**2
+ (z - bead_xyzq[j_bead,2])**2 )
if (d_ij < bead_radius_cel): d_ij = bead_radius_cel # for smoothing the scan
pot = pot + bead_xyzq[j_bead,3]/d_ij # pot = SUM(q_j_d_ij)
pot = pot * volt_conv_factor # Potential is now in Volts
line_out = str( format(z,"12.5f") + format(pot,"12.5f") + "\n" )
rad_cel_pot_content.append(line_out)
rad_cel_pot_out = open(rad_cel_pot_name,"w+")
rad_cel_pot_out.writelines(rad_cel_pot_content)
rad_cel_pot_out.close()
print(" ... saved as :",rad_cel_pot_name)
print("")
#------------------------------------------------------------------------------------------
# Calculating potential and field along the center of the capillary (= x axis)
# for the optimized charge distribution, and writting data to output file
def calculate_center_pot_field(rest_of_filename):
global volt_conv_factor # This is also needed for generate_charge_output()
# and calculate_rad_cel_pot_map()
# and calculate_xy_cap_charge_map
print(" calculating potential along center (x-axis) ...")
output_name = output_prefix + rest_of_filename
x_start = bead_xyzq[n_bead_cap-1,0]*1.1 # we scan from here
delta_x = abs( bead_radius_cap/5. ) # scanning increment
n_x = 2* int(abs(x_start/delta_x)+1) # number of data points
center_pot_field = np.zeros((n_x,3)) # center_pot_field[i_x,0] = x position
center_pot_av = 0. # center_pot_field[i_x,1] = potential
n_points_within_cap = 0 # center_pot_field[i_x,2] = electric field
center_out_content = []
center_out_content.append(" x(a.u.) pot(V) field(V/[a.u.]) along x-axis" + "\n" )
# this is the x-position in the middle of the capillary
x_middle_cap = bead_xyzq[n_bead_cap-1,0] + 0.5 * (bead_xyzq[0,0]-bead_xyzq[n_bead_cap-1,0])
#print(" x_middle_cap = ",x_middle_cap)
min_x_dist_from_x_middle_cap = 1E40
x_middle_pot = 0.
for i_x in range(0,n_x):
# defining x values for scan line
center_pot_field[i_x,0] = x_start + i_x * delta_x
# Calculate the electric potential at each point along the x-axis
# as potential = SUM(all j_bead) q_j/d_ij where q_j [elemtary charges] and d_ij [nm]
# and the sum includes all beads and image beads
for j_bead in range(0,2*n_bead_cap + n_bead_cel):
d_ij = np.sqrt( (center_pot_field[i_x,0] - bead_xyzq[j_bead,0])**2
+ ( bead_xyzq[j_bead,1])**2
+ ( bead_xyzq[j_bead,2])**2 )
if (d_ij < 0.5 * bead_radius_cap):d_ij = 0.5 * bead_radius_cap
center_pot_field[i_x,1] = center_pot_field[i_x,1] + bead_xyzq[j_bead,3] / d_ij
# Finding the potential value (in a.u.) in the middle of the capillary
test_x_dist_from_x_middle_cap = abs(center_pot_field[i_x,0] - x_middle_cap)
if(test_x_dist_from_x_middle_cap <= min_x_dist_from_x_middle_cap):
min_x_dist_from_x_middle_cap = test_x_dist_from_x_middle_cap
x_middle_pot = center_pot_field[i_x,1]
# Converting potentials to Volts, such that x_middle_pot is equal to the user-defined cap_voltage
volt_conv_factor = cap_voltage / x_middle_pot
# converting centerline potentials to Volts
for i_x in range(0,n_x):
center_pot_field[i_x,1] = center_pot_field[i_x,1]*volt_conv_factor # Potential is now in Volts
# calculating centerline electric field
for i_x in range(0,n_x):
if ( (i_x > 0) and ( i_x < (n_x-1) ) ):
# Electric field = E_x = -d_potential/d_x
# First and last E_x values are skipped in this loop
center_pot_field[i_x,2] = -1. * (center_pot_field[i_x+1,1] - center_pot_field[i_x-1,1] ) / (2 * delta_x)
center_pot_field[0 ,2] = center_pot_field[1 ,2] # First and last E_x values are
center_pot_field[n_x-1,2] = center_pot_field[n_x-2,2] # assigned their neighbor values.
# write center output
for i_x in range(0,n_x):
line_out =str( format(center_pot_field[i_x,0],"12.5f")
+ format(center_pot_field[i_x,1],"12.5f")
+ format(center_pot_field[i_x,2],"12.5f") + "\n" )
center_out_content.append(line_out)
center_out = open(output_name,"w+")
center_out.writelines(center_out_content)
center_out.close()
print(" ... saved as : ",output_name)
print("")
#------------------------------------------------------------------------------------------
# Generating .gro output file: x/y/z coordinates, charge in a.u.; charge in e
# (applies if length is in nm)
# Species included 1CAP capillary beads
# 2IMG image capillary beads
# 3CEL counter electrode beads
# 4BWA back wall beads
# 5CPO probe locations on outside for measuring capillary potential
# 5CPI probe locations on inside for measuring capillary potential
# 6EPP probe locations at positive x for measuring cel potential
# 6EPP probe locations at negative x for measuring cel potential
def generate_gro_output(rest_of_filename):
print(" saving coordinates in .gro format ...")
gro_out_name = output_prefix + rest_of_filename
gro_out_content = [] # will be written to .gro output
bead_xyzq_text = [] # capillary bead coordinates for .gro output
cap_probe_xyz_text = [] # cap_probe coordinates for .gro output
cel_probe_xyz_text = [] # cel_probe coordinates for .gro output
cel_xyz_text = [] # counter electrode coordinates for .gro output
bwa_xyz_text = [] # back wall coordinates for .gro output
# transferring capillary coordinates
i_bead = 0
for i_ring_cap in range(0,n_ring_cap):
for i_bead_in_ring_cap in range(0,n_bead_in_ring_cap[i_ring_cap]):
bead_type = "C" + str(i_ring_cap).zfill(3) # cap_ring number is coded in bead_type
# C for "Capillary"
# converting charge to e for the chosen cap_voltage (assuming length units are nm)
charge_in_e = bead_xyzq[i_bead,3] / 1.44 * volt_conv_factor
bead_xyzq_text.append(str( " 1CAP " + bead_type + format(i_bead,"5")
+ format(bead_xyzq[i_bead,0],"8.3f")
+ format(bead_xyzq[i_bead,1],"8.3f")
+ format(bead_xyzq[i_bead,2],"8.3f")
+ format(bead_xyzq[i_bead,3],"8.3f")
+ format(charge_in_e ,"8.3f")