From 3a11ba2918795821600b7113d0758415718d263a Mon Sep 17 00:00:00 2001 From: gebele Date: Mon, 11 Jun 2018 12:46:06 +0200 Subject: combine gui with rest --- lib/aa.rb | 82 +++++++++++++++++++++ lib/api.rb | 9 +++ lib/compound.rb | 64 ++++++++++++++++ lib/dataset.rb | 46 ++++++++++++ lib/feature.rb | 29 ++++++++ lib/lazar-rest.rb | 69 +++++++++++++++++ lib/model.rb | 38 ++++++++++ lib/nanoparticle.rb | 30 ++++++++ lib/report.rb | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/substance.rb | 30 ++++++++ lib/swagger.rb | 6 ++ lib/validation.rb | 71 ++++++++++++++++++ 12 files changed, 682 insertions(+) create mode 100644 lib/aa.rb create mode 100644 lib/api.rb create mode 100644 lib/compound.rb create mode 100644 lib/dataset.rb create mode 100644 lib/feature.rb create mode 100644 lib/lazar-rest.rb create mode 100644 lib/model.rb create mode 100644 lib/nanoparticle.rb create mode 100644 lib/report.rb create mode 100644 lib/substance.rb create mode 100644 lib/swagger.rb create mode 100644 lib/validation.rb (limited to 'lib') diff --git a/lib/aa.rb b/lib/aa.rb new file mode 100644 index 0000000..6dfec4b --- /dev/null +++ b/lib/aa.rb @@ -0,0 +1,82 @@ +post "/aa/authenticate/?" do + mime_types = ["text/plain"] + bad_request_error "Mime type #{@accept} not supported here. Please request data as #{mime_types.join(', ')}." unless mime_types.include? @accept + bad_request_error "Please send formdata username." unless params[:username] + bad_request_error "Please send formdata password." unless params[:password] + case @accept + when "text/plain" + if OpenTox::Authorization.authenticate(params[:username], params[:password]) + return OpenTox::RestClientWrapper.subjectid + else + return nil + end + else + bad_request_error "'#{@accept}' is not a supported content type." + end +end + +post "/aa/logout/?" do + mime_types = ["text/plain"] + bad_request_error "Mime type #{@accept} not supported here. Please request data as #{mime_types.join(', ')}." unless mime_types.include? @accept + bad_request_error "Please send formdata subjectid." unless params[:subjectid] + case @accept + when "text/plain" + if OpenTox::Authorization.logout(params[:subjectid]) + return "Successfully logged out. \n" + else + return "Logout failed.\n" + end + else + bad_request_error "'#{@accept}' is not a supported content type." + end +end + +module OpenTox + + AA = "https://opensso.in-silico.ch" + + module Authorization + #Authentication against OpenSSO. Returns token. Requires Username and Password. + # @param user [String] Username + # @param pw [String] Password + # @return [Boolean] true if successful + def self.authenticate(user, pw) + begin + res = RestClientWrapper.post("#{AA}/auth/authenticate",{:username=>user, :password => pw},{:subjectid => ""}).sub("token.id=","").sub("\n","") + if is_token_valid(res) + RestClientWrapper.subjectid = res + return true + else + bad_request_error "Authentication failed #{res.inspect}" + end + rescue + bad_request_error "Authentication failed #{res.inspect}" + end + end + + #Logout on opensso. Make token invalid. Requires token + # @param [String] subjectid the subjectid + # @return [Boolean] true if logout is OK + def self.logout(subjectid=RestClientWrapper.subjectid) + begin + out = RestClientWrapper.post("#{AA}/auth/logout", :subjectid => subjectid) + return true unless is_token_valid(subjectid) + rescue + return false + end + return false + end + + #Checks if a token is a valid token + # @param [String]subjectid subjectid from openSSO session + # @return [Boolean] subjectid is valid or not. + def self.is_token_valid(subjectid=RestClientWrapper.subjectid) + begin + return true if RestClientWrapper.post("#{AA}/auth/isTokenValid",:tokenid => subjectid) == "boolean=true\n" + rescue #do rescue because openSSO throws 401 + return false + end + return false + end + end +end \ No newline at end of file diff --git a/lib/api.rb b/lib/api.rb new file mode 100644 index 0000000..28e33df --- /dev/null +++ b/lib/api.rb @@ -0,0 +1,9 @@ +# route to swagger API file +get "/api/api.json" do + response['Content-Type'] = "application/json" + api_file = File.join("api", "api.json") + bad_request_error "API Documentation in Swagger JSON is not implemented." unless File.exists?(api_file) + api_hash = JSON.parse(File.read(api_file)) + api_hash["host"] = request.env['HTTP_HOST'] + return api_hash.to_json +end diff --git a/lib/compound.rb b/lib/compound.rb new file mode 100644 index 0000000..01ba036 --- /dev/null +++ b/lib/compound.rb @@ -0,0 +1,64 @@ +# Get a list of a single or all descriptors +# @param [Header] Accept one of text/plain, application/json +# @param [Path] Descriptor name or descriptor ID (e.G.: Openbabel.HBA1, 5755f8eb3cf99a00d8fedf2f) +# @return [text/plain, application/json] list of all prediction models +get "/compound/descriptor/?:descriptor?" do + case @accept + when "application/json" + return "#{JSON.pretty_generate PhysChem::DESCRIPTORS} " unless params[:descriptor] + return PhysChem.find_by(:name => params[:descriptor]).to_json if PhysChem::DESCRIPTORS.include?(params[:descriptor]) + return PhysChem.find(params[:descriptor]).to_json if PhysChem.find(params[:descriptor]) + else + return PhysChem::DESCRIPTORS.collect{|k, v| "#{k}: #{v}\n"} unless params[:descriptor] + return PhysChem::DESCRIPTORS[params[:descriptor]] if PhysChem::DESCRIPTORS.include?(params[:descriptor]) + return "#{PhysChem.find(params[:descriptor]).name}: #{PhysChem.find(params[:descriptor]).description}" if PhysChem.find(params[:descriptor]) + end +end + +post "/compound/descriptor/?" do + bad_request_error "Missing Parameter " unless params[:identifier] && params[:descriptor] + descriptors = params['descriptor'].split(',') + compound = Compound.from_smiles params[:identifier] + physchem_descriptors = [] + descriptors.each do |descriptor| + physchem_descriptors << PhysChem.find_by(:name => descriptor) + end + result = compound.calculate_properties physchem_descriptors + csv = (0..result.size-1).collect{|i| "\"#{physchem_descriptors[i].name}\",#{result[i]}"}.join("\n") + csv = "SMILES,\"#{params[:identifier]}\"\n#{csv}" if params[:identifier] + case @accept + when "text/csv","application/csv" + return csv + when "application/json" + result_hash = (0..result.size-1).collect{|i| {"#{physchem_descriptors[i].name}" => "#{result[i]}"}} + data = {"compound" => {"SMILES" => "#{params[:identifier]}"}} + data["compound"]["InChI"] = "#{compound.inchi}" if compound.inchi + data["compound"]["results"] = result_hash + return JSON.pretty_generate(data) + end +end + +get %r{/compound/(.+)} do |inchi| + bad_request_error "Input parameter #{inchi} is not an InChI" unless inchi.match(/^InChI=/) + compound = Compound.from_inchi URI.unescape(inchi) + response['Content-Type'] = @accept + case @accept + when "application/json" + return JSON.pretty_generate JSON.parse(compound.to_json) + when "chemical/x-daylight-smiles" + return compound.smiles + when "chemical/x-inchi" + return compound.inchi + when "chemical/x-mdl-sdfile" + return compound.sdf + when "chemical/x-mdl-molfile" + when "image/png" + return compound.png + when "image/svg+xml" + return compound.svg + when "text/plain" + return "#{compound.names}\n" + else + return compound.inspect + end +end \ No newline at end of file diff --git a/lib/dataset.rb b/lib/dataset.rb new file mode 100644 index 0000000..7c74f39 --- /dev/null +++ b/lib/dataset.rb @@ -0,0 +1,46 @@ +# Get all datasets +get "/dataset/?" do + datasets = Dataset.all + case @accept + when "text/uri-list" + uri_list = datasets.collect{|dataset| uri("/dataset/#{dataset.id}")} + return uri_list.join("\n") + "\n" + when "application/json" + datasets = JSON.parse datasets.to_json + list = [] + datasets.each{|d| list << uri("/dataset/#{d["_id"]["$oid"]}")} + return list.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +# Get a dataset +get "/dataset/:id/?" do + dataset = Dataset.find :id => params[:id] + not_found_error "Dataset with id: #{params[:id]} not found." unless dataset + case @accept + when "application/json" + dataset.data_entries.each do |k, v| + dataset.data_entries[k][:URI] = uri("/substance/#{k}") + end + dataset[:URI] = uri("/dataset/#{dataset.id}") + dataset[:substances] = uri("/dataset/#{dataset.id}/substances") + dataset[:features] = uri("/dataset/#{dataset.id}/features") + return dataset.to_json + when "text/csv", "application/csv" + return dataset.to_csv + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +# Get a dataset attribute. One of compounds, nanoparticles, substances, features +get "/dataset/:id/:attribute/?" do + dataset = Dataset.find :id => params[:id] + not_found_error "Dataset with id: #{params[:id]} not found." unless dataset + attribs = ["compounds", "nanoparticles", "substances", "features"] + return "Attribute '#{params[:attribute]}' is not available. Choose one of #{attribs.join(', ')}." unless attribs.include? params[:attribute] + out = dataset.send("#{params[:attribute]}") + return out.to_json +end diff --git a/lib/feature.rb b/lib/feature.rb new file mode 100644 index 0000000..06a5b37 --- /dev/null +++ b/lib/feature.rb @@ -0,0 +1,29 @@ +# Get all Features +get "/feature/?" do + features = Feature.all + case @accept + when "text/uri-list" + uri_list = features.collect{|feature| uri("/feature/#{feature.id}")} + return uri_list.join("\n") + "\n" + when "application/json" + features = JSON.parse features.to_json + list = [] + features.each{|f| list << uri("/feature/#{f["_id"]["$oid"]}")} + return list.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +# Get a feature +get "/feature/:id/?" do + case @accept + when "application/json" + feature = Feature.find :id => params[:id] + not_found_error "Feature with id: #{params[:id]} not found." unless feature + feature[:URI] = uri("/feature/#{feature.id}") + return feature.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end diff --git a/lib/lazar-rest.rb b/lib/lazar-rest.rb new file mode 100644 index 0000000..255c52f --- /dev/null +++ b/lib/lazar-rest.rb @@ -0,0 +1,69 @@ +require "sinatra" +require "sinatra/reloader" +require 'sinatra/cross_origin' + +configure do + $logger = Logger.new(STDOUT) + enable :reloader #if development? + enable :cross_origin + disable :show_exceptions + disable :raise_errors +end + +#set :protection, :except => :frame_options + +# Environment setup from unicorn -E param +ENV["LAZAR_ENV"] = ENV["RACK_ENV"] +require "../lazar/lib/lazar.rb" +require "../qsar-report/lib/qsar-report.rb" +=begin +if ENV["LAZAR_ENV"] == "development" + require "../lazar/lib/lazar.rb" + require "../qsar-report/lib/qsar-report.rb" +else + require "lazar" + require "qsar-report" +end +=end + +include OpenTox + +before do + @accept = request.env['HTTP_ACCEPT'] + response['Content-Type'] = @accept +end + +not_found do + 400 + "Path '#{request.env["REQUEST_PATH"]}' not found.\n" +end + +error do + response['Content-Type'] = "text/plain" + error = request.env['sinatra.error'] + body = error.message+"\n" + error.respond_to?(:http_code) ? code = error.http_code : code = 500 + halt code, body +end + +# https://github.com/britg/sinatra-cross_origin#responding-to-options +options "*" do + response.headers["Allow"] = "HEAD,GET,PUT,POST,DELETE,OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept" + 200 +end + +[ + "aa.rb", + "api.rb", + "compound.rb", + "dataset.rb", + "feature.rb", + "model.rb", + "nanoparticle.rb", + "report.rb", + "substance.rb", + "swagger.rb", + "validation.rb" +].each{ |f| require_relative f } + diff --git a/lib/model.rb b/lib/model.rb new file mode 100644 index 0000000..9fbd90f --- /dev/null +++ b/lib/model.rb @@ -0,0 +1,38 @@ + +# Get a list of all prediction models +# @param [Header] Accept one of text/uri-list, +# @return [text/uri-list] list of all prediction models +get "/model/?" do + models = Model::Validation.all + case @accept + when "text/uri-list" + uri_list = models.collect{|model| uri("/model/#{model.id}")} + return uri_list.join("\n") + "\n" + when "application/json" + models = JSON.parse models.to_json + list = [] + models.each{|m| list << uri("/model/#{m["_id"]["$oid"]}")} + return list.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +get "/model/:id/?" do + model = Model::Validation.find params[:id] + not_found_error "Model with id: #{params[:id]} not found." unless model + return model.to_json +end + + +post "/model/:id/?" do + identifier = params[:identifier].split(",") + compounds = identifier.collect{ |i| Compound.from_smiles i.strip } + model = Model::Validation.find params[:id] + batch = {} + compounds.each do |compound| + prediction = model.predict(compound) + batch[compound] = {:id => compound.id, :inchi => compound.inchi, :smiles => compound.smiles, :model => model, :prediction => prediction} + end + return batch.to_json +end diff --git a/lib/nanoparticle.rb b/lib/nanoparticle.rb new file mode 100644 index 0000000..332493d --- /dev/null +++ b/lib/nanoparticle.rb @@ -0,0 +1,30 @@ +# Get all Nanoparticles +get "/nanoparticle/?" do + nanoparticles = Nanoparticle.all + case @accept + when "text/uri-list" + uri_list = nanoparticles.collect{|nanoparticle| uri("/nanoparticle/#{nanoparticle.id}")} + return uri_list.join("\n") + "\n" + when "application/json" + nanoparticles = JSON.parse nanoparticles.to_json + nanoparticles.each_index do |idx| + nanoparticles[idx][:URI] = uri("/nanoparticle/#{nanoparticles[idx]["_id"]["$oid"]}") + end + return nanoparticles.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +# Get a nanoparticle +get "/nanoparticle/:id/?" do + case @accept + when "application/json" + nanoparticle = Nanoparticle.find :id => params[:id] + not_found_error "Nanoparticle with id: #{params[:id]} not found." unless nanoparticle + nanoparticle[:URI] = uri("/nanoparticle/#{nanoparticle.id}") + return nanoparticle.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end diff --git a/lib/report.rb b/lib/report.rb new file mode 100644 index 0000000..f576106 --- /dev/null +++ b/lib/report.rb @@ -0,0 +1,208 @@ +# Get a list of all possible reports to prediction models +# @param [Header] Accept one of text/uri-list, +# @return [text/uri-list] list of all prediction models +get "/report/?" do + models = Model::Validation.all + case @accept + when "text/uri-list" + uri_list = models.collect{|model| uri("/report/#{model.model_id}")} + return uri_list.join("\n") + "\n" + when "application/json" + models = JSON.parse models.to_json + list = [] + models.each{|m| list << uri("/report/#{m["model_id"]["$oid"]}")} + return list.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +get "/report/:id/?" do + case @accept + when "application/xml" + model = Model::Lazar.find params[:id] + not_found_error "Model with id: #{params[:id]} not found." unless model + prediction_model = Model::Validation.find_by :model_id => params[:id] + validation_template = File.join(File.dirname(__FILE__),"../views/model_details.haml") + + if File.directory?("#{File.dirname(__FILE__)}/../../lazar") + lazar_commit = `cd #{File.dirname(__FILE__)}/../../lazar; git rev-parse HEAD`.strip + lazar_commit = "https://github.com/opentox/lazar/tree/#{lazar_commit}" + else + lazar_commit = "https://github.com/opentox/lazar/releases/tag/v#{Gem.loaded_specs["lazar"].version}" + end + + report = OpenTox::QMRFReport.new + + # QSAR Identifier Title 1.1 + report.value "QSAR_title", "Lazar model for #{prediction_model.species} #{prediction_model.endpoint}" + + # Software coding the model 1.3 + report.change_catalog :software_catalog, :firstsoftware, {:name => "lazar", :description => "lazar Lazy Structure- Activity Relationships", :number => "1", :url => "https://lazar.in-silico.ch", :contact => "info@in-silico.ch"} + report.ref_catalog :QSAR_software, :software_catalog, :firstsoftware + + # Date of QMRF 2.1 + report.value "qmrf_date", "#{Time.now.strftime('%d %B %Y')}" + + # QMRF author(s) and contact details 2.1 + report.change_catalog :authors_catalog, :firstauthor, {:name => "Christoph Helma", :affiliation => "in silico toxicology gmbh", :contact => "Rastatterstr. 41, CH-4057 Basel", :email => "info@in-silico.ch", :number => "1", :url => "www.in-silico.ch"} + report.ref_catalog :qmrf_authors, :authors_catalog, :firstauthor + + # Model developer(s) and contact details 2.5 + report.change_catalog :authors_catalog, :modelauthor, {:name => "Christoph Helma", :affiliation => "in silico toxicology gmbh", :contact => "Rastatterstr. 41, CH-4057 Basel", :email => "info@in-silico.ch", :number => "1", :url => "www.in-silico.ch"} + report.ref_catalog :model_authors, :authors_catalog, :modelauthor + + # Date of model development and/or publication 2.6 + report.value "model_date", "#{Time.parse(model.created_at.to_s).strftime('%Y')}" + + # Reference(s) to main scientific papers and/or software package 2.7 + report.change_catalog :publications_catalog, :publications_catalog_1, {:title => "Maunz, Guetlein, Rautenberg, Vorgrimmler, Gebele and Helma (2013), lazar: a modular predictive toxicology framework ", :url => "http://dx.doi.org/10.3389/fphar.2013.00038"} + report.ref_catalog :references, :publications_catalog, :publications_catalog_1 + + # Reference(s) to main scientific papers and/or software package 2.7 + report.change_catalog :publications_catalog, :publications_catalog_2, {:title => "Maunz A and Helma C (2008) Prediction of chemical toxicity with local support vector regression and activity-specific kernels. SAR & QSAR in Environmental Research 19 (5-6), 413-431", :url => "http://dx.doi.org/10.1080/10629360802358430"} + report.ref_catalog :references, :publications_catalog, :publications_catalog_2 + + # Species 3.1 + report.value "model_species", prediction_model.species + + # Endpoint 3.2 + report.change_catalog :endpoints_catalog, :endpoints_catalog_1, {:name => prediction_model.endpoint, :group => ""} + report.ref_catalog :model_endpoint, :endpoints_catalog, :endpoints_catalog_1 + + # Endpoint Units 3.4 + report.value "endpoint_units", "#{prediction_model.unit}" + + model_type = model.class.to_s.gsub('OpenTox::Model::Lazar','') + + # Type of model 4.1 + report.value "algorithm_type", "#{model_type}" + + # Explicit algorithm 4.2 + report.change_catalog :algorithms_catalog, :algorithms_catalog_1, {:definition => "see Helma 2016 and lazar.in-silico.ch, submitted version: #{lazar_commit}", :description => "Neighbor algorithm: #{model.algorithms["similarity"]["method"].gsub('_',' ').titleize}#{(model.algorithms["similarity"][:min] ? ' with similarity > ' + model.algorithms["similarity"][:min].to_s : '')}"} + report.ref_catalog :algorithm_explicit, :algorithms_catalog, :algorithms_catalog_1 + report.change_catalog :algorithms_catalog, :algorithms_catalog_3, {:definition => "see Helma 2016 and lazar.in-silico.ch, submitted version: #{lazar_commit}", :description => "modified k-nearest neighbor #{model_type}"} + report.ref_catalog :algorithm_explicit, :algorithms_catalog, :algorithms_catalog_3 + if model.algorithms["prediction"] + pred_algorithm_params = (model.algorithms["prediction"][:method] == "rf" ? "random forest" : model.algorithms["prediction"][:method]) + end + report.change_catalog :algorithms_catalog, :algorithms_catalog_2, {:definition => "see Helma 2016 and lazar.in-silico.ch, submitted version: #{lazar_commit}", :description => "Prediction algorithm: #{model.algorithms["prediction"].to_s.gsub('OpenTox::Algorithm::','').gsub('_',' ').gsub('.', ' with ')} #{(pred_algorithm_params ? pred_algorithm_params : '')}"} + report.ref_catalog :algorithm_explicit, :algorithms_catalog, :algorithms_catalog_2 + + # Descriptors in the model 4.3 + if model.algorithms["descriptors"][:type] + report.change_catalog :descriptors_catalog, :descriptors_catalog_1, {:description => "", :name => "#{model.algorithms["descriptors"][:type]}", :publication_ref => "", :units => ""} + report.ref_catalog :algorithms_descriptors, :descriptors_catalog, :descriptors_catalog_1 + end + + # Descriptor selection 4.4 + report.value "descriptors_selection", "#{model.algorithms["feature_selection"].gsub('_',' ')} #{model.algorithms["feature_selection"].collect{|k,v| k.to_s + ': ' + v.to_s}.join(', ')}" if model.algorithms["feature_selection"] + + # Algorithm and descriptor generation 4.5 + report.value "descriptors_generation", "exhaustive breadth first search for paths in chemical graphs (simplified MolFea algorithm)" + + # Software name and version for descriptor generation 4.6 + report.change_catalog :software_catalog, :software_catalog_2, {:name => "lazar, submitted version: #{lazar_commit}", :description => "simplified MolFea algorithm", :number => "2", :url => "https://lazar.in-silico.ch", :contact => "info@in-silico.ch"} + report.ref_catalog :descriptors_generation_software, :software_catalog, :software_catalog_2 + + # Chemicals/Descriptors ratio 4.7 + report.value "descriptors_chemicals_ratio", "not applicable (classification based on activities of neighbors, descriptors are used for similarity calculation)" + + # Description of the applicability domain of the model 5.1 + report.value "app_domain_description", " +

+ The applicability domain (AD) of the training set is characterized by + the confidence index of a prediction (high confidence index: close to + the applicability domain of the training set/reliable prediction, low + confidence: far from the applicability domain of the + trainingset/unreliable prediction). The confidence index considers (i) + the similarity and number of neighbors and (ii) contradictory examples + within the neighbors. A formal definition can be found in Helma 2006. +

+

+ The reliability of predictions decreases gradually with increasing + distance from the applicability domain (i.e. decreasing confidence index) +

+ + " + + # Method used to assess the applicability domain 5.2 + report.value "app_domain_method", "see Helma 2006 and Maunz 2008" + + # Software name and version for applicability domain assessment 5.3 + report.change_catalog :software_catalog, :software_catalog_3, {:name => "lazar, submitted version: #{lazar_commit}", :description => "integrated into main lazar algorithm", :number => "3", :url => "https://lazar.in-silico.ch", :contact => "info@in-silico.ch"} + report.ref_catalog :app_domain_software, :software_catalog, :software_catalog_3 + + # Limits of applicability 5.4 + report.value "applicability_limits", "Predictions with low confidence index, unknown substructures and neighbors that might act by different mechanisms" + + # Availability of the training set 6.1 + report.change_attributes "training_set_availability", {:answer => "Yes"} + + # Available information for the training set 6.2 + report.change_attributes "training_set_data", {:cas => "Yes", :chemname => "Yes", :formula => "Yes", :inchi => "Yes", :mol => "Yes", :smiles => "Yes"} + + # Data for each descriptor variable for the training set 6.3 + report.change_attributes "training_set_descriptors", {:answer => "No"} + + # Data for the dependent variable for the training set 6.4 + report.change_attributes "dependent_var_availability", {:answer => "All"} + + # Other information about the training set 6.5 + report.value "other_info", "#{prediction_model.source}" + + # Pre-processing of data before modelling 6.6 + report.value "preprocessing", (model.class == OpenTox::Model::LazarRegression ? "-log10 transformation" : "none") + + # Robustness - Statistics obtained by leave-many-out cross-validation 6.9 + if prediction_model.repeated_crossvalidation + crossvalidations = prediction_model.crossvalidations + out = haml File.read(validation_template), :layout=> false, :locals => {:model => prediction_model} + report.value "lmo", out + end + + # Mechanistic basis of the model 8.1 + report.value "mechanistic_basis"," +

+ Compounds with similar structures (neighbors) are assumed to have + similar activities as the query compound. For the determination of + activity specific similarities only statistically relevant subtructures + (paths) are used. For this reason there is a priori no bias towards + specific mechanistic hypothesis. +

+ + " + + # A priori or a posteriori mechanistic interpretation 8.2 + report.value "mechanistic_basis_comments","a posteriori for individual predictions" + + # Other information about the mechanistic interpretation 8.3 + report.value "mechanistic_basis_info","

Hypothesis about biochemical mechanisms can be derived from individual + predictions by inspecting neighbors and relevant fragments.

+

Neighbors are compounds that are similar in respect to a certain + endpoint and it is likely that compounds with high similarity act by + similar mechanisms as the query compound. Links at the webinterface + prove an easy access to additional experimental data and literature + citations for the neighbors and the query structure.

+

Activating and deactivating parts of the query compound are highlighted + in red and green on the webinterface. Fragments that are unknown (or too + infrequent for statistical evaluation are marked in yellow and + additional statistical information about the individual fragments can be + retrieved. Please note that lazar predictions are based on neighbors and + not on fragments. Fragments and their statistical significance are used + for the calculation of activity specific similarities.

" + + # Bibliography 9.2 + report.ref_catalog :bibliography, :publications_catalog, :publications_catalog_1 + report.ref_catalog :bibliography, :publications_catalog, :publications_catalog_2 + report.change_catalog :publications_catalog, :publications_catalog_3, {:title => "Helma (2006), Lazy structure-activity relationships (lazar) for the prediction of rodent carcinogenicity and Salmonella mutagenicity.", :url => "http://dx.doi.org/10.1007/s11030-005-9001-5"} + report.ref_catalog :bibliography, :publications_catalog, :publications_catalog_3 + + # output + response['Content-Type'] = "application/xml" + return report.to_xml + else + bad_request_error "Mime type #{@accept} is not supported." + end + +end diff --git a/lib/substance.rb b/lib/substance.rb new file mode 100644 index 0000000..fef1b7e --- /dev/null +++ b/lib/substance.rb @@ -0,0 +1,30 @@ +# Get all substances +get "/substance/?" do + substances = Substance.all + case @accept + when "text/uri-list" + uri_list = substances.collect{|substance| uri("/substance/#{substance.id}")} + return uri_list.join("\n") + "\n" + when "application/json" + substances = JSON.parse substances.to_json + substances.each_index do |idx| + substances[idx][:URI] = uri("/substance/#{substances[idx]["_id"]["$oid"]}") + end + return substances.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +# Get a substance +get "/substance/:id/?" do + case @accept + when "application/json" + substance = Substance.find :id => params[:id] + not_found_error "Substance with id: #{params[:id]} not found." unless substance + substance[:URI] = uri("/substance/#{substance.id}") + return substance.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end diff --git a/lib/swagger.rb b/lib/swagger.rb new file mode 100644 index 0000000..efe1c9d --- /dev/null +++ b/lib/swagger.rb @@ -0,0 +1,6 @@ +get "/" do + response['Content-Type'] = "text/html" + index_file = File.join(ENV['HOME'],"swagger-ui/dist/index.html") + bad_request_error "API Documentation in Swagger JSON is not implemented." unless File.exists?(index_file) + File.read(index_file) +end diff --git a/lib/validation.rb b/lib/validation.rb new file mode 100644 index 0000000..fad8a44 --- /dev/null +++ b/lib/validation.rb @@ -0,0 +1,71 @@ +# All available validation types +VALIDATION_TYPES = ["repeatedcrossvalidation", "leaveoneout", "crossvalidation", "regressioncrossvalidation"] + +# Get a list of ayll possible validation types +# @param [Header] Accept one of text/uri-list, application/json +# @return [text/uri-list] URI list of all validation types +get "/validation/?" do + uri_list = VALIDATION_TYPES.collect{|validationtype| uri("/validation/#{validationtype}")} + case @accept + when "text/uri-list" + return uri_list.join("\n") + "\n" + when "application/json" + return uri_list.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +# Get a list of all validations +# @param [Header] Accept one of text/uri-list, application/json +# @param [Path] Validationtype One of "repeatedcrossvalidation", "leaveoneout", "crossvalidation", "regressioncrossvalidation" +# @return [text/uri-list] list of all validations of a validation type +get "/validation/:validationtype/?" do + bad_request_error "There is no such validation type as: #{params[:validationtype]}" unless VALIDATION_TYPES.include? params[:validationtype] + case params[:validationtype] + when "repeatedcrossvalidation" + validations = Validation::RepeatedCrossValidation.all + when "leaveoneout" + validations = Validation::LeaveOneOut.all + when "crossvalidation" + validations = Validation::CrossValidation.all + when "regressioncrossvalidation" + validations = Validation::RegressionCrossValidation.all + end + + case @accept + when "text/uri-list" + uri_list = validations.collect{|validation| uri("/validation/#{params[:validationtype]}/#{validation.id}")} + return uri_list.join("\n") + "\n" + when "application/json" + validations = JSON.parse validations.to_json + validations.each_index do |idx| + validations[idx][:URI] = uri("/validation/#{params[:validationtype]}/#{validations[idx]["_id"]["$oid"]}") + end + return validations.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +# Get validation representation +get "/validation/:validationtype/:id/?" do + bad_request_error "There is no such validation type as: #{params[:validationtype]}" unless VALIDATION_TYPES.include? params[:validationtype] + case params[:validationtype] + when "repeatedcrossvalidation" + validation = Validation::RepeatedCrossValidation.find params[:id] + when "leaveoneout" + validation = Validation::LeaveOneOut.find params[:id] + when "crossvalidation" + validation = Validation::CrossValidation.find params[:id] + when "regressioncrossvalidation" + validation = Validation::RegressionCrossValidation.find params[:id] + end + + not_found_error "#{params[:validationtype]} with id: #{params[:id]} not found." unless validation + #model[:URI] = uri("/model/#{model.id}") + #model[:neighbor_algorithm_parameters][:feature_dataset_uri] = uri("/dataset/#{model[:neighbor_algorithm_parameters][:feature_dataset_id]}") if model[:neighbor_algorithm_parameters][:feature_dataset_id] + #model[:training_dataset_uri] = uri("/dataset/#{model.training_dataset_id}") if model.training_dataset_id + #model[:prediction_feature_uri] = uri("/dataset/#{model.prediction_feature_id}") if model.prediction_feature_id + return validation.to_json +end -- cgit v1.2.3 From 878f014ec6cc808af99af5045bcc1a1143cab8d9 Mon Sep 17 00:00:00 2001 From: gebele Date: Thu, 5 Jul 2018 10:38:55 +0000 Subject: updated with endpoint list; refined error handling; refined prediction input --- lib/endpoint.rb | 23 +++++++++++++++++++++++ lib/model.rb | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 lib/endpoint.rb (limited to 'lib') diff --git a/lib/endpoint.rb b/lib/endpoint.rb new file mode 100644 index 0000000..ef39787 --- /dev/null +++ b/lib/endpoint.rb @@ -0,0 +1,23 @@ +# Get a list of all endpoints +# @param [Header] Accept one of text/uri-list, +# @return [text/uri-list] list of all prediction models +get "/endpoint/?" do + models = Model::Validation.all + endpoints = models.collect{|m| m.endpoint}.uniq + case @accept + when "text/uri-list" + return endpoints.join("\n") + "\n" + when "application/json" + return endpoints.to_json + else + bad_request_error "Mime type #{@accept} is not supported." + end +end + +get "/endpoint/:endpoint/?" do + models = Model::Validation.where(endpoint: params[:endpoint]) + list = [] + models.each{|m| list << {m.species => uri("/model/#{m.id}")} } + not_found_error "Endpoint: #{params[:endpoint]} not found." if models.blank? + return list.to_json +end diff --git a/lib/model.rb b/lib/model.rb index 9fbd90f..3764ee2 100644 --- a/lib/model.rb +++ b/lib/model.rb @@ -27,7 +27,7 @@ end post "/model/:id/?" do identifier = params[:identifier].split(",") - compounds = identifier.collect{ |i| Compound.from_smiles i.strip } + compounds = identifier.collect{ |i| Compound.from_smiles i.strip.gsub(/\A"|"\Z/,'') } model = Model::Validation.find params[:id] batch = {} compounds.each do |compound| -- cgit v1.2.3 From 9750e0309500259e9a56e267ce87984fb5bb5e53 Mon Sep 17 00:00:00 2001 From: gebele Date: Mon, 26 Nov 2018 15:29:26 +0000 Subject: clean out; better response codes; prepare for batch --- lib/aa.rb | 82 -------------------------------- lib/compound.rb | 48 ++++++++++--------- lib/dataset.rb | 122 ++++++++++++++++++++++++++++++++++++++++++------ lib/lazar-rest.rb | 69 --------------------------- lib/model.rb | 131 ++++++++++++++++++++++++++++++++++++++++++++++++---- lib/nanoparticle.rb | 30 ------------ lib/substance.rb | 24 ++++++---- 7 files changed, 270 insertions(+), 236 deletions(-) delete mode 100644 lib/aa.rb delete mode 100644 lib/lazar-rest.rb delete mode 100644 lib/nanoparticle.rb (limited to 'lib') diff --git a/lib/aa.rb b/lib/aa.rb deleted file mode 100644 index 6dfec4b..0000000 --- a/lib/aa.rb +++ /dev/null @@ -1,82 +0,0 @@ -post "/aa/authenticate/?" do - mime_types = ["text/plain"] - bad_request_error "Mime type #{@accept} not supported here. Please request data as #{mime_types.join(', ')}." unless mime_types.include? @accept - bad_request_error "Please send formdata username." unless params[:username] - bad_request_error "Please send formdata password." unless params[:password] - case @accept - when "text/plain" - if OpenTox::Authorization.authenticate(params[:username], params[:password]) - return OpenTox::RestClientWrapper.subjectid - else - return nil - end - else - bad_request_error "'#{@accept}' is not a supported content type." - end -end - -post "/aa/logout/?" do - mime_types = ["text/plain"] - bad_request_error "Mime type #{@accept} not supported here. Please request data as #{mime_types.join(', ')}." unless mime_types.include? @accept - bad_request_error "Please send formdata subjectid." unless params[:subjectid] - case @accept - when "text/plain" - if OpenTox::Authorization.logout(params[:subjectid]) - return "Successfully logged out. \n" - else - return "Logout failed.\n" - end - else - bad_request_error "'#{@accept}' is not a supported content type." - end -end - -module OpenTox - - AA = "https://opensso.in-silico.ch" - - module Authorization - #Authentication against OpenSSO. Returns token. Requires Username and Password. - # @param user [String] Username - # @param pw [String] Password - # @return [Boolean] true if successful - def self.authenticate(user, pw) - begin - res = RestClientWrapper.post("#{AA}/auth/authenticate",{:username=>user, :password => pw},{:subjectid => ""}).sub("token.id=","").sub("\n","") - if is_token_valid(res) - RestClientWrapper.subjectid = res - return true - else - bad_request_error "Authentication failed #{res.inspect}" - end - rescue - bad_request_error "Authentication failed #{res.inspect}" - end - end - - #Logout on opensso. Make token invalid. Requires token - # @param [String] subjectid the subjectid - # @return [Boolean] true if logout is OK - def self.logout(subjectid=RestClientWrapper.subjectid) - begin - out = RestClientWrapper.post("#{AA}/auth/logout", :subjectid => subjectid) - return true unless is_token_valid(subjectid) - rescue - return false - end - return false - end - - #Checks if a token is a valid token - # @param [String]subjectid subjectid from openSSO session - # @return [Boolean] subjectid is valid or not. - def self.is_token_valid(subjectid=RestClientWrapper.subjectid) - begin - return true if RestClientWrapper.post("#{AA}/auth/isTokenValid",:tokenid => subjectid) == "boolean=true\n" - rescue #do rescue because openSSO throws 401 - return false - end - return false - end - end -end \ No newline at end of file diff --git a/lib/compound.rb b/lib/compound.rb index 01ba036..77948ab 100644 --- a/lib/compound.rb +++ b/lib/compound.rb @@ -38,27 +38,31 @@ post "/compound/descriptor/?" do end end -get %r{/compound/(.+)} do |inchi| - bad_request_error "Input parameter #{inchi} is not an InChI" unless inchi.match(/^InChI=/) - compound = Compound.from_inchi URI.unescape(inchi) - response['Content-Type'] = @accept - case @accept - when "application/json" - return JSON.pretty_generate JSON.parse(compound.to_json) - when "chemical/x-daylight-smiles" - return compound.smiles - when "chemical/x-inchi" - return compound.inchi - when "chemical/x-mdl-sdfile" - return compound.sdf - when "chemical/x-mdl-molfile" - when "image/png" - return compound.png - when "image/svg+xml" - return compound.svg - when "text/plain" - return "#{compound.names}\n" +get %r{/compound/(InChI.+)} do |input| + compound = Compound.from_inchi URI.unescape(input) + if compound + response['Content-Type'] = @accept + case @accept + when "application/json" + c = {"compound": {"id": compound.id, "inchi": compound.inchi, "smiles": compound.smiles, "warnings": compound.warnings}} + return JSON.pretty_generate JSON.parse(c.to_json) + when "chemical/x-daylight-smiles" + return compound.smiles + when "chemical/x-inchi" + return compound.inchi + when "chemical/x-mdl-sdfile" + return compound.sdf + when "chemical/x-mdl-molfile" + when "image/png" + return compound.png + when "image/svg+xml" + return compound.svg + #when "text/plain" + #return "#{compound.names}\n" + else + halt 400, "Content type #{@accept} not supported." + end else - return compound.inspect + halt 400, "Compound with #{input} not found.".to_json end -end \ No newline at end of file +end diff --git a/lib/dataset.rb b/lib/dataset.rb index 7c74f39..749167b 100644 --- a/lib/dataset.rb +++ b/lib/dataset.rb @@ -17,28 +17,122 @@ end # Get a dataset get "/dataset/:id/?" do - dataset = Dataset.find :id => params[:id] - not_found_error "Dataset with id: #{params[:id]} not found." unless dataset - case @accept - when "application/json" - dataset.data_entries.each do |k, v| - dataset.data_entries[k][:URI] = uri("/substance/#{k}") + if Task.where(id: params[:id]).exists? + task = Task.find params[:id] + halt 404, "Dataset with id: #{params[:id]} not found." unless task.percent == 100 + $logger.debug task.inspect + response['Content-Type'] = "text/csv" + m = Model::Validation.find task.model_id + dataset = Batch.find task.dataset_id + @ids = dataset.ids + warnings = dataset.warnings.blank? ? nil : dataset.warnings.join("\n") + unless warnings.nil? + @parse = [] + warnings.split("\n").each do |warning| + if warning =~ /^Cannot/ + smi = warning.split("SMILES compound").last.split("at").first + line = warning.split("SMILES compound").last.split("at line").last.split("of").first.strip.to_i + @parse << "Cannot parse SMILES compound#{smi}at line #{line} of #{dataset.source.split("/").last}\n" + end + end + keys_array = [] + warnings.split("\n").each do |warning| + if warning =~ /^Duplicate/ + text = warning.split("ID").first + numbers = warning.split("ID").last.split("and") + keys_array << numbers.collect{|n| n.strip.to_i} + end + end + @dups = {} + keys_array.each do |keys| + keys.each do |key| + @dups[key] = "Duplicate compound at ID #{keys.join(" and ")}\n" + end + end + end + $logger.debug "dups: #{@dups}" + endpoint = "#{m.endpoint}_(#{m.species})" + tempfile = Tempfile.new + header = task.csv + lines = [] + $logger.debug task.predictions + task.predictions[m.id.to_s].each_with_index do |hash,idx| + identifier = hash.keys[0] + prediction_id = hash.values[0] + # add duplicate warning at the end of a line if ID matches + if @dups[idx+1] + if prediction_id.is_a? BSON::ObjectId + if @ids.blank? + lines << "#{idx+1},#{identifier},#{Prediction.find(prediction_id).csv.tr("\n","")},#{@dups[idx+1]}" + else + lines << "#{idx+1},#{@ids[idx]},#{identifier},#{Prediction.find(prediction_id).csv.tr("\n","")},#{@dups[idx+1]}" + end + else + if @ids.blank? + lines << "#{idx+1},#{identifier},\n" + else + lines << "#{idx+1},#{@ids[idx]}#{identifier},\n" + end + end + else + if prediction_id.is_a? BSON::ObjectId + if @ids.blank? + lines << "#{idx+1},#{identifier},#{Prediction.find(prediction_id).csv}" + else + lines << "#{idx+1},#{@ids[idx]},#{identifier},#{Prediction.find(prediction_id).csv}" + end + else + if @ids.blank? + lines << "#{idx+1},#{identifier},\n" + else + lines << "#{idx+1},#{@ids[idx]}#{identifier},\n" + end + end + end end - dataset[:URI] = uri("/dataset/#{dataset.id}") - dataset[:substances] = uri("/dataset/#{dataset.id}/substances") - dataset[:features] = uri("/dataset/#{dataset.id}/features") - return dataset.to_json - when "text/csv", "application/csv" - return dataset.to_csv + (@parse && !@parse.blank?) ? tempfile.write(header+lines.join("")+"\n"+@parse.join("\n")) : tempfile.write(header+lines.join("")) + #tempfile.write(header+lines.join("")) + tempfile.rewind + ######################## +=begin + header = task.csv + lines = [] + task.predictions.each_with_index do |result,idx| + identifier = result[0] + prediction_id = result[1] + prediction = Prediction.find prediction_id + lines << "#{idx+1},#{identifier},#{prediction.csv.tr("\n","")}" + end + return header+lines.join("\n") +=end + return tempfile.read else - bad_request_error "Mime type #{@accept} is not supported." + dataset = Dataset.find :id => params[:id] + halt 400, "Dataset with id: #{params[:id]} not found." unless dataset + case @accept + when "application/json" + dataset.data_entries.each do |k, v| + dataset.data_entries[k][:URI] = uri("/substance/#{k}") + end + dataset[:URI] = uri("/dataset/#{dataset.id}") + dataset[:substances] = uri("/dataset/#{dataset.id}/substances") + dataset[:features] = uri("/dataset/#{dataset.id}/features") + return dataset.to_json + when "text/csv", "application/csv" + return dataset.to_csv + else + bad_request_error "Mime type #{@accept} is not supported." + end end end # Get a dataset attribute. One of compounds, nanoparticles, substances, features get "/dataset/:id/:attribute/?" do + if Task.where(id: params[:id]).exists? + halt 400, "No attributes selection available for dataset with id: #{params[:id]}.".to_json + end dataset = Dataset.find :id => params[:id] - not_found_error "Dataset with id: #{params[:id]} not found." unless dataset + halt 400, "Dataset with id: #{params[:id]} not found." unless dataset attribs = ["compounds", "nanoparticles", "substances", "features"] return "Attribute '#{params[:attribute]}' is not available. Choose one of #{attribs.join(', ')}." unless attribs.include? params[:attribute] out = dataset.send("#{params[:attribute]}") diff --git a/lib/lazar-rest.rb b/lib/lazar-rest.rb deleted file mode 100644 index 255c52f..0000000 --- a/lib/lazar-rest.rb +++ /dev/null @@ -1,69 +0,0 @@ -require "sinatra" -require "sinatra/reloader" -require 'sinatra/cross_origin' - -configure do - $logger = Logger.new(STDOUT) - enable :reloader #if development? - enable :cross_origin - disable :show_exceptions - disable :raise_errors -end - -#set :protection, :except => :frame_options - -# Environment setup from unicorn -E param -ENV["LAZAR_ENV"] = ENV["RACK_ENV"] -require "../lazar/lib/lazar.rb" -require "../qsar-report/lib/qsar-report.rb" -=begin -if ENV["LAZAR_ENV"] == "development" - require "../lazar/lib/lazar.rb" - require "../qsar-report/lib/qsar-report.rb" -else - require "lazar" - require "qsar-report" -end -=end - -include OpenTox - -before do - @accept = request.env['HTTP_ACCEPT'] - response['Content-Type'] = @accept -end - -not_found do - 400 - "Path '#{request.env["REQUEST_PATH"]}' not found.\n" -end - -error do - response['Content-Type'] = "text/plain" - error = request.env['sinatra.error'] - body = error.message+"\n" - error.respond_to?(:http_code) ? code = error.http_code : code = 500 - halt code, body -end - -# https://github.com/britg/sinatra-cross_origin#responding-to-options -options "*" do - response.headers["Allow"] = "HEAD,GET,PUT,POST,DELETE,OPTIONS" - response.headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept" - 200 -end - -[ - "aa.rb", - "api.rb", - "compound.rb", - "dataset.rb", - "feature.rb", - "model.rb", - "nanoparticle.rb", - "report.rb", - "substance.rb", - "swagger.rb", - "validation.rb" -].each{ |f| require_relative f } - diff --git a/lib/model.rb b/lib/model.rb index 3764ee2..42f3a95 100644 --- a/lib/model.rb +++ b/lib/model.rb @@ -1,4 +1,3 @@ - # Get a list of all prediction models # @param [Header] Accept one of text/uri-list, # @return [text/uri-list] list of all prediction models @@ -24,15 +23,129 @@ get "/model/:id/?" do return model.to_json end - post "/model/:id/?" do - identifier = params[:identifier].split(",") - compounds = identifier.collect{ |i| Compound.from_smiles i.strip.gsub(/\A"|"\Z/,'') } - model = Model::Validation.find params[:id] - batch = {} - compounds.each do |compound| + if request.content_type == "application/x-www-form-urlencoded" + identifier = params[:identifier].strip.gsub(/\A"|"\Z/,'') + compound = Compound.from_smiles identifier + model = Model::Validation.find params[:id] prediction = model.predict(compound) - batch[compound] = {:id => compound.id, :inchi => compound.inchi, :smiles => compound.smiles, :model => model, :prediction => prediction} + output = {:compound => {:id => compound.id, :inchi => compound.inchi, :smiles => compound.smiles}, + :model => model, + :prediction => prediction + } + return 200, output.to_json + elsif request.content_type =~ /^multipart\/form-data/ && request.content_length.to_i > 0 + @task = Task.new + @task.save + task = Task.run do + m = Model::Validation.find params[:id] + @task.update_percent(0.1) + dataset = Batch.from_csv_file params[:fileName][:tempfile] + compounds = dataset.compounds + $logger.debug compounds.size + identifiers = dataset.identifiers + ids = dataset.ids + type = (m.regression? ? "Regression" : "Classification") + # add header for regression + if type == "Regression" + unit = (type == "Regression") ? "(#{m.unit})" : "" + converted_unit = (type == "Regression") ? "#{m.unit =~ /\b(mmol\/L)\b/ ? "(mg/L)" : "(mg/kg_bw/day)"}" : "" + if ids.blank? + header = "ID,Input,Endpoint,Unique SMILES,inTrainingSet,Measurements #{unit},Prediction #{unit},Prediction #{converted_unit},"\ + "Prediction Interval Low #{unit},Prediction Interval High #{unit},"\ + "Prediction Interval Low #{converted_unit},Prediction Interval High #{converted_unit},"\ + "inApplicabilityDomain,Note\n" + else + header = "ID,Original ID,Input,Endpoint,Unique SMILES,inTrainingSet,Measurements #{unit},Prediction #{unit},Prediction #{converted_unit},"\ + "Prediction Interval Low #{unit},Prediction Interval High #{unit},"\ + "Prediction Interval Low #{converted_unit},Prediction Interval High #{converted_unit},"\ + "inApplicabilityDomain,Note\n" + end + end + # add header for classification + if type == "Classification" + av = m.prediction_feature.accept_values + if ids.blank? + header = "ID,Input,Endpoint,Unique SMILES,inTrainingSet,Measurements,Prediction,"\ + "predProbability #{av[0]},predProbability #{av[1]},inApplicabilityDomain,Note\n" + else + header = "ID,Original ID,Input,Endpoint,Unique SMILES,inTrainingSet,Measurements,Prediction,"\ + "predProbability #{av[0]},predProbability #{av[1]},inApplicabilityDomain,Note\n" + end + end + # predict compounds + p = 100.0/compounds.size + counter = 1 + predictions = [] + compounds.each_with_index do |cid,idx| + compound = Compound.find cid + #$logger.debug compound.inspect + if Prediction.where(compound: compound.id, model: m.id).exists? + prediction_object = Prediction.find_by(compound: compound.id, model: m.id) + prediction = prediction_object.prediction + prediction_id = prediction_object.id + # in case prediction object was created by single prediction + if prediction_object.csv.blank? + prediction_object[:csv] = prediction_to_csv(m,compound,prediction) + prediction_object.save + end + # identifier + identifier = identifiers[idx] + else + prediction = m.predict(compound) + # save prediction object + prediction_object = Prediction.new + prediction_id = prediction_object.id + prediction_object[:compound] = compound.id + prediction_object[:model] = m.id + # add additionally fields for html representation + unless prediction[:value].blank? || type == "Classification" + prediction[:prediction_value] = "#{prediction[:value].delog10.signif(3)} #{unit}" + prediction["converted_prediction_value"] = "#{compound.mmol_to_mg(prediction[:value].delog10).signif(3)} #{converted_unit}" + end + unless prediction[:prediction_interval].blank? + interval = prediction[:prediction_interval] + prediction[:interval] = "#{interval[1].delog10.signif(3)} - #{interval[0].delog10.signif(3)} #{unit}" + prediction[:converted_interval] = "#{compound.mmol_to_mg(interval[1].delog10).signif(3)} - #{compound.mmol_to_mg(interval[0].delog10).signif(3)} #{converted_unit}" + end + prediction["unit"] = unit + prediction["converted_unit"] = converted_unit + if prediction[:measurements].is_a?(Array) + prediction["measurements_string"] = (type == "Regression") ? prediction[:measurements].collect{|value| "#{value.delog10.signif(3)} #{unit}"} : prediction[:measurements].join("
") + prediction["converted_measurements"] = prediction[:measurements].collect{|value| "#{compound.mmol_to_mg(value.delog10).signif(3)} #{unit =~ /mmol\/L/ ? "(mg/L)" : "(mg/kg_bw/day)"}"} if type == "Regression" + else + output["measurements_string"] = (type == "Regression") ? "#{prediction[:measurements].delog10.signif(3)} #{unit}}" : prediction[:measurements] + output["converted_measurements"] = "#{compound.mmol_to_mg(prediction[:measurements].delog10).signif(3)} #{(unit =~ /\b(mmol\/L)\b/) ? "(mg/L)" : "(mg/kg_bw/day)"}" if type == "Regression" + end + + # store in prediction_object + prediction_object[:prediction] = prediction + prediction_object[:csv] = prediction_to_csv(m,compound,prediction) + prediction_object.save + + # identifier + identifier = identifiers[idx] + end + # collect prediction_object ids with identifier + predictions << {"#{identifier}" => prediction_id} + $logger.debug predictions.inspect + @task.update_percent((counter*p).ceil > 100 ? 100 : (counter*p).ceil) + counter += 1 + end + # write csv + @task[:csv] = header + # write predictions + # save task + # append predictions as last action otherwise they won't save + # mongoid works with shallow copy via #dup + @task[:predictions] = {m.id.to_s => predictions} + @task[:dataset_id] = dataset.id + @task[:model_id] = m.id + @task.save + end#main task + tid = @task.id.to_s + return 202, to("/task/#{tid}").to_json + else + bad_request_error "No accepted content type" end - return batch.to_json end diff --git a/lib/nanoparticle.rb b/lib/nanoparticle.rb deleted file mode 100644 index 332493d..0000000 --- a/lib/nanoparticle.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Get all Nanoparticles -get "/nanoparticle/?" do - nanoparticles = Nanoparticle.all - case @accept - when "text/uri-list" - uri_list = nanoparticles.collect{|nanoparticle| uri("/nanoparticle/#{nanoparticle.id}")} - return uri_list.join("\n") + "\n" - when "application/json" - nanoparticles = JSON.parse nanoparticles.to_json - nanoparticles.each_index do |idx| - nanoparticles[idx][:URI] = uri("/nanoparticle/#{nanoparticles[idx]["_id"]["$oid"]}") - end - return nanoparticles.to_json - else - bad_request_error "Mime type #{@accept} is not supported." - end -end - -# Get a nanoparticle -get "/nanoparticle/:id/?" do - case @accept - when "application/json" - nanoparticle = Nanoparticle.find :id => params[:id] - not_found_error "Nanoparticle with id: #{params[:id]} not found." unless nanoparticle - nanoparticle[:URI] = uri("/nanoparticle/#{nanoparticle.id}") - return nanoparticle.to_json - else - bad_request_error "Mime type #{@accept} is not supported." - end -end diff --git a/lib/substance.rb b/lib/substance.rb index fef1b7e..f493714 100644 --- a/lib/substance.rb +++ b/lib/substance.rb @@ -6,24 +6,28 @@ get "/substance/?" do uri_list = substances.collect{|substance| uri("/substance/#{substance.id}")} return uri_list.join("\n") + "\n" when "application/json" - substances = JSON.parse substances.to_json - substances.each_index do |idx| - substances[idx][:URI] = uri("/substance/#{substances[idx]["_id"]["$oid"]}") - end - return substances.to_json + list = substances.collect{|substance| uri("/substance/#{substance.id}")} + substances = JSON.parse list.to_json + return JSON.pretty_generate substances else bad_request_error "Mime type #{@accept} is not supported." end end -# Get a substance +# Get a substance by ID get "/substance/:id/?" do case @accept when "application/json" - substance = Substance.find :id => params[:id] - not_found_error "Substance with id: #{params[:id]} not found." unless substance - substance[:URI] = uri("/substance/#{substance.id}") - return substance.to_json + mongoid = /^[a-f\d]{24}$/i + halt 400, "Input #{params[:id]} is no valid ID.".to_json unless params[:id].match(mongoid) + substance = Substance.find params[:id] + if substance + out = {"compound": {"id": substance.id, "inchi": substance.inchi, "smiles": substance.smiles, "warnings": substance.warnings}} + response['Content-Type'] = @accept + return JSON.pretty_generate JSON.parse(out.to_json) + else + halt 400, "Substance with ID #{input} not found." + end else bad_request_error "Mime type #{@accept} is not supported." end -- cgit v1.2.3 From 5a3be4190688bc8240327930b3e953b09ecc9d9e Mon Sep 17 00:00:00 2001 From: gebele Date: Tue, 28 May 2019 14:25:52 +0000 Subject: before clean up --- lib/swagger.rb | 1 - 1 file changed, 1 deletion(-) (limited to 'lib') diff --git a/lib/swagger.rb b/lib/swagger.rb index efe1c9d..acb2ad0 100644 --- a/lib/swagger.rb +++ b/lib/swagger.rb @@ -1,6 +1,5 @@ get "/" do response['Content-Type'] = "text/html" index_file = File.join(ENV['HOME'],"swagger-ui/dist/index.html") - bad_request_error "API Documentation in Swagger JSON is not implemented." unless File.exists?(index_file) File.read(index_file) end -- cgit v1.2.3 From 741701df8ff0861b3607a30e9aaf8b8a0c303cdf Mon Sep 17 00:00:00 2001 From: gebele Date: Thu, 13 Jun 2019 15:28:59 +0000 Subject: update with API --- lib/api.rb | 28 ++++++-- lib/compound.rb | 6 +- lib/dataset.rb | 133 ++++--------------------------------- lib/endpoint.rb | 10 +-- lib/feature.rb | 4 +- lib/model.rb | 16 ++--- lib/report.rb | 193 ++---------------------------------------------------- lib/substance.rb | 18 ++--- lib/swagger.rb | 6 +- lib/validation.rb | 24 +++---- 10 files changed, 80 insertions(+), 358 deletions(-) (limited to 'lib') diff --git a/lib/api.rb b/lib/api.rb index 28e33df..c3b27ce 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -1,9 +1,23 @@ -# route to swagger API file -get "/api/api.json" do - response['Content-Type'] = "application/json" +get "/api" do api_file = File.join("api", "api.json") - bad_request_error "API Documentation in Swagger JSON is not implemented." unless File.exists?(api_file) - api_hash = JSON.parse(File.read(api_file)) - api_hash["host"] = request.env['HTTP_HOST'] - return api_hash.to_json + halt 400, "API Documentation in Swagger JSON is not implemented." unless File.exists?(api_file) + case @accept + when "text/html" + response['Content-Type'] = "text/html" + index_file = File.join(ENV['HOME'],"swagger-ui/dist/index.html") + File.read(index_file) + when "application/json" + response['Content-Type'] = "application/json" + api_hash = JSON.parse(File.read(api_file)) + api_hash["host"] = request.env['HTTP_HOST'] + return api_hash.to_json + else + halt 400, "unknown MIME type '#{@accept}'" + end +end + +get "/api/api.json" do + response['Content-Type'] = "text/html" + index_file = File.join(ENV['HOME'],"swagger-ui/dist/index.html") + File.read(index_file) end diff --git a/lib/compound.rb b/lib/compound.rb index 77948ab..4606aa4 100644 --- a/lib/compound.rb +++ b/lib/compound.rb @@ -2,7 +2,7 @@ # @param [Header] Accept one of text/plain, application/json # @param [Path] Descriptor name or descriptor ID (e.G.: Openbabel.HBA1, 5755f8eb3cf99a00d8fedf2f) # @return [text/plain, application/json] list of all prediction models -get "/compound/descriptor/?:descriptor?" do +get "/api/compound/descriptor/?:descriptor?" do case @accept when "application/json" return "#{JSON.pretty_generate PhysChem::DESCRIPTORS} " unless params[:descriptor] @@ -15,7 +15,7 @@ get "/compound/descriptor/?:descriptor?" do end end -post "/compound/descriptor/?" do +post "/api/compound/descriptor/?" do bad_request_error "Missing Parameter " unless params[:identifier] && params[:descriptor] descriptors = params['descriptor'].split(',') compound = Compound.from_smiles params[:identifier] @@ -38,7 +38,7 @@ post "/compound/descriptor/?" do end end -get %r{/compound/(InChI.+)} do |input| +get %r{/api/compound/(InChI.+)} do |input| compound = Compound.from_inchi URI.unescape(input) if compound response['Content-Type'] = @accept diff --git a/lib/dataset.rb b/lib/dataset.rb index 749167b..00685b8 100644 --- a/lib/dataset.rb +++ b/lib/dataset.rb @@ -1,136 +1,29 @@ # Get all datasets -get "/dataset/?" do - datasets = Dataset.all +get "/api/dataset/?" do + datasets = Dataset.all #.limit(100) case @accept - when "text/uri-list" - uri_list = datasets.collect{|dataset| uri("/dataset/#{dataset.id}")} - return uri_list.join("\n") + "\n" when "application/json" - datasets = JSON.parse datasets.to_json - list = [] - datasets.each{|d| list << uri("/dataset/#{d["_id"]["$oid"]}")} - return list.to_json + list = datasets.collect{|dataset| uri("/api/dataset/#{dataset.id}")}.to_json + return list else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end # Get a dataset -get "/dataset/:id/?" do - if Task.where(id: params[:id]).exists? - task = Task.find params[:id] - halt 404, "Dataset with id: #{params[:id]} not found." unless task.percent == 100 - $logger.debug task.inspect - response['Content-Type'] = "text/csv" - m = Model::Validation.find task.model_id - dataset = Batch.find task.dataset_id - @ids = dataset.ids - warnings = dataset.warnings.blank? ? nil : dataset.warnings.join("\n") - unless warnings.nil? - @parse = [] - warnings.split("\n").each do |warning| - if warning =~ /^Cannot/ - smi = warning.split("SMILES compound").last.split("at").first - line = warning.split("SMILES compound").last.split("at line").last.split("of").first.strip.to_i - @parse << "Cannot parse SMILES compound#{smi}at line #{line} of #{dataset.source.split("/").last}\n" - end - end - keys_array = [] - warnings.split("\n").each do |warning| - if warning =~ /^Duplicate/ - text = warning.split("ID").first - numbers = warning.split("ID").last.split("and") - keys_array << numbers.collect{|n| n.strip.to_i} - end - end - @dups = {} - keys_array.each do |keys| - keys.each do |key| - @dups[key] = "Duplicate compound at ID #{keys.join(" and ")}\n" - end - end - end - $logger.debug "dups: #{@dups}" - endpoint = "#{m.endpoint}_(#{m.species})" - tempfile = Tempfile.new - header = task.csv - lines = [] - $logger.debug task.predictions - task.predictions[m.id.to_s].each_with_index do |hash,idx| - identifier = hash.keys[0] - prediction_id = hash.values[0] - # add duplicate warning at the end of a line if ID matches - if @dups[idx+1] - if prediction_id.is_a? BSON::ObjectId - if @ids.blank? - lines << "#{idx+1},#{identifier},#{Prediction.find(prediction_id).csv.tr("\n","")},#{@dups[idx+1]}" - else - lines << "#{idx+1},#{@ids[idx]},#{identifier},#{Prediction.find(prediction_id).csv.tr("\n","")},#{@dups[idx+1]}" - end - else - if @ids.blank? - lines << "#{idx+1},#{identifier},\n" - else - lines << "#{idx+1},#{@ids[idx]}#{identifier},\n" - end - end - else - if prediction_id.is_a? BSON::ObjectId - if @ids.blank? - lines << "#{idx+1},#{identifier},#{Prediction.find(prediction_id).csv}" - else - lines << "#{idx+1},#{@ids[idx]},#{identifier},#{Prediction.find(prediction_id).csv}" - end - else - if @ids.blank? - lines << "#{idx+1},#{identifier},\n" - else - lines << "#{idx+1},#{@ids[idx]}#{identifier},\n" - end - end - end - end - (@parse && !@parse.blank?) ? tempfile.write(header+lines.join("")+"\n"+@parse.join("\n")) : tempfile.write(header+lines.join("")) - #tempfile.write(header+lines.join("")) - tempfile.rewind - ######################## -=begin - header = task.csv - lines = [] - task.predictions.each_with_index do |result,idx| - identifier = result[0] - prediction_id = result[1] - prediction = Prediction.find prediction_id - lines << "#{idx+1},#{identifier},#{prediction.csv.tr("\n","")}" - end - return header+lines.join("\n") -=end - return tempfile.read +get "/api/dataset/:id/?" do + dataset = Dataset.find :id => params[:id] + halt 400, "Dataset with id: #{params[:id]} not found." unless dataset + case @accept + when "text/csv", "application/csv" + return dataset.to_csv else - dataset = Dataset.find :id => params[:id] - halt 400, "Dataset with id: #{params[:id]} not found." unless dataset - case @accept - when "application/json" - dataset.data_entries.each do |k, v| - dataset.data_entries[k][:URI] = uri("/substance/#{k}") - end - dataset[:URI] = uri("/dataset/#{dataset.id}") - dataset[:substances] = uri("/dataset/#{dataset.id}/substances") - dataset[:features] = uri("/dataset/#{dataset.id}/features") - return dataset.to_json - when "text/csv", "application/csv" - return dataset.to_csv - else - bad_request_error "Mime type #{@accept} is not supported." - end + bad_request_error "Mime type #{@accept} is not supported." end end # Get a dataset attribute. One of compounds, nanoparticles, substances, features -get "/dataset/:id/:attribute/?" do - if Task.where(id: params[:id]).exists? - halt 400, "No attributes selection available for dataset with id: #{params[:id]}.".to_json - end +get "/api/dataset/:id/:attribute/?" do dataset = Dataset.find :id => params[:id] halt 400, "Dataset with id: #{params[:id]} not found." unless dataset attribs = ["compounds", "nanoparticles", "substances", "features"] diff --git a/lib/endpoint.rb b/lib/endpoint.rb index ef39787..66b7ab2 100644 --- a/lib/endpoint.rb +++ b/lib/endpoint.rb @@ -1,7 +1,7 @@ # Get a list of all endpoints # @param [Header] Accept one of text/uri-list, # @return [text/uri-list] list of all prediction models -get "/endpoint/?" do +get "/api/endpoint/?" do models = Model::Validation.all endpoints = models.collect{|m| m.endpoint}.uniq case @accept @@ -10,14 +10,14 @@ get "/endpoint/?" do when "application/json" return endpoints.to_json else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end -get "/endpoint/:endpoint/?" do +get "/api/endpoint/:endpoint/?" do models = Model::Validation.where(endpoint: params[:endpoint]) list = [] - models.each{|m| list << {m.species => uri("/model/#{m.id}")} } - not_found_error "Endpoint: #{params[:endpoint]} not found." if models.blank? + models.each{|m| list << {m.species => uri("/api/model/#{m.id}")} } + halt 404, "Endpoint: #{params[:endpoint]} not found." if models.blank? return list.to_json end diff --git a/lib/feature.rb b/lib/feature.rb index 06a5b37..3123997 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -1,5 +1,5 @@ # Get all Features -get "/feature/?" do +get "/api/feature/?" do features = Feature.all case @accept when "text/uri-list" @@ -16,7 +16,7 @@ get "/feature/?" do end # Get a feature -get "/feature/:id/?" do +get "/api/feature/:id/?" do case @accept when "application/json" feature = Feature.find :id => params[:id] diff --git a/lib/model.rb b/lib/model.rb index 42f3a95..9bf4f53 100644 --- a/lib/model.rb +++ b/lib/model.rb @@ -1,29 +1,29 @@ # Get a list of all prediction models # @param [Header] Accept one of text/uri-list, # @return [text/uri-list] list of all prediction models -get "/model/?" do +get "/api/model/?" do models = Model::Validation.all case @accept when "text/uri-list" - uri_list = models.collect{|model| uri("/model/#{model.id}")} + uri_list = models.collect{|model| uri("/api/model/#{model.id}")} return uri_list.join("\n") + "\n" when "application/json" models = JSON.parse models.to_json list = [] - models.each{|m| list << uri("/model/#{m["_id"]["$oid"]}")} + models.each{|m| list << uri("/api/model/#{m["_id"]["$oid"]}")} return list.to_json else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end -get "/model/:id/?" do +get "/api/model/:id/?" do model = Model::Validation.find params[:id] - not_found_error "Model with id: #{params[:id]} not found." unless model + halt 400, "Model with id: #{params[:id]} not found." unless model return model.to_json end -post "/model/:id/?" do +post "/api/model/:id/?" do if request.content_type == "application/x-www-form-urlencoded" identifier = params[:identifier].strip.gsub(/\A"|"\Z/,'') compound = Compound.from_smiles identifier @@ -146,6 +146,6 @@ post "/model/:id/?" do tid = @task.id.to_s return 202, to("/task/#{tid}").to_json else - bad_request_error "No accepted content type" + halt 400, "No accepted content type" end end diff --git a/lib/report.rb b/lib/report.rb index f576106..7c06d60 100644 --- a/lib/report.rb +++ b/lib/report.rb @@ -1,208 +1,29 @@ # Get a list of all possible reports to prediction models # @param [Header] Accept one of text/uri-list, # @return [text/uri-list] list of all prediction models -get "/report/?" do +get "/api/report/?" do models = Model::Validation.all case @accept when "text/uri-list" - uri_list = models.collect{|model| uri("/report/#{model.model_id}")} + uri_list = models.collect{|model| uri("/api/report/#{model.model_id}")} return uri_list.join("\n") + "\n" when "application/json" models = JSON.parse models.to_json list = [] - models.each{|m| list << uri("/report/#{m["model_id"]["$oid"]}")} + models.each{|m| list << uri("/api/report/#{m["_id"]["$oid"]}")} return list.to_json else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end -get "/report/:id/?" do +get "/api/report/:id/?" do case @accept when "application/xml" - model = Model::Lazar.find params[:id] - not_found_error "Model with id: #{params[:id]} not found." unless model - prediction_model = Model::Validation.find_by :model_id => params[:id] - validation_template = File.join(File.dirname(__FILE__),"../views/model_details.haml") - - if File.directory?("#{File.dirname(__FILE__)}/../../lazar") - lazar_commit = `cd #{File.dirname(__FILE__)}/../../lazar; git rev-parse HEAD`.strip - lazar_commit = "https://github.com/opentox/lazar/tree/#{lazar_commit}" - else - lazar_commit = "https://github.com/opentox/lazar/releases/tag/v#{Gem.loaded_specs["lazar"].version}" - end - - report = OpenTox::QMRFReport.new - - # QSAR Identifier Title 1.1 - report.value "QSAR_title", "Lazar model for #{prediction_model.species} #{prediction_model.endpoint}" - - # Software coding the model 1.3 - report.change_catalog :software_catalog, :firstsoftware, {:name => "lazar", :description => "lazar Lazy Structure- Activity Relationships", :number => "1", :url => "https://lazar.in-silico.ch", :contact => "info@in-silico.ch"} - report.ref_catalog :QSAR_software, :software_catalog, :firstsoftware - - # Date of QMRF 2.1 - report.value "qmrf_date", "#{Time.now.strftime('%d %B %Y')}" - - # QMRF author(s) and contact details 2.1 - report.change_catalog :authors_catalog, :firstauthor, {:name => "Christoph Helma", :affiliation => "in silico toxicology gmbh", :contact => "Rastatterstr. 41, CH-4057 Basel", :email => "info@in-silico.ch", :number => "1", :url => "www.in-silico.ch"} - report.ref_catalog :qmrf_authors, :authors_catalog, :firstauthor - - # Model developer(s) and contact details 2.5 - report.change_catalog :authors_catalog, :modelauthor, {:name => "Christoph Helma", :affiliation => "in silico toxicology gmbh", :contact => "Rastatterstr. 41, CH-4057 Basel", :email => "info@in-silico.ch", :number => "1", :url => "www.in-silico.ch"} - report.ref_catalog :model_authors, :authors_catalog, :modelauthor - - # Date of model development and/or publication 2.6 - report.value "model_date", "#{Time.parse(model.created_at.to_s).strftime('%Y')}" - - # Reference(s) to main scientific papers and/or software package 2.7 - report.change_catalog :publications_catalog, :publications_catalog_1, {:title => "Maunz, Guetlein, Rautenberg, Vorgrimmler, Gebele and Helma (2013), lazar: a modular predictive toxicology framework ", :url => "http://dx.doi.org/10.3389/fphar.2013.00038"} - report.ref_catalog :references, :publications_catalog, :publications_catalog_1 - - # Reference(s) to main scientific papers and/or software package 2.7 - report.change_catalog :publications_catalog, :publications_catalog_2, {:title => "Maunz A and Helma C (2008) Prediction of chemical toxicity with local support vector regression and activity-specific kernels. SAR & QSAR in Environmental Research 19 (5-6), 413-431", :url => "http://dx.doi.org/10.1080/10629360802358430"} - report.ref_catalog :references, :publications_catalog, :publications_catalog_2 - - # Species 3.1 - report.value "model_species", prediction_model.species - - # Endpoint 3.2 - report.change_catalog :endpoints_catalog, :endpoints_catalog_1, {:name => prediction_model.endpoint, :group => ""} - report.ref_catalog :model_endpoint, :endpoints_catalog, :endpoints_catalog_1 - - # Endpoint Units 3.4 - report.value "endpoint_units", "#{prediction_model.unit}" - - model_type = model.class.to_s.gsub('OpenTox::Model::Lazar','') - - # Type of model 4.1 - report.value "algorithm_type", "#{model_type}" - - # Explicit algorithm 4.2 - report.change_catalog :algorithms_catalog, :algorithms_catalog_1, {:definition => "see Helma 2016 and lazar.in-silico.ch, submitted version: #{lazar_commit}", :description => "Neighbor algorithm: #{model.algorithms["similarity"]["method"].gsub('_',' ').titleize}#{(model.algorithms["similarity"][:min] ? ' with similarity > ' + model.algorithms["similarity"][:min].to_s : '')}"} - report.ref_catalog :algorithm_explicit, :algorithms_catalog, :algorithms_catalog_1 - report.change_catalog :algorithms_catalog, :algorithms_catalog_3, {:definition => "see Helma 2016 and lazar.in-silico.ch, submitted version: #{lazar_commit}", :description => "modified k-nearest neighbor #{model_type}"} - report.ref_catalog :algorithm_explicit, :algorithms_catalog, :algorithms_catalog_3 - if model.algorithms["prediction"] - pred_algorithm_params = (model.algorithms["prediction"][:method] == "rf" ? "random forest" : model.algorithms["prediction"][:method]) - end - report.change_catalog :algorithms_catalog, :algorithms_catalog_2, {:definition => "see Helma 2016 and lazar.in-silico.ch, submitted version: #{lazar_commit}", :description => "Prediction algorithm: #{model.algorithms["prediction"].to_s.gsub('OpenTox::Algorithm::','').gsub('_',' ').gsub('.', ' with ')} #{(pred_algorithm_params ? pred_algorithm_params : '')}"} - report.ref_catalog :algorithm_explicit, :algorithms_catalog, :algorithms_catalog_2 - - # Descriptors in the model 4.3 - if model.algorithms["descriptors"][:type] - report.change_catalog :descriptors_catalog, :descriptors_catalog_1, {:description => "", :name => "#{model.algorithms["descriptors"][:type]}", :publication_ref => "", :units => ""} - report.ref_catalog :algorithms_descriptors, :descriptors_catalog, :descriptors_catalog_1 - end - - # Descriptor selection 4.4 - report.value "descriptors_selection", "#{model.algorithms["feature_selection"].gsub('_',' ')} #{model.algorithms["feature_selection"].collect{|k,v| k.to_s + ': ' + v.to_s}.join(', ')}" if model.algorithms["feature_selection"] - - # Algorithm and descriptor generation 4.5 - report.value "descriptors_generation", "exhaustive breadth first search for paths in chemical graphs (simplified MolFea algorithm)" - - # Software name and version for descriptor generation 4.6 - report.change_catalog :software_catalog, :software_catalog_2, {:name => "lazar, submitted version: #{lazar_commit}", :description => "simplified MolFea algorithm", :number => "2", :url => "https://lazar.in-silico.ch", :contact => "info@in-silico.ch"} - report.ref_catalog :descriptors_generation_software, :software_catalog, :software_catalog_2 - - # Chemicals/Descriptors ratio 4.7 - report.value "descriptors_chemicals_ratio", "not applicable (classification based on activities of neighbors, descriptors are used for similarity calculation)" - - # Description of the applicability domain of the model 5.1 - report.value "app_domain_description", " -

- The applicability domain (AD) of the training set is characterized by - the confidence index of a prediction (high confidence index: close to - the applicability domain of the training set/reliable prediction, low - confidence: far from the applicability domain of the - trainingset/unreliable prediction). The confidence index considers (i) - the similarity and number of neighbors and (ii) contradictory examples - within the neighbors. A formal definition can be found in Helma 2006. -

-

- The reliability of predictions decreases gradually with increasing - distance from the applicability domain (i.e. decreasing confidence index) -

- - " - - # Method used to assess the applicability domain 5.2 - report.value "app_domain_method", "see Helma 2006 and Maunz 2008" - - # Software name and version for applicability domain assessment 5.3 - report.change_catalog :software_catalog, :software_catalog_3, {:name => "lazar, submitted version: #{lazar_commit}", :description => "integrated into main lazar algorithm", :number => "3", :url => "https://lazar.in-silico.ch", :contact => "info@in-silico.ch"} - report.ref_catalog :app_domain_software, :software_catalog, :software_catalog_3 - - # Limits of applicability 5.4 - report.value "applicability_limits", "Predictions with low confidence index, unknown substructures and neighbors that might act by different mechanisms" - - # Availability of the training set 6.1 - report.change_attributes "training_set_availability", {:answer => "Yes"} - - # Available information for the training set 6.2 - report.change_attributes "training_set_data", {:cas => "Yes", :chemname => "Yes", :formula => "Yes", :inchi => "Yes", :mol => "Yes", :smiles => "Yes"} - - # Data for each descriptor variable for the training set 6.3 - report.change_attributes "training_set_descriptors", {:answer => "No"} - - # Data for the dependent variable for the training set 6.4 - report.change_attributes "dependent_var_availability", {:answer => "All"} - - # Other information about the training set 6.5 - report.value "other_info", "#{prediction_model.source}" - - # Pre-processing of data before modelling 6.6 - report.value "preprocessing", (model.class == OpenTox::Model::LazarRegression ? "-log10 transformation" : "none") - - # Robustness - Statistics obtained by leave-many-out cross-validation 6.9 - if prediction_model.repeated_crossvalidation - crossvalidations = prediction_model.crossvalidations - out = haml File.read(validation_template), :layout=> false, :locals => {:model => prediction_model} - report.value "lmo", out - end - - # Mechanistic basis of the model 8.1 - report.value "mechanistic_basis"," -

- Compounds with similar structures (neighbors) are assumed to have - similar activities as the query compound. For the determination of - activity specific similarities only statistically relevant subtructures - (paths) are used. For this reason there is a priori no bias towards - specific mechanistic hypothesis. -

- - " - - # A priori or a posteriori mechanistic interpretation 8.2 - report.value "mechanistic_basis_comments","a posteriori for individual predictions" - - # Other information about the mechanistic interpretation 8.3 - report.value "mechanistic_basis_info","

Hypothesis about biochemical mechanisms can be derived from individual - predictions by inspecting neighbors and relevant fragments.

-

Neighbors are compounds that are similar in respect to a certain - endpoint and it is likely that compounds with high similarity act by - similar mechanisms as the query compound. Links at the webinterface - prove an easy access to additional experimental data and literature - citations for the neighbors and the query structure.

-

Activating and deactivating parts of the query compound are highlighted - in red and green on the webinterface. Fragments that are unknown (or too - infrequent for statistical evaluation are marked in yellow and - additional statistical information about the individual fragments can be - retrieved. Please note that lazar predictions are based on neighbors and - not on fragments. Fragments and their statistical significance are used - for the calculation of activity specific similarities.

" - - # Bibliography 9.2 - report.ref_catalog :bibliography, :publications_catalog, :publications_catalog_1 - report.ref_catalog :bibliography, :publications_catalog, :publications_catalog_2 - report.change_catalog :publications_catalog, :publications_catalog_3, {:title => "Helma (2006), Lazy structure-activity relationships (lazar) for the prediction of rodent carcinogenicity and Salmonella mutagenicity.", :url => "http://dx.doi.org/10.1007/s11030-005-9001-5"} - report.ref_catalog :bibliography, :publications_catalog, :publications_catalog_3 - - # output - response['Content-Type'] = "application/xml" + report = qmrf_report params[:id] return report.to_xml else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end diff --git a/lib/substance.rb b/lib/substance.rb index f493714..5d57505 100644 --- a/lib/substance.rb +++ b/lib/substance.rb @@ -1,5 +1,5 @@ # Get all substances -get "/substance/?" do +get "/api/substance/?" do substances = Substance.all case @accept when "text/uri-list" @@ -10,25 +10,25 @@ get "/substance/?" do substances = JSON.parse list.to_json return JSON.pretty_generate substances else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end # Get a substance by ID -get "/substance/:id/?" do +get "/api/substance/:id/?" do case @accept when "application/json" - mongoid = /^[a-f\d]{24}$/i - halt 400, "Input #{params[:id]} is no valid ID.".to_json unless params[:id].match(mongoid) substance = Substance.find params[:id] if substance - out = {"compound": {"id": substance.id, "inchi": substance.inchi, "smiles": substance.smiles, "warnings": substance.warnings}} - response['Content-Type'] = @accept + out = {"compound": {"id": substance.id, + "inchi": substance.inchi, + "smiles": substance.smiles + }} return JSON.pretty_generate JSON.parse(out.to_json) else - halt 400, "Substance with ID #{input} not found." + halt 400, "Substance with ID #{params[:id]} not found." end else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end diff --git a/lib/swagger.rb b/lib/swagger.rb index acb2ad0..2c3ea87 100644 --- a/lib/swagger.rb +++ b/lib/swagger.rb @@ -1,5 +1,3 @@ -get "/" do - response['Content-Type'] = "text/html" - index_file = File.join(ENV['HOME'],"swagger-ui/dist/index.html") - File.read(index_file) +get "/swagger" do + redirect("/api") end diff --git a/lib/validation.rb b/lib/validation.rb index fad8a44..031b9e1 100644 --- a/lib/validation.rb +++ b/lib/validation.rb @@ -4,7 +4,7 @@ VALIDATION_TYPES = ["repeatedcrossvalidation", "leaveoneout", "crossvalidation", # Get a list of ayll possible validation types # @param [Header] Accept one of text/uri-list, application/json # @return [text/uri-list] URI list of all validation types -get "/validation/?" do +get "/api/validation/?" do uri_list = VALIDATION_TYPES.collect{|validationtype| uri("/validation/#{validationtype}")} case @accept when "text/uri-list" @@ -12,7 +12,7 @@ get "/validation/?" do when "application/json" return uri_list.to_json else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end @@ -20,8 +20,8 @@ end # @param [Header] Accept one of text/uri-list, application/json # @param [Path] Validationtype One of "repeatedcrossvalidation", "leaveoneout", "crossvalidation", "regressioncrossvalidation" # @return [text/uri-list] list of all validations of a validation type -get "/validation/:validationtype/?" do - bad_request_error "There is no such validation type as: #{params[:validationtype]}" unless VALIDATION_TYPES.include? params[:validationtype] +get "/api/validation/:validationtype/?" do + halt 400, "There is no such validation type as: #{params[:validationtype]}" unless VALIDATION_TYPES.include? params[:validationtype] case params[:validationtype] when "repeatedcrossvalidation" validations = Validation::RepeatedCrossValidation.all @@ -35,22 +35,22 @@ get "/validation/:validationtype/?" do case @accept when "text/uri-list" - uri_list = validations.collect{|validation| uri("/validation/#{params[:validationtype]}/#{validation.id}")} + uri_list = validations.collect{|validation| uri("/api/validation/#{params[:validationtype]}/#{validation.id}")} return uri_list.join("\n") + "\n" when "application/json" validations = JSON.parse validations.to_json validations.each_index do |idx| - validations[idx][:URI] = uri("/validation/#{params[:validationtype]}/#{validations[idx]["_id"]["$oid"]}") + validations[idx][:URI] = uri("/api/validation/#{params[:validationtype]}/#{validations[idx]["_id"]["$oid"]}") end return validations.to_json else - bad_request_error "Mime type #{@accept} is not supported." + halt 400, "Mime type #{@accept} is not supported." end end # Get validation representation -get "/validation/:validationtype/:id/?" do - bad_request_error "There is no such validation type as: #{params[:validationtype]}" unless VALIDATION_TYPES.include? params[:validationtype] +get "/api/validation/:validationtype/:id/?" do + halt 400, "There is no such validation type as: #{params[:validationtype]}" unless VALIDATION_TYPES.include? params[:validationtype] case params[:validationtype] when "repeatedcrossvalidation" validation = Validation::RepeatedCrossValidation.find params[:id] @@ -62,10 +62,6 @@ get "/validation/:validationtype/:id/?" do validation = Validation::RegressionCrossValidation.find params[:id] end - not_found_error "#{params[:validationtype]} with id: #{params[:id]} not found." unless validation - #model[:URI] = uri("/model/#{model.id}") - #model[:neighbor_algorithm_parameters][:feature_dataset_uri] = uri("/dataset/#{model[:neighbor_algorithm_parameters][:feature_dataset_id]}") if model[:neighbor_algorithm_parameters][:feature_dataset_id] - #model[:training_dataset_uri] = uri("/dataset/#{model.training_dataset_id}") if model.training_dataset_id - #model[:prediction_feature_uri] = uri("/dataset/#{model.prediction_feature_id}") if model.prediction_feature_id + halt 404, "#{params[:validationtype]} with id: #{params[:id]} not found." unless validation return validation.to_json end -- cgit v1.2.3 From 3869670b3acfb4de982f45ea24af940eccac5474 Mon Sep 17 00:00:00 2001 From: gebele Date: Tue, 18 Jun 2019 12:49:35 +0000 Subject: update routes and mime type and generate server uri for API file --- lib/api.rb | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/api.rb b/lib/api.rb index c3b27ce..dffa9d7 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -1,23 +1,29 @@ get "/api" do api_file = File.join("api", "api.json") + `sed -i 's/SERVER_URI/#{request.env['HTTP_HOST']}/' #{api_file}` halt 400, "API Documentation in Swagger JSON is not implemented." unless File.exists?(api_file) case @accept when "text/html" response['Content-Type'] = "text/html" index_file = File.join(ENV['HOME'],"swagger-ui/dist/index.html") - File.read(index_file) + return File.read(index_file) when "application/json" - response['Content-Type'] = "application/json" - api_hash = JSON.parse(File.read(api_file)) - api_hash["host"] = request.env['HTTP_HOST'] - return api_hash.to_json + redirect("/api/api.json") else halt 400, "unknown MIME type '#{@accept}'" end end get "/api/api.json" do - response['Content-Type'] = "text/html" - index_file = File.join(ENV['HOME'],"swagger-ui/dist/index.html") - File.read(index_file) + api_file = File.join("api", "api.json") + `sed -i 's/SERVER_URI/#{request.env['HTTP_HOST']}/' #{api_file}` + case @accept + when "text/html" + response['Content-Type'] = "application/json" + return File.read(api_file) + when "application/json" + return File.read(api_file) + else + halt 400, "unknown MIME type '#{@accept}'" + end end -- cgit v1.2.3 From bba7061d7ff2420f4eb2b6f88362edb71bc6bf25 Mon Sep 17 00:00:00 2001 From: gebele Date: Tue, 18 Jun 2019 15:04:55 +0000 Subject: return training dataset from source --- lib/dataset.rb | 2 +- lib/model.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/dataset.rb b/lib/dataset.rb index 00685b8..51407ca 100644 --- a/lib/dataset.rb +++ b/lib/dataset.rb @@ -16,7 +16,7 @@ get "/api/dataset/:id/?" do halt 400, "Dataset with id: #{params[:id]} not found." unless dataset case @accept when "text/csv", "application/csv" - return dataset.to_csv + return File.read dataset.source else bad_request_error "Mime type #{@accept} is not supported." end diff --git a/lib/model.rb b/lib/model.rb index 9bf4f53..b26447d 100644 --- a/lib/model.rb +++ b/lib/model.rb @@ -20,6 +20,7 @@ end get "/api/model/:id/?" do model = Model::Validation.find params[:id] halt 400, "Model with id: #{params[:id]} not found." unless model + model["training_dataset"] = model.model.training_dataset.id.to_s return model.to_json end -- cgit v1.2.3 From 1546e77e9b2796f20215caa23f06822c96be27ee Mon Sep 17 00:00:00 2001 From: gebele Date: Mon, 5 Aug 2019 09:48:54 +0000 Subject: ensure xhr requests are https to avoid mixed content issue in browsers --- lib/model.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/model.rb b/lib/model.rb index b26447d..55172b0 100644 --- a/lib/model.rb +++ b/lib/model.rb @@ -145,7 +145,7 @@ post "/api/model/:id/?" do @task.save end#main task tid = @task.id.to_s - return 202, to("/task/#{tid}").to_json + return 202, "//#{ENV['VIRTUAL_HOST']}/task/#{tid}".to_json else halt 400, "No accepted content type" end -- cgit v1.2.3 From 270bddf5081671cd1905d6cce6eb3659159f573a Mon Sep 17 00:00:00 2001 From: gebele Date: Fri, 16 Aug 2019 09:49:53 +0000 Subject: change hostname method --- lib/model.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/model.rb b/lib/model.rb index 55172b0..dfc779c 100644 --- a/lib/model.rb +++ b/lib/model.rb @@ -145,7 +145,7 @@ post "/api/model/:id/?" do @task.save end#main task tid = @task.id.to_s - return 202, "//#{ENV['VIRTUAL_HOST']}/task/#{tid}".to_json + return 202, "//#{$host_with_port}/task/#{tid}".to_json else halt 400, "No accepted content type" end -- cgit v1.2.3