From 6a2c3f2100d030c30b7d8ac8c95dcece7edb040c Mon Sep 17 00:00:00 2001 From: Christoph Helma Date: Tue, 8 Sep 2009 16:04:23 +0200 Subject: api separated into individual components, adapted for new webservice versions --- Rakefile | 4 +- lib/algorithm.rb | 35 ++++++++ lib/compound.rb | 67 +++++++++++++++ lib/dataset.rb | 76 +++++++++++++++++ lib/environment.rb | 12 ++- lib/feature.rb | 45 ++++++++++ lib/model.rb | 51 ++++++++++++ lib/opentox-ruby-api-wrapper.rb | 174 +++------------------------------------ lib/tasks/opentox.rb | 86 ++++++++++++++----- lib/templates/config.ru | 23 ++++++ lib/templates/config.yaml | 12 +-- lib/utils.rb | 11 +++ opentox-ruby-api-wrapper.gemspec | 15 ++-- 13 files changed, 412 insertions(+), 199 deletions(-) create mode 100644 lib/algorithm.rb create mode 100644 lib/compound.rb create mode 100644 lib/dataset.rb create mode 100644 lib/feature.rb create mode 100644 lib/model.rb create mode 100644 lib/templates/config.ru create mode 100644 lib/utils.rb diff --git a/Rakefile b/Rakefile index 87da98d..675ac64 100644 --- a/Rakefile +++ b/Rakefile @@ -11,8 +11,8 @@ begin gem.homepage = "http://github.com/helma/opentox-ruby-api-wrapper" gem.authors = ["Christoph Helma"] gem.add_dependency "rest-client" - gem.files.include %w(lib/tasks/opentox.rb, lib/environment.rb, lib/templates/*) - #gem.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore'] + gem.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore'] + gem.files.include %w(lib/tasks/opentox.rb, lib/environment.rb, lib/algorithm.rb, lib/compound.rb, lib/dataset.rb, lib/feature.rb, lib/model.rb, lib/utils.rb, lib/templates/*) # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings end rescue LoadError diff --git a/lib/algorithm.rb b/lib/algorithm.rb new file mode 100644 index 0000000..912e14d --- /dev/null +++ b/lib/algorithm.rb @@ -0,0 +1,35 @@ +module OpenTox + module Algorithm + + class Fminer < OpenTox + # Create a new dataset with BBRC features + def initialize(training_dataset) + @uri = RestClient.post @@config[:services]["opentox-fminer"], :dataset_uri => training_dataset.uri + end + end + + class Similarity < OpenTox + + def initialize + @uri = @@config[:services]["opentox-similarity"] + end + + def self.tanimoto(dataset,compounds) + RestClient.post @uri + 'tanimoto', :dataset_uri => dataset.uri, :compound_uris => compounds.collect{ |c| c.uri } + end + + def self.weighted_tanimoto(dataset,compounds) + RestClient.post @uri + 'weighted_tanimoto', :dataset_uri => dataset.uri, :compound_uris => compounds.collect{ |c| c.uri } + end + + end + + class Lazar < OpenTox + # Create a new prediction model from a dataset + def initialize(params) + @uri = RestClient.post @@config[:services]["opentox-lazar"] + 'models' , :dataset_uri => params[:dataset_uri] + end + end + + end +end diff --git a/lib/compound.rb b/lib/compound.rb new file mode 100644 index 0000000..4652770 --- /dev/null +++ b/lib/compound.rb @@ -0,0 +1,67 @@ +module OpenTox + + # uri: /compound/:inchi + class Compound < OpenTox + + attr_reader :inchi + + # Initialize with :uri => uri, :smiles => smiles or :name => name (name can be also an InChI/InChiKey, CAS number, etc) + def initialize(params) + @@cactus_uri="http://cactus.nci.nih.gov/chemical/structure/" + if params[:smiles] + @inchi = smiles2inchi(params[:smiles]) + @uri = File.join(@@config[:services]["opentox-dataset"],"compound",@inchi) + elsif params[:inchi] + @inchi = inchi + @uri = File.join(@@config[:services]["opentox-dataset"],"compound",@inchi) + elsif params[:name] + @inchi = RestClient.get "#{@@cactus_uri}#{params[:name]}/stdinchi" + @uri = File.join(@@config[:services]["opentox-dataset"],"compound",@inchi) + elsif params[:uri] + @inchi = params[:uri].sub(/^.*InChI/, 'InChI') + @uri = params[:uri] + end + end + + # Get the (canonical) smiles + def smiles + obconversion(@inchi,'inchi','can') + end + + def sdf + obconversion(@inchi,'inchi','sdf') + end + + # Matchs a smarts string + def match?(smarts) + obconversion = OpenBabel::OBConversion.new + obmol = OpenBabel::OBMol.new + obconversion.set_in_format('inchi') + obconversion.read_string(obmol,@inchi) + smarts_pattern = OpenBabel::OBSmartsPattern.new + smarts_pattern.init(smarts) + smarts_pattern.match(obmol) + end + + # Match an array of smarts features, returns matching features + def match(smarts_features) + smarts_features.all_features.collect{ |smarts| smarts if self.match?(smarts.name) }.compact + end + + def smiles2inchi(smiles) + obconversion(smiles,'smi','inchi') + end + + def smiles2cansmi(smiles) + obconversion(smiles,'smi','can') + end + + def obconversion(identifier,input_format,output_format) + obconversion = OpenBabel::OBConversion.new + obmol = OpenBabel::OBMol.new + obconversion.set_in_and_out_formats input_format, output_format + obconversion.read_string obmol, identifier + obconversion.write_string(obmol).gsub(/\s/,'').chomp + end + end +end diff --git a/lib/dataset.rb b/lib/dataset.rb new file mode 100644 index 0000000..ca82f11 --- /dev/null +++ b/lib/dataset.rb @@ -0,0 +1,76 @@ +module OpenTox + + # key: /datasets + # set: dataset uris + # key: /dataset/:dataset/compounds + # set: compound uris + # key: /dataset/:dataset/compound/:inchi/:feature_type + # set: feature uris + class Dataset < OpenTox + + # Initialize with :uri => uri or :name => name (creates a new dataset) + def initialize(uri) + super(uri) + end + + def self.create(params) + uri = RestClient.post File.join(@@config[:services]["opentox-dataset"],"datasets"), :name => params[:name] + Dataset.new(uri.to_s) + end + + def self.find(params) + if params[:name] + uri = RestClient.get File.join(@@config[:services]["opentox-dataset"], params[:name]) + elsif params[:uri] + uri = params[:uri] + end + if RestClient.get uri + Dataset.new(uri) + else + nil + end + end + + def import(params) + if params[:csv] + # RestClient seems not to work for file uploads + `curl -X POST -F "file=@#{params[:csv]};type=text/csv" -F compound_format=#{params[:compound_format]} -F feature_type=#{params[:feature_type]} #{@uri + '/import'}` + end + end + + def add_features(features,feature_type) + #puts @uri + #puts feature_type + #puts features.to_yaml + HTTPClient.post @uri, {:feature_type => feature_type, :features => features.to_yaml} + #`curl -X POST -F feature_type="#{feature_type}" -F features="#{features.to_yaml}" #{@uri}` + end + + # Get all compounds from a dataset + def compound_uris + RestClient.get(File.join(@uri, 'compounds')).split("\n") + end + + def compounds + compound_uris.collect{|uri| Compound.new(:uri => uri)} + end + + # Get all features for a compound + def feature_uris(compound,feature_type) + #puts File.join(@uri, 'compound', compound.inchi, feature_type) + RestClient.get(File.join(@uri, 'compound', compound.inchi, feature_type)).split("\n") + end + + # Get all features for a compound + def features(compound,feature_type) + feature_uris(compound,feature_type).collect{|uri| Feature.new(:uri => uri)} + end + + # Delete a dataset + def delete + RestClient.delete @uri + end + + end + +end diff --git a/lib/environment.rb b/lib/environment.rb index 793fef9..c65e968 100644 --- a/lib/environment.rb +++ b/lib/environment.rb @@ -1,14 +1,18 @@ # load configuration -ENV['RACK_ENV'] = 'development' unless ENV['RACK_ENV'] +ENV['RACK_ENV'] = 'test' unless ENV['RACK_ENV'] + +basedir = File.join(ENV['HOME'], ".opentox") +config_dir = File.join(basedir, "config") +@@tmp_dir = File.join(basedir, "tmp") +config_file = File.join(config_dir, "#{ENV['RACK_ENV']}.yaml") -config_file = File.join(ENV['HOME'], ".opentox/config/#{ENV['RACK_ENV']}.yaml") if File.exist?(config_file) @@config = YAML.load_file(config_file) else - FileUtils.mkdir_p File.dirname(config_file) + FileUtils.mkdir_p config_dir + FileUtils.mkdir_p @@tmp_dir FileUtils.cp(File.join(File.dirname(__FILE__), 'templates/config.yaml'), config_file) puts "Please edit #{config_file} and restart your application." exit end - diff --git a/lib/feature.rb b/lib/feature.rb new file mode 100644 index 0000000..0cad7c0 --- /dev/null +++ b/lib/feature.rb @@ -0,0 +1,45 @@ +module OpenTox + + # uri: /feature/:name/:property_name/:property_value/... + class Feature < OpenTox + + attr_accessor :name, :values + + def initialize(params) + if params[:uri] + @uri = params[:uri] + items = URI.split(@uri)[5].split(/\//) + @name = items[1] + @values = {} + i = 3 + while i < items.size + @values[items[i]] = items[i+1] + i += 2 + end + else + @name = URI.encode(URI.decode(params[:name])) + @values = params[:values] + @uri = File.join(@@config[:services]["opentox-dataset"],"feature",path) + end + end + + def values_path + path = '' + @values.each do |k,v| + path += '/' + URI.encode(k.to_s) + '/' + URI.encode(v.to_s) + end + path + end + + def path + File.join(@name,values_path) + end + + def value(property) + items = @uri.split(/\//) + i = items.index(property) + items[i+1] + end + + end +end diff --git a/lib/model.rb b/lib/model.rb new file mode 100644 index 0000000..7f43860 --- /dev/null +++ b/lib/model.rb @@ -0,0 +1,51 @@ +module OpenTox + module Model + + class Lazar < OpenTox + + # Create a new prediction model from a dataset + def initialize(params) + super(params[:uri]) + end + + # Predict a compound + def predict(compound) + LazarPrediction.new(:uri => RestClient.post(@uri, :compound_uri => compound.uri)) + end + + end + + end + + module Prediction + + module Classification + + class Lazar < OpenTox + + def initialize(params) + super(params[:uri]) + end + + def classification + YAML.load(RestClient.get @uri)[:classification] + end + + def confidence + YAML.load(RestClient.get @uri)[:confidence] + end + + def neighbors + RestClient.get @uri + '/neighbors' + end + + def features + RestClient.get @uri + '/features' + end + + end + + end + + end +end diff --git a/lib/opentox-ruby-api-wrapper.rb b/lib/opentox-ruby-api-wrapper.rb index d74b412..e504e65 100644 --- a/lib/opentox-ruby-api-wrapper.rb +++ b/lib/opentox-ruby-api-wrapper.rb @@ -1,4 +1,5 @@ -['rubygems', 'sinatra', 'sinatra/respond_to', 'sinatra/url_for', 'builder', 'rest_client', 'yaml', 'spork', 'environment'].each do |lib| +#['rubygems', 'sinatra', 'sinatra/respond_to', 'sinatra/url_for', 'builder', 'rest_client', 'yaml', 'spork', 'environment', 'openbabel', 'httpclient'].each do |lib| +['rubygems', 'sinatra', 'sinatra/url_for', 'builder', 'rest_client', 'yaml', 'spork', 'environment', 'openbabel', 'httpclient'].each do |lib| require lib end @@ -7,10 +8,8 @@ module OpenTox class OpenTox attr_reader :uri - # Escape all nonword characters - def uri_escape(string) - #URI.escape(string, /[^\w]/) - URI.escape(string, /[^#{URI::PATTERN::UNRESERVED}]/) + def initialize(uri) + @uri = uri end # Get the object name @@ -23,168 +22,15 @@ module OpenTox RestClient.delete @uri end - end - - class Compound < OpenTox - - # Initialize with :uri => uri, :smiles => smiles or :name => name (name can be also an InChI/InChiKey, CAS number, etc) - def initialize(params) - if params[:uri] - @uri = params[:uri].to_s - elsif params[:smiles] - @uri = RestClient.post @@config[:services]["opentox-compound"] ,:smiles => uri_escape(params[:smiles]) - elsif params[:name] - @uri = RestClient.post @@config[:services]["opentox-compound"] ,:name => uri_escape(params[:name]) - end - end - - # Get the (canonical) smiles - def smiles - RestClient.get @uri - end - - # Get the unique id (URI encoded canonical smiles) - def uid - RestClient.get @uri + '.uid' - end - - # Matchs a smarts string - def match?(smarts) - if RestClient.get(@uri + '/match/' + uri_escape(smarts)) == 'true' - true - else - false - end - end - - # Match an array of smarts features, returns matching features - def match(smarts_features) - smarts_features.collect{ |smarts| smarts if self.match?(smarts.name) }.compact - end - - end - - class Feature < OpenTox - - # Initialize with :uri => uri, or :name => name, :values => hash_of_property_names_and_values - def initialize(params) - if params[:uri] - @uri = params[:uri].to_s - else - @uri = @@config[:services]["opentox-feature"] + uri_escape(params[:name]) - params[:values].each do |k,v| - @uri += '/' + k.to_s + '/' + v.to_s - end - end - end - - # Get the value of a property - def value(property) - RestClient.get @uri + '/' + property + # Object path without hostname + def path + URI.split(@uri)[5] end end - class Dataset < OpenTox - - # Initialize with :uri => uri or :name => name (creates a new dataset) - def initialize(params) - if params[:uri] - @uri = params[:uri].to_s - elsif params[:name] and params[:filename] - @uri = `curl -X POST -F file=@#{params[:filename]} -F name="#{params[:name]}" #{@@config[:services]["opentox-dataset"]}` - elsif params[:name] - @uri = RestClient.post @@config[:services]["opentox-dataset"], :name => params[:name] - end - end - - # Get all compounds from a dataset - def compounds - RestClient.get(@uri + '/compounds').split("\n").collect{ |c| Compound.new(:uri => c) } - end - - # Get all compounds and features from a dataset, returns a hash with compound_uris as keys and arrays of feature_uris as values - def all_compounds_and_features_uris - YAML.load(RestClient.get(@uri + '/compounds/features.yaml')) - end - - # Get all features from a dataset - def all_features - RestClient.get(@uri + '/features').split("\n").collect{|f| Feature.new(:uri => f)} - end - - # Get all features for a compound - def features(compound) - RestClient.get(@uri + '/compound/' + uri_escape(compound.uri) + '/features').split("\n").collect{|f| Feature.new(:uri => f) } - end - - # Add a compound and a feature to a dataset - def add(compound,feature) - RestClient.put @uri, :compound_uri => compound.uri, :feature_uri => feature.uri - end - - # Tell the dataset that it is complete - def close - RestClient.put @uri, :finished => 'true' - end - - end - - class Fminer < OpenTox - - # Create a new dataset with BBRC features - def initialize(training_dataset) - @dataset_uri = RestClient.post @@config[:services]["opentox-fminer"], :dataset_uri => training_dataset.uri - end - - def dataset - Dataset.new(:uri => @dataset_uri) - end - - end - - class Lazar < OpenTox - - # Create a new prediction model from a dataset - def initialize(params) - if params[:uri] - @uri = params[:uri] - elsif params[:dataset_uri] - @uri = RestClient.post @@config[:services]["opentox-lazar"] + 'models' , :dataset_uri => params[:dataset_uri] - end - end - - # Predict a compound - def predict(compound) - LazarPrediction.new(:uri => RestClient.post(@uri, :compound_uri => compound.uri)) - end - - end - - class LazarPrediction < OpenTox - - def initialize(params) - if params[:uri] - @uri = params[:uri] - end - end - - def classification - YAML.load(RestClient.get @uri)[:classification] - end - - def confidence - YAML.load(RestClient.get @uri)[:confidence] - end - - def neighbors - RestClient.get @uri + '/neighbors' - end - - def features - RestClient.get @uri + '/features' - end - - end +end +['compound','feature','dataset','algorithm','model','utils'].each do |lib| + require lib end diff --git a/lib/tasks/opentox.rb b/lib/tasks/opentox.rb index 6f1284f..afb8cb6 100644 --- a/lib/tasks/opentox.rb +++ b/lib/tasks/opentox.rb @@ -1,4 +1,4 @@ -require "environment" +load File.join(File.dirname(__FILE__), '..', 'environment.rb') namespace :opentox do @@ -18,18 +18,22 @@ namespace :opentox do task :start do @@config[:services].each do |service,uri| dir = File.join(@@config[:base_dir], service) - case @@config[:webserver] - when 'thin' + server = @@config[:webserver] + `redis-server &` + case server + when /thin|mongrel|webrick/ port = uri.sub(/^.*:/,'').sub(/\/$/,'') Dir.chdir dir + pid_file = File.join(@@tmp_dir,"#{service}.pid") begin - `thin --trace --rackup config.ru start -p #{port} -e #{ENV['RACK_ENV']} &` - puts "#{service} started on port #{port}." + `#{server} --trace --rackup config.ru start -p #{port} -e #{ENV['RACK_ENV']} -P #{pid_file} -d &` + puts "#{service} started on localhost:#{port} in #{ENV['RACK_ENV']} environment with PID file #{pid_file}." rescue puts "Cannot start #{service} on port #{port}." end when 'passenger' - puts "not yet implemented" + `touch #{File.join(dir, 'tmp/restart.txt')}` + puts "#{service} restarted." else puts "not yet implemented" end @@ -38,9 +42,18 @@ namespace :opentox do desc "Stop opentox services" task :stop do - @@config[:services].each do |service,uri| - port = uri.sub(/^.*:/,'').sub(/\/$/,'') - `echo "SHUTDOWN" | nc localhost #{port}` if port + server = @@config[:webserver] + if server =~ /thin|mongrel|webrick/ + @@config[:services].each do |service,uri| + port = uri.sub(/^.*:/,'').sub(/\/$/,'') + pid_file = File.join(@@tmp_dir,"#{service}.pid") + begin + puts `#{server} stop -P #{pid_file}` + puts "#{service} stopped on localhost:#{port}" + rescue + puts "Cannot stop #{service} on port #{port}." + end + end end end @@ -49,20 +62,55 @@ namespace :opentox do end - namespace :test do + desc "Run all OpenTox tests" + task :test do + @@config[:services].each do |service,uri| + dir = File.join(@@config[:base_dir], service) + Dir.chdir dir + puts "Running tests in #{dir}" + `rake test -t 1>&2` + end + end - ENV['RACK_ENV'] = 'test' - test = "#{Dir.pwd}/test/test.rb" +end - desc "Run local tests" - task :local => "opentox:services:restart" do - load test +desc "Start service in current directory" +task :start do + service = File.basename(Dir.pwd).intern + server = @@config[:webserver] + case server + when /thin|mongrel|webrick/ + port = @@config[:services][service].sub(/^.*:/,'').sub(/\/$/,'') + pid_file = File.join(@@tmp_dir,"#{service}.pid") + begin + `#{server} --trace --rackup config.ru start -p #{port} -e #{ENV['RACK_ENV']} -P #{pid_file} -d &` + puts "#{service} started on localhost:#{port} in #{ENV['RACK_ENV']} environment with PID file #{pid_file}." + rescue + puts "Cannot start #{service} on port #{port}." + end + when 'passenger' + `touch tmp/restart.txt` + puts "#{service} restarted." + else + puts "not yet implemented" end +end - task :remote do - #load 'test.rb' +desc "Stop service in current directory" +task :stop do + service = File.basename(Dir.pwd).intern + server = @@config[:webserver] + if server =~ /thin|mongrel|webrick/ + port = @@config[:services][service].sub(/^.*:/,'').sub(/\/$/,'') + pid_file = File.join(@@tmp_dir,"#{service}.pid") + begin + puts `thin stop -P #{pid_file}` + puts "#{service} stopped on localhost:#{port}" + rescue + puts "Cannot stop #{service} on port #{port}." end - end - end + +desc "Restart service in current directory" +task :restart => [:stop, :start] diff --git a/lib/templates/config.ru b/lib/templates/config.ru new file mode 100644 index 0000000..63dd2ce --- /dev/null +++ b/lib/templates/config.ru @@ -0,0 +1,23 @@ +require 'rubygems' +require 'sinatra' +require 'application.rb' +require 'rack' +require 'rack/contrib' + +FileUtils.mkdir_p 'log' unless File.exists?('log') +log = File.new("log/#{ENV["RACK_ENV"]}.log", "a") +$stdout.reopen(log) +$stderr.reopen(log) + +if ENV['RACK_ENV'] == 'production' + use Rack::MailExceptions do |mail| + mail.to 'helma@in-silico.ch' + mail.subject '[ERROR] %s' + end +elsif ENV['RACK_ENV'] == 'development' + use Rack::Reloader + use Rack::ShowExceptions +end + +run Sinatra::Application + diff --git a/lib/templates/config.yaml b/lib/templates/config.yaml index 768fa01..14326fe 100644 --- a/lib/templates/config.yaml +++ b/lib/templates/config.yaml @@ -1,8 +1,10 @@ :base_dir: /home/ch/webservices :webserver: thin :services: - - opentox-feature - - opentox-compound - - opentox-dataset - - opentox-fminer - - opentox-lazar +# make sure to provide a full uri (including training slash) + opentox-feature: "http://localhost:5000/" + opentox-compound: "http://localhost:5001/" + opentox-dataset: "http://localhost:5002/" + opentox-fminer: "http://localhost:5003/" + opentox-similarity: "http://localhost:5004/" + opentox-lazar: "http://localhost:5005/" diff --git a/lib/utils.rb b/lib/utils.rb new file mode 100644 index 0000000..2716f45 --- /dev/null +++ b/lib/utils.rb @@ -0,0 +1,11 @@ +module OpenTox + module Utils + + # gauss kernel + def self.gauss(sim, sigma = 0.3) + x = 1.0 - sim + Math.exp(-(x*x)/(2*sigma*sigma)) + end + + end +end diff --git a/opentox-ruby-api-wrapper.gemspec b/opentox-ruby-api-wrapper.gemspec index 5c72422..7ef0252 100644 --- a/opentox-ruby-api-wrapper.gemspec +++ b/opentox-ruby-api-wrapper.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Christoph Helma"] - s.date = %q{2009-08-25} + s.date = %q{2009-09-08} s.description = %q{Ruby wrapper for the OpenTox REST API (http://www.opentox.org)} s.email = %q{helma@in-silico.ch} s.extra_rdoc_files = [ @@ -17,20 +17,25 @@ Gem::Specification.new do |s| "README.rdoc" ] s.files = [ - ".document", - ".gitignore", - "LICENSE", + "LICENSE", "README.rdoc", "Rakefile", "VERSION", + "lib/algorithm.rb", + "lib/compound.rb", + "lib/dataset.rb", "lib/environment.rb", + "lib/feature.rb", "lib/helper.rb", + "lib/model.rb", "lib/opentox-ruby-api-wrapper.rb", "lib/spork.rb", "lib/tasks/opentox.rb", + "lib/templates/config.ru", + "lib/templates/config.ru", "lib/templates/config.yaml", "lib/templates/config.yaml", - "opentox-ruby-api-wrapper.gemspec", + "lib/utils.rb", "test/hamster_carcinogenicity.csv", "test/opentox-ruby-api-wrapper_test.rb", "test/start-local-webservices.rb", -- cgit v1.2.3