OGS
PhreeqcIO.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Copyright (c) OpenGeoSys Community (opengeosys.org)
2// SPDX-License-Identifier: BSD-3-Clause
3
4#include "PhreeqcIO.h"
5
6#include <IPhreeqc.h>
7#ifdef _OPENMP
8#include <omp.h>
9#endif
10
11#include <algorithm>
12#include <boost/algorithm/string.hpp>
13#include <cmath>
14#include <fstream>
15#include <iomanip>
16#include <mutex>
17#include <numeric>
18#include <sstream>
19
20#include "BaseLib/Algorithm.h"
22#include "BaseLib/MPI.h"
25#include "MeshLib/Mesh.h"
28#include "PhreeqcIOData/Dump.h"
32#include "PhreeqcIOData/Knobs.h"
37
38namespace ChemistryLib
39{
41{
42namespace
43{
44template <class... Ts>
45struct overloaded : Ts...
46{
47 using Ts::operator()...;
48};
49template <class... Ts>
50overloaded(Ts...) -> overloaded<Ts...>;
51
52// Helper to iterate lines in a string_view without copying
54{
55public:
56 explicit StringViewLineIterator(std::string_view const text,
57 std::size_t pos = 0)
58 : text_(text), pos_(pos)
59 {
60 }
61
62 bool getline(std::string_view& line)
63 {
64 if (pos_ >= text_.size())
65 {
66 return false;
67 }
68
69 if (const auto newline_pos = text_.find('\n', pos_);
70 newline_pos == std::string_view::npos)
71 {
72 line = text_.substr(pos_);
73 pos_ = text_.size();
74 }
75 else
76 {
77 line = text_.substr(pos_, newline_pos - pos_);
78 pos_ = newline_pos + 1;
79 }
80
81 // Remove trailing \r if present (Windows line endings)
82 if (!line.empty() && line.back() == '\r')
83 {
84 line.remove_suffix(1);
85 }
86
87 return true;
88 }
89
90 void skip(int const num_lines)
91 {
92 for (int i = 0; i < num_lines && pos_ < text_.size(); ++i)
93 {
94 const auto newline_pos = text_.find('\n', pos_);
95 pos_ = (newline_pos == std::string_view::npos) ? text_.size()
96 : newline_pos + 1;
97 }
98 }
99
100private:
101 std::string_view text_;
102 std::size_t pos_;
103};
104
105std::vector<std::string> extractItemsFromLine(std::string_view const line)
106{
107 std::vector<std::string> items;
108 std::string line_str(line);
109 boost::trim_if(line_str, boost::is_any_of("\t "));
110 boost::algorithm::split(items, line_str, boost::is_any_of("\t "),
111 boost::token_compress_on);
112 return items;
113}
114
115std::vector<double> parseAndFilterChemicalData(
116 std::string_view const line,
117 std::vector<int> const& dropped_item_ids,
118 std::size_t const chemical_system_id)
119{
120 std::vector<double> accepted_items;
121 std::vector<std::string> const items = extractItemsFromLine(line);
122 for (int item_id = 0; item_id < static_cast<int>(items.size()); ++item_id)
123 {
124 if (std::find(dropped_item_ids.begin(), dropped_item_ids.end(),
125 item_id) != dropped_item_ids.end())
126 {
127 continue;
128 }
129 double value;
130 try
131 {
132 value = std::stod(items[item_id]);
133 }
134 catch (const std::invalid_argument& e)
135 {
136 OGS_FATAL(
137 "Invalid argument. Could not convert string '{:s}' to "
138 "double for chemical system {:d}, column {:d}. "
139 "Exception '{:s}' was thrown.",
140 items[item_id], chemical_system_id + 1, item_id, e.what());
141 }
142 catch (const std::out_of_range& e)
143 {
144 OGS_FATAL(
145 "Out of range error. Could not convert string "
146 "'{:s}' to double for chemical system {:d}, column "
147 "{:d}. Exception '{:s}' was thrown.",
148 items[item_id], chemical_system_id + 1, item_id, e.what());
149 }
150 accepted_items.push_back(value);
151 }
152 return accepted_items;
153}
154
155template <typename DataBlock>
156std::ostream& operator<<(std::ostream& os,
157 std::vector<DataBlock> const& data_blocks)
158{
159 std::copy(data_blocks.begin(), data_blocks.end(),
160 std::ostream_iterator<DataBlock>(os));
161 return os;
162}
163
164template <typename Reactant>
165void initializeReactantMolality(Reactant& reactant,
166 GlobalIndexType const& chemical_system_id,
167 MaterialPropertyLib::Phase const& solid_phase,
168 MaterialPropertyLib::Phase const& liquid_phase,
169 MaterialPropertyLib::Medium const& medium,
171 double const t)
172{
173 auto const& solid_constituent = solid_phase.component(reactant.name);
174
175 if (solid_constituent.hasProperty(
177 {
178 auto const molality =
180 .template initialValue<double>(pos, t);
181
182 (*reactant.molality)[chemical_system_id] = molality;
183 (*reactant.molality_prev)[chemical_system_id] = molality;
184 }
185 else
186 {
187 auto const volume_fraction =
188 solid_constituent
190 .template initialValue<double>(pos, t);
191
192 (*reactant.volume_fraction)[chemical_system_id] = volume_fraction;
193
194 (*reactant.volume_fraction_prev)[chemical_system_id] = volume_fraction;
195
196 auto const fluid_density =
198 .template initialValue<double>(pos, t);
199
200 auto const porosity =
202 .template initialValue<double>(pos, t);
203
204 auto const molar_volume =
206 .template initialValue<double>(pos, t);
207
208 (*reactant.molality)[chemical_system_id] =
209 volume_fraction / fluid_density / porosity / molar_volume;
210
211 (*reactant.molality_prev)[chemical_system_id] =
212 (*reactant.molality)[chemical_system_id];
213 }
214}
215
216template <typename Reactant>
217void setReactantMolality(Reactant& reactant,
218 GlobalIndexType const& chemical_system_id,
219 MaterialPropertyLib::Phase const& solid_phase,
220 MaterialPropertyLib::Phase const& liquid_phase,
223 double const t, double const dt)
224{
225 auto const& solid_constituent = solid_phase.component(reactant.name);
226
227 if (solid_constituent.hasProperty(
229 {
230 (*reactant.molality_prev)[chemical_system_id] =
231 (*reactant.molality)[chemical_system_id];
232
233 return;
234 }
235
236 auto const volume_fraction =
237 (*reactant.volume_fraction)[chemical_system_id];
238
239 (*reactant.volume_fraction_prev)[chemical_system_id] =
240 (*reactant.volume_fraction)[chemical_system_id];
241
242 auto const fluid_density =
244 .template value<double>(vars, pos, t, dt);
245
246 auto const molar_volume =
248 .template value<double>(vars, pos, t, dt);
249
250 (*reactant.molality)[chemical_system_id] =
251 volume_fraction / fluid_density / vars.porosity / molar_volume;
252
253 (*reactant.molality_prev)[chemical_system_id] =
254 (*reactant.molality)[chemical_system_id];
255}
256
257template <typename Site>
259 GlobalIndexType const& chemical_system_id,
260 MaterialPropertyLib::Phase const& solid_phase,
262 double const t)
263{
264 auto const& solid_constituent = solid_phase.component(site.name);
265
266 auto const molality =
268 .template initialValue<double>(pos, t);
269
270 (*site.molality)[chemical_system_id] = molality;
271}
272
273template <typename Reactant>
274void updateReactantVolumeFraction(Reactant& reactant,
275 GlobalIndexType const& chemical_system_id,
276 MaterialPropertyLib::Medium const& medium,
278 double const porosity, double const t,
279 double const dt)
280{
281 auto const& solid_phase =
283 auto const& liquid_phase =
285
287
288 auto const liquid_density =
290 .template value<double>(vars, pos, t, dt);
291
292 auto const& solid_constituent = solid_phase.component(reactant.name);
293
294 if (solid_constituent.hasProperty(
296 {
297 return;
298 }
299
300 auto const molar_volume =
302 .template value<double>(vars, pos, t, dt);
303
304 (*reactant.volume_fraction)[chemical_system_id] +=
305 ((*reactant.molality)[chemical_system_id] -
306 (*reactant.molality_prev)[chemical_system_id]) *
307 liquid_density * porosity * molar_volume;
308}
309
310template <typename Reactant>
311void setPorosityPostReaction(Reactant& reactant,
312 GlobalIndexType const& chemical_system_id,
313 MaterialPropertyLib::Medium const& medium,
314 double& porosity)
315{
316 auto const& solid_phase =
318
319 auto const& solid_constituent = solid_phase.component(reactant.name);
320
321 if (solid_constituent.hasProperty(
323 {
324 return;
325 }
326
327 porosity -= ((*reactant.volume_fraction)[chemical_system_id] -
328 (*reactant.volume_fraction_prev)[chemical_system_id]);
329}
330
331template <typename Reactant>
333 Reactant const& reactant,
334 std::vector<GlobalIndexType> const& chemical_system_indices)
335{
336 double const sum = std::accumulate(
337 chemical_system_indices.begin(), chemical_system_indices.end(), 0.0,
338 [&](double const s, GlobalIndexType const id)
339 { return s + (*reactant.molality)[id]; });
340 return sum / chemical_system_indices.size();
341}
342} // namespace
343
344extern std::string specifyFileName(std::string const& project_file_name,
345 std::string const& file_extension);
346
349 std::string const& project_file_name,
350 std::string&& database,
351 std::unique_ptr<ChemicalSystem>&& chemical_system,
352 std::vector<ReactionRate>&& reaction_rates,
353 std::unique_ptr<UserPunch>&& user_punch,
354 std::unique_ptr<Output>&& output,
355 std::unique_ptr<Dump>&& dump,
356 Knobs&& knobs,
357 bool const use_stream_mode,
358 int const num_chemistry_threads,
359 double const concentration_warning_threshold)
361 _phreeqc_input_file(specifyFileName(project_file_name, ".inp")),
362 _database(std::move(database)),
363 _knobs(std::move(knobs)),
364 _reaction_rates(std::move(reaction_rates)),
365 _chemical_system(std::move(chemical_system)),
366 _user_punch(std::move(user_punch)),
367 _output(std::move(output)),
368 _dump(std::move(dump)),
369 _concentration_warning_threshold(concentration_warning_threshold),
370 num_chemistry_threads_(num_chemistry_threads),
371 _use_stream_mode(use_stream_mode)
372{
373 INFO("Chemistry threads per MPI rank: {}.", num_chemistry_threads_);
374
376 {
377 // Stream mode runs exclusively on the instance pool (even for a single
378 // thread) so there is one unified execution path. The standalone
379 // phreeqc_instance_id is not used here and stays -1.
381 {
382 INFO(
383 "Parallel chemistry enabled: {} threads will be used for "
384 "PHREEQC calculations.",
386 }
387 INFO(
388 "PhreeqcIO is configured for stream-based data exchange: input "
389 "and output will be exchanged via in-memory strings.");
390 instance_pool_ = std::make_unique<PhreeqcInstancePool>(
391 _database, std::max(1, num_chemistry_threads_));
392 }
393 else
394 {
395 // File mode: create and load the standalone PHREEQC instance and
396 // enable file-based selected output. PhreeqcInstancePool::
397 // createInstance() OGS_FATALs on failure, so any returned id is valid.
400 if (SetSelectedOutputFileOn(phreeqc_instance_id, 1) != IPQ_OK)
401 {
402 OGS_FATAL(
403 "Failed to fly the flag for the specified file {:s} where "
404 "phreeqc will write output.",
405 _output->basic_output_setups.output_file);
406 }
407 if (_dump)
408 {
409 SetDumpFileOn(phreeqc_instance_id, 1);
410 }
411 }
412}
413
415{
416 // The standalone instance is only created in file mode; in stream mode
417 // phreeqc_instance_id stays -1 and there is nothing to destroy.
418 if (phreeqc_instance_id >= 0)
419 {
420 DestroyIPhreeqc(phreeqc_instance_id);
421 }
422}
423
425{
427
429
430 if (_user_punch)
431 {
433 }
434}
435
437 std::vector<double> const& concentrations,
438 GlobalIndexType const& chemical_system_id,
439 MaterialPropertyLib::Medium const& medium,
441 double const t)
442{
443 setAqueousSolution(concentrations, chemical_system_id,
444 *_chemical_system->aqueous_solution,
446
447 auto const& solid_phase =
449 auto const& liquid_phase =
451
452 for (auto& kinetic_reactant : _chemical_system->kinetic_reactants)
453 {
454 initializeReactantMolality(kinetic_reactant, chemical_system_id,
455 solid_phase, liquid_phase, medium, pos, t);
456 }
457
458 for (auto& equilibrium_reactant : _chemical_system->equilibrium_reactants)
459 {
460 initializeReactantMolality(equilibrium_reactant, chemical_system_id,
461 solid_phase, liquid_phase, medium, pos, t);
462 }
463
464 for (auto& exchanger : _chemical_system->exchangers)
465 {
466 initializeSiteMolality(exchanger, chemical_system_id, solid_phase, pos,
467 t);
468 }
469
470 for (auto& surface_site : _chemical_system->surface)
471 {
472 if (auto const surface_site_ptr =
473 std::get_if<MoleBasedSurfaceSite>(&surface_site))
474 {
475 initializeSiteMolality(*surface_site_ptr, chemical_system_id,
476 solid_phase, pos, t);
477 }
478 }
479}
480
482 std::vector<double> const& concentrations,
483 GlobalIndexType const& chemical_system_id,
484 MaterialPropertyLib::Medium const* medium,
486 ParameterLib::SpatialPosition const& pos, double const t, double const dt)
487{
488 // Accumulate clamping over all chemical systems; reported in a single
489 // aggregated message before speciation (see executeSpeciationCalculation).
490 _clamping_totals += setAqueousSolution(concentrations, chemical_system_id,
491 *_chemical_system->aqueous_solution,
493
494 auto const& solid_phase =
496 auto const& liquid_phase =
498
499 for (auto& kinetic_reactant : _chemical_system->kinetic_reactants)
500 {
501 setReactantMolality(kinetic_reactant, chemical_system_id, solid_phase,
502 liquid_phase, vars, pos, t, dt);
503 }
504
505 for (auto& equilibrium_reactant : _chemical_system->equilibrium_reactants)
506 {
507 setReactantMolality(equilibrium_reactant, chemical_system_id,
508 solid_phase, liquid_phase, vars, pos, t, dt);
509 }
510}
511
513{
514 // Report the negative-concentration clamping accumulated over all chemical
515 // systems in this step as a single aggregated message, then reset the
516 // accumulator for the next step.
518 _clamping_totals = {};
519
521 {
522 // Stream mode always uses the pool (serial or parallel), giving one
523 // unified execution path. The pool was created with max(1, threads).
525 return;
526 }
527
528 // File-based data exchange.
529 DBUG("Executing speciation with file-based data exchange.");
531 callPhreeqc();
533}
534
536 int const component_id, GlobalIndexType const chemical_system_id) const
537{
538 auto const& aqueous_solution = *_chemical_system->aqueous_solution;
539 auto const& components = aqueous_solution.components;
540 auto const& H_plus_activity = aqueous_solution.H_plus_activity;
541
542 if (component_id < static_cast<int>(components.size()))
543 {
544 return components[component_id].amount[chemical_system_id];
545 }
546
547 if (component_id != static_cast<int>(components.size()))
548 {
549 OGS_FATAL(
550 "Invalid component_id {:d}: must be in [0, {:d}] "
551 "(the last index represents H+ activity).",
552 component_id, components.size());
553 }
554
555 // H+ activity 10^-pH (the transport-side state vector stores this
556 // activity for the pH "component" — see setAqueousSolution).
557 return H_plus_activity[chemical_system_id];
558}
559
561{
562 if (!_dump)
563 {
564 return;
565 }
566
567 auto const& dump_file = _dump->dump_file;
568 std::ifstream in(dump_file);
569 if (!in)
570 {
571 // return if phreeqc dump file doesn't exist. This happens in
572 // the first time step when no dump file is provided by the user.
573 return;
574 }
575
576 _dump->readDumpFile(in, _num_chemical_systems);
577
578 if (!in)
579 {
580 OGS_FATAL("Error when reading phreeqc dump file '{:s}'", dump_file);
581 }
582
583 in.close();
584}
585
587 std::string_view const dump_content)
588{
589 if (!_dump)
590 {
591 return;
592 }
593
594 if (dump_content.empty())
595 {
596 // return if dump content is empty. This happens in
597 // the first time step when no dump data is available.
598 DBUG(
599 "Dump content is empty, skipping aqueous solutions initialization "
600 "from dump.");
601 return;
602 }
603
604 _dump->readDumpFromString(dump_content, _num_chemical_systems);
605}
606
607void PhreeqcIO::writeInputsToFile(double const dt)
608{
609 DBUG("Writing phreeqc inputs into file '{:s}'.", _phreeqc_input_file);
610 std::ofstream out(_phreeqc_input_file, std::ofstream::out);
611
612 if (!out)
613 {
614 OGS_FATAL("Could not open file '{:s}' for writing phreeqc inputs.",
616 }
617
618 out << std::scientific
619 << std::setprecision(std::numeric_limits<double>::max_digits10);
620 *this << dt;
621 out << *this;
622
623 if (!out)
624 {
625 OGS_FATAL("Failed in generating phreeqc input file '{:s}'.",
627 }
628
629 out.close();
630}
631
632void PhreeqcIO::writeInputHeader(std::ostream& os) const
633{
634 bool const fixing_pe = _chemical_system->aqueous_solution->fixing_pe;
635 if (fixing_pe)
636 {
637 os << "PHASES\n"
638 << "Fix_pe\n"
639 << "e- = e-\n"
640 << "log_k 0.0\n\n";
641 }
642
643 os << _knobs << "\n";
644 os << *_output << "\n";
645
646 if (_user_punch)
647 {
648 os << *_user_punch << "\n";
649 }
650
651 if (!_reaction_rates.empty())
652 {
653 os << "RATES\n";
654 os << _reaction_rates << "\n";
655 }
656}
657
658void PhreeqcIO::writeSystemBlock(std::ostream& os,
659 std::size_t const chemical_system_id,
660 std::size_t const solution_id,
661 std::size_t const prev_solution_id,
662 double const dt) const
663{
664 bool const fixing_pe = _chemical_system->aqueous_solution->fixing_pe;
665
666 os << "SOLUTION " << solution_id << "\n";
667 _chemical_system->aqueous_solution->print(os, chemical_system_id);
668
669 if (_dump && !_dump->aqueous_solutions_prev.empty())
670 {
671 os << _dump->aqueous_solutions_prev[chemical_system_id] << "\n\n";
672 }
673
674 os << "USE solution none\n";
675 os << "END\n\n";
676
677 os << "USE solution " << solution_id << "\n\n";
678
679 auto const& equilibrium_reactants = _chemical_system->equilibrium_reactants;
680 if (!equilibrium_reactants.empty() || fixing_pe)
681 {
682 os << "EQUILIBRIUM_PHASES " << solution_id << "\n";
683 for (auto const& r : equilibrium_reactants)
684 {
685 r.print(os, chemical_system_id);
686 }
687 fixing_pe ? os << "Fix_pe " << -_chemical_system->aqueous_solution->pe0
688 << " O2(g)\n\n"
689 : os << "\n";
690 }
691
692 auto const& kinetic_reactants = _chemical_system->kinetic_reactants;
693 if (!kinetic_reactants.empty())
694 {
695 os << "KINETICS " << solution_id << "\n";
696 for (auto const& k : kinetic_reactants)
697 {
698 k.print(os, chemical_system_id);
699 }
700 os << "-steps " << dt << "\n\n";
701 }
702
703 auto const& surface = _chemical_system->surface;
704 if (!surface.empty())
705 {
706 // To get the amount of surface species from the previous time step,
707 // an equilibration calculation with the previous aqueous solution
708 // is needed. The previous aqueous solution is saved using the
709 // PHREEQC keyword "DUMP" and stored as SOLUTION_RAW within
710 // aqueous_solutions_prev. Along with the PHREEQC keyword 'SURFACE',
711 // distinguish between the current and previous solutions via
712 // prev_solution_id.
713 os << "SURFACE " << solution_id << "\n";
714 std::size_t const aq_id =
715 (_dump && !_dump->aqueous_solutions_prev.empty()) ? prev_solution_id
716 : solution_id;
717 os << "-equilibrate with solution " << aq_id << "\n";
718
719 if (std::holds_alternative<DensityBasedSurfaceSite>(surface.front()))
720 {
721 os << "-sites_units density\n";
722 }
723 else
724 {
725 os << "-sites_units absolute\n";
726 }
727
728 for (auto const& surface_site : surface)
729 {
730 std::visit(
731 overloaded{
732 [&os](DensityBasedSurfaceSite const& s)
733 {
734 os << s.name << " " << s.site_density << " "
735 << s.specific_surface_area << " " << s.mass << "\n";
736 },
737 [&os, chemical_system_id](MoleBasedSurfaceSite const& s)
738 {
739 os << s.name << " " << (*s.molality)[chemical_system_id]
740 << "\n";
741 }},
742 surface_site);
743 }
744
745 if (std::holds_alternative<MoleBasedSurfaceSite>(surface.front()))
746 {
747 os << "-no_edl\n";
748 }
749 os << "SAVE solution " << solution_id << "\n";
750 }
751
752 auto const& exchangers = _chemical_system->exchangers;
753 if (!exchangers.empty())
754 {
755 os << "EXCHANGE " << solution_id << "\n";
756 std::size_t const aq_id =
757 (_dump && !_dump->aqueous_solutions_prev.empty()) ? prev_solution_id
758 : solution_id;
759 os << "-equilibrate with solution " << aq_id << "\n";
760 for (auto const& exchanger : exchangers)
761 {
762 exchanger.print(os, chemical_system_id);
763 }
764 os << "SAVE solution " << solution_id << "\n";
765 }
766
767 os << "END\n\n";
768}
769
770void PhreeqcIO::updateSystemFromOutputLine(std::string_view const line,
771 std::size_t const chemical_system_id)
772{
773 auto const& output = *_output;
774 auto accepted_items = parseAndFilterChemicalData(
775 line, output.dropped_item_ids, chemical_system_id);
776 assert(accepted_items.size() == output.accepted_items.size());
777 updateChemicalSystemFromOutput(accepted_items, chemical_system_id);
778}
779
780std::ostream& operator<<(std::ostream& os, PhreeqcIO const& phreeqc_io)
781{
782 phreeqc_io.writeInputHeader(os);
783
784 for (std::size_t chemical_system_id = 0;
785 chemical_system_id < phreeqc_io._num_chemical_systems;
786 ++chemical_system_id)
787 {
788 std::size_t const solution_id = chemical_system_id + 1;
789 std::size_t const prev_solution_id =
790 phreeqc_io._num_chemical_systems + chemical_system_id + 1;
791 phreeqc_io.writeSystemBlock(os, chemical_system_id, solution_id,
792 prev_solution_id, phreeqc_io._dt);
793 }
794
795 if (phreeqc_io._dump)
796 {
797 phreeqc_io._dump->print(os, phreeqc_io._num_chemical_systems);
798 }
799
800 return os;
801}
802
804{
805 INFO("Phreeqc: Executing chemical calculation.");
806 if (RunFile(phreeqc_instance_id, _phreeqc_input_file.c_str()) != IPQ_OK)
807 {
808 OutputErrorString(phreeqc_instance_id);
809 OGS_FATAL(
810 "Failed in performing speciation calculation with the generated "
811 "phreeqc input file '{:s}'.",
813 }
814}
815
817{
818 auto const& basic_output_setups = _output->basic_output_setups;
819 auto const& phreeqc_result_file = basic_output_setups.output_file;
820 DBUG("Reading phreeqc results from file '{:s}'.", phreeqc_result_file);
821 std::ifstream in(phreeqc_result_file);
822
823 if (!in)
824 {
825 OGS_FATAL("Could not open phreeqc result file '{:s}'.",
826 phreeqc_result_file);
827 }
828
829 in >> *this;
830
831 if (!in)
832 {
833 OGS_FATAL("Error when reading phreeqc result file '{:s}'",
834 phreeqc_result_file);
835 }
836
837 in.close();
838}
839
841 std::vector<double> const& accepted_items, std::size_t chemical_system_id)
842{
843 auto const& output = *_output;
844 auto& aqueous_solution = _chemical_system->aqueous_solution;
845 auto& components = aqueous_solution->components;
846 auto& equilibrium_reactants = _chemical_system->equilibrium_reactants;
847 auto& kinetic_reactants = _chemical_system->kinetic_reactants;
848
849 for (int item_id = 0; item_id < static_cast<int>(accepted_items.size());
850 ++item_id)
851 {
852 auto const& accepted_item = output.accepted_items[item_id];
853 auto const& item_name = accepted_item.name;
854
855 auto compare_by_name = [&item_name](auto const& item)
856 { return item.name == item_name; };
857
858 switch (accepted_item.item_type)
859 {
860 case ItemType::pH:
861 {
862 aqueous_solution->H_plus_activity[chemical_system_id] =
863 std::pow(10, -accepted_items[item_id]);
864 break;
865 }
866 case ItemType::pe:
867 {
868 (*aqueous_solution->pe)[chemical_system_id] =
869 accepted_items[item_id];
870 break;
871 }
873 {
874 auto& component = BaseLib::findElementOrError(
875 components, compare_by_name,
876 [&]()
877 {
878 OGS_FATAL("Could not find component '{:s}'.",
879 item_name);
880 });
881 component.amount[chemical_system_id] = accepted_items[item_id];
882 break;
883 }
885 {
886 auto const& equilibrium_reactant = BaseLib::findElementOrError(
887 equilibrium_reactants, compare_by_name,
888 [&]()
889 {
890 OGS_FATAL("Could not find equilibrium reactant '{:s}'",
891 item_name);
892 });
893 (*equilibrium_reactant.molality)[chemical_system_id] =
894 accepted_items[item_id];
895 break;
896 }
898 {
899 auto const& kinetic_reactant = BaseLib::findElementOrError(
900 kinetic_reactants, compare_by_name,
901 [&]()
902 {
903 OGS_FATAL("Could not find kinetic reactant '{:s}'.",
904 item_name);
905 });
906 (*kinetic_reactant.molality)[chemical_system_id] =
907 accepted_items[item_id];
908 break;
909 }
911 {
912 assert(_user_punch);
913 auto const& secondary_variables =
914 _user_punch->secondary_variables;
915 auto const& secondary_variable = BaseLib::findElementOrError(
916 secondary_variables, compare_by_name,
917 [&]()
918 {
919 OGS_FATAL("Could not find secondary variable '{:s}'.",
920 item_name);
921 });
922 (*secondary_variable.value)[chemical_system_id] =
923 accepted_items[item_id];
924 break;
925 }
926 }
927 }
928}
929
930std::istream& operator>>(std::istream& in, PhreeqcIO& phreeqc_io)
931{
932 // Skip the headline
933 in.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
934
935 std::string line;
936
937 auto const& surface = phreeqc_io._chemical_system->surface;
938 auto const& exchangers = phreeqc_io._chemical_system->exchangers;
939
940 int const num_skipped_lines =
941 1 + (!surface.empty() ? 1 : 0) + (!exchangers.empty() ? 1 : 0);
942
943 for (std::size_t chemical_system_id = 0;
944 chemical_system_id < phreeqc_io._num_chemical_systems;
945 ++chemical_system_id)
946 {
947 for (int i = 0; i < num_skipped_lines; ++i)
948 {
949 in.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
950 }
951
952 if (!std::getline(in, line))
953 {
954 OGS_FATAL(
955 "Error when reading calculation result of Solution {:d} "
956 "after the reaction.",
957 chemical_system_id);
958 }
959
960 phreeqc_io.updateSystemFromOutputLine(line, chemical_system_id);
961 }
962
963 return in;
964}
965
966std::vector<std::string> const PhreeqcIO::getComponentList() const
967{
968 std::vector<std::string> component_names;
969 auto const& components = _chemical_system->aqueous_solution->components;
970 std::transform(components.begin(), components.end(),
971 std::back_inserter(component_names),
972 [](auto const& c) { return c.name; });
973
974 component_names.emplace_back("H");
975
976 return component_names;
977}
978
980 GlobalIndexType const& chemical_system_id,
981 MaterialPropertyLib::Medium const& medium,
982 ParameterLib::SpatialPosition const& pos, double const porosity,
983 double const t, double const dt)
984{
985 for (auto& kinetic_reactant : _chemical_system->kinetic_reactants)
986 {
987 updateReactantVolumeFraction(kinetic_reactant, chemical_system_id,
988 medium, pos, porosity, t, dt);
989 }
990
991 for (auto& equilibrium_reactant : _chemical_system->equilibrium_reactants)
992 {
993 updateReactantVolumeFraction(equilibrium_reactant, chemical_system_id,
994 medium, pos, porosity, t, dt);
995 }
996}
997
999 GlobalIndexType const& chemical_system_id,
1000 MaterialPropertyLib::Medium const& medium,
1001 double& porosity)
1002{
1003 for (auto& kinetic_reactant : _chemical_system->kinetic_reactants)
1004 {
1005 setPorosityPostReaction(kinetic_reactant, chemical_system_id, medium,
1006 porosity);
1007 }
1008
1009 for (auto& equilibrium_reactant : _chemical_system->equilibrium_reactants)
1010 {
1011 setPorosityPostReaction(equilibrium_reactant, chemical_system_id,
1012 medium, porosity);
1013 }
1014}
1015
1017 std::size_t const ele_id,
1018 std::vector<GlobalIndexType> const& chemical_system_indices)
1019{
1020 for (auto const& kinetic_reactant : _chemical_system->kinetic_reactants)
1021 {
1022 (*kinetic_reactant.mesh_prop_molality)[ele_id] =
1023 averageReactantMolality(kinetic_reactant, chemical_system_indices);
1024 }
1025
1026 for (auto const& equilibrium_reactant :
1027 _chemical_system->equilibrium_reactants)
1028 {
1029 (*equilibrium_reactant.mesh_prop_molality)[ele_id] =
1030 averageReactantMolality(equilibrium_reactant,
1031 chemical_system_indices);
1032 }
1033}
1034
1036 std::size_t const chemical_system_id, double const dt) const
1037{
1038 std::ostringstream os;
1039 os << std::scientific
1040 << std::setprecision(std::numeric_limits<double>::max_digits10);
1041
1042 writeInputHeader(os);
1043
1044 // Each pool instance processes exactly one system with solution ID 1.
1045 // The prev_solution_id follows the same remapping convention as in file
1046 // mode: num_chemical_systems + chemical_system_id + 1.
1047 std::size_t const prev_solution_id =
1048 _num_chemical_systems + chemical_system_id + 1;
1049 writeSystemBlock(os, chemical_system_id, 1, prev_solution_id, dt);
1050
1051 // Each pool instance dumps its single solution (ID 1) for the next
1052 // timestep's dump-restore cycle.
1053 if (_dump)
1054 {
1055 os << "DUMP\n";
1056 os << "-solution 1\n";
1057 os << "END\n";
1058 }
1059
1060 return os.str();
1061}
1062
1063void PhreeqcIO::parseOutputForSystem(std::string_view const output_content,
1064 std::size_t const chemical_system_id)
1065{
1066 if (output_content.empty())
1067 {
1068 OGS_FATAL("Empty output for chemical system {}.", chemical_system_id);
1069 }
1070
1071 StringViewLineIterator line_iter(output_content);
1072 std::string_view line;
1073
1074 line_iter.getline(line); // skip headline
1075
1076 int const num_skipped_lines =
1077 1 + (!_chemical_system->surface.empty() ? 1 : 0) +
1078 (!_chemical_system->exchangers.empty() ? 1 : 0);
1079 line_iter.skip(num_skipped_lines);
1080
1081 if (!line_iter.getline(line))
1082 {
1083 OGS_FATAL(
1084 "Error when reading calculation result of Solution {} after the "
1085 "reaction.",
1086 chemical_system_id);
1087 }
1088
1089 updateSystemFromOutputLine(line, chemical_system_id);
1090}
1091
1093{
1094 INFO("Phreeqc: Executing parallel chemical calculation with {} threads.",
1096
1097 // 1. Pre-generate inputs for all chemical systems (sequential)
1098 // This must be sequential because it reads from shared data structures
1099 std::vector<std::string> inputs(_num_chemical_systems);
1100 for (std::size_t i = 0; i < _num_chemical_systems; ++i)
1101 {
1102 inputs[i] = generateInputForSystem(i, dt);
1103 }
1104
1105 // 2. Storage for outputs
1106 std::vector<std::string> outputs(_num_chemical_systems);
1107 std::vector<std::string> dumps(_num_chemical_systems);
1108
1109 // 3. Parallel execution
1110 std::vector<std::size_t> failed_systems;
1111 std::mutex failed_mutex;
1112
1113#pragma omp parallel num_threads(num_chemistry_threads_)
1114 {
1115#ifdef _OPENMP
1116 int const thread_id = omp_get_thread_num();
1117#else
1118 int const thread_id = 0;
1119#endif
1120 int const phreeqc_id = instance_pool_->getInstanceForThread(thread_id);
1121
1122#pragma omp for schedule(dynamic)
1123 for (std::ptrdiff_t i = 0;
1124 i < static_cast<std::ptrdiff_t>(_num_chemical_systems);
1125 ++i)
1126 {
1127 if (RunString(phreeqc_id, inputs[i].c_str()) != IPQ_OK)
1128 {
1129 OutputErrorString(phreeqc_id);
1130 std::lock_guard<std::mutex> guard(failed_mutex);
1131 failed_systems.push_back(i);
1132 continue;
1133 }
1134
1135 // Retrieve output string
1136 const char* output_ptr = GetSelectedOutputString(phreeqc_id);
1137 if (output_ptr)
1138 {
1139 outputs[i] = output_ptr;
1140 }
1141
1142 // Retrieve dump string if needed
1143 if (_dump)
1144 {
1145 const char* dump_ptr = GetDumpString(phreeqc_id);
1146 if (dump_ptr)
1147 {
1148 dumps[i] = dump_ptr;
1149 }
1150 }
1151 }
1152 }
1153
1154 std::exception_ptr local_error;
1155 if (!failed_systems.empty())
1156 {
1157 std::sort(failed_systems.begin(), failed_systems.end());
1158 std::string ids;
1159 for (auto const id : failed_systems)
1160 {
1161 if (!ids.empty())
1162 {
1163 ids += ", ";
1164 }
1165 ids += std::to_string(id);
1166 }
1167 local_error = std::make_exception_ptr(std::runtime_error(
1168 "Failed in performing speciation calculation for "
1169 "chemical system(s) " +
1170 ids + "."));
1171 }
1173
1174 // 4. Parse results (sequential, updates shared state)
1175 for (std::size_t i = 0; i < _num_chemical_systems; ++i)
1176 {
1177 parseOutputForSystem(outputs[i], i);
1178 }
1179
1180 // 5. Handle dump data if surfaces/exchangers exist.
1181 // Each pool instance dumped solution 1; remap to the correct system ID.
1182 if (_dump && !dumps.empty())
1183 {
1184 _dump->aqueous_solutions_prev.resize(_num_chemical_systems);
1185 for (std::size_t i = 0; i < _num_chemical_systems; ++i)
1186 {
1187 if (!dumps[i].empty())
1188 {
1189 _dump->readDumpFromStringForSystem(dumps[i], i,
1191 }
1192 }
1193 }
1194}
1195} // namespace PhreeqcIOData
1196} // namespace ChemistryLib
Definition of one reactive chemical system for PHREEQC coupling.
#define OGS_FATAL(...)
Definition Error.h:10
MathLib::EigenLisLinearSolver GlobalLinearSolver
GlobalMatrix::IndexType GlobalIndexType
void INFO(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:28
void DBUG(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:22
Per-system aqueous state exchanged with PHREEQC.
PHREEQC-backed ChemicalSolverInterface implementation.
std::vector< GlobalIndexType > chemical_system_index_map
ChemicalSolverInterface(MeshLib::Mesh const &mesh, GlobalLinearSolver &linear_solver_)
void parseOutputForSystem(std::string_view output_content, std::size_t chemical_system_id)
std::string generateInputForSystem(std::size_t chemical_system_id, double const dt) const
void writeSystemBlock(std::ostream &os, std::size_t chemical_system_id, std::size_t solution_id, std::size_t prev_solution_id, double dt) const
void updateSystemFromOutputLine(std::string_view line, std::size_t chemical_system_id)
double getConcentration(int const component_id, GlobalIndexType const chemical_system_id) const override
void setAqueousSolutionsPrevFromDumpFile() override
std::unique_ptr< ChemicalSystem > _chemical_system
Definition PhreeqcIO.h:246
std::vector< ReactionRate > const _reaction_rates
Definition PhreeqcIO.h:245
std::unique_ptr< UserPunch > _user_punch
Definition PhreeqcIO.h:247
std::unique_ptr< PhreeqcInstancePool > instance_pool_
Definition PhreeqcIO.h:250
void writeInputsToFile(double const dt)
void setChemicalSystemConcrete(std::vector< double > const &concentrations, GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Medium const *medium, MaterialPropertyLib::VariableArray const &vars, ParameterLib::SpatialPosition const &pos, double const t, double const dt) override
PhreeqcIO(MeshLib::Mesh const &mesh, GlobalLinearSolver &linear_solver, std::string const &project_file_name, std::string &&database, std::unique_ptr< ChemicalSystem > &&chemical_system, std::vector< ReactionRate > &&reaction_rates, std::unique_ptr< UserPunch > &&user_punch, std::unique_ptr< Output > &&output, std::unique_ptr< Dump > &&dump, Knobs &&knobs, bool use_stream_mode, int num_chemistry_threads, double concentration_warning_threshold)
void executeSpeciationCalculationParallel(double const dt)
std::unique_ptr< Dump > const _dump
Definition PhreeqcIO.h:249
std::unique_ptr< Output > const _output
Definition PhreeqcIO.h:248
void computeSecondaryVariable(std::size_t const ele_id, std::vector< GlobalIndexType > const &chemical_system_indices) override
void updateChemicalSystemFromOutput(std::vector< double > const &accepted_items, std::size_t chemical_system_id)
void writeInputHeader(std::ostream &os) const
void initializeChemicalSystemConcrete(std::vector< double > const &concentrations, GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Medium const &medium, ParameterLib::SpatialPosition const &pos, double const t) override
void updateVolumeFractionPostReaction(GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Medium const &medium, ParameterLib::SpatialPosition const &pos, double const porosity, double const t, double const dt) override
void executeSpeciationCalculation(double const dt) override
void setAqueousSolutionsPrevFromDumpString(std::string_view dump_content)
std::vector< std::string > const getComponentList() const override
void updatePorosityPostReaction(GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Medium const &medium, double &porosity) override
static int createInstance(std::string const &database)
Phase const & phase(std::size_t index) const
Definition Medium.cpp:24
Component const & component(std::size_t const &index) const
Definition Phase.cpp:61
void allRanksThrowOrNone(std::exception_ptr const &exception, auto &&warning_callback)
Definition MPI.h:236
ranges::range_reference_t< Range > findElementOrError(Range &range, std::predicate< ranges::range_reference_t< Range > > auto &&predicate, std::invocable auto error_callback)
Definition Algorithm.h:75
void initializeSiteMolality(Site &site, GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Phase const &solid_phase, ParameterLib::SpatialPosition const &pos, double const t)
void initializeReactantMolality(Reactant &reactant, GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Phase const &solid_phase, MaterialPropertyLib::Phase const &liquid_phase, MaterialPropertyLib::Medium const &medium, ParameterLib::SpatialPosition const &pos, double const t)
std::vector< double > parseAndFilterChemicalData(std::string_view const line, std::vector< int > const &dropped_item_ids, std::size_t const chemical_system_id)
void setPorosityPostReaction(Reactant &reactant, GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Medium const &medium, double &porosity)
std::vector< std::string > extractItemsFromLine(std::string_view const line)
void setReactantMolality(Reactant &reactant, GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Phase const &solid_phase, MaterialPropertyLib::Phase const &liquid_phase, MaterialPropertyLib::VariableArray const &vars, ParameterLib::SpatialPosition const &pos, double const t, double const dt)
static double averageReactantMolality(Reactant const &reactant, std::vector< GlobalIndexType > const &chemical_system_indices)
void updateReactantVolumeFraction(Reactant &reactant, GlobalIndexType const &chemical_system_id, MaterialPropertyLib::Medium const &medium, ParameterLib::SpatialPosition const &pos, double const porosity, double const t, double const dt)
std::ostream & operator<<(std::ostream &os, PhreeqcIO const &phreeqc_io)
std::string specifyFileName(std::string const &project_file_name, std::string const &file_extension)
ClampingStats setAqueousSolution(std::vector< double > const &concentrations, std::size_t const chemical_system_id, AqueousSolution &aqueous_solution, double const warning_threshold)
std::istream & operator>>(std::istream &in, PhreeqcIO &phreeqc_io)