Skip to content

Commit

Permalink
Merge pull request #53 from usnistgov/schema
Browse files Browse the repository at this point in the history
Model input schema validation
  • Loading branch information
ianhbell authored Sep 6, 2023
2 parents 31ead69 + 632c192 commit 217c578
Show file tree
Hide file tree
Showing 14 changed files with 2,143 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
[submodule "externals/Catch2"]
path = externals/Catch2
url = https://github.com/catchorg/Catch2.git
[submodule "externals/json-schema-validator"]
path = externals/json-schema-validator
url = https://github.com/pboettch/json-schema-validator
19 changes: 15 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ target_include_directories(teqpinterface INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/
target_include_directories(teqpinterface INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/boost_teqp")

add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/externals/Catch2")
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/externals/json-schema-validator")

set(EIGEN3_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/externals/Eigen" CACHE INTERNAL "Path to Eigen, for autodiff")
set(EIGEN3_VERSION_OK TRUE CACHE BOOL "Yes eigen is fine")
Expand Down Expand Up @@ -93,11 +94,20 @@ if (NOT TEQP_NO_TEQPCPP)
# doesn't require a full compile for a single LOC change
file(GLOB sources "${CMAKE_CURRENT_SOURCE_DIR}/interface/CPP/*.cpp")
add_library(teqpcpp STATIC ${sources})
target_link_libraries(teqpcpp PUBLIC teqpinterface PUBLIC autodiff)
target_link_libraries(teqpcpp PUBLIC nlohmann_json_schema_validator PUBLIC teqpinterface PUBLIC autodiff)
target_include_directories(teqpcpp PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/interface/CPP")
set_property(TARGET teqpcpp PROPERTY POSITION_INDEPENDENT_CODE ON)
target_compile_definitions(teqpcpp PRIVATE -DMULTICOMPLEX_NO_MULTIPRECISION)
target_compile_definitions(teqpcpp PUBLIC -DUSE_AUTODIFF)

# Populate the model schema cpp file with the contents
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/notebooks/schemas.json" MODEL_SCHEMA_CONTENTS)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/dev/templates/model_schema.cpp.in" MODEL_JSON_SCHEMA_TEMPLATE)
file(CONFIGURE
OUTPUT model_schemas.cpp
CONTENT ${MODEL_JSON_SCHEMA_TEMPLATE}
@ONLY)
target_sources(teqpcpp PRIVATE model_schemas.cpp)

if (TEQP_TESTTEQPCPP)
add_executable(test_teqpcpp "${CMAKE_CURRENT_SOURCE_DIR}/interface/CPP/test/test_teqpcpp.cpp")
Expand Down Expand Up @@ -134,7 +144,7 @@ if (NOT TEQP_NO_PYTHON)
file(GLOB pybind11_files "${CMAKE_CURRENT_SOURCE_DIR}/interface/*.cpp")
pybind11_add_module(teqp "${pybind11_files}")
target_include_directories(teqp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/externals/pybind11_json/include")
target_link_libraries(teqp PRIVATE autodiff PRIVATE teqpinterface PRIVATE teqpcpp)
target_link_libraries(teqp PRIVATE teqpcpp PRIVATE autodiff PRIVATE teqpinterface )
target_compile_definitions(teqp PUBLIC -DUSE_AUTODIFF)
if (MSVC)
target_compile_options(teqp PRIVATE "/Zm1000")
Expand All @@ -151,7 +161,8 @@ if (NOT TEQP_NO_TESTS)
endif()
target_compile_definitions(catch_tests PRIVATE -DTEQPC_CATCH)
target_compile_definitions(catch_tests PRIVATE -DTEQP_MULTICOMPLEX_ENABLED)
target_link_libraries(catch_tests PRIVATE autodiff PRIVATE teqpinterface PRIVATE Catch2WithMain PUBLIC teqpcpp)
target_compile_definitions(catch_tests PRIVATE -DTEQP_MULTIPRECISION_ENABLED)
target_link_libraries(catch_tests PUBLIC teqpcpp PRIVATE autodiff PRIVATE teqpinterface PRIVATE Catch2WithMain)
add_test(normal_tests catch_tests)
endif()

Expand Down Expand Up @@ -202,7 +213,7 @@ if (TEQP_SNIPPETS)
target_sources(${snippet_exe} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/externals/Eigen/debug/msvc/eigen.natvis")
endif()

target_link_libraries(${snippet_exe} PRIVATE autodiff PRIVATE teqpinterface PRIVATE Catch2WithMain PRIVATE teqpcpp)
target_link_libraries(${snippet_exe} PRIVATE teqpcpp PRIVATE autodiff PRIVATE teqpinterface PRIVATE Catch2WithMain )
if(UNIX)
target_link_libraries (${snippet_exe} PRIVATE ${CMAKE_DL_LIBS})
endif()
Expand Down
9 changes: 9 additions & 0 deletions dev/templates/model_schema.cpp.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include "nlohmann/json.hpp"

// The contents of this file are populated by CMake
// Note the protectparens( ... )protectparens is to ensure that ( can be used inside the raw string literal
extern const nlohmann::json model_schema_library = R"protectparens(
@MODEL_SCHEMA_CONTENTS@
)protectparens"_json\;


2 changes: 1 addition & 1 deletion doc/source/models/model_potentials.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
" 'model': {\n",
" \"author\": \"2CLJF_Lisal\",\n",
" 'L^*': 0.5,\n",
" '(mu^*)^2': 0.1\n",
" '(Q^*)^2': 0.1\n",
" }\n",
"})\n",
"print(model.solve_pure_critical(1.3, 0.3))"
Expand Down
1 change: 1 addition & 0 deletions externals/json-schema-validator
Submodule json-schema-validator added at f4194d
3 changes: 3 additions & 0 deletions include/teqp/cpp/teqpcpp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,5 +176,8 @@ namespace teqp {
);

std::unique_ptr<AbstractModel> build_model_ptr(const nlohmann::json& json);

/// Return the schema for the given model kind
nlohmann::json get_model_schema(const std::string& kind);
}
}
16 changes: 16 additions & 0 deletions include/teqp/exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,20 @@ namespace teqp {
NotImplementedError(const std::string& msg) : teqpException(200, msg) {};
};

/// Validation of a JSON schema failed
class JSONValidationError : public teqpException {
private:
auto errors_to_string(const std::vector<std::string> &errors, const std::string delim = "|/|\\|"){
std::string o = "";
if (errors.empty()){ return o; }
o = errors[0];
for (auto j = 1; j < errors.size(); ++j){
o += delim + errors[j];
}
return o;
}
public:
JSONValidationError(const std::vector<std::string>& errors) : teqpException(300, errors_to_string(errors)) {};
};

}; // namespace teqp
45 changes: 45 additions & 0 deletions include/teqp/json_tools.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
#pragma once
#include "nlohmann/json.hpp"
#include <nlohmann/json-schema.hpp>

#include <set>
#include <filesystem>
#include <fstream>
#include "teqp/exceptions.hpp"

#include <Eigen/Dense>

using nlohmann::json;
using nlohmann::json_schema::json_validator;

namespace teqp{

/// Load a JSON file from a specified file
Expand Down Expand Up @@ -105,4 +111,43 @@ namespace teqp{
throw teqp::InvalidArgument("Unable to load the argument to multilevel_JSON_load");
}
}

/**
This class is not thread-safe for construction because the validator is not
*/
class JSONValidator{
public:
const nlohmann::json schema;

json_validator validator; // create validator

// Instantiate the validator object, will throw if the schema is invalid
JSONValidator(const nlohmann::json& schema) : schema(schema) {
validator.set_root_schema(schema); // insert root-schema
}

// Return the validation errors when trying to validate the JSON
std::vector<std::string> get_validation_errors(const nlohmann::json& j) const{

/* Custom error handler */
class custom_error_handler : public nlohmann::json_schema::basic_error_handler
{
public:
std::vector<std::string> errors;
void error(const nlohmann::json::json_pointer &ptr, const json &instance, const std::string &message) override
{
nlohmann::json_schema::basic_error_handler::error(ptr, instance, message);
std::stringstream ss;
ss << ptr << ":" << instance << "': " << message << "\n";
errors.push_back(ss.str());
}
} handler;
validator.validate(j, handler); // validate the document
return handler.errors;
}

// A quick checker for validation of the JSON
bool is_valid(const nlohmann::json&j ) const { return get_validation_errors(j).empty(); }
};

}
4 changes: 2 additions & 2 deletions include/teqp/models/model_potentials/2center_ljf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ namespace teqp {
}

// build the 2-center Lennard-Jones model with quadrupole
inline auto build_two_center_model_quadrupole(const std::string& model_version, const double& L = 0.0, const double& mu_sq = 0.0) {
inline auto build_two_center_model_quadrupole(const std::string& model_version, const double& L = 0.0, const double& Q_sq = 0.0) {

// Get reducing for temperature and density
auto DC_funcs = get_density_reducing(model_version);
Expand All @@ -438,7 +438,7 @@ namespace teqp {
auto EOS_quadrupolar = get_Quadrupolar_contribution(model_version);

// Build the 2-center Lennard-Jones model
auto model = Twocenterljf(std::move(DC_funcs), std::move(TC_func), std::move(EOS_hard), std::move(EOS_att), std::move(EOS_quadrupolar), L, mu_sq);
auto model = Twocenterljf(std::move(DC_funcs), std::move(TC_func), std::move(EOS_hard), std::move(EOS_att), std::move(EOS_quadrupolar), L, Q_sq);

return model;
}
Expand Down
15 changes: 14 additions & 1 deletion interface/CPP/teqp_impl_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
#include "teqp/json_builder.hpp"
#include "teqp/cpp/deriv_adapter.hpp"

// This large block of schema definitions is populated by cmake
// at cmake configuration time
extern const nlohmann::json model_schema_library;

namespace teqp {
namespace cppinterface {

std::unique_ptr<teqp::cppinterface::AbstractModel> make_SAFTVRMie(const nlohmann::json &j);

using makefunc = std::function<std::unique_ptr<teqp::cppinterface::AbstractModel>(const nlohmann::json &j)>;
using namespace teqp::cppinterface::adapter;

nlohmann::json get_model_schema(const std::string& kind) { model_schema_library.at(kind); }

static std::unordered_map<std::string, makefunc> pointer_factory = {
{"vdW1", [](const nlohmann::json& spec){ return make_owned(vdWEOS1(spec.at("a"), spec.at("b"))); }},
Expand All @@ -30,7 +36,7 @@ namespace teqp {
{"LJ126_Johnson1993", [](const nlohmann::json& spec){ return make_owned(LJ126Johnson1993());}},
{"Mie_Pohl2023", [](const nlohmann::json& spec){ return make_owned(Mie::Mie6Pohl2023(spec.at("lambda_a")));}},
{"2CLJF-Dipole", [](const nlohmann::json& spec){ return make_owned(twocenterljf::build_two_center_model_dipole(spec.at("author"), spec.at("L^*"), spec.at("(mu^*)^2")));}},
{"2CLJF-Quadrupole", [](const nlohmann::json& spec){ return make_owned(twocenterljf::build_two_center_model_quadrupole(spec.at("author"), spec.at("L^*"), spec.at("(mu^*)^2")));}},
{"2CLJF-Quadrupole", [](const nlohmann::json& spec){ return make_owned(twocenterljf::build_two_center_model_quadrupole(spec.at("author"), spec.at("L^*"), spec.at("(Q^*)^2")));}},
{"IdealHelmholtz", [](const nlohmann::json& spec){ return make_owned(IdealHelmholtz(spec));}},

// Implemented in its own compilation unit to help with compilation time
Expand All @@ -45,6 +51,13 @@ namespace teqp {

auto itr = pointer_factory.find(kind);
if (itr != pointer_factory.end()){
if (model_schema_library.contains(kind)){
// This block is not thread-safe, needs a mutex or something
JSONValidator validator(model_schema_library.at(kind));
if (!validator.is_valid(spec)){
throw teqp::JSONValidationError(validator.get_validation_errors(spec));
}
}
return (itr->second)(spec);
}
else{
Expand Down
Loading

0 comments on commit 217c578

Please sign in to comment.