summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorChristoph Helma <helma@in-silico.ch>2019-09-03 13:45:36 +0200
committerChristoph Helma <helma@in-silico.ch>2019-09-03 13:45:36 +0200
commitd1032e4f40d9fbb212e85e0db4f0ecd2e8ac9a88 (patch)
tree48922d60d750839dacd5d0a4a6e50ea3fe68da63 /lib
parent5bb4c24c6cfc1ddfae14eb9543b283baae2d75be (diff)
parenta84d9eabf1b921086a688f81df28b0f21ba4df19 (diff)
development merged, git links in FAQ.md fixed1.4.0
Diffstat (limited to 'lib')
-rw-r--r--lib/api.rb29
-rw-r--r--lib/compound.rb68
-rw-r--r--lib/dataset.rb33
-rw-r--r--lib/endpoint.rb23
-rw-r--r--lib/feature.rb29
-rw-r--r--lib/model.rb152
-rw-r--r--lib/report.rb29
-rw-r--r--lib/substance.rb34
-rw-r--r--lib/swagger.rb3
-rw-r--r--lib/validation.rb67
10 files changed, 467 insertions, 0 deletions
diff --git a/lib/api.rb b/lib/api.rb
new file mode 100644
index 0000000..dffa9d7
--- /dev/null
+++ b/lib/api.rb
@@ -0,0 +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")
+ return File.read(index_file)
+ when "application/json"
+ redirect("/api/api.json")
+ else
+ halt 400, "unknown MIME type '#{@accept}'"
+ end
+end
+
+get "/api/api.json" do
+ 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
diff --git a/lib/compound.rb b/lib/compound.rb
new file mode 100644
index 0000000..4606aa4
--- /dev/null
+++ b/lib/compound.rb
@@ -0,0 +1,68 @@
+# 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 "/api/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 "/api/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{/api/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
+ halt 400, "Compound with #{input} not found.".to_json
+ end
+end
diff --git a/lib/dataset.rb b/lib/dataset.rb
new file mode 100644
index 0000000..51407ca
--- /dev/null
+++ b/lib/dataset.rb
@@ -0,0 +1,33 @@
+# Get all datasets
+get "/api/dataset/?" do
+ datasets = Dataset.all #.limit(100)
+ case @accept
+ when "application/json"
+ list = datasets.collect{|dataset| uri("/api/dataset/#{dataset.id}")}.to_json
+ return list
+ else
+ halt 400, "Mime type #{@accept} is not supported."
+ end
+end
+
+# Get a dataset
+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 File.read dataset.source
+ else
+ bad_request_error "Mime type #{@accept} is not supported."
+ end
+end
+
+# Get a dataset attribute. One of compounds, nanoparticles, substances, features
+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"]
+ 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/endpoint.rb b/lib/endpoint.rb
new file mode 100644
index 0000000..66b7ab2
--- /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 "/api/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
+ halt 400, "Mime type #{@accept} is not supported."
+ end
+end
+
+get "/api/endpoint/:endpoint/?" do
+ models = Model::Validation.where(endpoint: params[:endpoint])
+ list = []
+ 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
new file mode 100644
index 0000000..3123997
--- /dev/null
+++ b/lib/feature.rb
@@ -0,0 +1,29 @@
+# Get all Features
+get "/api/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 "/api/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/model.rb b/lib/model.rb
new file mode 100644
index 0000000..dfc779c
--- /dev/null
+++ b/lib/model.rb
@@ -0,0 +1,152 @@
+# 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 "/api/model/?" do
+ models = Model::Validation.all
+ case @accept
+ when "text/uri-list"
+ 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("/api/model/#{m["_id"]["$oid"]}")}
+ return list.to_json
+ else
+ halt 400, "Mime type #{@accept} is not supported."
+ end
+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
+
+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
+ model = Model::Validation.find params[:id]
+ prediction = model.predict(compound)
+ 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("</br>")
+ 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, "//#{$host_with_port}/task/#{tid}".to_json
+ else
+ halt 400, "No accepted content type"
+ end
+end
diff --git a/lib/report.rb b/lib/report.rb
new file mode 100644
index 0000000..7c06d60
--- /dev/null
+++ b/lib/report.rb
@@ -0,0 +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 "/api/report/?" do
+ models = Model::Validation.all
+ case @accept
+ when "text/uri-list"
+ 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("/api/report/#{m["_id"]["$oid"]}")}
+ return list.to_json
+ else
+ halt 400, "Mime type #{@accept} is not supported."
+ end
+end
+
+get "/api/report/:id/?" do
+ case @accept
+ when "application/xml"
+ report = qmrf_report params[:id]
+ return report.to_xml
+ else
+ halt 400, "Mime type #{@accept} is not supported."
+ end
+
+end
diff --git a/lib/substance.rb b/lib/substance.rb
new file mode 100644
index 0000000..5d57505
--- /dev/null
+++ b/lib/substance.rb
@@ -0,0 +1,34 @@
+# Get all substances
+get "/api/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"
+ list = substances.collect{|substance| uri("/substance/#{substance.id}")}
+ substances = JSON.parse list.to_json
+ return JSON.pretty_generate substances
+ else
+ halt 400, "Mime type #{@accept} is not supported."
+ end
+end
+
+# Get a substance by ID
+get "/api/substance/:id/?" do
+ case @accept
+ when "application/json"
+ substance = Substance.find params[:id]
+ if substance
+ 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 #{params[:id]} not found."
+ end
+ else
+ halt 400, "Mime type #{@accept} is not supported."
+ end
+end
diff --git a/lib/swagger.rb b/lib/swagger.rb
new file mode 100644
index 0000000..2c3ea87
--- /dev/null
+++ b/lib/swagger.rb
@@ -0,0 +1,3 @@
+get "/swagger" do
+ redirect("/api")
+end
diff --git a/lib/validation.rb b/lib/validation.rb
new file mode 100644
index 0000000..031b9e1
--- /dev/null
+++ b/lib/validation.rb
@@ -0,0 +1,67 @@
+# 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 "/api/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
+ halt 400, "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 "/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
+ 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("/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("/api/validation/#{params[:validationtype]}/#{validations[idx]["_id"]["$oid"]}")
+ end
+ return validations.to_json
+ else
+ halt 400, "Mime type #{@accept} is not supported."
+ end
+end
+
+# Get validation representation
+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]
+ 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
+
+ halt 404, "#{params[:validationtype]} with id: #{params[:id]} not found." unless validation
+ return validation.to_json
+end