Page MenuHomec4science

yaml_reader.cpp
No OneTemporary

File Metadata

Created
Fri, Dec 27, 06:59

yaml_reader.cpp

/*-------------------------------------------------------------------------------
Copyright (c) 2015 F. Georget <fabieng@princeton.edu>, Princeton University
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----------------------------------------------------------------------------*/
#include "yaml_reader.hpp"
#include "reader_common.hpp"
#include "section_name.hpp"
#include <yaml-cpp/yaml.h>
#include "../utils/io/yaml.hpp"
#include <fstream>
#include "../utils/log.hpp"
#include "../utils/compat.hpp"
#define PRECISION_TEST_CHARGE 1e-6
#include <iostream>
namespace specmicp {
namespace database {
struct DataReaderYaml::DataReaderYamlElementData
{
bool check_compo {true};
std::vector<element_map> components;
bool is_init {false};
};
DataReaderYaml::DataReaderYaml():
m_elem_data(make_unique<DataReaderYamlElementData>())
{}
DataReaderYaml::~DataReaderYaml() = default;
//! \brief Constructor
DataReaderYaml::DataReaderYaml(RawDatabasePtr data, bool check_compo):
DatabaseModule(data),
m_elem_data(make_unique<DataReaderYamlElementData>())
{
m_elem_data->check_compo = check_compo;
}
//! \brief Constructor
//!
//! @param filepath string containing the path to the database
DataReaderYaml::DataReaderYaml(const std::string& filepath, bool check_compo):
DatabaseModule(),
m_elem_data(make_unique<DataReaderYamlElementData>())
{
m_elem_data->check_compo = check_compo;
parse(filepath);
}
//! \brief Constructor
//!
//! @param input input stream that contains the database
DataReaderYaml::DataReaderYaml(std::istream& input, bool check_compo):
DatabaseModule(),
m_elem_data(make_unique<DataReaderYamlElementData>())
{
m_elem_data->check_compo = check_compo;
parse(input);
}
// safe access to values
std::string obtain_label(const YAML::Node& species, const std::string& section);
void obtain_composition(const YAML::Node& species, std::map<std::string, scalar_t>& compo, const std::string& label);
scalar_t obtain_logk(const YAML::Node& species, const std::string& label);
bool is_kinetic(const YAML::Node& species,const std::string& label);
void DataReaderYaml::parse(const std::string& filepath)
{
std::ifstream datafile(filepath, std::ifstream::binary);
if (not datafile.is_open())
{
ERROR << "Database not found : " << filepath;
std::string message("Database not found : " + filepath);
throw std::invalid_argument(message);
}
data->metadata.path = filepath;
parse(datafile);
datafile.close();
}
void DataReaderYaml::parse(std::istream& input )
{
const YAML::Node root = io::parse_yaml(input);
const char* indb_main = INDB_MAIN;
io::check_mandatory_yaml_node(root, INDB_SECTION_METADATA, indb_main);
parse_metadata(root[INDB_SECTION_METADATA]);
// Basis
// ------
io::check_mandatory_yaml_node(root, INDB_SECTION_BASIS, indb_main);
parse_basis(root[INDB_SECTION_BASIS]);
// Elements
// --------
io::check_mandatory_yaml_node(root, INDB_SECTION_ELEMENTS, indb_main);
ElementList elements;
parse_elements(root[INDB_SECTION_ELEMENTS], elements);
data->elements = std::move(elements);
// Aqueous species
// ---------------
io::check_mandatory_yaml_node(root, INDB_SECTION_AQUEOUS, indb_main);
AqueousList alist;
parse_aqueous(root[INDB_SECTION_AQUEOUS], alist);
data->aqueous = std::move(alist);
// Minerals
// --------
io::check_mandatory_yaml_node(root, INDB_SECTION_MINERALS, indb_main);
MineralList minerals, minerals_kinetic;
parse_minerals(root[INDB_SECTION_MINERALS], minerals, minerals_kinetic);
data->minerals = std::move(minerals);
data->minerals_kinetic = std::move(minerals_kinetic);
// Gas
// ----
GasList glist(0, data->nb_component());
if (root[INDB_SECTION_GAS])
parse_gas(root[INDB_SECTION_GAS], glist);
else glist.set_valid();
data->gas = std::move(glist);
// Sorbed species
// --------------
SorbedList slist(0, data->nb_component());
if (root[INDB_SECTION_SORBED])
parse_sorbed(root[INDB_SECTION_SORBED], slist);
else slist.set_valid();
data->sorbed = std::move(slist);
// Compounds
// ---------
CompoundList clist(0, data->nb_component());
if (root[INDB_SECTION_COMPOUNDS])
parse_compounds(root[INDB_SECTION_COMPOUNDS], clist);
else clist.set_valid();
data->compounds = std::move(clist);
}
void DataReaderYaml::parse_metadata(const YAML::Node& root)
{
data->metadata.name =
io::get_yaml_mandatory<std::string>(root, INDB_ATTRIBUTE_NAME, INDB_SECTION_METADATA);
data->metadata.version =
io::get_yaml_mandatory<std::string>(root, INDB_ATTRIBUTE_VERSION, INDB_SECTION_METADATA);
m_elem_data->check_compo = io::get_yaml_optional<bool>(root, INDB_ATTRIBUTE_CHECKCOMPO, INDB_SECTION_METADATA, m_elem_data->check_compo);
}
void DataReaderYaml::init_elemental_composition()
{
if (m_elem_data->is_init) return;
m_elem_data->components.reserve(data->nb_component());
// Parse label to find elemental composition
for (auto id: data->range_component())
{
element_map elem_compo;
element_composition_from_label(data->get_label_component(id), elem_compo);
m_elem_data->components.emplace_back(elem_compo);
}
m_elem_data->is_init = true;
}
void DataReaderYaml::parse_basis(const YAML::Node& basis)
{
DEBUG << "Size basis : " << basis.size();
data->components = ComponentList(basis.size());
if (data->nb_component() <= 3)
throw std::invalid_argument("The basis is too small.");
index_t starting_point = 2;
for (int id: data->components.range())
{
index_t id_in_db = starting_point;
if (id_in_db >= data->nb_component())
{
throw std::invalid_argument("The basis should contain H2O and E[-].");
}
const YAML::Node& species = basis[id];
std::string label = obtain_label(species, INDB_SECTION_BASIS);
if (label == water_label) {
id_in_db = 0;
//throw invalid_database("The first component should be "+water_label+".");
}
else if (label == electron_label) {
id_in_db = 1;
//throw invalid_database("The second component should be "+electron_label+".");
}
else {
++starting_point;
}
auto is_already_in_the_database = data->components.get_id(label);
if (is_already_in_the_database != no_species)
throw invalid_database("Component : " + label + "is already in the database.");
SPAM << "Parsing component : " << label;
ComponentValues values;
values.label = label;
// activity coeeficients
values.ionic_values.charge = charge_from_label(label);
if (species[INDB_ATTRIBUTE_ACTIVITY])
{
values.ionic_values.a_debye = io::get_yaml_mandatory<scalar_t>(
species[INDB_ATTRIBUTE_ACTIVITY], INDB_ATTRIBUTE_ACTIVITY_A, label);
values.ionic_values.b_debye = io::get_yaml_mandatory<scalar_t>(
species[INDB_ATTRIBUTE_ACTIVITY], INDB_ATTRIBUTE_ACTIVITY_B, label);
}
// molar mass
values.molar_mass = io::get_yaml_mandatory<scalar_t>(
species, INDB_ATTRIBUTE_MOLARMASS, label);
data->components.set_values(id_in_db, std::move(values));
}
if (m_elem_data->check_compo) init_elemental_composition();
// after this the basis is ready to use
data->components.set_valid();
}
void DataReaderYaml::parse_aqueous(const YAML::Node& aqueous, AqueousList& alist)
{
DEBUG << "Parse aqueous species";
alist = AqueousList(aqueous.size(), data->nb_component());
//const auto size = data->nb_aqueous();
for (auto id: alist.range())
{
const YAML::Node& species = aqueous[static_cast<int>(id)];
AqueousValues values;
values.label = obtain_label(species, INDB_SECTION_AQUEOUS);
values.ionic_values.charge = charge_from_label(values.label);
if (species[INDB_ATTRIBUTE_ACTIVITY])
{
values.ionic_values.a_debye = io::get_yaml_mandatory<scalar_t>(
species[INDB_ATTRIBUTE_ACTIVITY], INDB_ATTRIBUTE_ACTIVITY_A, values.label);
values.ionic_values.b_debye = io::get_yaml_mandatory<scalar_t>(
species[INDB_ATTRIBUTE_ACTIVITY], INDB_ATTRIBUTE_ACTIVITY_B, values.label);
}
values.logk = obtain_logk(species, values.label);
alist.set_values(id, values);
// equation
std::map<std::string, scalar_t> compo;
obtain_composition(species, compo, values.label);
scalar_t test_charge {0.0};
std::map<index_t, scalar_t> to_canonicalize;
for (const auto& it: compo)
{
// component ?
const auto search = data->components.get_id(it.first);
if(search != no_species) {
alist.set_nu_ji(id, search, it.second);
test_charge += it.second*data->charge_component(search);
}
// aqueous species ?
else {
// in the document we parse ?
const auto id_aq = alist.get_id(it.first);
if (id_aq != no_species)
{
to_canonicalize.insert({id_aq, it.second});
test_charge += it.second*alist.charge(id_aq);
}
// cannot be found...
else
{
throw db_invalid_syntax("Unknown species : '" + it.first
+ "' while parsing equations for " + alist.get_label(id) +".");
}
}
}
if (std::abs(test_charge - alist.charge(id)) > PRECISION_TEST_CHARGE)
{
throw db_invalid_data("Total charge is not zero for aqueous species : '" +
alist.get_label(id) +
"' - Charge-decomposition - charge species : "+
std::to_string(test_charge - alist.charge(id) )+".");
}
for (const auto& tocanon: to_canonicalize)
{
alist.canonicalize(id, tocanon.first, tocanon.second);
}
// check composition
if (m_elem_data->check_compo)
{
if (species[INDB_ATTRIBUTE_FORMULA])
check_composition(species[INDB_ATTRIBUTE_FORMULA].as<std::string>(), alist, id);
else
check_composition(alist.get_label(id), alist, id);
}
}
// valid set of aqueous species
alist.set_valid();
}
void DataReaderYaml::parse_minerals(
const YAML::Node& minerals,
MineralList& minerals_list,
MineralList& minerals_kinetic_list
)
{
// this one is a little bit more complicated since we need to detect
// if it is a solid phase governed by equilibrium or kinetic
DEBUG << "Parse minerals";
index_t nb_mineral = minerals.size();
// find kinetic minerals
int nb_kin = 0;
for (int id=0; id<nb_mineral; ++id)
{
if (is_kinetic(minerals[id], INDB_SECTION_MINERALS)) ++nb_kin;
}
minerals_list = MineralList(nb_mineral - nb_kin, data->nb_component());
minerals_kinetic_list = MineralList(nb_kin, data->nb_component());
// id for each class of minerals
index_t id_in_eq=0;
index_t id_in_kin=0;
for (index_t id=0; id<nb_mineral; ++id)
{
const YAML::Node& species = minerals[static_cast<int>(id)];
//check if material is at equilibrium or governed by kinetics
MineralValue value;
value.label = std::move(obtain_label(species, INDB_SECTION_MINERALS));
value.logk = obtain_logk(species, value.label);
bool is_kin = is_kinetic(species, value.label);
// ###FIXME
value.molar_volume = io::get_yaml_optional<scalar_t>(species, INDB_ATTRIBUTE_MOLARVOLUME, value.label, -1);
auto& mlist = is_kin?minerals_kinetic_list:minerals_list;
auto& true_id = is_kin?id_in_kin:id_in_eq;
mlist.set_values(true_id, value);
// equation
// --------
std::map<std::string, scalar_t> compo;
obtain_composition(species, compo, value.label);
double test_charge = 0;
std::map<index_t, scalar_t> to_canonicalize;
for (const auto& it: compo)
{
auto idc = data->components.get_id(it.first);
if(idc != no_species) { // this is a component
mlist.set_nu_ji(true_id, idc, it.second);
test_charge += it.second * data->charge_component(idc);
}
else { // this is an aqueous species
auto idaq = data->aqueous.get_id(it.first);
if (idaq != no_species)
{
to_canonicalize.insert({idaq, it.second});
test_charge += it.second * data->charge_aqueous(idaq);
}
else // or something we don't know
{
throw db_invalid_syntax("Unknown species : '" + it.first
+ "' while parsing equations for " + mlist.get_label(true_id) +".");
}
}
}
if (std::abs(test_charge) > PRECISION_TEST_CHARGE)
{
throw db_invalid_data("Total charge is not zero for mineral : '"+ mlist.get_label(true_id) +
"' - total charge : "+std::to_string(test_charge)+".");
}
// canonicalise
for (const auto& tocanon: to_canonicalize)
{
mlist.canonicalize(true_id, data->aqueous, tocanon.first, tocanon.second);
}
// check composition
if (m_elem_data->check_compo)
{
if (species[INDB_ATTRIBUTE_FORMULA])
check_composition(species[INDB_ATTRIBUTE_FORMULA].as<std::string>(), mlist, true_id);
else
check_composition(mlist.get_label(true_id), mlist, true_id);
}
// update index
++true_id;
}
specmicp_assert(id_in_kin == minerals_kinetic_list.size());
specmicp_assert(id_in_eq == minerals_list.size());
// ok after that point
minerals_list.set_valid();
minerals_kinetic_list.set_valid();
}
void DataReaderYaml::parse_gas(const YAML::Node& gas, GasList& glist)
{
DEBUG << "Parse gas";
glist = GasList(gas.size(), data->nb_component());
for (auto id: glist.range())
{
const YAML::Node& species = gas[static_cast<int>(id)];
GasValues values;
values.label = obtain_label(species, INDB_SECTION_GAS);
auto is_already_in_the_database = data->gas.get_id(values.label);
if (is_already_in_the_database != no_species)
throw invalid_database("Component : " + values.label + "is already in the database.");
values.logk = obtain_logk(species, values.label);
glist.set_values(id, values);
// equation
std::map<std::string, scalar_t> compo;
obtain_composition(species, compo, values.label);
scalar_t test_charge = 0.0;
std::map<index_t, scalar_t> to_canonicalize;
for (auto it: compo)
{
const auto idc = data->components.get_id(it.first);
// component
if(idc != no_species) {
glist.set_nu_ji(id, idc, it.second);
test_charge += it.second*data->charge_component(idc);
}
// aqueous species
else {
const auto idaq = data->aqueous.get_id(it.first);
if (idaq != no_species)
{
to_canonicalize.insert({idaq, it.second});
test_charge += it.second*data->charge_aqueous(idaq);
}
else
{
throw db_invalid_syntax("Unknown species : '" + it.first +
"' while parsing equations for " + data->gas.get_label(id) +".");
}
}
}
if (std::abs(test_charge) > PRECISION_TEST_CHARGE)
{
throw db_invalid_data("Total charge is not zero for gas '"+data->gas.get_label(id)+
"' : " + std::to_string(test_charge)+".");
}
// canonicalise
for (const auto& tocanon: to_canonicalize)
{
glist.canonicalize(id, data->aqueous, tocanon.first, tocanon.second);
}
// check composition
if (m_elem_data->check_compo)
{
if (species[INDB_ATTRIBUTE_FORMULA])
check_composition(species[INDB_ATTRIBUTE_FORMULA].as<std::string>(), glist, id);
else
check_composition(glist.get_label(id), glist, id);
}
}
glist.set_valid();
}
void DataReaderYaml::parse_sorbed(const YAML::Node& sorbed, SorbedList& slist)
{
DEBUG << "Parse sorbed species";
slist = SorbedList(sorbed.size(), data->nb_component());
for (auto id: slist.range())
{
const YAML::Node& species = sorbed[static_cast<int>(id)];
SorbedValues values;
values.label = obtain_label(species, INDB_SECTION_SORBED);
auto is_already_in_the_database = slist.get_id(values.label);
if (is_already_in_the_database != no_species)
throw invalid_database("Sorbed species : " + values.label + "is already in the database.");
values.logk = obtain_logk(species, values.label);
const scalar_t nb_site_occupied = io::get_yaml_mandatory<scalar_t>
(species, INDB_ATTRIBUTE_NBSITEOCCUPIED, values.label);
if (nb_site_occupied < 0)
{
throw db_invalid_data("The number of sites occupied by a sorbed species must be positive. (Species : "
+ values.label + ", number of sites : " +std::to_string(nb_site_occupied) + ")");
}
values.sorption_site_occupied = nb_site_occupied;
slist.set_values(id, values);
std::map<std::string, scalar_t> compo;
obtain_composition(species, compo, values.label);
double test_charge = 0;
std::map<index_t, scalar_t> to_canonicalize;
for (auto it: compo)
{
const auto idc = data->components.get_id(it.first);
if(idc != no_species) {
slist.set_nu_ji(id, idc, it.second);
test_charge += it.second*data->charge_component(idc);
}
else {
const auto idaq = data->aqueous.get_id(it.first);
if (idaq != no_species)
{
to_canonicalize.insert({idaq, it.second});
test_charge += it.second*data->charge_aqueous(idaq);
}
else
{
throw db_invalid_syntax("Unknown species : '" + it.first
+ "' while parsing equations for " + slist.get_label(id) +".");
}
}
}
if (std::abs(test_charge) > PRECISION_TEST_CHARGE)
{
throw db_invalid_data("Total charge is not zero for gas '"+slist.get_label(id)+
"' : " + std::to_string(test_charge)+".");
}
// canonicalise
for (const auto& tocanon: to_canonicalize)
{
slist.canonicalize(id, data->aqueous, tocanon.first, tocanon.second);
}
}
slist.set_valid();
}
void DataReaderYaml::parse_compounds(const YAML::Node& compounds, CompoundList &clist)
{
DEBUG << "Parse compounds";
clist = CompoundList(compounds.size(), data->nb_component());
for (auto id: clist.range())
{
const YAML::Node& species = compounds[static_cast<int>(id)];
std::string label = obtain_label(species, INDB_SECTION_COMPOUNDS);
auto is_already_in_the_database = clist.get_id(label);
if (is_already_in_the_database != no_species)
throw invalid_database("Compounds : " + label + "is already in the database.");
clist.set_values(id, label);
std::map<std::string, scalar_t> compo;
obtain_composition(species, compo, label);
double test_charge = 0;
std::map<index_t, scalar_t> to_canonicalize;
for (auto it: compo)
{
const auto idc = data->components.get_id(it.first);
if(idc != no_species) {
clist.set_nu_ji(id, idc, it.second);
test_charge += it.second*data->charge_component(idc);
}
else {
const auto idaq = data->aqueous.get_id(it.first);
if (idaq != no_species)
{
to_canonicalize.insert({idaq, it.second});
test_charge += it.second*data->charge_aqueous(idaq);
}
else
{
throw db_invalid_syntax("Unknown species : '" + it.first
+ "' while parsing equations for " + clist.get_label(id) +".");
}
}
}
if (std::abs(test_charge) > PRECISION_TEST_CHARGE)
{
throw db_invalid_data("Total charge is not zero for gas '"+clist.get_label(id)+
"' : " + std::to_string(test_charge)+".");
}
// canonicalise
for (const auto& tocanon: to_canonicalize)
{
clist.canonicalize(id, data->aqueous, tocanon.first, tocanon.second);
}
// check composition
if (m_elem_data->check_compo)
{
if (species[INDB_ATTRIBUTE_FORMULA])
check_composition(species[INDB_ATTRIBUTE_FORMULA].as<std::string>(), clist, id);
else
check_composition(clist.get_label(id), clist, id);
}
}
clist.set_valid();
}
void check_element_map(const element_map& formula, const element_map& composition, const std::string& label);
void DataReaderYaml::check_composition(const std::string& formula, ReactiveSpeciesList& rlist, index_t id)
{
if (not m_elem_data->is_init) init_elemental_composition();
// parse the formula
element_map compo_from_label;
element_composition_from_label(formula, compo_from_label);
// parse the composition
element_map compo_from_compo;
for (auto idc: data->range_component())
{
if (rlist.nu_ji(id, idc) == 0.0) continue;
add_to_element_map(compo_from_compo,
m_elem_data->components[idc],
rlist.nu_ji(id, idc)
);
}
// clean the composition
for (auto it=compo_from_compo.begin(); it!=compo_from_compo.end(); )
{
if (it->first == "E" or it->second == 0.0)
it = compo_from_compo.erase(it);
else
++it;
}
// check that both map are equal
check_element_map(compo_from_label, compo_from_compo, rlist.get_label(id));
}
void check_element_map(const element_map& formula, const element_map& composition, const std::string& label)
{
for (const auto& it1: formula)
{
const auto it2 = composition.find(it1.first);
if (it2 == composition.cend())
{
throw db_invalid_data("Species '"+label+"' : element '"+
it1.first+"' is in the formula but not in the composition");
}
if (std::abs(it1.second - it2->second) > PRECISION_TEST_CHARGE)
{
throw db_invalid_data("Species '"+label+"' : element '"+
it1.first+"' has a different stoichiometry in the formula ("+
std::to_string(it1.second)+
") than in the composition ("+
std::to_string(it2->second)+").");
}
}
for (const auto& it1: composition)
{
const auto it2 = formula.find(it1.first);
if (it2 == composition.cend())
{
throw db_invalid_data("Species '"+label+"' : element '"+
it1.first+"' is in the composition but not in the formula");
}
}
}
void DataReaderYaml::parse_elements(const YAML::Node& elements, ElementList &elist)
{
if (elements.size() != data->nb_component())
throw db_invalid_data("The number of elements does not corresponds to the number of components");
for (index_t id: data->range_component())
{
const YAML::Node& elem = elements[static_cast<int>(id)];
const std::string elem_label = io::get_yaml_mandatory<std::string>(
elem, INDB_ATTRIBUTE_ELEMENT, INDB_SECTION_ELEMENTS);
const std::string comp_label = io::get_yaml_mandatory<std::string>(
elem, INDB_ATTRIBUTE_COMPONENT, elem_label);
index_t id_comp = data->get_id_component(comp_label);
if (id_comp == no_species)
{
throw db_invalid_label("'"+comp_label+"' is not a valid component");
}
elist.add_element(elem_label, id_comp);
}
}
std::string obtain_label(const YAML::Node& species, const std::string& section)
{
return io::get_yaml_mandatory<std::string>(species, INDB_ATTRIBUTE_LABEL, section);
}
void obtain_composition(const YAML::Node& species, std::map<std::string, scalar_t>& compo, const std::string& label)
{
const std::string compo_string = io::get_yaml_mandatory<std::string>(species, INDB_ATTRIBUTE_COMPOSITION, label);
parse_equation(compo_string, compo);
}
scalar_t obtain_logk(const YAML::Node& species, const std::string& label)
{
return io::get_yaml_mandatory<scalar_t>(species, INDB_ATTRIBUTE_LOGK, label);
}
bool is_kinetic(const YAML::Node& species, const std::string& label)
{
return io::get_yaml_optional<bool>(species, INDB_ATTRIBUTE_FLAG_KINETIC, label, false);
}
} //end namespace database
} //end namespace specmicp

Event Timeline