diff --git a/libsrc/pylith/Makefile.am b/libsrc/pylith/Makefile.am index 5fdcffa44b..fb006649d3 100644 --- a/libsrc/pylith/Makefile.am +++ b/libsrc/pylith/Makefile.am @@ -148,7 +148,6 @@ libpylith_la_SOURCES = \ sources/RickerWavelet.cc \ sources/Source.cc \ sources/SourceTimeFunctionMomentTensorForce.cc \ - sources/SourceTimeFunctionPointForce.cc \ sources/SquarePulseSource.cc \ sources/WellboreSource.cc \ topology/Mesh.cc \ diff --git a/libsrc/pylith/sources/Makefile.am b/libsrc/pylith/sources/Makefile.am index f51f3b46ce..9af94918f1 100644 --- a/libsrc/pylith/sources/Makefile.am +++ b/libsrc/pylith/sources/Makefile.am @@ -35,7 +35,6 @@ subpkginclude_HEADERS = \ PointForce.hh \ AuxiliaryFactoryPointForce.hh \ SourceTimeFunctionMomentTensorForce.hh \ - SourceTimeFunctionPointForce.hh \ sourcesfwd.hh diff --git a/libsrc/pylith/sources/SourceTimeFunctionMomentTensorForce.hh b/libsrc/pylith/sources/SourceTimeFunctionMomentTensorForce.hh index b1b6a2951c..0e23b8c4cb 100644 --- a/libsrc/pylith/sources/SourceTimeFunctionMomentTensorForce.hh +++ b/libsrc/pylith/sources/SourceTimeFunctionMomentTensorForce.hh @@ -36,7 +36,7 @@ #include "petscds.h" // USES PetscPointFunc, PetscPointJac class pylith::sources::SourceTimeFunctionMomentTensorForce : public pylith::utils::PyreComponent { - friend class TestRickerFunction; // unit testing + friend class TestSourceTimeFunctionMomentTensorForce; // unit testing // PUBLIC METHODS ////////////////////////////////////////////////////////////////////////////////////////////////// public: @@ -103,7 +103,8 @@ private: SourceTimeFunctionMomentTensorForce(const SourceTimeFunctionMomentTensorForce&); ///< Not implemented. const SourceTimeFunctionMomentTensorForce& operator=(const SourceTimeFunctionMomentTensorForce&); /// Not - /// implemented. + + /// implemented. }; // class SourceTimeFunctionMomentTensorForce diff --git a/libsrc/pylith/sources/SourceTimeFunctionPointForce.cc b/libsrc/pylith/sources/SourceTimeFunctionPointForce.cc deleted file mode 100644 index 3a23fa7b63..0000000000 --- a/libsrc/pylith/sources/SourceTimeFunctionPointForce.cc +++ /dev/null @@ -1,87 +0,0 @@ -// -*- C++ -*- -// -// ---------------------------------------------------------------------- -// -// Brad T. Aagaard, U.S. Geological Survey -// Charles A. Williams, GNS Science -// Matthew G. Knepley, University at Buffalo -// -// This code was developed as part of the Computational Infrastructure -// for Geodynamics (http://geodynamics.org). -// -// Copyright (c) 2010-2021 University of California, Davis -// -// See LICENSE.md for license information. -// -// ---------------------------------------------------------------------- -// - -#include - -#include "pylith/sources/SourceTimeFunctionPointForce.hh" // implementation of object methods - -#include "pylith/feassemble/Integrator.hh" // USES NEW_JACOBIAN_NEVER - -#include "pylith/utils/error.hh" // USES PYLITH_METHOD_BEGIN/END -#include "pylith/utils/journals.hh" // USES PYLITH_COMPONENT_DEBUG - -#include "spatialdata/geocoords/CoordSys.hh" // USES CoordSys - -#include // USES typeid() - -// --------------------------------------------------------------------------------------------------------------------- -// Default constructor. -pylith::sources::SourceTimeFunctionPointForce::SourceTimeFunctionPointForce(void) : - _JacobianTriggers(pylith::feassemble::Integrator::NEW_JACOBIAN_NEVER) {} - - -// --------------------------------------------------------------------------------------------------------------------- -// Destructor. -pylith::sources::SourceTimeFunctionPointForce::~SourceTimeFunctionPointForce(void) { - deallocate(); -} // destructor - - -// --------------------------------------------------------------------------------------------------------------------- -// Deallocate PETSc and local data structures. -void -pylith::sources::SourceTimeFunctionPointForce::deallocate(void) {} - - -// --------------------------------------------------------------------------------------------------------------------- -// Get triggers for needing to compute the elastic constants for the RHS Jacobian. -int -pylith::sources::SourceTimeFunctionPointForce::getJacobianTriggers(void) const { - return _JacobianTriggers; -} // getJacobianTriggers - - -// --------------------------------------------------------------------------------------------------------------------- -// Update kernel constants. -void -pylith::sources::SourceTimeFunctionPointForce::updateKernelConstants(pylith::real_array* kernelConstants, - const PylithReal dt) const { - PYLITH_METHOD_BEGIN; - PYLITH_COMPONENT_DEBUG("updateKernelConstants(kernelConstants"<* kernels, - const spatialdata::geocoords::CoordSys* coordsys) const { - PYLITH_METHOD_BEGIN; - PYLITH_COMPONENT_DEBUG("addKernelsUpdateStateVars(kernels="<* kernels, - const spatialdata::geocoords::CoordSys* coordsys) const; - - /** Update kernel constants. - * - * @param[inout] kernelConstants Array of constants used in integration kernels. - * @param[in] dt Current time step. - */ - virtual - void updateKernelConstants(pylith::real_array* kernelConstants, - const PylithReal dt) const; - - // PROTECTED MEMBERS /////////////////////////////////////////////////////////////////////////////////////////////// - - int _JacobianTriggers; ///< Triggers for needing to recompute the RHS Jacobian. - - // NOT IMPLEMENTED ///////////////////////////////////////////////////////////////////////////////////////////////// -private: - - SourceTimeFunctionPointForce(const SourceTimeFunctionPointForce&); ///< Not implemented. - const SourceTimeFunctionPointForce& operator=(const SourceTimeFunctionPointForce&); /// Not implemented. - -}; // class SourceTimeFunctionPointForce - -#endif // pylith_sources_sourcetimefunctionpointforce_hh - -// End of file diff --git a/libsrc/pylith/sources/sourcesfwd.hh b/libsrc/pylith/sources/sourcesfwd.hh index f792bec2cc..2a625d28da 100644 --- a/libsrc/pylith/sources/sourcesfwd.hh +++ b/libsrc/pylith/sources/sourcesfwd.hh @@ -43,7 +43,6 @@ namespace pylith { class GaussianWavelet; class PointForce; - class SourceTimeFunctionPointForce; class AuxiliaryFactoryPointForce; class SquarePulseSource; diff --git a/tests/libtests/sources/TestSource.cc b/tests/libtests/sources/TestSource.cc new file mode 100644 index 0000000000..559cf60408 --- /dev/null +++ b/tests/libtests/sources/TestSource.cc @@ -0,0 +1,781 @@ +// -*- C++ -*- +// +// ---------------------------------------------------------------------- +// +// Brad T. Aagaard, U.S. Geological Survey +// Charles A. Williams, GNS Science +// Matthew G. Knepley, University at Buffalo +// +// This code was developed as part of the Computational Infrastructure +// for Geodynamics (http://geodynamics.org). +// +// Copyright (c) 2010-2022 University of California, Davis +// +// See LICENSE.md for license information. +// +// ---------------------------------------------------------------------- +// + +#include + +#include "TestSource.hh" // Implementation of class methods + +#include "pylith/sources/Source.hh" // USES Source +#include "pylith/sources/Query.hh" // USES Query + +#include "pylith/topology/Mesh.hh" // USES Mesh +#include "pylith/topology/MeshOps.hh" // USES MeshOps::nondimensionalize() +#include "pylith/topology/Field.hh" // USES Field +#include "pylith/topology/Fields.hh" // USES Fields +#include "pylith/topology/VisitorMesh.hh" // USES VecVisitorMesh +#include "pylith/topology/FieldQuery.hh" // USES FieldQuery +#include "pylith/feassemble/AuxiliaryFactory.hh" // USES AuxiliaryFactory +#include "pylith/meshio/MeshIOAscii.hh" // USES MeshIOAscii +#include "pylith/utils/error.hh" // USES PYLITH_METHOD_BEGIN/END +#include "pylith/utils/journals.hh" // pythia::journal + +#include "spatialdata/spatialdb/UserFunctionDB.hh" // USES UserFunctionDB +#include "spatialdata/geocoords/CoordSys.hh" // USES CoordSys +#include "spatialdata/spatialdb/GravityField.hh" // USES GravityField +#include "spatialdata/units/Nondimensional.hh" // USES Nondimensional + +// ---------------------------------------------------------------------- +// Setup testing data. +void +pylith::sources::TestSource::setUp(void) { + _mesh = new pylith::topology::Mesh();CPPUNIT_ASSERT(_mesh); + _solutionFields = NULL; +} // setUp + + +// ---------------------------------------------------------------------- +// Deallocate testing data. +void +pylith::sources::TestSource::tearDown(void) { + delete _solutionFields;_solutionFields = NULL; + delete _mesh;_mesh = NULL; +} // tearDown + + +// ---------------------------------------------------------------------- +// Test auxField(). +void +pylith::sources::TestSource::testAuxField(void) { + PYLITH_METHOD_BEGIN; + + _initializeFull(); + + Source* source = _source();CPPUNIT_ASSERT(source); + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + + const pylith::topology::Field* auxField = source->auxField();CPPUNIT_ASSERT(auxField); + for (int i = 0; i < data->numAuxSubfields; ++i) { + CPPUNIT_ASSERT(auxField->hasSubfield(data->auxSubfields[i])); + } // for + + CPPUNIT_ASSERT(!auxField->hasSubfield("abc4598245")); + + PYLITH_METHOD_END; +} // testAuxField + + +// ---------------------------------------------------------------------- +// Test auxSubfieldDiscretization(). +void +pylith::sources::TestSource::testAuxSubfieldDiscretization(void) { + PYLITH_METHOD_BEGIN; + + const topology::FieldBase::Discretization infoDefault = pylith::topology::Field::Discretization(1, 1, true, pylith::topology::FieldBase::POLYNOMIAL_SPACE); + const topology::FieldBase::Discretization infoA = pylith::topology::Field::Discretization(1, 2, false, pylith::topology::FieldBase::POLYNOMIAL_SPACE); + const topology::FieldBase::Discretization infoB = pylith::topology::Field::Discretization(2, 2, true, pylith::topology::FieldBase::POINT_SPACE); + + Source* source = _source();CPPUNIT_ASSERT(source); + source->auxSubfieldDiscretization("A", infoA.basisOrder, infoA.quadOrder, infoA.isBasisContinuous, infoA.feSpace); + source->auxSubfieldDiscretization("B", infoB.basisOrder, infoB.quadOrder, infoB.isBasisContinuous, infoB.feSpace); + + CPPUNIT_ASSERT(source->_auxiliaryFactory()); + { // A + const topology::FieldBase::Discretization& test = source->_auxiliaryFactory()->getSubfieldDiscretization("A"); + CPPUNIT_ASSERT_EQUAL(infoA.basisOrder, test.basisOrder); + CPPUNIT_ASSERT_EQUAL(infoA.quadOrder, test.quadOrder); + CPPUNIT_ASSERT_EQUAL(infoA.isBasisContinuous, test.isBasisContinuous); + CPPUNIT_ASSERT_EQUAL(infoA.feSpace, test.feSpace); + } // A + + { // B + const topology::FieldBase::Discretization& test = source->_auxiliaryFactory()->getSubfieldDiscretization("B"); + CPPUNIT_ASSERT_EQUAL(infoB.basisOrder, test.basisOrder); + CPPUNIT_ASSERT_EQUAL(infoB.quadOrder, test.quadOrder); + CPPUNIT_ASSERT_EQUAL(infoB.isBasisContinuous, test.isBasisContinuous); + CPPUNIT_ASSERT_EQUAL(infoB.feSpace, test.feSpace); + } // B + + { // C (default) + const topology::FieldBase::Discretization& test = source->_auxiliaryFactory()->getSubfieldDiscretization("C"); + CPPUNIT_ASSERT_EQUAL(infoDefault.basisOrder, test.basisOrder); + CPPUNIT_ASSERT_EQUAL(infoDefault.quadOrder, test.quadOrder); + CPPUNIT_ASSERT_EQUAL(infoDefault.isBasisContinuous, test.isBasisContinuous); + CPPUNIT_ASSERT_EQUAL(infoDefault.feSpace, test.feSpace); + } // C (default) + + { // default + const topology::FieldBase::Discretization& test = source->_auxiliaryFactory()->getSubfieldDiscretization("default"); + CPPUNIT_ASSERT_EQUAL(infoDefault.basisOrder, test.basisOrder); + CPPUNIT_ASSERT_EQUAL(infoDefault.quadOrder, test.quadOrder); + CPPUNIT_ASSERT_EQUAL(infoDefault.isBasisContinuous, test.isBasisContinuous); + CPPUNIT_ASSERT_EQUAL(infoDefault.feSpace, test.feSpace); + } // default + + PYLITH_METHOD_END; +} // testAuxSubfieldDiscretization + + +// ---------------------------------------------------------------------- +// Test auxFieldDB(). +void +pylith::sources::TestSource::testAuxFieldDB(void) { + PYLITH_METHOD_BEGIN; + + const std::string label = "test db"; + spatialdata::spatialdb::UserFunctionDB db; + db.setLabel(label.c_str()); + + Source* source = _source();CPPUNIT_ASSERT(source); + source->auxFieldDB(&db); + + CPPUNIT_ASSERT(source->_auxiliaryFactory()); + CPPUNIT_ASSERT(source->_auxiliaryFactory()->queryDB()); + CPPUNIT_ASSERT_EQUAL(label, std::string(source->_auxiliaryFactory()->queryDB()->getLabel())); + + PYLITH_METHOD_END; +} // testAuxFieldDB + + +// ---------------------------------------------------------------------- +// Test normalizer(). +void +pylith::sources::TestSource::testNormalizer(void) { + PYLITH_METHOD_BEGIN; + + spatialdata::units::Nondimensional normalizer; + const double scale = 5.0; + normalizer.setLengthScale(scale); + + Source* source = _source();CPPUNIT_ASSERT(source); + source->normalizer(normalizer); + CPPUNIT_ASSERT_EQUAL(scale, source->_normalizer->getLengthScale()); + + PYLITH_METHOD_END; +} // testNormalizer + + +// ---------------------------------------------------------------------- +// Test verifyConfiguration(). +void +pylith::sources::TestSource::testVerifyConfiguration(void) { + PYLITH_METHOD_BEGIN; + + // Call verifyConfiguration() + Source* source = _source();CPPUNIT_ASSERT(source); + CPPUNIT_ASSERT(_solutionFields); + source->verifyConfiguration(_solutionFields->get("solution")); + + // Nothing to test. + + PYLITH_METHOD_END; +} // testVerifyConfiguration + + +// ---------------------------------------------------------------------- +// Test dimension(), id(), and getLabel(). +void +pylith::sources::TestSource::testAccessors(void) { + PYLITH_METHOD_BEGIN; + + Source* source = _source();CPPUNIT_ASSERT(source); + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + + CPPUNIT_ASSERT_EQUAL_MESSAGE("Test of Source::dimension() failed.", data->dimension, source->dimension()); + + const int matId = 1234; + source->id(matId); + CPPUNIT_ASSERT_EQUAL_MESSAGE("Test of Source::id() failed.", matId, source->id()); + + const std::string& matLabel = "xyz"; + source->setLabel(matLabel.c_str()); + CPPUNIT_ASSERT_EQUAL_MESSAGE("Test of Source::getLabel() failed.", matLabel, std::string(source->getLabel())); + + PYLITH_METHOD_END; +} // testAccessors + + +// ---------------------------------------------------------------------- +// Test initialize(). +void +pylith::sources::TestSource::testInitialize(void) { + PYLITH_METHOD_BEGIN; + + // Call initialize() + _initializeFull(); // includes setting up auxField + + Source* source = _source();CPPUNIT_ASSERT(source); + const pylith::topology::Field* auxField = source->auxField();CPPUNIT_ASSERT(auxField); + + // source->_auxiliaryField->view("AUX FIELDS"); // :DEBUGGING: + + // Check result + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + CPPUNIT_ASSERT_EQUAL(std::string("auxiliary subfields"), std::string(auxField->getLabel())); + CPPUNIT_ASSERT_EQUAL(data->dimension, auxField->getSpaceDim()); + + PylithReal norm = 0.0; + PylithReal t = 0.0; + const PetscDM dm = auxField->dmMesh();CPPUNIT_ASSERT(dm); + pylith::topology::FieldQuery query(*auxField); + query.initializeWithDefaultQueryFns(); + CPPUNIT_ASSERT(data->normalizer); + query.openDB(data->auxDB, data->normalizer->getLengthScale()); + PetscErrorCode err = DMPlexComputeL2DiffLocal(dm, t, query.functions(), (void**)query.contextPtrs(), auxField->localVector(), &norm);CPPUNIT_ASSERT(!err); + query.closeDB(data->auxDB); + const PylithReal tolerance = 1.0e-6; + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("Test of auxiliary field values failed.", 0.0, norm, tolerance); + +#if 1 + // Verify solution and perturbation fields can be exactly represented by discretization. + norm = 0.0; + t = 0.0; + + pylith::topology::Field& solution = _solutionFields->get("solution"); + // solution.view("SOLUTION"); // :DEBUG: + const PetscDM dmSoln = solution.dmMesh();CPPUNIT_ASSERT(dmSoln); + pylith::topology::FieldQuery solnQuery(solution); + solnQuery.initializeWithDefaultQueryFns(); + CPPUNIT_ASSERT(data->normalizer); + solnQuery.openDB(data->solnDB, data->normalizer->getLengthScale()); + err = DMPlexComputeL2DiffLocal(dmSoln, t, solnQuery.functions(), (void**)solnQuery.contextPtrs(), solution.localVector(), &norm);CPPUNIT_ASSERT(!err); + solnQuery.closeDB(data->solnDB); + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("Discretized solution field failed representation test.", 0.0, norm, tolerance); + + pylith::topology::Field& perturbation = _solutionFields->get("perturbation"); + // perturbation.view("PERTURBATION"); // :DEBUG: + const PetscDM dmPerturb = perturbation.dmMesh();CPPUNIT_ASSERT(dmPerturb); + pylith::topology::FieldQuery perturbQuery(perturbation); + perturbQuery.initializeWithDefaultQueryFns(); + CPPUNIT_ASSERT(data->normalizer); + perturbQuery.openDB(data->perturbDB, data->normalizer->getLengthScale()); + err = DMPlexComputeL2DiffLocal(dmPerturb, t, perturbQuery.functions(), (void**)perturbQuery.contextPtrs(), perturbation.localVector(), &norm);CPPUNIT_ASSERT(!err); + perturbQuery.closeDB(data->perturbDB); + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("Discretized perturbation field failed representation test.", 0.0, norm, tolerance); +#endif + + PYLITH_METHOD_END; +} // testInitialize + + +// ---------------------------------------------------------------------- +// Test computeResidual(). +void +pylith::sources::TestSource::testComputeResidual(void) { + PYLITH_METHOD_BEGIN; + + // Call initialize() + _initializeFull(); // includes setting up auxField + + CPPUNIT_ASSERT(_mesh); + CPPUNIT_ASSERT(_solutionFields); + pylith::topology::Field& solution = _solutionFields->get("solution"); + pylith::topology::Field& solutionDot = _solutionFields->get("solution_dot"); + + pylith::topology::Field residualRHS(*_mesh); + residualRHS.cloneSection(solution); + residualRHS.setLabel("residual RHS"); + residualRHS.createDiscretization(); + residualRHS.allocate(); + + pylith::topology::Field residualLHS(*_mesh); + residualLHS.cloneSection(solution); + residualLHS.setLabel("residual LHS"); + residualLHS.createDiscretization(); + residualLHS.allocate(); + + Source* source = _source();CPPUNIT_ASSERT(source); + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + +#if 0 // :DEBUG: + PetscOptionsSetValue(NULL, "-dm_plex_print_fem", "2"); // :DEBUG: + DMSetFromOptions(residualRHS.dmMesh()); // :DEBUG: +#endif // :DEBUG: + + const PylithReal t = data->t; + const PylithReal dt = data->dt; + source->computeRHSResidual(&residualRHS, t, dt, solution); + source->computeLHSResidual(&residualLHS, t, dt, solution, solutionDot); + + // We don't use Dirichlet BC, so we must manually zero out the residual values for constrained DOF. + _zeroBoundary(&residualRHS); + _zeroBoundary(&residualLHS); + +#if 0 // :DEBUG: + solution.view("SOLUTION"); // :DEBUG: + solutionDot.view("SOLUTION_DOT"); // :DEBUG: + residualRHS.view("RESIDUAL RHS"); // :DEBUG: + residualLHS.view("RESIDUAL LHS"); // :DEBUG: +#endif // :DEBUG: + + PetscErrorCode err; + PetscVec residualVec = NULL; + err = VecDuplicate(residualRHS.localVector(), &residualVec);CPPUNIT_ASSERT(!err); + err = VecWAXPY(residualVec, -1.0, residualRHS.localVector(), residualLHS.localVector());CPPUNIT_ASSERT(!err); + + PylithReal norm = 0.0; + PylithReal normRHS = 0.0; + PylithReal normLHS = 0.0; + err = VecNorm(residualRHS.localVector(), NORM_2, &normRHS);CPPUNIT_ASSERT(!err); + err = VecNorm(residualLHS.localVector(), NORM_2, &normLHS);CPPUNIT_ASSERT(!err); + err = VecNorm(residualVec, NORM_2, &norm);CPPUNIT_ASSERT(!err); + err = VecDestroy(&residualVec);CPPUNIT_ASSERT(!err); + const PylithReal tolerance = 1.0e-6; + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("Test of F(s) - G(s) == 0 failed.", 0.0, norm, tolerance); + // Avoid trivial satisfaction of norm with zero values. + CPPUNIT_ASSERT_MESSAGE("RHS and LHS residuals are both exactly zero, which is suspicious.", normRHS > 0.0 || normLHS > 0.0); + + PYLITH_METHOD_END; +} // testComputeResidual + + +// ---------------------------------------------------------------------- +// Test computeJacobian(). +void +pylith::sources::TestSource::testComputeJacobian(void) { + PYLITH_METHOD_BEGIN; + + // Create linear problem (MMS) with two trial solutions, s and p. + // + // Check that Jg(s)*(p - s) = G(p) - G(s). + + // Call initialize() + _initializeFull(); + + CPPUNIT_ASSERT(_mesh); + CPPUNIT_ASSERT(_solutionFields); + pylith::topology::Field& solution = _solutionFields->get("solution"); + pylith::topology::Field& perturbation = _solutionFields->get("perturbation"); + + Source* source = _source();CPPUNIT_ASSERT(source); + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + + pylith::topology::Field residual1(*_mesh); + residual1.cloneSection(solution); + residual1.setLabel("residual1"); + residual1.createDiscretization(); + residual1.allocate(); + + pylith::topology::Field residual2(*_mesh); + residual2.cloneSection(perturbation); + residual2.setLabel("residual2"); + residual2.createDiscretization(); + residual2.allocate(); + +#if 0 // :DEBUG: + PetscOptionsSetValue(NULL, "-dm_plex_print_fem", "3"); // :DEBUG: + DMSetFromOptions(_solution1->dmMesh()); // :DEBUG: +#endif // :DEBUG: + + const PylithReal t = data->t; + const PylithReal dt = data->dt; + source->computeLHSResidual(&residual1, t, dt, solution); + source->computeLHSResidual(&residual2, t, dt, perturbation); + + // residual1.view("RESIDUAL 1 RHS"); // :DEBUG: + // residual2.view("RESIDUAL 2 RHS"); // :DEBUG: + + // Compute Jacobian + PetscErrorCode err; + PetscMat jacobianMat = NULL; + err = DMCreateMatrix(solution.dmMesh(), &jacobianMat);CPPUNIT_ASSERT(!err); + err = MatZeroEntries(jacobianMat);CPPUNIT_ASSERT(!err); + PetscMat precondMat = jacobianMat; // Use Jacobian == preconditioner + + source->computeLHSJacobian(jacobianMat, precondMat, t, dt, solution); + CPPUNIT_ASSERT_EQUAL(false, source->needNewLHSJacobian()); + // _zeroBoundary(&residual1); + // _zeroBoundary(&residual2, jacobianMat); + err = MatAssemblyBegin(jacobianMat, MAT_FINAL_ASSEMBLY);PYLITH_CHECK_ERROR(err); + err = MatAssemblyEnd(jacobianMat, MAT_FINAL_ASSEMBLY);PYLITH_CHECK_ERROR(err); + + // Check that J(s)*(p - s) = G(p) - G(s). + + PetscVec residualVec = NULL; + err = VecDuplicate(residual1.localVector(), &residualVec);CPPUNIT_ASSERT(!err); + err = VecWAXPY(residualVec, -1.0, residual1.localVector(), residual2.localVector());CPPUNIT_ASSERT(!err); + + PetscVec solnIncrVec = NULL; + err = VecDuplicate(solution.localVector(), &solnIncrVec);CPPUNIT_ASSERT(!err); + err = VecWAXPY(solnIncrVec, -1.0, solution.localVector(), perturbation.localVector());CPPUNIT_ASSERT(!err); + + // result = Jg*(-solnIncr) + residual + PetscVec resultVec = NULL; + err = VecDuplicate(residualVec, &resultVec);CPPUNIT_ASSERT(!err); + err = VecZeroEntries(resultVec);CPPUNIT_ASSERT(!err); + err = VecScale(solnIncrVec, -1.0);CPPUNIT_ASSERT(!err); + err = MatMultAdd(jacobianMat, solnIncrVec, residualVec, resultVec);CPPUNIT_ASSERT(!err); + +#if 0 // :DEBUG: + std::cout << "SOLN INCR" << std::endl; + VecView(solnIncrVec, PETSC_VIEWER_STDOUT_SELF); + std::cout << "G2-G1" << std::endl; + VecView(residualVec, PETSC_VIEWER_STDOUT_SELF); + std::cout << "RESULT" << std::endl; + VecView(resultVec, PETSC_VIEWER_STDOUT_SELF); +#endif // :DEBUG: + + PylithReal norm = 0.0; + err = VecNorm(resultVec, NORM_2, &norm);CPPUNIT_ASSERT(!err); + err = VecDestroy(&resultVec);CPPUNIT_ASSERT(!err); + err = VecDestroy(&solnIncrVec);CPPUNIT_ASSERT(!err); + err = VecDestroy(&residualVec);CPPUNIT_ASSERT(!err); + err = MatDestroy(&jacobianMat);CPPUNIT_ASSERT(!err); + + const PylithReal tolerance = 1.0e-6; + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("Check of Jg(s)*(p-s) - (G(p) - G(s)) == 0 failed.", 0.0, norm, tolerance); + CPPUNIT_ASSERT_MESSAGE("Norm of resulting vector is exactly zero, which is suspicious.", norm > 0.0); + + PYLITH_METHOD_END; +} // testComputeJacobian + + +// ---------------------------------------------------------------------- +// Test computeLHSJacobianImplicit(). +void +pylith::sources::TestSource::testComputeLHSJacobianImplicit(void) { + PYLITH_METHOD_BEGIN; + + Source* source = _source();CPPUNIT_ASSERT(source); + const TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + if (data->isExplicit) { + PYLITH_METHOD_END; + } // if + + // Create linear problem (MMS) with two trial solutions, s,s_dor and p,p_dot. + // + // Check that Jf(s,s_dot)*(p - s) = F(p,p_dot) - F(s,s_dot). + + // Call initialize() + _initializeFull(); // includes setting up auxField + + CPPUNIT_ASSERT(_mesh); + CPPUNIT_ASSERT(_solutionFields); + pylith::topology::Field& solution = _solutionFields->get("solution"); + pylith::topology::Field& solutionDot = _solutionFields->get("solution_dot"); + pylith::topology::Field& perturbation = _solutionFields->get("perturbation"); + pylith::topology::Field& perturbationDot = _solutionFields->get("perturbation_dot"); + + pylith::topology::Field residual1(*_mesh); + residual1.cloneSection(solution); + residual1.setLabel("residual1"); + residual1.createDiscretization(); + residual1.allocate(); + + pylith::topology::Field residual2(*_mesh); + residual2.cloneSection(perturbation); + residual2.setLabel("residual2"); + residual2.createDiscretization(); + residual2.allocate(); + +#if 0 // :DEBUG: + PetscOptionsSetValue(NULL, "-dm_plex_print_fem", "2"); // :DEBUG: + DMSetFromOptions(_solution1->dmMesh()); // :DEBUG: +#endif // :DEBUG: + + const PylithReal t = data->t; + const PylithReal dt = data->dt; + const PylithReal s_tshift = data->s_tshift; + source->computeLHSResidual(&residual1, t, dt, solution, solutionDot); + source->computeLHSResidual(&residual2, t, dt, perturbation, perturbationDot); + + // residual1.view("RESIDUAL 1 LHS"); // :DEBUG: + // residual2.view("RESIDUAL 2 LHS"); // :DEBUG: + + PetscErrorCode err; + + PetscVec residualVec = NULL; + err = VecDuplicate(residual1.localVector(), &residualVec);CPPUNIT_ASSERT(!err); + err = VecWAXPY(residualVec, -1.0, residual1.localVector(), residual2.localVector());CPPUNIT_ASSERT(!err); + + PetscVec solnIncrVec = NULL; + err = VecDuplicate(solution.localVector(), &solnIncrVec);CPPUNIT_ASSERT(!err); + err = VecWAXPY(solnIncrVec, -1.0, solution.localVector(), perturbation.localVector());CPPUNIT_ASSERT(!err); + + // Compute Jacobian + PetscMat jacobianMat = NULL; + err = DMCreateMatrix(solution.dmMesh(), &jacobianMat);CPPUNIT_ASSERT(!err); + err = MatZeroEntries(jacobianMat);CPPUNIT_ASSERT(!err); + PetscMat precondMat = jacobianMat; // Use Jacobian == preconditioner + + source->computeLHSJacobianImplicit(jacobianMat, precondMat, t, dt, s_tshift, solution, solutionDot); + CPPUNIT_ASSERT_EQUAL(false, source->needNewLHSJacobian()); + err = MatAssemblyBegin(jacobianMat, MAT_FINAL_ASSEMBLY);PYLITH_CHECK_ERROR(err); + err = MatAssemblyEnd(jacobianMat, MAT_FINAL_ASSEMBLY);PYLITH_CHECK_ERROR(err); + + // result = J*(-solnIncr) + residual + PetscVec resultVec = NULL; + err = VecDuplicate(residualVec, &resultVec);CPPUNIT_ASSERT(!err); + err = VecZeroEntries(resultVec);CPPUNIT_ASSERT(!err); + err = VecScale(solnIncrVec, -1.0);CPPUNIT_ASSERT(!err); + err = MatMultAdd(jacobianMat, solnIncrVec, residualVec, resultVec);CPPUNIT_ASSERT(!err); + +#if 0 // :DEBUG: + std::cout << "SOLN INCR" << std::endl; + VecView(solnIncrVec, PETSC_VIEWER_STDOUT_SELF); + std::cout << "F2-F1" << std::endl; + VecView(residualVec, PETSC_VIEWER_STDOUT_SELF); + std::cout << "RESULT" << std::endl; + VecView(resultVec, PETSC_VIEWER_STDOUT_SELF); +#endif // :DEBUG: + + PylithReal norm = 0.0, normSolnIncr = 0.0, normResidual = 0.0; + err = VecNorm(resultVec, NORM_2, &norm);CPPUNIT_ASSERT(!err); + err = VecNorm(solnIncrVec, NORM_2, &normSolnIncr);CPPUNIT_ASSERT(!err); + err = VecNorm(residualVec, NORM_2, &normResidual);CPPUNIT_ASSERT(!err); + err = VecDestroy(&resultVec);CPPUNIT_ASSERT(!err); + err = VecDestroy(&solnIncrVec);CPPUNIT_ASSERT(!err); + err = VecDestroy(&residualVec);CPPUNIT_ASSERT(!err); + err = MatDestroy(&jacobianMat);CPPUNIT_ASSERT(!err); + + const PylithReal tolerance = 1.0e-6; + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("Check of Jf(s)*(p-s) - (F(p) - F(s)) == 0 failed.", 0.0, norm, tolerance); + CPPUNIT_ASSERT_MESSAGE("Norm of resulting vector is exactly zero, which is suspicious.", (0 < normResidual && 0 < norm) || (0 == normResidual && 0 == norm)); + + PYLITH_METHOD_END; +} // testComputeLHSJacobianImplicit + + +// ---------------------------------------------------------------------- +// Test computeLHSJacobianExplicit(). +void +pylith::sources::TestSource::testComputeLHSJacobianInverseExplicit(void) { + PYLITH_METHOD_BEGIN; + + Source* source = _source();CPPUNIT_ASSERT(source); + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + if (!data->isExplicit) { + PYLITH_METHOD_END; + } // if + + CPPUNIT_ASSERT_MESSAGE("Test not implemented.", false); // :TODO: ADD MORE HERE + + PYLITH_METHOD_END; +} // testComputeLHSJacobianInverseExplicit + + +// ---------------------------------------------------------------------- +// Test updateStateVars(). +void +pylith::sources::TestSource::testUpdateStateVars(void) { + PYLITH_METHOD_BEGIN; + + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + if (!data->auxUpdateDB) { + PYLITH_METHOD_END; + } // if + + // Call initialize() + _initializeFull(); // includes setting up auxField + + // We test updating the state variables in the auxiliary field by + // passing the perturbation as the "new" solution and the existing + // auxiliary field. We test whether the "updated" auxiliary field + // matches the database with the updated auxiliary field. + + Source* source = _source();CPPUNIT_ASSERT(source); + CPPUNIT_ASSERT(_solutionFields); + pylith::topology::Field& perturbation = _solutionFields->get("perturbation"); +#if 0 + source->_auxiliaryField->view("INITIAL_AUX FIELDS"); // :DEBUGGING: +#endif + source->_updateStateVars(data->t, data->dt, perturbation); + + const pylith::topology::Field* auxField = source->auxField();CPPUNIT_ASSERT(auxField); + source->_auxiliaryField->view("UPDATED_AUX FIELDS"); // :DEBUGGING: + + // Check updated auxiliary field. + PylithReal norm = 0.0; + PylithReal t = 0.0; + const PetscDM dm = auxField->dmMesh();CPPUNIT_ASSERT(dm); + pylith::topology::FieldQuery query(*auxField); + query.initializeWithDefaultQueryFns(); + CPPUNIT_ASSERT(data->normalizer); + query.openDB(data->auxUpdateDB, data->normalizer->getLengthScale()); +#if 0 // :DEBUG: + PetscOptionsSetValue(NULL, "-dm_plex_print_l2", "1"); // :DEBUG: + DMSetFromOptions(dm); // :DEBUG: +#endif + PetscErrorCode err = DMPlexComputeL2DiffLocal(dm, t, query.functions(), (void**)query.contextPtrs(), auxField->localVector(), &norm);CPPUNIT_ASSERT(!err); + query.closeDB(data->auxUpdateDB); + const PylithReal tolerance = 1.0e-6; + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("Check of updated auxiliary field values failed.", 0.0, norm, tolerance); + + PYLITH_METHOD_END; +} // testUpdateStateVars + + +// ---------------------------------------------------------------------- +// Do minimal initilaization of test data. +void +pylith::sources::TestSource::_initializeMin(void) { + PYLITH_METHOD_BEGIN; + + Source* source = _source();CPPUNIT_ASSERT(source); + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + + pylith::meshio::MeshIOAscii iohandler; + CPPUNIT_ASSERT(data->meshFilename); + iohandler.filename(data->meshFilename); + iohandler.read(_mesh);CPPUNIT_ASSERT(_mesh); + + CPPUNIT_ASSERT_MESSAGE("Test mesh does not contain any cells.", _mesh->numCells() > 0); + CPPUNIT_ASSERT_MESSAGE("Test mesh does not contain any vertices.", _mesh->numVertices() > 0); + + // Setup coordinates. + _mesh->setCoordSys(data->cs); + CPPUNIT_ASSERT(data->normalizer); + pylith::topology::MeshOps::nondimensionalize(_mesh, *data->normalizer); + + // id and label initialized in derived class + source->normalizer(*data->normalizer); + source->gravityField(data->gravityField); + + // Setup solution fields. + delete _solutionFields;_solutionFields = new pylith::topology::Fields(*_mesh);CPPUNIT_ASSERT(_solutionFields); + _solutionFields->add("solution","solution"); + _solutionFields->add("solution_dot","solution_dot"); + _solutionFields->add("perturbation","perturbation"); + _solutionFields->add("perturbation_dot","perturbation_dot"); + this->_setupSolutionFields(); + + PYLITH_METHOD_END; +} // _initializeMin + + +// ---------------------------------------------------------------------- +// Complete initilaization of test data. +void +pylith::sources::TestSource::_initializeFull(void) { + PYLITH_METHOD_BEGIN; + + Source* source = _source();CPPUNIT_ASSERT(source); + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + CPPUNIT_ASSERT(_mesh); + + // Set auxiliary fields spatial database. + source->auxFieldDB(data->auxDB); + + for (int i = 0; i < data->numAuxSubfields; ++i) { + const pylith::topology::FieldBase::Discretization& info = data->auxDiscretizations[i]; + source->auxSubfieldDiscretization(data->auxSubfields[i], info.basisOrder, info.quadOrder, info.isBasisContinuous, info.feSpace); + } // for + + CPPUNIT_ASSERT(_solutionFields); + source->initialize(_solutionFields->get("solution")); + + PYLITH_METHOD_END; +} // _initializeFull + + +// ---------------------------------------------------------------------- +// Set field to zero on the boundary. +void +pylith::sources::TestSource::_zeroBoundary(pylith::topology::Field* field) { + PYLITH_METHOD_BEGIN; + + CPPUNIT_ASSERT(field); + TestSource_Data* data = _data();CPPUNIT_ASSERT(data); + CPPUNIT_ASSERT(data->boundaryLabel); + + PetscDM dmMesh = field->mesh().dmMesh();CPPUNIT_ASSERT(dmMesh); + PetscDMLabel label = NULL; + PetscIS pointIS = NULL; + const PetscInt *points; + PetscInt numPoints = 0; + PetscBool hasLabel = PETSC_FALSE; + PetscErrorCode err; + err = DMHasLabel(dmMesh, data->boundaryLabel, &hasLabel);CPPUNIT_ASSERT(!err);CPPUNIT_ASSERT(hasLabel); + err = DMGetLabel(dmMesh, data->boundaryLabel, &label);CPPUNIT_ASSERT(!err); + err = DMLabelGetStratumIS(label, 1, &pointIS);CPPUNIT_ASSERT(!err);CPPUNIT_ASSERT(pointIS); + err = ISGetLocalSize(pointIS, &numPoints);CPPUNIT_ASSERT(!err); + err = ISGetIndices(pointIS, &points);CPPUNIT_ASSERT(!err); + + pylith::topology::VecVisitorMesh fieldVisitor(*field); + PylithScalar* fieldArray = fieldVisitor.localArray();CPPUNIT_ASSERT(fieldArray); + + for (PylithInt p = 0; p < numPoints; ++p) { + const PylithInt p_bc = points[p]; + + const PylithInt off = fieldVisitor.sectionOffset(p_bc); + const PylithInt dof = fieldVisitor.sectionDof(p_bc); + for (PylithInt i = 0; i < dof; ++i) { + fieldArray[off+i] = 0.0; + } // for + } // for + + err = ISRestoreIndices(pointIS, &points);PYLITH_CHECK_ERROR(err); + err = ISDestroy(&pointIS);PYLITH_CHECK_ERROR(err); + + PYLITH_METHOD_END; +} // _zeroBoundary + + +// ---------------------------------------------------------------------- +// Constructor +pylith::sources::TestSource_Data::TestSource_Data(void) : + dimension(0), + meshFilename(0), + boundaryLabel(NULL), + cs(NULL), + gravityField(NULL), + + normalizer(new spatialdata::units::Nondimensional), + + t(0.0), + dt(0.0), + s_tshift(0.0), + perturbation(1.0e-4), + + numSolnSubfields(0), + solnDiscretizations(NULL), + solnDB(new spatialdata::spatialdb::UserFunctionDB), + perturbDB(new spatialdata::spatialdb::UserFunctionDB), + + numAuxSubfields(0), + auxSubfields(NULL), + auxDiscretizations(NULL), + auxDB(new spatialdata::spatialdb::UserFunctionDB), + auxUpdateDB(NULL), + + isExplicit(false) { // constructor + CPPUNIT_ASSERT(normalizer); + + CPPUNIT_ASSERT(solnDB); + solnDB->setLabel("solution"); + + CPPUNIT_ASSERT(perturbDB); + perturbDB->setLabel("solution+perturbation"); + + CPPUNIT_ASSERT(auxDB); + auxDB->setLabel("auxiliary field"); +} // constructor + + +// ---------------------------------------------------------------------- +// Destructor +pylith::sources::TestSource_Data::~TestSource_Data(void) { + delete cs;cs = NULL; + delete gravityField;gravityField = NULL; + delete normalizer;normalizer = NULL; + delete solnDB;solnDB = NULL; + delete auxDB;auxDB = NULL; + delete auxUpdateDB;auxUpdateDB = NULL; +} // destructor + + +// End of file diff --git a/tests/libtests/sources/TestSource.hh b/tests/libtests/sources/TestSource.hh new file mode 100644 index 0000000000..9245c92889 --- /dev/null +++ b/tests/libtests/sources/TestSource.hh @@ -0,0 +1,211 @@ +// -*- C++ -*- +// +// ---------------------------------------------------------------------- +// +// Brad T. Aagaard, U.S. Geological Survey +// Charles A. Williams, GNS Science +// Matthew G. Knepley, University at Buffalo +// +// This code was developed as part of the Computational Infrastructure +// for Geodynamics (http://geodynamics.org). +// +// Copyright (c) 2010-2022 University of California, Davis +// +// See LICENSE.md for license information. +// +// ---------------------------------------------------------------------- +// + +/** + * @file tests/libtests/sources/TestSource.hh + * + * @brief C++ abstract base class for testing source objects. + */ + +#if !defined(pylith_sources_testSource_hh) +#define pylith_sources_testSource_hh + +#include +#include "pylith/utils/GenericComponent.hh" // ISA GenericComponent + +#include "pylith/sources/sourcesfwd.hh" // forward declarations +#include "pylith/topology/topologyfwd.hh" // forward declarations +#include "pylith/topology/Field.hh" // HASA FieldBase::Discretization + +#include "spatialdata/spatialdb/spatialdbfwd.hh" // HOLDSA UserFunctionDB +#include "spatialdata/geocoords/geocoordsfwd.hh" // HOLDSA CoordSys +#include "spatialdata/units/unitsfwd.hh" // HOLDSA Nondimensional + +/// Namespace for pylith package +namespace pylith { + namespace sources { + class TestSource; + + class TestSource_Data; // test data + } // sources +} // pylith + +/// C++ abstract base class for testing source objects. +class pylith::sources::TestSource : public CppUnit::TestFixture, public pylith::utils::GenericComponent { + // CPPUNIT TEST SUITE ///////////////////////////////////////////////// + CPPUNIT_TEST_SUITE(TestSource); + + CPPUNIT_TEST(testAuxField); + CPPUNIT_TEST(testAuxSubfieldDiscretization); + CPPUNIT_TEST(testAuxFieldDB); + CPPUNIT_TEST(testNormalizer); + + CPPUNIT_TEST(testVerifyConfiguration); + + CPPUNIT_TEST(testAccessors); + CPPUNIT_TEST(testInitialize); + + CPPUNIT_TEST(testComputeResidual); + CPPUNIT_TEST(testComputeJacobian); + CPPUNIT_TEST(testComputeLHSJacobianImplicit); + CPPUNIT_TEST(testComputeLHSJacobianInverseExplicit); + CPPUNIT_TEST(testUpdateStateVars); + + CPPUNIT_TEST_SUITE_END_ABSTRACT(); + + // PUBLIC METHODS ///////////////////////////////////////////////////// +public: + + /// Setup testing data. + virtual + void setUp(void); + + /// Deallocate testing data. + void tearDown(void); + + /// Test auxField(). + void testAuxField(void); + + /// Test constructor. + void testConstructor(void); + + // /// Test accessors (field, dbTimeHistory, useInitial, useRate, useTimeHistory). + // void testAccessors(void); + + /// Test dimension(), id(), and getLabel(). + void testAccessors(void); + + /// Test auxFieldDiscretization(). + void testAuxFieldDiscretization(void); + + /// Test auxFieldDB(). + void testAuxFieldDB(void); + + /// Test normalizer(). + void testNormalizer(void); + + /// Test verifyConfiguration(). + void testVerifyConfiguration(void); + + /// Test checkConstraints(). + void testCheckConstraints(void); + + /// Test initialize(). + void testInitialize(void); + + /// Test computeRHSResidual(), computeLHSResidual(). + void testComputeResidual(void); + + /// Test computeJacobian(). + void testComputeJacobian(void); + + /// Test computeLHSJacobianImplicit(). + void testComputeLHSJacobianImplicit(void); + + /// Test computeLHSJacobianInverseExplicit(). + void testComputeLHSJacobianInverseExplicit(void); + + /// Test _updateStateVars(). + void testUpdateStateVars(void); + + // PROTECTED METHODS ////////////////////////////////////////////////// +protected: + + /** Get source. + * + * @returns Pointer to source. + */ + virtual + Source* _source(void) = 0; + + /** Get test data. + * + * @returns Pointer to test data. + */ + virtual + TestSource_Data* _data(void) = 0; + + /// Do minimal initilaization of test data. + void _initializeMin(void); + + /// Do full initilaization of test data. + void _initializeFull(void); + + /** Set field (and, optionally, matrix rows and columns) to zero on the boundary. + * + * @param[out] field Field in which to set boundary values to zero. + */ + void _zeroBoundary(pylith::topology::Field* field); + + /// Setup and populate solution fields. + virtual + void _setupSolutionFields(void) = 0; + + // PROTECTED MEMBERS ////////////////////////////////////////////////// +protected: + + // TestSource + pylith::topology::Mesh* _mesh; ///< Finite-element mesh. + pylith::topology::Fields* _solutionFields; ///< Contrainer for solution fields. + +}; // class TestSource + +// ============================================================================= +class pylith::sources::TestSource_Data { + // PUBLIC METHODS /////////////////////////////////////////////////////// +public: + + /// Constructor + TestSource_Data(void); + + /// Destructor + ~TestSource_Data(void); + + // PUBLIC MEMBERS /////////////////////////////////////////////////////// +public: + + int dimension; ///< Dimension of source. + const char* meshFilename; ///< Name of file with ASCII mesh. + const char* boundaryLabel; ///< Group defining domain boundary. + + spatialdata::geocoords::CoordSys* cs; ///< Coordinate system. + spatialdata::spatialdb::GravityField* gravityField; ///< Gravity field. + spatialdata::units::Nondimensional* normalizer; ///< Scales for nondimensionalization. + + PylithReal t; ///< Time for solution in simulation. + PylithReal dt; ///< Time step in simulation. + PylithReal s_tshift; ///< Time shift for LHS Jacobian. + PylithReal perturbation; ///< Maximum amplitude of random perturbation. + + int numSolnSubfields; ///< Number of solution fields. + pylith::topology::Field::Discretization* solnDiscretizations; ///< Discretizations for solution fields. + spatialdata::spatialdb::UserFunctionDB* solnDB; ///< Spatial database with solution. + spatialdata::spatialdb::UserFunctionDB* perturbDB; ///< Spatial database with solution + perturbation. + + int numAuxSubfields; ///< Number of auxiliary subfields. + const char** auxSubfields; ///< Names of auxiliary subfields. + pylith::topology::Field::Discretization* auxDiscretizations; ///< Discretizations for auxiliary subfields. + spatialdata::spatialdb::UserFunctionDB* auxDB; ///< Spatial database with auxiliary field. + spatialdata::spatialdb::UserFunctionDB* auxUpdateDB; ///< Spatial database with updated auxiliary field. + + bool isExplicit; ///< True for explicit time stepping. +}; + +#endif // pylith_sources_testSource_hh + +// End of file