-
Notifications
You must be signed in to change notification settings - Fork 3
/
solver.pyx
1242 lines (1100 loc) · 52.9 KB
/
solver.pyx
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
# distutils: language = c++
# cython: boundscheck=False, wraparound=False, nonecheck=False, cdivision=True, initializedcheck=False
from libc.math cimport NAN, isnan
from cpython.mem cimport PyMem_Free
import numpy as np
cimport numpy as np
from scipy.constants import G as G_
from CyRK.utils.utils cimport allocate_mem, reallocate_mem
# Import cythonized functions
from TidalPy.utilities.math.complex cimport cf_build_dblcmplx
from TidalPy.utilities.dimensions.nondimensional cimport (
cf_non_dimensionalize_physicals,
cf_redimensionalize_physicals,
cf_redimensionalize_radial_functions
)
from TidalPy.RadialSolver.starting.driver cimport cf_find_starting_conditions
from TidalPy.RadialSolver.solutions cimport cf_find_num_solutions
from TidalPy.RadialSolver.interfaces.interfaces cimport cf_solve_upper_y_at_interface
from TidalPy.RadialSolver.interfaces.reversed cimport cf_top_to_bottom_interface_bc
from TidalPy.RadialSolver.derivatives.base cimport RadialSolverBase
from TidalPy.RadialSolver.derivatives.odes cimport cf_build_solver
from TidalPy.RadialSolver.boundaries.boundaries cimport cf_apply_surface_bc
from TidalPy.RadialSolver.collapse.collapse cimport cf_collapse_layer_solution
# Setup globals
cdef double G
G = G_
# Maximum size for array building
cdef size_t MAX_NUM_Y = 6
cdef size_t MAX_NUM_Y_REAL = 2 * MAX_NUM_Y
cdef size_t MAX_NUM_SOL = 3
cdef class RadialSolverSolution():
def __init__(
self,
size_t num_slices,
tuple solve_for,
size_t num_ytypes
):
# loop indicies
cdef size_t i
cdef size_t love_array_size
# Initialize pointers
self.full_solution_ptr = NULL
self.complex_love_ptr = NULL
# Initialize status
self.message = 'RadialSolverSolution has not had its status set.'
self.success = False
# Store number of solution types
self.num_ytypes = num_ytypes
self.ytypes = solve_for
# Store size information
self.num_slices = num_slices
self.total_size = MAX_NUM_Y * self.num_slices * self.num_ytypes
# Have the radial solver take control of the full solution memory
self.full_solution_ptr = <double complex*> allocate_mem(
self.total_size * sizeof(double complex),
'full_solution_ptr (RadialSolverSolution; init)'
)
if not (self.full_solution_ptr is NULL):
self.full_solution_view = <double complex[:self.total_size]> self.full_solution_ptr
# Set all values of the solution array to NANs. This ensures that if there is a problem with the solver then
# the solutions will be defined (but nan).
for i in range(self.total_size):
self.full_solution_ptr[i] = NAN
# Initialize Love numbers (3 love numbers for each requested y-type)
# Love numbers are stored (k, h, l)_ytype0, (k, h, l)_ytype1, (k, h, l)_ytype2, ...
love_array_size = 3 * self.num_ytypes
self.complex_love_ptr = <double complex*> allocate_mem(
love_array_size * sizeof(double complex),
'complex_love_ptr (RadialSolverSolution; init)'
)
if not (self.complex_love_ptr is NULL):
self.complex_love_view = <double complex[:love_array_size]> self.complex_love_ptr
for i in range(love_array_size):
self.complex_love_ptr[i] = NAN
@property
def result(self):
""" Return result array. """
return np.ascontiguousarray(
self.full_solution_view,
dtype=np.complex128
).reshape((self.num_slices, self.num_ytypes * MAX_NUM_Y)).T
@property
def love(self):
""" Return all complex love numbers. """
return np.ascontiguousarray(
self.complex_love_view,
dtype=np.complex128
).reshape((self.num_ytypes, 3))
@property
def k(self):
""" Tidal Love number k. """
return np.ascontiguousarray(
self.complex_love_view[0::3],
dtype=np.complex128
)
@property
def h(self):
""" Tidal Love number h. """
return np.ascontiguousarray(
self.complex_love_view[1::3],
dtype=np.complex128
)
@property
def l(self):
""" Tidal Shida number l. """
return np.ascontiguousarray(
self.complex_love_view[2::3],
dtype=np.complex128
)
def __len__(self):
"""Return number of solution types."""
return <Py_ssize_t>self.num_ytypes
def __getitem__(self, str ytype_name):
"""Get a specific solution type array."""
cdef size_t i
cdef size_t requested_sol_num = 0
cdef bool_cpp_t found = False
cdef str sol_test_name
for i in range(self.num_ytypes):
sol_test_name = self.ytypes[i]
if sol_test_name == ytype_name:
requested_sol_num = i
found = True
break
if not found:
raise AttributeError('Unknown solution type requested. Key must match names passed to radial_solver "solve_for" argument and be lower case.')
# Slice the result and return only the requested solution type.
return self.result[MAX_NUM_Y * (requested_sol_num): MAX_NUM_Y * (requested_sol_num + 1)]
def __dealloc__(self):
# The RadialSolverSolution class has full control of the solution so it is responsible for releasing its memory.
if not (self.full_solution_ptr is NULL):
PyMem_Free(self.full_solution_ptr)
if not (self.complex_love_ptr is NULL):
PyMem_Free(self.complex_love_ptr)
cdef RadialSolverSolution cf_radial_solver(
size_t total_slices,
double* radius_array_ptr,
double* density_array_ptr,
double* gravity_array_ptr,
double* bulk_modulus_array_ptr,
double complex* complex_shear_modulus_array_ptr,
double frequency,
double planet_bulk_density,
size_t num_layers,
bool_cpp_t* is_solid_by_layer_ptr,
bool_cpp_t* is_static_by_layer_ptr,
bool_cpp_t* is_incompressible_by_layer_ptr,
double* upper_radius_by_layer_ptr,
unsigned int degree_l = 2,
tuple solve_for = None,
bool_cpp_t use_kamata = False,
int integration_method = 1,
double integration_rtol = 1.0e-4,
double integration_atol = 1.0e-12,
bool_cpp_t scale_rtols_by_layer_type = True,
size_t max_num_steps = 500_000,
size_t expected_size = 500,
size_t max_ram_MB = 500,
double max_step = 0,
bool_cpp_t limit_solution_to_radius = True,
bool_cpp_t nondimensionalize = True,
bool_cpp_t verbose = False,
bool_cpp_t raise_on_fail = False
):
"""
Radial solver for
"""
# General indexing
# Indexing for: Layer | Solution | ys | slices | solutions
cdef size_t layer_i
cdef size_t slice_i
# Indexing for: Solution | ys | ytypes
cdef unsigned char solution_i
cdef unsigned char y_i
cdef unsigned char ytype_i
cdef unsigned char lhs_y_index
# Type conversions
cdef double degree_l_dbl = <double>degree_l
# Pull out key information
cdef size_t top_slice_i = total_slices - 1
cdef double radius_planet = radius_array_ptr[total_slices - 1]
cdef size_t num_interfaces = num_layers - 1
# Ensure there is at least one layer.
if num_layers <= 0:
raise AttributeError('Radial solver requires at least one layer.')
# Ensure there are enough slices for radial interpolations.
if total_slices <= (3 * num_layers):
raise AttributeError('Radial solver requires at least 3 radial slices per layer (ideally >= 10 per).')
# Non-dimensionalize inputs
cdef double G_to_use = NAN
cdef double frequency_to_use = NAN
if nondimensionalize:
cf_non_dimensionalize_physicals(
total_slices,
frequency,
radius_planet,
planet_bulk_density,
radius_array_ptr,
density_array_ptr,
gravity_array_ptr,
bulk_modulus_array_ptr,
complex_shear_modulus_array_ptr,
&frequency_to_use,
&G_to_use
)
# Ensure that no errors occured during the non-dim process
if isnan(frequency_to_use) or isnan(G_to_use):
raise ValueError('NaNs encountered after non-dimensionalize call.')
else:
# Leave inputs alone.
G_to_use = G
frequency_to_use = frequency
cdef double surface_gravity = gravity_array_ptr[total_slices - 1]
# Find boundary condition at the top of the planet -- this is dependent on the forcing type.
# Tides (default here) follow the (y2, y4, y6) = (0, 0, (2l+1)/R) rule
# The [5] represents the maximum number of solvers that can be invoked with a single call to radial_solver
cdef size_t max_num_solutions = 5
cdef size_t num_ytypes = 1
cdef str solver_name
# 15 = 5 (max_num_solutions) * 3 (number of surface conditions)
cdef double[15] boundary_conditions
cdef double* bc_pointer = &boundary_conditions[0]
if solve_for is None:
# Assume we are solving for tides
if nondimensionalize:
bc_pointer[0] = 0.
bc_pointer[1] = 0.
bc_pointer[2] = (2. * degree_l_dbl + 1.) / 1.
else:
bc_pointer[0] = 0.
bc_pointer[1] = 0.
bc_pointer[2] = (2. * degree_l_dbl + 1.) / radius_planet
solve_for = ('tidal',)
else:
# Use user input
num_ytypes = len(solve_for)
if num_ytypes > max_num_solutions:
raise AttributeError(f'Unsupported number of solvers requested (max is {max_num_solutions}).')
# Parse user input for the types of solvers that should be used.
ytype_i = 0
while num_ytypes > ytype_i:
solver_name = solve_for[ytype_i]
if solver_name.lower() == 'tidal':
if nondimensionalize:
bc_pointer[ytype_i * 3 + 0] = 0.
bc_pointer[ytype_i * 3 + 1] = 0.
bc_pointer[ytype_i * 3 + 2] = (2. * degree_l_dbl + 1.) / 1.
else:
bc_pointer[ytype_i * 3 + 0] = 0.
bc_pointer[ytype_i * 3 + 1] = 0.
bc_pointer[ytype_i * 3 + 2] = (2. * degree_l_dbl + 1.) / radius_planet
elif solver_name.lower() == 'loading':
if nondimensionalize:
bc_pointer[ytype_i * 3 + 0] = -(2. * degree_l_dbl + 1.) / 3.
bc_pointer[ytype_i * 3 + 1] = 0.
bc_pointer[ytype_i * 3 + 2] = (2. * degree_l_dbl + 1.) / 1.
else:
bc_pointer[ytype_i * 3 + 0] = -(2. * degree_l_dbl + 1.) * planet_bulk_density / 3.
bc_pointer[ytype_i * 3 + 1] = 0.
bc_pointer[ytype_i * 3 + 2] = (2. * degree_l_dbl + 1.) / radius_planet
elif solver_name.lower() == 'free':
bc_pointer[ytype_i * 3 + 0] = 0.
bc_pointer[ytype_i * 3 + 1] = 0.
bc_pointer[ytype_i * 3 + 2] = 0.
else:
raise NotImplementedError(f'Requested solver, {solver_name}, has not been implemented.\n\tSupported solvers are: tidal, loading, free.')
ytype_i += 1
# Integration information
# Max step size
cdef double max_step_touse
cdef bool_cpp_t max_step_from_arrays
max_step_from_arrays = False
if max_step == 0:
# If max_step is zero use the array information to determine max_step_size
max_step_from_arrays = True
else:
# Otherwise use user input.
max_step_touse = max_step
# Setup tolerance arrays
# For simplicity just make these all as large as the maximum number of ys.
# Maximum number of ys = 6. Then 2x for conversion from complex to real
cdef double[12] rtols_array
cdef double* rtols_ptr = &rtols_array[0]
cdef double[12] atols_array
cdef double* atols_ptr = &atols_array[0]
# Create storage for flags and information about each layer.
cdef size_t* layer_int_data_ptr = <size_t *> allocate_mem(
3 * num_layers * sizeof(size_t),
'layer_int_data_ptr (radial_solver; init)'
)
cdef size_t* num_solutions_by_layer_ptr = &layer_int_data_ptr[0]
cdef size_t* start_index_by_layer_ptr = &layer_int_data_ptr[num_layers]
cdef size_t* num_slices_by_layer_ptr = &layer_int_data_ptr[2 * num_layers]
# Opt: The bools above could be stores in a single char variable (per layer).
# Eg., 0x00 All false, 0x01 is solid, 0x10 is static and liquid, 0x11 is static and solid, etc.
cdef bool_cpp_t layer_is_solid
cdef bool_cpp_t layer_is_static
cdef bool_cpp_t layer_is_incomp
cdef bool_cpp_t layer_below_is_solid
cdef bool_cpp_t layer_below_is_static
cdef bool_cpp_t layer_below_is_incomp
cdef double layer_upper_radius, radius_check
cdef double layer_rtol_real
cdef double layer_rtol_imag
cdef double layer_atol_real
cdef double layer_atol_imag
cdef size_t layer_slices
cdef unsigned char num_sols
cdef unsigned char num_ys
cdef unsigned char num_ys_dbl
cdef unsigned char layer_below_num_sols
cdef unsigned char layer_below_num_ys
for layer_i in range(num_layers):
# Pull out information on this layer
layer_is_solid = is_solid_by_layer_ptr[layer_i]
layer_is_static = is_static_by_layer_ptr[layer_i]
layer_is_incomp = is_incompressible_by_layer_ptr[layer_i]
layer_upper_radius = upper_radius_by_layer_ptr[layer_i]
# Make any dimension corrections
if nondimensionalize:
layer_upper_radius = layer_upper_radius / radius_planet
# Find number of solutions based on this layer's assumptions
num_sols = cf_find_num_solutions(
layer_is_solid,
layer_is_static,
layer_is_incomp
)
num_ys = 2 * num_sols
num_solutions_by_layer_ptr[layer_i] = num_sols
# Determine how many slices are in this layer
if layer_i == 0:
# First layer starts at 0.
start_index_by_layer_ptr[0] = 0
else:
# Not first layer. Starting point is based on number of slices in previous layer.
start_index_by_layer_ptr[layer_i] = \
start_index_by_layer_ptr[layer_i - 1] + num_slices_by_layer_ptr[layer_i - 1]
layer_slices = 0
for slice_i in range(start_index_by_layer_ptr[layer_i], total_slices):
radius_check = radius_array_ptr[slice_i]
if radius_check > layer_upper_radius:
# We have passed this layer.
break
layer_slices += 1
if layer_slices <= 3:
raise ValueError('At least three layer slices per layer are required. Try using more slices in the' + \
'input arrays.')
num_slices_by_layer_ptr[layer_i] = layer_slices
# We have all the size information needed to build storage pointers
# Main storage pointer is setup like [layer_i][solution_i][y_i + r_i]
cdef double complex*** main_storage = <double complex ***> allocate_mem(
num_layers * sizeof(double complex**),
'main_storage (radial_solver; init)'
)
cdef double complex** storage_by_solution = NULL
cdef double complex* storage_by_y = NULL
for layer_i in range(num_layers):
num_sols = num_solutions_by_layer_ptr[layer_i]
layer_slices = num_slices_by_layer_ptr[layer_i]
# Number of ys = 2x num sols
num_ys = 2 * num_sols
storage_by_solution = <double complex **> allocate_mem(
num_sols * sizeof(double complex*),
'storage_by_solution (radial_solver; init)'
)
for solution_i in range(num_sols):
storage_by_y = <double complex *> allocate_mem(
layer_slices * num_ys * sizeof(double complex),
'storage_by_y (radial_solver; init)'
)
storage_by_solution[solution_i] = storage_by_y
storage_by_y = NULL
main_storage[layer_i] = storage_by_solution
storage_by_solution = NULL
# Create storage for uppermost ys for each solution. We don't know how many solutions or ys per layer so assume the
# worst.
cdef double complex[36] uppermost_y_per_solution
cdef double complex* uppermost_y_per_solution_ptr = &uppermost_y_per_solution[0]
# Layer specific pointers; set the size based on the layer with the most slices.
cdef double* layer_radius_ptr
cdef double* layer_density_ptr
cdef double* layer_gravity_ptr
cdef double* layer_bulk_mod_ptr
cdef double complex* layer_shear_mod_ptr
# Properties at top and bottom of layer
cdef double radius_lower, radius_upper
cdef double density_lower, density_upper
cdef double gravity_lower, gravity_upper
cdef double bulk_lower
cdef double complex shear_lower
cdef tuple radial_span
# Properties at interfaces between layers
cdef double static_liquid_density
cdef double interface_gravity
cdef double last_layer_upper_gravity, last_layer_upper_density
# Starting solutions (initial conditions / lower boundary conditions)
# Allocate memory for the initial value arrays now. We don't know the number of solutions or ys. But the max number
# is not all that different from the min. So it is more efficient to assume the largest size.
# Largest size = 6 (ys) x 3 (sols) = 18
# For the "only_real" this is further multiplied by 2 since we convert the number of _complex_ ys to 2x _real_ ys
cdef double complex[18] initial_y
cdef double complex* initial_y_ptr = &initial_y[0]
cdef double[36] initial_y_only_real
cdef double* initial_y_only_real_ptr = &initial_y_only_real[0]
cdef bool_cpp_t starting_y_check = False
# Intermediate values
cdef double complex dcomplex_tmp
# Solver class
cdef RadialSolverBase solver
cdef double* solver_solution_ptr = NULL
cdef bool_cpp_t cysolver_setup = False
# Feedback
cdef str feedback_str
cdef bool_cpp_t error
feedback_str = 'Starting integration'
if verbose:
print(feedback_str)
error = False
cdef size_t start_index
# No matter the results of the integration, we know the shape and size of the final solution.
# The number of rows will depend on if the user wants to simultaneously calculate loading Love numbers.
cdef size_t num_output_ys = MAX_NUM_Y * num_ytypes
# Build final output solution
cdef RadialSolverSolution solution = RadialSolverSolution(total_slices, solve_for, num_ytypes)
# Get a reference pointer to solution array
cdef double complex* solution_ptr = solution.full_solution_ptr
# During collapse, variables for the layer above the target one are used. Declare these and preset them.
cdef double layer_above_lower_gravity
cdef double layer_above_lower_density
cdef double liquid_density_at_interface
cdef bool_cpp_t layer_above_is_solid
cdef bool_cpp_t layer_above_is_static
cdef bool_cpp_t layer_above_is_incomp
# The constant vectors are the same size as the number of solutions in the layer. But since the largest they can
# ever be is 3, it is more efficient to just preallocate them on the stack rather than dynamically allocate them
# on the heap.
cdef double complex[3] constant_vector
cdef double complex* constant_vector_ptr = &constant_vector[0]
cdef double complex[3] layer_above_constant_vector
cdef double complex* layer_above_constant_vector_ptr = &layer_above_constant_vector[0]
cdef double complex[6] surface_solutions
cdef double complex* surface_solutions_ptr = &surface_solutions[0]
# Variables used to solve the linear equation at the planet's surface.
# Info = flag set by the solver. Set equal to -999. This will indicate that the solver has not been called yet.
cdef int bc_solution_info = -999
# Shifted or reversed indices used during collapse.
cdef size_t layer_i_reversed
try:
for layer_i in range(num_layers):
# Get layer's index information
start_index = start_index_by_layer_ptr[layer_i]
layer_slices = num_slices_by_layer_ptr[layer_i]
# Get solution and y information
num_sols = num_solutions_by_layer_ptr[layer_i]
num_ys = 2 * num_sols
num_ys_dbl = 2 * num_ys
# Setup pointer array slices starting at this layer's beginning
layer_radius_ptr = &radius_array_ptr[start_index]
layer_density_ptr = &density_array_ptr[start_index]
layer_gravity_ptr = &gravity_array_ptr[start_index]
layer_bulk_mod_ptr = &bulk_modulus_array_ptr[start_index]
layer_shear_mod_ptr = &complex_shear_modulus_array_ptr[start_index]
# Get physical parameters at the top and bottom of the layer
radius_lower = layer_radius_ptr[0]
density_lower = layer_density_ptr[0]
gravity_lower = layer_gravity_ptr[0]
bulk_lower = layer_bulk_mod_ptr[0]
shear_lower = layer_shear_mod_ptr[0]
radius_upper = layer_radius_ptr[layer_slices - 1]
density_upper = layer_density_ptr[layer_slices - 1]
gravity_upper = layer_gravity_ptr[layer_slices - 1]
# Determine max step size (if not provided by user)
if max_step_from_arrays:
# Maximum step size during integration can not exceed the average radial slice size.
max_step_touse = (radius_upper - radius_lower) / <double>layer_slices
# Get assumptions for layer
layer_is_solid = is_solid_by_layer_ptr[layer_i]
layer_is_static = is_static_by_layer_ptr[layer_i]
layer_is_incomp = is_incompressible_by_layer_ptr[layer_i]
# Determine rtols and atols for this layer.
# Scale rtols by layer type
y_i = 0
while num_ys > y_i:
# Default is that each layer's rtol and atol equal user input.
# TODO: Change up the tolerance scaling between real and imaginary?
layer_rtol_real = integration_rtol
layer_rtol_imag = integration_rtol
layer_atol_real = integration_atol
layer_atol_imag = integration_atol
if scale_rtols_by_layer_type:
# Certain layer assumptions can affect solution stability so use additional scales on the relevant rtols
# TODO test that these scales are the best.
if layer_is_solid:
# Scale y2 and y3 by 0.1
if (y_i == 1) or (y_i == 2):
# Scale both the real and imaginary portions by the same amount.
layer_rtol_real *= 0.1
layer_rtol_imag *= 0.1
else:
if not layer_is_static:
# Scale dynamic liquid layer's y2 by additional 0.01.
if (y_i == 1):
# Scale both the real and imaginary portions by the same amount.
layer_rtol_real *= 0.01
layer_rtol_imag *= 0.01
# Populate rtol and atol pointers.
rtols_ptr[2 * y_i] = layer_rtol_real
rtols_ptr[2 * y_i + 1] = layer_rtol_imag
atols_ptr[2 * y_i] = layer_atol_real
atols_ptr[2 * y_i + 1] = layer_atol_imag
y_i += 1
# Find initial conditions for each solution at the base of this layer.
radial_span = (radius_lower, radius_upper)
if layer_i == 0:
# Use initial condition function
cf_find_starting_conditions(
layer_is_solid,
layer_is_static,
layer_is_incomp,
use_kamata,
frequency_to_use,
radius_lower,
density_lower,
bulk_lower,
shear_lower,
degree_l,
G_to_use,
MAX_NUM_Y,
initial_y_ptr, # Modified Variable
starting_y_check
)
else:
layer_below_is_solid = is_solid_by_layer_ptr[layer_i - 1]
layer_below_is_static = is_static_by_layer_ptr[layer_i - 1]
layer_below_is_incomp = is_incompressible_by_layer_ptr[layer_i - 1]
# Find gravity at the base interface using bottom of this layer and top of previous.
interface_gravity = 0.5 * (gravity_lower + last_layer_upper_gravity)
# Find the density needed for some initial conditions.
if layer_is_solid and layer_below_is_solid:
# Both layers are solid. A liquid interface density is not needed.
static_liquid_density = NAN
elif not layer_is_solid and layer_below_is_solid:
# Layer below is solid, this layer is liquid. Use its density.
static_liquid_density = density_lower
elif layer_is_solid and not layer_below_is_solid:
# Layer below is liquid, this layer is solid. Use layer below's density.
static_liquid_density = last_layer_upper_density
else:
# Both layers are liquid. Choose the layer's density which is static.
if layer_is_static and layer_below_is_static:
# Both layers are static.
# TODO: Not sure what to do here so just use this layer's density.
static_liquid_density = density_lower
elif layer_is_static and not layer_below_is_static:
# This layer is static, one below is not. Use this layer's density
static_liquid_density = density_lower
elif not layer_is_static and layer_below_is_static:
# This layer is dynamic, layer below is static. Use layer below's density
static_liquid_density = last_layer_upper_density
else:
# Both layers are dynamic. Static liquid density is not needed.
static_liquid_density = NAN
# Find the starting values for this layer using the results a the top of the previous layer + an interface
# function.
cf_solve_upper_y_at_interface(
uppermost_y_per_solution_ptr,
initial_y_ptr,
layer_below_num_sols,
num_sols,
layer_below_num_ys,
num_ys,
layer_below_is_solid,
layer_below_is_static,
layer_below_is_incomp,
layer_is_solid,
layer_is_static,
layer_is_incomp,
interface_gravity,
static_liquid_density,
G_to_use
)
# Change initial conditions into 2x real values instead of complex for integration
solution_i = 0
while num_sols > solution_i:
y_i = 0
while num_ys > y_i:
dcomplex_tmp = initial_y_ptr[solution_i * MAX_NUM_Y + y_i]
initial_y_only_real_ptr[solution_i * MAX_NUM_Y_REAL + 2 * y_i] = dcomplex_tmp.real
initial_y_only_real_ptr[solution_i * MAX_NUM_Y_REAL + 2 * y_i + 1] = dcomplex_tmp.imag
y_i += 1
solution_i += 1
# Build solver instance
solver = cf_build_solver(
layer_is_solid,
layer_is_static,
layer_is_incomp,
layer_slices,
2 * num_ys, # Solver needs to know how many ys it is working with. 2x ys in this case.
layer_radius_ptr,
layer_density_ptr,
layer_gravity_ptr,
layer_bulk_mod_ptr,
layer_shear_mod_ptr,
frequency_to_use,
degree_l,
G_to_use,
radial_span,
&initial_y_only_real_ptr[0], # Start the pointer at the beginning of the array
atols_ptr,
rtols_ptr,
integration_method,
max_step_touse,
max_num_steps,
expected_size,
max_ram_MB,
limit_solution_to_radius
)
cysolver_setup = True
# Get storage pointer for this layer
storage_by_solution = main_storage[layer_i]
# Solve for each solution
solution_i = 0
while num_sols > solution_i:
if solution_i > 0:
# Reset solver with new initial condition (this is already done for j==0)
# This pointer has already been passed to the solver during initialization but we need the values at
# the next solution. Pass new pointer at that address.
solver.change_y0_pointer(
&initial_y_only_real_ptr[solution_i * MAX_NUM_Y_REAL],
auto_reset_state=False
)
###### Integrate! #######
solver._solve(reset=True)
#########################
# Check for problems
if not solver.success:
# Problem with integration.
feedback_str = f'Integration problem at layer {layer_i}; solution {solution_i}:\n\t{solver.message}'
if verbose:
print(feedback_str)
if raise_on_fail:
raise RuntimeError(feedback_str)
error = True
break
# If no problems, store results.
solver_solution_ptr = solver.solution_y_ptr
# Need to make a copy because the solver pointers will be reallocated during the next solution.
# Get storage pointer for this solution
storage_by_y = storage_by_solution[solution_i]
y_i = 0
while num_ys > y_i:
for slice_i in range(layer_slices):
# Convert 2x real ys to 1x complex ys
storage_by_y[num_ys * slice_i + y_i] = cf_build_dblcmplx(
solver_solution_ptr[num_ys_dbl * slice_i + (2 * y_i)],
solver_solution_ptr[num_ys_dbl * slice_i + (2 * y_i) + 1]
)
# Store top most result for initial condition for the next layer
# slice_i should already be set to the top of this layer after the end of the previous loop.
uppermost_y_per_solution_ptr[solution_i * MAX_NUM_Y + y_i] = storage_by_y[num_ys * slice_i + y_i]
y_i += 1
# Ready for next solution
solution_i += 1
if error:
# Error was encountered during integration
break
# Prepare for next layer
layer_below_num_sols = num_sols
layer_below_num_ys = num_ys
last_layer_upper_gravity = gravity_upper
last_layer_upper_density = density_upper
del solver
cysolver_setup = False
if error:
feedback_str = f'Integration failed. {solver.message}'
if verbose:
print(feedback_str)
if raise_on_fail:
raise RuntimeError(feedback_str)
else:
feedback_str = 'Integration completed for all layers. Beginning solution collapse.'
ytype_i = 0
while num_ytypes > ytype_i:
feedback_str = f'Collapsing radial solutions for "{ytype_i}" solver.'
# Reset variables for this solver
bc_solution_info = -999
constant_vector_ptr[0] = NAN
constant_vector_ptr[1] = NAN
constant_vector_ptr[2] = NAN
layer_above_lower_gravity = 0.
layer_above_lower_density = 0.
liquid_density_at_interface = 0.
layer_above_is_solid = False
layer_above_is_static = False
layer_above_is_incomp = False
if verbose:
print(feedback_str)
# Collapse the multiple solutions for each layer into one final combined solution.
# Work from the surface to the core.
for layer_i in range(num_layers):
layer_i_reversed = num_layers - (layer_i + 1)
# Pull out layer information.
start_index = start_index_by_layer_ptr[layer_i_reversed]
layer_slices = num_slices_by_layer_ptr[layer_i_reversed]
# Get solution and y information
num_sols = num_solutions_by_layer_ptr[layer_i_reversed]
num_ys = 2 * num_sols
# Setup pointer array slices starting at this layer's beginning
layer_radius_ptr = &radius_array_ptr[start_index]
layer_density_ptr = &density_array_ptr[start_index]
layer_gravity_ptr = &gravity_array_ptr[start_index]
layer_bulk_mod_ptr = &bulk_modulus_array_ptr[start_index]
layer_shear_mod_ptr = &complex_shear_modulus_array_ptr[start_index]
# Get physical parameters at the top and bottom of the layer
radius_lower = layer_radius_ptr[0]
density_lower = layer_density_ptr[0]
gravity_lower = layer_gravity_ptr[0]
bulk_lower = layer_bulk_mod_ptr[0]
shear_lower = layer_shear_mod_ptr[0]
radius_upper = layer_radius_ptr[layer_slices - 1]
density_upper = layer_density_ptr[layer_slices - 1]
gravity_upper = layer_gravity_ptr[layer_slices - 1]
# Get assumptions for layer
layer_is_solid = is_solid_by_layer_ptr[layer_i_reversed]
layer_is_static = is_static_by_layer_ptr[layer_i_reversed]
layer_is_incomp = is_incompressible_by_layer_ptr[layer_i_reversed]
# Get full solutions for this layer
storage_by_solution = main_storage[layer_i_reversed]
# Get value at the top of the layer
solution_i = 0
while num_sols > solution_i:
y_i = 0
while num_ys > y_i:
uppermost_y_per_solution_ptr[solution_i * MAX_NUM_Y + y_i] = \
storage_by_solution[solution_i][(layer_slices - 1) * num_ys + y_i]
y_i += 1
solution_i += 1
if layer_i == 0:
# Working on surface (uppermost) layer -- Apply surface boundary conditions.
cf_apply_surface_bc(
constant_vector_ptr, # Modified Variable
&bc_solution_info, # Modified Variable
bc_pointer,
uppermost_y_per_solution_ptr,
surface_gravity,
G_to_use,
num_sols,
MAX_NUM_Y,
ytype_i,
layer_is_solid,
layer_is_static,
layer_is_incomp
)
# Check that the boundary condition was successfully applied.
if bc_solution_info != 0:
feedback_str = \
(f'Error encountered while applying surface boundary condition. ZGESV code: {bc_solution_info}'
f'\nThe solutions may not be valid at the surface.')
if verbose:
print(feedback_str)
if raise_on_fail:
raise RuntimeError(feedback_str)
error = True
break
else:
# Working on interior layers. Will need to find the constants of integration based on the layer above.
cf_top_to_bottom_interface_bc(
constant_vector_ptr, # Modified Variable
layer_above_constant_vector_ptr,
uppermost_y_per_solution_ptr,
gravity_upper, layer_above_lower_gravity,
density_upper, layer_above_lower_density,
layer_is_solid, layer_above_is_solid,
layer_is_static, layer_above_is_static,
layer_is_incomp, layer_above_is_incomp,
num_sols, MAX_NUM_Y
)
# Use constant vectors to find the full y from all of the solutions in this layer
cf_collapse_layer_solution(
solution_ptr, # Modified Variable
constant_vector_ptr,
storage_by_solution,
layer_radius_ptr,
layer_density_ptr,
layer_gravity_ptr,
frequency_to_use,
start_index,
layer_slices,
num_sols,
MAX_NUM_Y,
num_ys,
num_output_ys,
ytype_i,
layer_is_solid,
layer_is_static,
layer_is_incomp
)
# Setup for next layer
layer_above_lower_gravity = gravity_lower
layer_above_lower_density = density_lower
layer_above_is_solid = layer_is_solid
layer_above_is_static = layer_is_static
layer_above_is_incomp = layer_is_incomp
if num_sols == 1:
layer_above_constant_vector_ptr[0] = constant_vector_ptr[0]
layer_above_constant_vector_ptr[1] = NAN
layer_above_constant_vector_ptr[2] = NAN
elif num_sols == 2:
layer_above_constant_vector_ptr[0] = constant_vector_ptr[0]
layer_above_constant_vector_ptr[1] = constant_vector_ptr[1]
layer_above_constant_vector_ptr[2] = NAN
elif num_sols == 3:
layer_above_constant_vector_ptr[0] = constant_vector_ptr[0]
layer_above_constant_vector_ptr[1] = constant_vector_ptr[1]
layer_above_constant_vector_ptr[2] = constant_vector_ptr[2]
# Ready for next y-type
ytype_i += 1
# Calculate Love numbers and install in final solution.
# First find the solution at the planet's surface.
for ytype_i in range(num_ytypes):
for y_i in range(MAX_NUM_Y):
lhs_y_index = ytype_i * MAX_NUM_Y + y_i
surface_solutions_ptr[y_i] = solution_ptr[top_slice_i * num_output_ys + lhs_y_index]
# Solve Love numbers for this y-type.
find_love_cf(&solution.complex_love_ptr[ytype_i * 3], surface_solutions_ptr, surface_gravity)
finally:
# Redim the input pointers if they were non-dim'd.
if nondimensionalize:
cf_redimensionalize_physicals(
total_slices,
frequency,
radius_planet,
planet_bulk_density,
radius_array_ptr,
density_array_ptr,
gravity_array_ptr,
bulk_modulus_array_ptr,
complex_shear_modulus_array_ptr,
&frequency_to_use,
&G_to_use
)
# Free memory
if cysolver_setup:
del solver
# Deconstruct main solution pointer
# Main storage pointers are structured like [layer_i][solution_i][y_i + slice_i]
# Then main storage
if not (main_storage is NULL):
storage_by_solution = NULL
storage_by_y = NULL
for layer_i in range(num_layers):
num_sols = num_solutions_by_layer_ptr[layer_i]
if not (main_storage[layer_i] is NULL):
solution_i = 0
while num_sols > solution_i:
if not (main_storage[layer_i][solution_i] is NULL):
PyMem_Free(main_storage[layer_i][solution_i])
main_storage[layer_i][solution_i] = NULL
solution_i += 1
PyMem_Free(main_storage[layer_i])
main_storage[layer_i] = NULL
PyMem_Free(main_storage)
main_storage = NULL
# Release layer information pointers
if not (layer_int_data_ptr is NULL):
num_solutions_by_layer_ptr = NULL
start_index_by_layer_ptr = NULL
num_slices_by_layer_ptr = NULL
PyMem_Free(layer_int_data_ptr)
layer_int_data_ptr = NULL
# Update solution status and return
if not error:
# Radial solver completed successfully.