Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model input schema validation #53

Merged
merged 12 commits into from
Sep 6, 2023
Merged
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 @@ -178,7 +178,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 @@ -175,5 +175,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
Loading