151 const std::size_t n_points = state.
rho.size();
152 const std::size_t n_species = state.
n_species;
155 throw std::invalid_argument(
156 "hydroStep: needs at least 2 radial nodes, got " +
157 std::to_string(n_points) +
158 "; a single node has no face to carry a flux, and no node "
159 "spacing to place the ghost cells one beyond each end.");
163 throw std::invalid_argument(
"hydroStep: the state has no species.");
165 requireSize(state.
u_r, n_points,
"state.u_r");
166 requireSize(state.
Tg, n_points,
"state.Tg");
167 requireSize(state.
Te, n_points,
"state.Te");
168 requireSize(state.
Y, n_points * n_species,
"state.Y");
169 requireSize(cfg.
r_node, n_points,
"cfg.r_node");
170 requireSize(cfg.
r_face, n_points + 1,
"cfg.r_face");
171 requireSize(cfg.
V_cell, n_points,
"cfg.V_cell");
172 requireFinite(state.
rho,
"state.rho");
173 requireFinite(state.
u_r,
"state.u_r");
174 requireFinite(state.
Tg,
"state.Tg");
175 requireFinite(state.
Te,
"state.Te");
176 requireFinite(state.
Y,
"state.Y");
177 requireFinite(cfg.
r_node,
"cfg.r_node");
178 requireFinite(cfg.
r_face,
"cfg.r_face");
179 requireFinite(cfg.
V_cell,
"cfg.V_cell");
180 requireFinite(cfg.
Y_amb,
"cfg.Y_amb");
181 requireFinite(cfg.
p_amb,
"cfg.p_amb");
182 requireFinite(cfg.
T_amb,
"cfg.T_amb");
183 requireFinite(cfg.
t0,
"cfg.t0");
184 requireFinite(cfg.
dt_total,
"cfg.dt_total");
185 requireFinite(cfg.
cfl,
"cfg.cfl");
186 if (cfg.
thermo ==
nullptr)
188 throw std::invalid_argument(
"hydroStep: cfg.thermo is null.");
192 throw std::invalid_argument(
"hydroStep: dt_total must be >= 0.");
194 if (!(cfg.
cfl > 0.0 && cfg.
cfl <= 1.0))
196 throw std::invalid_argument(
197 "hydroStep: cfl must be in (0, 1], got " + std::to_string(cfg.
cfl));
201 throw std::invalid_argument(
202 "hydroStep: electron_index must be -1 or a valid species index.");
204 for (std::size_t i = 0; i < n_points; i++)
206 if (!(cfg.
V_cell[i] > 0.0))
208 throw std::invalid_argument(
209 "hydroStep: V_cell[" + std::to_string(i) +
"] must be > 0.");
213 throw std::invalid_argument(
214 "hydroStep: r_face must be non-decreasing, but r_face[" +
215 std::to_string(i + 1) +
"] < r_face[" + std::to_string(i) +
216 "]. Equal face radii are the planar entry point, where every "
217 "face carries unit area.");
219 if (!(cfg.
r_face[i] >= 0.0))
221 throw std::invalid_argument(
222 "hydroStep: r_face[" + std::to_string(i) +
"] must be >= 0.");
225 const bool inner_face_has_area = cfg.
r_face[0] > 0.0;
228 throw std::invalid_argument(
229 "hydroStep: r_face[0] > 0, so the inner face has real area and needs "
230 "a boundary state, but no ghost_state callback was given. "
231 "outer_bc_mode governs the outer face only.");
240 throw std::invalid_argument(
241 "hydroStep: outer_bc_mode needs p_amb > 0 and T_amb > 0, got "
243 std::to_string(cfg.
p_amb) +
" Pa, T_amb = " +
244 std::to_string(cfg.
T_amb) +
245 " K. A zero closes the ghost "
246 "density at zero, which is a vacuum wall rather than an error.");
250 requireSize(cfg.
Y_amb, n_species,
"cfg.Y_amb");
251 requireUnitSum(cfg.
Y_amb,
"cfg.Y_amb");
256 const std::size_t electron =
273 std::vector<double> face_span(n_points, 0.0);
274 std::vector<double> cell_width(n_points, 0.0);
275 for (std::size_t i = 0; i < n_points; i++)
278 const double mean_face_area =
280 if (!(mean_face_area > 0.0))
282 throw std::invalid_argument(
283 "hydroStep: cell " + std::to_string(i) +
284 " has zero mean face area, so no CFL length can be derived.");
286 cell_width[i] = cfg.
V_cell[i] / mean_face_area;
294 work.resize(n_points + 2, n_species);
295 const std::size_t n_faces = n_points + 1;
296 std::vector<double> flux_rho(n_faces, 0.0);
297 std::vector<double> flux_mom(n_faces, 0.0);
298 std::vector<double> flux_Y(n_faces * n_species, 0.0);
299 std::vector<double> flux_total_energy(n_faces, 0.0);
300 std::vector<double> flux_electron_energy(n_faces, 0.0);
301 std::vector<double> velocity_flux(n_faces, 0.0);
306 result.
rho_min = std::numeric_limits<double>::infinity();
307 result.
p_min = std::numeric_limits<double>::infinity();
312 for (std::size_t i = 0; i < n_points; i++)
314 const std::size_t c = i + 1;
315 work.rho[c] = state.
rho[i];
316 work.u_r[c] = state.
u_r[i];
317 work.Tg[c] = state.
Tg[i];
318 work.Te[c] = state.
Te[i];
319 for (std::size_t k = 0; k < n_species; k++)
321 work.Y[c * n_species + k] = state.
Y[i * n_species + k];
340 const double inner_spacing = cfg.
r_node[1] - cfg.
r_node[0];
341 const double outer_spacing =
346 const double r_ghost_inner =
347 inner_face_has_area ? cfg.
r_node[0] - inner_spacing : cfg.
r_node[0];
348 const double r_ghost_outer = cfg.
r_node[n_points - 1] + outer_spacing;
366 throw std::invalid_argument(
367 "hydroStep: the inner ghost cell lands at r = " +
368 std::to_string(r_ghost_inner) +
369 " m, past the axis. The annulus inner radius (" +
370 std::to_string(cfg.
r_node[0]) +
371 " m) is smaller than its first node spacing (" +
372 std::to_string(inner_spacing) +
373 " m), so there is no radius a ghost one spacing inside the "
374 "first node could occupy; use more nodes or a larger r_min.");
377 auto cellRadius = [&](std::size_t c)
380 return r_ghost_inner;
381 if (c == n_points + 1)
382 return r_ghost_outer;
396 auto writeGhost = [&](std::size_t c,
const NodeState &ghost)
398 if (ghost.Y.size() != n_species)
400 throw std::invalid_argument(
401 "hydroStep: a ghost state carries " +
402 std::to_string(ghost.Y.size()) +
403 " mass fractions, expected " + std::to_string(n_species) +
".");
405 work.rho[c] = ghost.rho;
406 work.u_r[c] = ghost.u_r;
407 work.Tg[c] = ghost.Tg;
408 work.Te[c] = ghost.Te;
409 for (std::size_t k = 0; k < n_species; k++)
411 work.Y[c * n_species + k] = ghost.Y[k];
415 auto cellAsNodeState = [&](std::size_t c)
418 node.
rho = work.rho[c];
419 node.
u_r = work.u_r[c];
420 node.
Tg = work.Tg[c];
421 node.
Te = work.Te[c];
422 const auto begin = work.Y.begin() +
static_cast<std::ptrdiff_t
>(c * n_species);
423 node.
Y.assign(begin, begin +
static_cast<std::ptrdiff_t
>(n_species));
438 ambient_ghost.
u_r = 0.0;
441 ambient_ghost.
Y = cfg.
Y_amb;
448 const std::size_t outer = n_points + 1;
460 auto checkGhost = [&](
const NodeState &ghost)
462 requireSize(ghost.Y, n_species,
"a ghost_state callback's Y");
463 requireUnitSum(ghost.Y,
"a ghost_state callback's Y");
468 requireFinite(ghost.rho,
"a ghost_state callback's rho");
469 requireFinite(ghost.u_r,
"a ghost_state callback's u_r");
470 requireFinite(ghost.Tg,
"a ghost_state callback's Tg");
471 requireFinite(ghost.Te,
"a ghost_state callback's Te");
473 if (inner_face_has_area)
481 cfg.
ghost_state(r_ghost_inner, t,
true, cellAsNodeState(1));
483 writeGhost(0, inner);
486 r_ghost_outer, t,
false, cellAsNodeState(n_points));
487 checkGhost(outer_ghost);
488 writeGhost(outer, outer_ghost);
492 writeGhost(outer, ambient_ghost);
504 NodeState ghost = cellAsNodeState(n_points);
506 cfg.
p_amb, ghost.
Tg, ghost.
Te, ghost.
Y.data());
507 writeGhost(outer, ghost);
509 if (!inner_face_has_area)
519 for (std::size_t c = 0; c < work.n_cells; c++)
521 const double *Y_c = &work.Y[c * n_species];
522 const double r_c = cellRadius(c);
526 r_c, t, work.rho[c], work.Tg[c], work.Te[c], Y_c);
527 work.p_heavy[c] = properties.
p_heavy;
529 work.p_total[c] = work.p_heavy[c] + work.p_electron[c];
535 result.
p_min = std::min(result.
p_min, work.p_total[c]);
540 work.U_rho[c] = work.rho[c];
541 work.U_mom[c] = work.rho[c] * work.u_r[c];
542 for (std::size_t k = 0; k < n_species; k++)
544 work.U_Y[c * n_species + k] = work.rho[c] * Y_c[k];
554 const double Y_electron = has_electron ? Y_c[electron] : 0.0;
555 const double rho_electron = work.rho[c] * Y_electron;
556 work.U_electron_energy[c] = rho_electron * cv_electron * work.Te[c];
557 work.U_total_energy[c] =
559 work.U_electron_energy[c] +
560 0.5 * work.rho[c] * work.u_r[c] * work.u_r[c];
575 double dt_cfl = std::numeric_limits<double>::infinity();
576 for (std::size_t i = 0; i < n_points; i++)
578 const std::size_t c = i + 1;
579 const double speed = std::max(
581 work.u_r[c], work.sound_speed[c]),
583 work.u_r[c + 1], work.sound_speed[c + 1]));
586 dt_cfl = std::min(dt_cfl, cell_width[i] / speed);
597 for (std::size_t c = 1; c <= n_points; c++)
600 result.
p_min = std::min(result.
p_min, work.p_total[c]);
613 throw std::invalid_argument(
614 "hydroStep: the pinned sub-step " +
significant(dt) +
615 " s exceeds the CFL bound " +
significant(dt_cfl) +
616 " s at this state, where the explicit update is unstable. "
617 "Pin a smaller hydro_substep.");
622 if (!std::isfinite(dt_cfl))
624 throw std::invalid_argument(
625 "hydroStep: every wave speed is zero, so no CFL step can be "
626 "derived; pass n_subcycles explicitly.");
628 dt = cfg.
cfl * dt_cfl;
634 throw std::invalid_argument(
635 "hydroStep: the derived sub-step is not positive.");
641 const double u_wall = work.u_r[n_points];
642 const double c_wall = work.sound_speed[n_points];
651 constexpr double inflow_mach_threshold = 1.0e-3;
652 if (u_wall < -inflow_mach_threshold * c_wall || u_wall >= c_wall)
654 throw std::invalid_argument(
655 "hydroStep: outer_bc_mode=pressure_outlet assumes SUBSONIC "
656 "OUTFLOW (0 <= u_r < c) at R_max, but u_r = " +
657 std::to_string(u_wall) +
" m/s against c = " +
658 std::to_string(c_wall) +
659 " m/s. Inflow, supersonic outflow and supersonic inflow "
660 "each change the characteristic count in a different "
661 "direction and need their own treatment.");
666 for (std::size_t f = 0; f < n_faces; f++)
668 const std::size_t left = f;
669 const std::size_t right = f + 1;
670 const double r_f = cfg.
r_face[f];
672 work.u_r[left], work.sound_speed[left], work.u_r[right],
673 work.sound_speed[right]);
678 r_f, work.U_rho[left], work.U_rho[right], work.u_r[left],
679 work.u_r[right], wave);
681 r_f, work.rho[left], work.u_r[left], work.p_total[left],
682 work.rho[right], work.u_r[right], work.p_total[right], wave);
683 for (std::size_t k = 0; k < n_species; k++)
686 r_f, work.U_Y[left * n_species + k],
687 work.U_Y[right * n_species + k], work.u_r[left],
688 work.u_r[right], wave);
691 r_f, work.U_total_energy[left], work.p_total[left],
692 work.u_r[left], work.U_total_energy[right],
693 work.p_total[right], work.u_r[right], wave);
695 r_f, work.U_electron_energy[left],
696 work.U_electron_energy[right], work.u_r[left],
697 work.u_r[right], wave);
706 for (std::size_t i = 0; i < n_points; i++)
708 const std::size_t c = i + 1;
709 const double inv_volume = 1.0 / cfg.
V_cell[i];
711 work.U_rho[c] -= dt * (flux_rho[i + 1] - flux_rho[i]) * inv_volume;
713 double momentum_rate = -(flux_mom[i + 1] - flux_mom[i]) * inv_volume;
721 work.U_mom[c] += dt * momentum_rate;
723 for (std::size_t k = 0; k < n_species; k++)
725 work.U_Y[c * n_species + k] -=
726 dt * (flux_Y[(i + 1) * n_species + k] - flux_Y[i * n_species + k]) * inv_volume;
734 work.U_total_energy[c] -=
735 dt * (flux_total_energy[i + 1] - flux_total_energy[i]) * inv_volume;
743 const double divergence =
744 (velocity_flux[i + 1] - velocity_flux[i]) * inv_volume;
745 work.U_electron_energy[c] +=
746 dt * (-(flux_electron_energy[i + 1] - flux_electron_energy[i]) * inv_volume -
747 work.p_electron[c] * divergence);
749 dt * work.p_electron[c] * divergence * cfg.
V_cell[i];
756 for (std::size_t k = 0; k < n_species; k++)
759 dt * (flux_Y[n_points * n_species + k] - flux_Y[k]);
762 dt * (flux_total_energy[n_points] - flux_total_energy[0]);
764 dt * (flux_electron_energy[n_points] - flux_electron_energy[0]);
767 for (std::size_t i = 0; i < n_points; i++)
769 const std::size_t c = i + 1;
774 const double rho = work.U_rho[c];
777 throw std::runtime_error(
778 "hydroStep: density went non-positive at node " +
781 std::to_string(rho) +
782 " kg/m^3). Lower cfl or refine the grid.");
787 const double carried_kinetic = 0.5 * work.U_mom[c] * work.U_mom[c] / rho;
790 double *Y_c = &work.Y[c * n_species];
791 for (std::size_t k = 0; k < n_species; k++)
796 Y_c[k] = work.U_Y[c * n_species + k] / rho;
801 const double Y_electron = has_electron ? Y_c[electron] : 0.0;
802 const double rho_electron = rho * Y_electron;
803 if (has_electron && !(rho_electron > 0.0))
813 throw std::runtime_error(
814 "hydroStep: node " + std::to_string(i) +
815 " has a non-positive electron density (rho_e = " +
816 std::to_string(rho_electron) +
817 " kg/m^3), so its electron temperature is undefined. "
818 "Either the node started without electrons (seed a positive "
819 "electron fraction at every node) or the reaction sub-step "
820 "returned a negative electron mass fraction.");
831 work.U_electron_energy[c] / (rho_electron * cv_electron);
838 const double internal_heavy =
839 work.U_total_energy[c] - carried_kinetic -
840 rho_electron * cv_electron * work.Te[c];
843 rho, Y_c, internal_heavy / rho, work.Tg[c]);
846 work.u_r[c] = work.U_mom[c] / rho;
854 if (!std::isfinite(rho) || !std::isfinite(work.u_r[c]) ||
855 !std::isfinite(work.Tg[c]) || !std::isfinite(work.Te[c]))
857 throw std::runtime_error(
858 "hydroStep: node " + std::to_string(i) +
859 " went non-finite on sub-cycle " +
861 std::to_string(rho) +
" kg/m^3, u_r = " +
862 std::to_string(work.u_r[c]) +
" m/s, Tg = " +
863 std::to_string(work.Tg[c]) +
" K, Te = " +
864 std::to_string(work.Te[c]) +
" K).");
875 if (time_left < 1.0e-12 * cfg.
dt_total ||
880 if (!(time_left > 0.0))
890 for (std::size_t i = 0; i < n_points; i++)
892 const std::size_t c = i + 1;
893 double p_heavy = 0.0;
894 double p_electron = 0.0;
896 work.Te[c], &work.Y[c * n_species], p_heavy,
898 result.
p_min = std::min(result.
p_min, p_heavy + p_electron);
903 for (std::size_t i = 0; i < n_points; i++)
905 const std::size_t c = i + 1;
906 state.
rho[i] = work.rho[c];
907 state.
u_r[i] = work.u_r[c];
908 state.
Tg[i] = work.Tg[c];
909 state.
Te[i] = work.Te[c];
910 for (std::size_t k = 0; k < n_species; k++)
912 state.
Y[i * n_species + k] = work.Y[c * n_species + k];