-
Notifications
You must be signed in to change notification settings - Fork 0
/
seagen.py
1734 lines (1409 loc) · 61.5 KB
/
seagen.py
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
""" SEAGen
A python implementation of the stretched equal area (SEA) algorithm for
generating spherically symmetric arrangements of particles with accurate
particle densities, e.g. for SPH initial conditions that precisely match an
arbitrary density profile (Kegerreis et al. 2019,
https://arxiv.org/pdf/1901.09934.pdf).
See README.md and https://github.com/jkeger/seagen for more information.
SEAGen has also ben implemented and enhanced as part of the WoMa package
for creating rotating and non-rotating planetary (or other) profiles and
also placing the particles: github.com/srbonilla/WoMa.
Jacob Kegerreis (2020) [email protected]
GNU General Public License v3+, see LICENSE.txt.
See the __init__() doc strings of the GenShell and GenSphere classes for
the main documentation details, and see examples.py for example uses.
"""
# ========
# Contents:
# ========
# I Functions
# II Classes
# III Main
import numpy as np
import sys
# ========
# Constants
# ========
deg_to_rad = np.pi/180
banner = "# SEAGen \n" "# https://github.com/jkeger/seagen \n"
# //////////////////////////////////////////////////////////////////////////// #
# I. Functions #
# //////////////////////////////////////////////////////////////////////////// #
def polar_to_cartesian(r, theta, phi):
""" Convert spherical polars to cartesian coordinates.
Args:
r (float or [float])
Radius.
theta (float or [float])
Zenith angle (colatitude) (radians).
phi (float or [float])
Azimuth angle (longitude) (radians).
Returns:
x, y, z (float or [float])
Cartesian coordinates.
"""
x = r * np.cos(phi) * np.sin(theta)
y = r * np.sin(phi) * np.sin(theta)
z = r * np.cos(theta)
return x, y, z
def get_euler_rotation_matrix(alpha, beta, gamma):
""" Return the rotation matrix for three Euler angles.
Args:
alpha, beta, gamma (float)
Euler angles (radians).
Returns:
A2_rot ([[float]])
3x3 rotation matrix.
"""
sa = np.sin(alpha)
ca = np.cos(alpha)
sb = np.sin(beta)
cb = np.cos(beta)
sg = np.sin(gamma)
cg = np.cos(gamma)
return np.array([
[cg*cb*ca - sg*sa, cg*cb*sa + sg*ca, -cg*sb],
[-sg*cb*ca - cg*sa, -sg*cb*sa + cg*ca, sg*sb],
[sb*ca, sb*sa, cb ]
])
def get_shell_mass(r_inner, r_outer, rho):
""" Calculate the mass of a uniform-density shell. """
return 4/3*np.pi * rho * (r_outer**3 - r_inner**3)
def get_weighted_mean(A1_weight, A1_value):
""" Calculate the mean of the value array weighted by the weights array. """
return np.sum(A1_weight * A1_value) / np.sum(A1_weight)
# //////////////////////////////////////////////////////////////////////////// #
# II. Classes #
# //////////////////////////////////////////////////////////////////////////// #
class GenShell(object):
""" Generate a single spherical shell of points ("particles") at a fixed
radius, using the SEA method described in Kegerreis et al. 2019 ("K19").
See __init__()'s documentation for more details.
Basic Usage:
e.g. Create a single shell of particles and print their positions:
>>> import seagen
>>> N = 100
>>> r = 1
>>> particles = seagen.GenShell(N, r)
>>> print(particles.x, particles.y, particles.z)
"""
def __init__(self, N, r, do_stretch=True, do_rotate=True, verbosity=1):
""" Generate a single spherical shell of particles.
Args:
N (int)
The number of cells/particles to create.
r (float)
The radius of the shell.
do_stretch (opt. bool)
Default True. Set False to not do the SEA method's latitude
stretching.
do_rotate (opt. bool)
Default True. Set False to not randomly rotate the sphere of
particles after their intial placement.
verbosity (opt. int)
The verbosity to control printed output:
0 None
1 Standard (default)
2 Extra
3 Debug
Output attrs: (Also accessable without the A1_ prefix)
A1_x, A1_y, A1_z ([float])
Particle cartesian position arrays.
Note: Spherical polar coordinates are used for the particles
internally but do not have the final rotation applied to them.
"""
self.N = N
self.A1_r = r * np.ones(N)
self.verbosity = verbosity
# Derived properties
self.A_reg = 4 * np.pi / N
# Start in spherical polar coordinates for the initial placement
self.get_collar_areas()
self.update_collar_colatitudes()
self.get_point_positions()
if do_stretch:
a, b = self.get_stretch_params(self.N)
self.apply_stretch_factor(a, b)
# Now convert to cartesian coordinates for the rotation and output
self.A1_x, self.A1_y, self.A1_z = polar_to_cartesian(
self.A1_r, self.A1_theta, self.A1_phi)
if do_rotate:
self.apply_random_rotation()
def get_cap_colatitude(self):
""" Calculate the cap colatitude.
K19 eqn. (3)
Returns:
theta_cap (float)
The cap colatitude (radians).
"""
return 2 * np.arcsin(np.sqrt(1 / self.N))
def get_number_of_collars(self):
""" Calculate the number of collars (not including the polar caps).
K19 eqn. (4)
Sets and returns:
N_col (int)
The number of collars (not including the polar caps).
"""
theta_cap = self.get_cap_colatitude()
self.N_col = int(round((np.pi - 2 * theta_cap)/(np.sqrt(self.A_reg))))
return self.N_col
def get_collar_colatitudes(self):
""" Calculate the top colatitudes of all of the collars, including the
bottom cap's.
Sets and returns:
A1_collar_theta ([float])
The top colatitudes of all of the collars.
"""
self.get_number_of_collars()
cap_height = self.get_cap_colatitude()
height_of_collar = (np.pi - 2 * cap_height) / self.N_col
# Allocate collars array
self.A1_collar_theta = np.arange(self.N_col + 1, dtype=float)
# Collars have a fixed height initially
self.A1_collar_theta *= height_of_collar
# Starting at the bottom of the top polar cap
self.A1_collar_theta += (cap_height)
return self.A1_collar_theta
def get_collar_area(self, theta_i, theta_i_minus_one):
""" Calculate the area of a collar given the collar heights of itself
and its neighbour.
K19 eqn. (5)
Args:
theta_i (float)
The colatitude of the bottom of the collar.
theta_i_minus_one (float)
The colatitudes of the bottom of the previous collar, i.e.
the top of this collar.
Returns:
collar_area (float)
The collar's area.
"""
sin2_theta_i = np.sin(theta_i / 2)**2
sin2_theta_i_m_o = np.sin(theta_i_minus_one / 2)**2
return 4 * np.pi * (sin2_theta_i - sin2_theta_i_m_o)
def get_collar_areas(self):
""" Calculate the collar areas.
Sets and returns:
A1_collar_area ([float])
The collar areas.
"""
self.get_collar_colatitudes()
self.A1_collar_area = np.empty(self.N_col)
self.A1_collar_area[:] = self.get_collar_area(
self.A1_collar_theta[1:], self.A1_collar_theta[:-1])
return self.A1_collar_area
def get_ideal_N_regions_in_collar(self, A_col):
""" Calculate the ideal number of regions in a collar.
K19 eqn (7).
Returns:
N_reg_ideal (float)
The ideal number of regions in a collar.
"""
return A_col / self.A_reg
def get_N_regions_in_collars(self):
""" Calculate the number of regions in each collar, not including the
top polar cap.
K19 eqn (8,9).
Sets and returns:
A1_N_reg_in_collar ([int])
The number of regions in each collar.
"""
self.A1_N_reg_in_collar = np.empty(self.N_col, dtype=int)
A1_collar_area = self.get_collar_areas()
discrepancy = 0
for i in range(len(self.A1_N_reg_in_collar)):
N_reg_ideal = self.get_ideal_N_regions_in_collar(A1_collar_area[i])
self.A1_N_reg_in_collar[i] = int(round(N_reg_ideal + discrepancy))
discrepancy += N_reg_ideal - self.A1_N_reg_in_collar[i]
return self.A1_N_reg_in_collar
def update_collar_colatitudes(self):
""" Update the collar colatitudes to use the now-integer numbers of
regions in each collar instead of the ideal.
K19 eqn (10).
Sets and returns:
A1_collar_theta ([float])
The top colatitudes of all of the collars.
"""
# First we must get the cumulative number of regions in each collar,
# including the top polar cap
A1_N_reg_in_collar_cum = np.cumsum(self.get_N_regions_in_collars()) + 1
A1_N_reg_in_collar_cum = np.append([1], A1_N_reg_in_collar_cum)
self.A1_collar_theta = 2 * np.arcsin(
np.sqrt(A1_N_reg_in_collar_cum * self.A_reg / (4 * np.pi)))
return self.A1_collar_theta
def choose_longitude_offset(self, N_i, N_i_minus_one, d_phi_i,
d_phi_i_minus_one):
""" Choose the starting longitude for particles in this collar.
K19 paragraph after eqn (12).
Args:
N_i, N_i_minus_one (int)
The number of regions in this collar and the previous one.
d_phi_i, d_phi_i_minus_one (float)
The longitude angle between adjacent particles in this
collar and the previous one.
Returns:
phi_0 (float)
The starting longitude for particles in this collar.
"""
is_N_i_even = abs((N_i % 2) - 1)
is_N_i_minus_one_even = abs((N_i_minus_one % 2) - 1)
# Exclusive or
if is_N_i_even != is_N_i_minus_one_even:
phi_0 = 0.5 * (is_N_i_even * d_phi_i
+ is_N_i_minus_one_even * d_phi_i_minus_one)
else:
phi_0 = 0.5 * min(d_phi_i, d_phi_i_minus_one)
return phi_0
def get_point_positions(self):
""" Calculate the point positions in the centres of every region.
K19 eqn (11,12).
Sets and returns:
A1_theta ([float])
The point colatitudes.
A1_phi ([float])
The point longitudes.
"""
N_tot = self.A1_N_reg_in_collar.sum() + 2
self.A1_theta = np.empty(N_tot)
self.A1_phi = np.empty(N_tot)
# The cap particles are at the poles, listed at the end of these arrays.
self.A1_theta[-2] = 0.0
self.A1_theta[-1] = np.pi
self.A1_phi[-2] = 0.0
self.A1_phi[-1] = 0.0
# All regions in a collar are at the same colatitude, theta.
A1_theta = np.zeros(self.N_col + 2)
A1_theta[:-2] = 0.5 * (
self.A1_collar_theta[:-1] + self.A1_collar_theta[1:])
# Particles in each collar are equally spaced in longitude, phi,
# and offset appropriately from the previous collar.
A1_d_phi = 2 * np.pi / self.A1_N_reg_in_collar
A1_phi_0 = np.empty(self.N_col)
for i, phi_0_i in enumerate(A1_phi_0):
# The first collar has no previous collar to rotate away from
# so doesn't need an offset.
if i == 0:
phi_0_i = 0
else:
phi_0_i = self.choose_longitude_offset(
self.A1_N_reg_in_collar[i], self.A1_N_reg_in_collar[i-1],
A1_d_phi[i], A1_d_phi[i-1]
)
# Also add a random initial offset to ensure that successive
# collars do not create lines of ~adjacent particles.
# (Second paragraph following K19 eqn (12).)
m = np.random.randint(0, self.A1_N_reg_in_collar[i-1])
phi_0_i += (m * A1_d_phi[i-1])
# Fill the position arrays.
N_regions_done = 0
for region, N_regions_in_collar in enumerate(self.A1_N_reg_in_collar):
N_regions_done_next = N_regions_in_collar + N_regions_done
# Set A1_theta
self.A1_theta[N_regions_done:N_regions_done_next] = A1_theta[region]
# Set phi (K19 eqn (12))
j = np.arange(N_regions_in_collar, dtype=float)
A1_phi_collar = A1_phi_0[region] + j * A1_d_phi[region]
self.A1_phi[N_regions_done:N_regions_done_next] = A1_phi_collar
N_regions_done = N_regions_done_next
self.A1_phi %= 2 * np.pi
self.A1_theta %= np.pi
self.A1_theta[-1] = np.pi
return self.A1_theta, self.A1_phi
def get_stretch_params(self, N):
""" Return the a and b parameters for the latitude stretching.
Empirically, b = 10 * a gives an excellent low density scatter.
For N > 80, a = 0.2. For N < 80, a has been fit by trial and error.
Args:
N (int)
The number of particles in the shell.
Returns:
a, b (float)
The stretch parameters.
"""
if N > 80:
a = 0.20
elif N == 79:
a = 0.20
elif N == 78:
a = 0.20
elif N == 77:
a = 0.20
elif N == 76:
a = 0.21
elif N == 75:
a = 0.21
elif N == 74:
a = 0.22
elif N == 73:
a = 0.23
elif N == 72:
a = 0.23
elif N == 71:
a = 0.24
elif N == 70:
a = 0.25
elif N == 69:
a = 0.24
elif N == 68:
a = 0.24
elif N == 67:
a = 0.24
elif N == 66:
a = 0.24
elif N == 65:
a = 0.21
elif N == 64:
a = 0.20
elif N == 63:
a = 0.21
elif N == 62:
a = 0.21
elif N == 61:
a = 0.22
elif N == 60:
a = 0.21
elif N == 59:
a = 0.21
elif N == 58:
a = 0.22
elif N == 57:
a = 0.215
elif N == 56:
a = 0.22
elif N == 55:
a = 0.20
elif N == 54:
a = 0.21
elif N == 53:
a = 0.20
elif N == 52:
a = 0.23
elif N == 51:
a = 0.23
elif N == 50:
a = 0.25
elif N == 49:
a = 0.21
elif N == 48:
a = 0.22
elif N == 47:
a = 0.225
elif N == 46:
a = 0.22
elif N == 45:
a = 0.23
elif N == 44:
a = 0.19
elif N == 43:
a = 0.235
elif N == 42:
a = 0.25
elif N == 41:
a = 0.18
elif N == 40:
a = 0.23
elif N == 39:
a = 0.25
elif N == 38:
a = 0.25
elif N == 37:
a = 0.26
elif N == 36:
a = 0.27
elif N == 35:
a = 0.21
elif N == 34:
a = 0.22
elif N == 33:
a = 0.20
elif N == 32:
a = 0.25
elif N == 31:
a = 0.27
elif N == 28:
a = 0.20
elif N == 27:
a = 0.19
else:
a = 0.20
if self.verbosity >= 2:
print("\nWarning: stretching N = %d not manually tested" % N)
return a, 10*a
def apply_stretch_factor(self, a=0.2, b=2.0):
""" Apply the SEA stretch factor.
K19 eqn (13).
Args:
a, b (float)
The stretching parameters.
Sets:
A1_theta ([float])
The point colatitudes.
"""
pi_o_2 = np.pi / 2
inv_sqrtN = 1 / np.sqrt(self.N)
A1_prefactor = (pi_o_2 - self.A1_theta) * a * inv_sqrtN
A1_exp_factor = - ((pi_o_2 - abs(pi_o_2 - self.A1_theta))
/ (np.pi * b * inv_sqrtN))
self.A1_theta += (A1_prefactor * np.exp(A1_exp_factor))
# Leave the cap points at the poles
self.A1_theta[-2] = 0.0
self.A1_theta[-1] = np.pi
return
def apply_random_rotation(self):
""" Rotate the shell with three random Euler angles. """
# Random Euler angles
alpha = np.random.rand() * 2*np.pi
beta = np.random.rand() * 2*np.pi
gamma = np.random.rand() * 2*np.pi
A2_rot = get_euler_rotation_matrix(alpha, beta, gamma)
# Array of position vectors
A2_pos = np.array([self.A1_x, self.A1_y, self.A1_z]).transpose()
# Rotate each position vector
for i in range(len(A2_pos)):
A2_pos[i] = np.dot(A2_rot, A2_pos[i])
# Unpack positions
self.A1_x, self.A1_y, self.A1_z = A2_pos.transpose()
return
# Aliases for the main outputs without my array notation
@property
def x(self):
return self.A1_x
@property
def y(self):
return self.A1_y
@property
def z(self):
return self.A1_z
class GenSphere(object):
""" Generate particle initial conditions with the SEA method and nested
shells, following a density profile.
See __init__()'s documentation for more details.
Basic Usage:
e.g. Create a full sphere of particles on a simple density profile
and print their positions and masses:
>>> import seagen
>>> import numpy as np
>>> N = 100000
>>> radii = np.arange(0.01, 10, 0.01)
>>> densities = np.ones(len(radii)) # e.g. constant density
>>> particles = seagen.GenSphere(N, radii, densities)
>>> print(particles.x, particles.y, particles.z, particles.m)
"""
def __init__(self, N_picle_des, A1_r_prof, A1_rho_prof, A1_mat_prof=None,
A1_u_prof=None, A1_T_prof=None, A1_P_prof=None,
A1_m_rel_prof=None, do_stretch=True, A1_force_more_shells=None,
tuner=None, force_num_shell=None, verbosity=1):
""" Generate nested spherical shells of particles to match radial
profiles.
The profiles should give the outer radius of each thin profile shell
and the corresponding density (and other values) within that shell.
So, the first profile radius should be small but greater than zero.
Args:
N_picle_des (int)
The desired number of particles.
A1_r_prof ([float])
The array of profile radii.
A1_rho_prof ([float])
The array of densities at the profile radii.
A1_mat_prof (opt. [int])
The array of material identifiers at the profile radii. If
not provided, then default to all zeros.
A1_u_prof A1_T_prof A1_P_prof (opt. [float])
Optional arrays of other values at the profile radii:
specific internal energy, temperature, and pressure.
Default None.
A1_m_rel_prof (opt. [float])
Default None to keep the particle mass ~constant and adjust
the shell widths to match the density profile. Set to a
radial profile of 0 < m_rel <= 1 to instead change the
shell particle masses relative to the input particle mass.
e.g. [1, 1, ..., 1, 0.9, 0.8, ..., 0.2, 0.1, 0.1, ..., 0.1]
for input-mass particles in the centre, 1/10th mass
particles in the outer region, and a smooth transition in
the middle.
do_stretch (opt. bool)
Default True. Set False to not do the SEA method's latitude
stretching.
A1_force_more_shells (opt [bool])
For each layer, if True, then only allow the shell tweaking
to add more shells. Default False. Useful for e.g. a low-
density outer layer that doesn't have many shells.
verbosity (opt. int)
The verbosity to control printed output:
0 None
1 Standard (default)
2 Extra
3 Debug
Output attrs: (Also accessable without the A1_ prefix)
A1_x, A1_y, A1_z, A1_r ([float])
The arrays of the particles' cartesian coordinates and
radii.
A1_m, A1_rho ([float])
The arrays of the particles' masses and densities,
using the profiles at the right radius.
A1_mat ([int])
The arrays of the particles' material identifiers.
A1_u, A1_T, A1_P ([float] or None)
If the profile arrays were provided, then the corresponding
arrays of the particles' sp. int. energies, temperature,
and/or pressures. Otherwise, None.
N_picle (int)
The final number of particles.
N_shell_tot (int)
The total number of shells.
"""
# ========
# Setup
# ========
self.N_picle_des = N_picle_des
self.A1_r_prof = A1_r_prof
self.A1_rho_prof = A1_rho_prof
self.A1_mat_prof = A1_mat_prof
self.A1_u_prof = A1_u_prof
self.A1_T_prof = A1_T_prof
self.A1_P_prof = A1_P_prof
self.A1_m_rel_prof = A1_m_rel_prof
self.do_stretch = do_stretch
self.gap_tuner = tuner
self.verbosity = verbosity
# Maximum number of attempts allowed for tweaking particle mass and
# number of particles in the first shell of an outer layer
attempt_max = int(max(self.N_picle_des / 1e3, 1e5))
# Optional profiles
if self.A1_u_prof is not None:
self.do_u = True
else:
self.do_u = False
if self.A1_T_prof is not None:
self.do_T = True
else:
self.do_T = False
if self.A1_P_prof is not None:
self.do_P = True
else:
self.do_P = False
if self.A1_m_rel_prof is not None:
self.do_m_rel = True
else:
self.do_m_rel = False
# Verbosity
if self.verbosity >= 1:
print(banner)
self.verbosity_options = {
0: "None", 1: "Standard", 2: "Extra", 3: "Debug"}
print("Verbosity %d: %s printing" %
(self.verbosity, self.verbosity_options[self.verbosity]))
self.N_prof = len(self.A1_r_prof)
self.N_shell_tot = 0
# Default material IDs if not provided
if self.A1_mat_prof is None:
self.A1_mat_prof = np.zeros(self.N_prof)
# Values for each shell
A1_N_shell = []
A1_m_shell = []
A1_m_picle_shell = []
A1_r_shell = []
A1_rho_shell = []
A1_mat_shell = []
if self.do_u:
A1_u_shell = []
if self.do_T:
A1_T_shell = []
if self.do_P:
A1_P_shell = []
# All particle data
self.A1_m = []
self.A1_r = []
self.A1_rho = []
self.A1_mat = []
self.A1_x = []
self.A1_y = []
self.A1_z = []
if self.do_u:
self.A1_u = []
if self.do_T:
self.A1_T = []
if self.do_P:
self.A1_P = []
# Calculate the mass profile
self.get_mass_profile()
# Enclosed mass profile
self.A1_m_enc_prof = np.cumsum(self.A1_m_prof)
self.m_tot = self.A1_m_enc_prof[-1]
self.m_picle_des = self.m_tot / self.N_picle_des
# Find the radii of all material boundaries (including the outer edge)
self.find_material_boundaries()
self.N_layer = len(self.A1_r_bound)
# Check the profiles and update them if necessary
self.check_interp_profiles()
# Force the shell tweaking to increase the number in each layer
if A1_force_more_shells is None:
A1_force_more_shells = [False] * self.N_layer
# Max allowed particle mass
m_picle_max = self.m_picle_des * 1.01
if self.do_m_rel:
m_rel_max = np.amax(self.A1_m_rel_prof)
# Initial relative particle mass tweak
dm_picle_init = 1e-3
if self.verbosity >= 1:
A1_m_layer = self.A1_m_enc_prof[self.A1_idx_bound]
A1_m_layer[1:] -= A1_m_layer[:-1]
print("\n%d layer(s):" % self.N_layer)
print(" Outer radius Mass Material")
for r_bound, m_layer, mat in zip(
self.A1_r_bound, A1_m_layer, self.A1_mat_layer):
print(" %5e %.5e %d" % (r_bound, m_layer, mat))
print("\n> Divide the profile into shells")
# ================
# First (innermost) layer
# ================
i_layer = 0
if self.verbosity >= 1 and self.N_layer > 1:
print("\n==== Layer %d ====" % (i_layer + 1))
# Index of first profile shell in the next layer, or of the final shell
idx_bound = self.A1_idx_bound[0] + 1
if idx_bound == self.N_prof:
idx_bound = self.N_prof - 1
r_bound = self.A1_r_bound[0]
# ========
# Vary the particle mass until the particle shell boundary matches the
# profile boundary
# ========
# Start at the maximum allowed particle mass then decrease to fit
self.m_picle = m_picle_max
self.dm_picle = dm_picle_init
N_shell_init = 0
if self.verbosity >= 1:
print("\n> Tweak the particle mass to fix the outer boundary")
if self.verbosity == 3:
if self.do_m_rel:
header = " Attempt Particle mass Max rel mass Relative tweak "
else:
header = " Attempt Particle mass Relative tweak "
print(header, end='')
# Tweak the particle mass
is_done = False
#print('A1_idx_bound=',self.A1_idx_bound)
# This also sets the shell radius data: A1_idx_outer and A1_r_outer
for attempt in range(attempt_max + 1):
if attempt == attempt_max:
raise RuntimeError("Failed after %d attempts!" % attempt_max)
if self.verbosity == 3:
# No endline so can add more on this line in the loop
if self.do_m_rel:
print("\n %07d %.5e %.5f %.1e " %
(attempt, self.m_picle * self.A1_m_rel_prof[0],
m_rel_max, self.dm_picle), end='')
else:
print("\n %07d %.5e %.1e " %
(attempt, self.m_picle, self.dm_picle), end='')
sys.stdout.flush()
# Find the outer boundary radii of all shells
A1_idx_outer = []
A1_r_outer = []
# Set the core dr with the radius containing the mass of the central
# tetrahedron of 4 particles
N_picle_shell = 4
if self.do_m_rel:
idx_outer = np.searchsorted(
self.A1_m_enc_prof,
N_picle_shell * self.m_picle * self.A1_m_rel_prof[0]
)
else:
idx_outer = np.searchsorted(self.A1_m_enc_prof,
N_picle_shell * self.m_picle)
r_outer = self.A1_r_prof[idx_outer]
self.dr_core = r_outer
# Mass-weighted mean density
self.rho_core = get_weighted_mean(self.A1_m_prof[:idx_outer],
self.A1_rho_prof[:idx_outer])
# Record shell boundary
A1_idx_outer.append(idx_outer)
A1_r_outer.append(self.dr_core)
# ========
# Find the shells that fit in this layer
# ========
N_shell = 1
#print('idx_bound=',idx_bound)
# Continue until a break from inside, to do properly the final shell
while N_shell < self.N_prof:
# Calculate the shell width from the profile density relative to
# the core radius and density
rho = self.A1_rho_prof[idx_outer]
if self.do_m_rel:
dr = self.dr_core * np.cbrt(self.A1_m_rel_prof[idx_outer])
else:
dr = self.dr_core * np.cbrt(self.rho_core / rho)
# Find the profile radius just beyond this shell (r_outer + dr)
idx_outer = np.searchsorted(self.A1_r_prof, r_outer + dr)
#print('idx_outer=',idx_outer)
# Hit outer edge, stop
if idx_outer >= idx_bound:
# Extend the final shell to include the tiny extra bit of
# this layer
if is_done:
A1_idx_outer[-1] = idx_bound
A1_r_outer[-1] = r_bound
break
r_outer = self.A1_r_prof[idx_outer]
# Record the shell
A1_idx_outer.append(idx_outer)
A1_r_outer.append(r_outer)
N_shell += 1
#print('idx_outer=',idx_outer)
#print('*******N_shell=',N_shell)
if is_done:
if self.verbosity == 3:
print("")
break
# Number of shells for the starting particle mass
if N_shell_init == 0:
N_shell_init = N_shell
#print('$$$$$$N_shell_init=',N_shell_init)
# ========
# Reduce the particle mass until one more shell *just* fits
# ========
# Not got another shell yet, so reduce the mass
if N_shell == N_shell_init:
if self.do_m_rel:
# Reduce the maximum relative mass, so that any smaller
# masses are kept unchanged
A1_sel = np.where(m_rel_max < self.A1_m_rel_prof)[0]
m_rel_max *= 1 - self.dm_picle
self.A1_m_rel_prof[A1_sel] = m_rel_max
else:
self.m_picle *= 1 - self.dm_picle
# Got one more shell, but need it to *just* fit, so go back one step
# and retry with smaller mass changes (repeat this twice!)
elif self.dm_picle > dm_picle_init * 1e-2:
if self.verbosity == 3:
print(" Reduce tweak", end='')
#print(" Reduce tweak")
self.m_picle *= 1 + self.dm_picle
self.dm_picle *= 1e-1
# Got one more shell and refined the mass so it just fits, so done!
else:
# Repeat one more time to extend the final shell to include the
# tiny extra bit of this layer
is_done = True
if self.verbosity == 3:
print(header)
if self.verbosity >= 1:
print("> Done particle mass tweaking!")
if self.verbosity >= 2:
print(" from %.5e to %.5e after %d attempts" %
(self.m_picle_des, self.m_picle, attempt))
if self.verbosity >= 1:
print("\n%d shells in layer %d" % (N_shell, i_layer + 1))
i_layer += 1