Cholla 3.0.1-dev
Cholla - Massively parallel hydro on GPUs
Loading...
Searching...
No Matches
selfgrav_hydrostatic_col.h
1#ifndef SELFGRAV_HYDROSTATIC_COL
2#define SELFGRAV_HYDROSTATIC_COL
3
4#include <array>
5#include <cassert>
6#include <cmath>
7#include <cstdio>
8#include <limits>
9#include <type_traits>
10
11#include "../../global/global.h"
12#include "../../utils/error_handling.h"
13
14namespace ode_detail
15{
16
17struct NullFn {
19 template <int N>
20 void operator()(Real x, Real cur_step, std::array<Real, N> yvec, std::array<Real, N> cur_yvec_step) const noexcept
21 {
22 }
23};
24
25} // namespace ode_detail
26
27template <int N, class DerivFn, class LogFn = ode_detail::NullFn>
29{
36
37 public:
38 ODEIntegrator() = delete;
39
40 ODEIntegrator(DerivFn deriv_fn, LogFn log_fn = ode_detail::NullFn{}) : deriv_fn_(deriv_fn), log_fn_(log_fn) {}
41
42 std::array<Real, N> integrate(Real xstart, Real xend, Real nominal_step,
43 std::array<Real, N> yvec_start) const noexcept
44 {
45 Real cur_x = xstart;
46 std::array<Real, N> cur_yvec = yvec_start;
47
48 while (true) {
49 Real remaining_xgap = xend - cur_x;
50 Real cur_step = std::min(remaining_xgap, nominal_step);
51
52 // calculate the change in yvec over cur_step
53 std::array<Real, N> d_yvec = calc_midpoint_step(cur_x, cur_yvec, cur_step);
54 // std::array<Real, N> d_yvec = calc_rk4_step(cur_x, cur_yvec, cur_step);
55
56 // potentially call the logger function
57 if constexpr (not std::is_same_v<LogFn, ode_detail::NullFn>) {
58 log_fn_(cur_x, cur_step, cur_yvec, d_yvec);
59 }
60
61 // update cur_x and cur_yvec
62 cur_x += cur_step;
63 for (int i = 0; i < N; i++) {
64 cur_yvec[i] += d_yvec[i];
65 }
66
67 if (remaining_xgap <= nominal_step) {
68 break;
69 }
70 }
71
72 return cur_yvec;
73 }
74
75 private: // helper functions
84 std::array<Real, N> calc_midpoint_step(Real cur_x, std::array<Real, N> yvec, Real step) const noexcept
85 {
86 // first, estimate the yvec at cur_x + 0.5*step
87 std::array<Real, N> deriv_guess = deriv_fn_(cur_x, yvec);
88 std::array<Real, N> y_midpoint;
89 for (int i = 0; i < 3; i++) {
90 y_midpoint[i] = yvec[i] + 0.5 * step * deriv_guess[i];
91 }
92
93 std::array<Real, N> full_step = deriv_fn_(cur_x + 0.5 * step, y_midpoint);
94 for (int i = 0; i < 3; i++) {
95 full_step[i] *= step;
96 }
97 return full_step;
98 }
99
100 std::array<Real, N> calc_rk4_step(Real cur_x, std::array<Real, N> yvec, Real step) const noexcept
101 {
102 auto step_multiply = [=](std::array<Real, N> arg) -> std::array<Real, N> {
103 std::array<Real, N> out;
104 for (int i = 0; i < N; i++) {
105 out[i] = step * arg[i];
106 }
107 return out;
108 };
109
110 std::array<Real, N> k1 = step_multiply(deriv_fn_(cur_x, yvec));
111
112 std::array<Real, N> tmp;
113 for (int i = 0; i < N; i++) {
114 tmp[i] = yvec[i] + 0.5 * k1[i];
115 }
116 std::array<Real, N> k2 = step_multiply(deriv_fn_(cur_x + 0.5 * step, tmp));
117
118 for (int i = 0; i < N; i++) {
119 tmp[i] = yvec[i] + 0.5 * k2[i];
120 }
121 std::array<Real, N> k3 = step_multiply(deriv_fn_(cur_x + 0.5 * step, tmp));
122
123 for (int i = 0; i < N; i++) {
124 tmp[i] = yvec[i] + k3[i];
125 }
126 std::array<Real, N> k4 = step_multiply(deriv_fn_(cur_x + step, tmp));
127
128 std::array<Real, N> d_yvec{}; // uses value-initialization to initialize
129 // all elements to Real{} (aka 0.0)
130 for (int i = 0; i < N; i++) {
131 d_yvec[i] = (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) / 6.0;
132 }
133 return d_yvec;
134 }
135
136 DerivFn deriv_fn_;
137 LogFn log_fn_;
138};
139
140// tracks some basic components about the grid's z-component
142 const Real global_min;
143 const Real global_max;
144 const int global_ncells;
145 const Real cell_width;
146
147 public:
148 ZGridProps() = delete;
149
150 ZGridProps(Real global_min, Real global_width, int global_ncells)
151 : global_min(global_min),
152 global_max(global_min + global_width),
153 global_ncells(global_ncells),
154 cell_width(global_width / global_ncells)
155 {
156 CHOLLA_ASSERT((global_min < 0) and (global_width > 0),
157 "global-min must be negative & global-width must be positive");
158 Real diff = std::fabs((global_min * -2) - global_width);
159 CHOLLA_ASSERT(diff <= (4 * std::numeric_limits<Real>::epsilon() * global_width),
160 "The global-domain must be centered on z = 0");
161 CHOLLA_ASSERT((global_ncells > 0) and ((global_ncells % 2) == 0),
162 "Currently, there is only support for simulations with an integer number of cells");
163 }
164
165 bool origin_is_cell_edge_aligned() const { return true; }
166 Real left_cell_edge(int i) const { return global_min + i * cell_width; }
167 Real right_cell_edge(int i) const { return global_min + (i + 1) * cell_width; }
168};
169
170namespace selfgrav_hydrostatic_col
171{
172
173// a crude lookup table mapping the quantity names to indices
174struct LUT {
175 enum { dPhiGasZ_dz = 0, PhiGasZ = 1, posZ_unnormalized_Sigma = 2 };
176};
177
242template <typename OtherPhiFn>
244{
245 private: // attributes
246 Real alpha_;
247 Real isoth_term_;
248 Real cur_R_;
249 Real rho_midplane_;
250 OtherPhiFn other_phi_fn_;
252 Real other_phi_midplane_;
253
254 public:
255 DerivFn() = delete;
256
257 DerivFn(Real isoth_term, Real cur_R, Real rho_midplane_guess, OtherPhiFn other_phi_fn) noexcept
258 : alpha_(4.0 * M_PI * GN),
259 isoth_term_(isoth_term),
260 cur_R_(cur_R),
261 rho_midplane_(rho_midplane_guess),
262 other_phi_fn_(other_phi_fn),
263 other_phi_midplane_(other_phi_fn(cur_R, 0.0))
264 {
265 }
266
269 Real calc_PhiOtherZ(Real z) const noexcept { return other_phi_fn_(cur_R_, z) - other_phi_midplane_; }
270
273 std::array<Real, 3> operator()(Real z, std::array<Real, 3> cur_val) const noexcept
274 {
275 // compute the gravitational potential (with respect to the midplane) of
276 // the non-gas component
277 Real PhiOtherZ = calc_PhiOtherZ(z);
278 Real exp_term = std::exp(-1.0 * (cur_val[LUT::PhiGasZ] + PhiOtherZ) / isoth_term_);
279
280 std::array<Real, 3> deriv;
281 deriv[LUT::dPhiGasZ_dz] = alpha_ * rho_midplane_ * exp_term;
282 deriv[LUT::PhiGasZ] = cur_val[LUT::dPhiGasZ_dz];
283 deriv[LUT::posZ_unnormalized_Sigma] = exp_term;
284 return deriv;
285 }
286};
287
288} // namespace selfgrav_hydrostatic_col
289
313template <typename NonGasPhiFn>
314Real find_zend_(Real cur_R, ZGridProps z_grid_props, Real isoth_term, NonGasPhiFn& calc_other_phi, Real eta = 7.0)
315{
316 assert(z_grid_props.origin_is_cell_edge_aligned());
317
318 Real dz = z_grid_props.cell_width;
319 if (calc_other_phi(cur_R, dz) >= ((2.303 * isoth_term) + calc_other_phi(cur_R, 0))) {
320 std::printf(
321 "WARNING: Simulation resolution is too coarse!\n"
322 "-> At Rcyl = %e, the gas mass density drops by >= 90%% when "
323 " increasing z by a cell-width of %e\n",
324 cur_R, dz);
325 }
326 Real thresh = (eta * isoth_term) + calc_other_phi(cur_R, 0.0);
327
328 Real z = 0.0;
329 int current_offset = 0;
330 while ((calc_other_phi(cur_R, z) < thresh) and (z < z_grid_props.global_max)) {
331 current_offset++;
332 z = current_offset * dz;
333 }
334 return std::fmin(z, z_grid_props.global_max);
335}
336
337template <typename NonGasPhiFn>
339{
340 public:
342
343 SelfGravHydroStaticColMaker(int ghost_depth, ZGridProps z_grid_props, Real isoth_term, NonGasPhiFn calc_other_phi,
344 Real initial_scale_height_guess)
345 : ghost_depth(ghost_depth),
346 z_grid_props(z_grid_props),
347 isoth_term(isoth_term),
348 calc_other_phi(calc_other_phi),
349 initial_scale_height_guess(initial_scale_height_guess)
350 {
351 }
352
353 /* global total number of ghost zones along z-axis plus twice the ghost depth */
354 int buffer_len() const noexcept { return this->z_grid_props.global_ncells + 2 * ghost_depth; }
355
363 Real construct_col(Real cur_R, Real cur_Sigma, Real* buffer) const noexcept
364 {
365 for (int i = 0; i < this->ghost_depth; i++) {
366 buffer[i] = 0.0;
367 }
368 for (int i = this->z_grid_props.global_ncells + ghost_depth; i < this->buffer_len(); i++) {
369 buffer[i] = 0.0;
370 }
371 return this->construct_col_helper(cur_R, cur_Sigma, buffer + this->ghost_depth);
372 }
373
374 private: // helper methods
383 Real construct_col_helper(Real cur_R, Real cur_Sigma, Real* buffer) const noexcept;
384
385 int ghost_depth;
386 ZGridProps z_grid_props;
387 Real isoth_term;
388 NonGasPhiFn calc_other_phi;
389 Real initial_scale_height_guess;
390};
391
392template <typename NonGasPhiFn>
394 Real* buffer) const noexcept
395{
397
398 // CHOLLA_ASSERT(cur_Sigma > 0, "Surface density must be positive");
399 // STEP 0: come up with an initial guess for the midplane mass density.
400 // -> to do that, we assume mass is distributed in the vertical direction
401 // with an exponential distribution
402 // -> if we came up with something more sensible, our solution would
403 // converge faster
404 Real initial_rho_midplane_guess = cur_Sigma / (2 * initial_scale_height_guess);
405
406 // STEP 1: identify max z, zend, that we will integrate up to
407 const Real zstart = 0.0;
408 const Real zend = find_zend_(cur_R, z_grid_props, isoth_term, calc_other_phi);
409
410 // STEP 2: come up with the nominal step-size that we'll use throughout the
411 // rest of this function during integration
412 const Real nominal_step = z_grid_props.cell_width / 10.0;
413
414 // STEP 3: iteratively solve for density profile
415 // -> each time we enter the loop, we integrate over the vertical density
416 // profile given the latest guess for the midplane density profile
417 // -> During that integration, we effectively compute the unnormalized
418 // surface density for z>=0. We can use this to compute a new estimate
419 // for the midplane mass density.
420 // -> If we are satisfied by the agreement between the estimates, then we're
421 // done! Otherwise, we re-enter the loop
422 Real prev_rho_midplane_est = NAN;
423 Real latest_rho_midplane_est = initial_rho_midplane_guess;
424 Real est_abs_diff = NAN;
425 const Real TOL = 1.0e-12; // may be smaller than necessary
426 for (int i = 0; i < 100; i++) {
427 // construct a new integrator instance configured with the latest estimate
428 // for the midplane density
429 MyDerivFn deriv_fn(isoth_term, cur_R, latest_rho_midplane_est, calc_other_phi);
430 const ODEIntegrator<3, MyDerivFn> integrator(deriv_fn);
431
432 // the integrator integrates a vector yvec over independent variable x
433 // -> the independent variable, is actually just the height above the disk
434 Real xstart = zstart;
435 Real xend = zend;
436
437 // -> the entries of the vector are specified in
438 // selfgrav_hydrostatic_col::LUT
439 // -> at z = 0, they all have values of 0.0
440 std::array<Real, 3> yvec_start = {0.0, 0.0, 0.0};
441
442 // perform the integration:
443 std::array<Real, 3> yvec_end = integrator.integrate(xstart, xend, nominal_step, yvec_start);
444
445 // we can now come up with a new estimate for the midplane density:
446 prev_rho_midplane_est = latest_rho_midplane_est;
447 latest_rho_midplane_est = (cur_Sigma / (2 * yvec_end[selfgrav_hydrostatic_col::LUT::posZ_unnormalized_Sigma]));
448
449 est_abs_diff = std::fabs(prev_rho_midplane_est - latest_rho_midplane_est);
450 // printf("R = %e, midplane density est, old: %.15e, new: %.15e, rdiff: %e\n",
451 // cur_R, prev_rho_midplane_est, latest_rho_midplane_est,
452 // est_abs_diff/prev_rho_midplane_est);
453
454 if (est_abs_diff < fabs(TOL * prev_rho_midplane_est)) {
455 break;
456 }
457 }
458
459 // STEP 4: some error checking
460 if ((not std::isfinite(est_abs_diff)) or (est_abs_diff >= fabs(TOL * prev_rho_midplane_est))) {
461 CHOLLA_ERROR(
462 "vertical density profile unconverged at Rcyl = %g.\n"
463 " rho_midplane estimate used to compute profile: %e\n"
464 " rho_midplane estimate computed from profile: %e\n"
465 " The relative difference is %e",
466 cur_R, prev_rho_midplane_est, prev_rho_midplane_est, est_abs_diff / prev_rho_midplane_est);
467 }
468
469 // so I think it probably makes more sense to use prev_rho_midplane_est for
470 // the rest of this since we know the integral using it produces a consistent
471 // result (latest_rho_midplane_est is probably more accurate, but we don't
472 // know for sure)
473 const Real rho_midplane = prev_rho_midplane_est;
474
475 // STEP 5: clear contents of buffer
476 for (int i = 0; i < z_grid_props.global_ncells; i++) {
477 buffer[i] = 0.0;
478 }
479
480 // STEP 6: selectively fill in the contents of buffer
481 if (not z_grid_props.origin_is_cell_edge_aligned()) {
482 exit(1);
483 } else {
484 // we are going to integrate over the density/gravitational profiles 1 cell
485 // at a time.
486 // -> As we do that, we'll use the following logger function to accumulate
487 // changes in selfgrav_hydrostatic_col::LUT::posZ_unnormalized_Sigma.
488 // We can use these changes to determine the average mass-density in the
489 // cell.
490
491 Real inv_dz = 1.0 / z_grid_props.cell_width;
492 Real accum = 0.0;
493
494 auto log_fn = [&accum](Real z, Real cur_step, std::array<Real, 3> vec, std::array<Real, 3> v_step) {
495 accum += v_step[selfgrav_hydrostatic_col::LUT::posZ_unnormalized_Sigma];
496 };
497
498 MyDerivFn deriv_fn(isoth_term, cur_R, rho_midplane, calc_other_phi);
499 const ODEIntegrator<3, MyDerivFn, decltype(log_fn)> integrator(deriv_fn, log_fn);
500
501 // we start the integral at the midplane. The entries of the integrated
502 // vector all start out with a value of 0.
503 std::array<Real, 3> cur_vec = {0.0, 0.0, 0.0};
504
505 // Let's call each cell-width a "segment". We are going to iterate over
506 // segments
507 const int num_segments = z_grid_props.global_ncells / 2;
508 for (int seg_ind = 0; seg_ind < num_segments; seg_ind++) {
509 // clear the accumulator variable:
510 accum = 0.0;
511
512 // determine z_start and z_end for current segment (it's important that
513 // z_start of the current segment matches z_end of previous segment):
514 Real seg_z_start = seg_ind * z_grid_props.cell_width;
515 Real seg_z_end = (seg_ind + 1) * z_grid_props.cell_width;
516
517 // actually perform the integral, updating cur_vec
518 cur_vec = integrator.integrate(seg_z_start, std::fmin(seg_z_end, zend), nominal_step, cur_vec);
519
520 // compute the average mass-density in the current segment
521 Real avg_rho = rho_midplane * accum * inv_dz;
522
523 // printf("%e, ", avg_rho);
524
525 // set values above and below the disk:
526 buffer[num_segments + seg_ind] = avg_rho;
527 buffer[num_segments - (seg_ind + 1)] = avg_rho;
528
529 if (seg_z_end >= zend) {
530 break;
531 }
532 }
533 // printf("\n");
534 }
535
536 return rho_midplane;
537}
538
539#endif /* SELFGRAV_HYDROSTATIC_COL */
Definition selfgrav_hydrostatic_col.h:29
ODEIntegrator()=delete
Definition selfgrav_hydrostatic_col.h:339
Real construct_col(Real cur_R, Real cur_Sigma, Real *buffer) const noexcept
Definition selfgrav_hydrostatic_col.h:363
Definition selfgrav_hydrostatic_col.h:244
DerivFn()=delete
!< Caches the value of other_phi_fn(0.0)
Real calc_PhiOtherZ(Real z) const noexcept
Definition selfgrav_hydrostatic_col.h:269
std::array< Real, 3 > operator()(Real z, std::array< Real, 3 > cur_val) const noexcept
Definition selfgrav_hydrostatic_col.h:273
Definition selfgrav_hydrostatic_col.h:141
Definition selfgrav_hydrostatic_col.h:17
void operator()(Real x, Real cur_step, std::array< Real, N > yvec, std::array< Real, N > cur_yvec_step) const noexcept
acts as a dummy placeholder for representing a "logger function"
Definition selfgrav_hydrostatic_col.h:20
Definition selfgrav_hydrostatic_col.h:174