diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/api.rb | 29 | ||||
-rw-r--r-- | lib/compound.rb | 68 | ||||
-rw-r--r-- | lib/dataset.rb | 33 | ||||
-rw-r--r-- | lib/endpoint.rb | 23 | ||||
-rw-r--r-- | lib/feature.rb | 29 | ||||
-rw-r--r-- | lib/model.rb | 152 | ||||
-rw-r--r-- | lib/report.rb | 29 | ||||
-rw-r--r-- | lib/substance.rb | 34 | ||||
-rw-r--r-- | lib/swagger.rb | 3 | ||||
-rw-r--r-- | lib/validation.rb | 67 |
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 |