Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactor and improve Molecule API's SMILES endpoint for better maintainability and performance #2246

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 17 additions & 67 deletions app/api/chemotion/molecule_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,59 +30,11 @@ class MoleculeAPI < Grape::API
optional :editor, type: String, desc: 'SVGProcessor', default: 'ketcher'
end
post do
smiles = params[:smiles]
svg = params[:svg_file]
result = Chemotion::SmilesProcessor.new(params).process
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/ClassLength: Class has too many lines. [243/200]

molecule = result[:molecule]
return {} unless molecule

babel_info = OpenBabelService.molecule_info_from_structure(smiles, 'smi')
inchikey = babel_info[:inchikey]
return {} if inchikey.blank?

molecule = Molecule.find_by(inchikey: inchikey, is_partial: false)
unless molecule
molfile = babel_info[:molfile] if babel_info
begin
rw_mol = RDKitChem::RWMol.mol_from_smiles(smiles)
rd_mol = rw_mol.mol_to_mol_block unless rw_mol.nil?
rescue StandardError => e
Rails.logger.error ["with smiles: #{smiles}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR)
rd_mol = rw_mol.mol_to_mol_block(true, -1, false) unless rw_mol.nil?
end
if rd_mol.nil?
begin
pc_mol = Chemotion::PubchemService.molfile_from_smiles(smiles)
pc_mol = Chemotion::OpenBabelService.molfile_clear_hydrogens(pc_mol) unless pc_mol.nil?
molfile = pc_mol unless pc_mol.nil?
rescue StandardError => e
Rails.logger.error ["with smiles: #{smiles}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR)
end
else
molfile = rd_mol
end
return {} unless molfile
molecule = Molecule.find_or_create_by_molfile(molfile, babel_info)
molecule = Molecule.find_or_create_dummy if molecule.blank?
end
return unless molecule

svg_digest = "#{molecule.inchikey}#{Time.now}"
if svg.present?
svg_process = SVG::Processor.new.structure_svg(params[:editor], svg, svg_digest)
else
svg_process = SVG::Processor.new.generate_svg_info('samples', svg_digest)
svg_file_src = Rails.public_path.join('images', 'molecules', molecule.molecule_svg_file)
if File.exist?(svg_file_src)
mol = molecule.molfile.lines[0..1]
if svg.nil? || svg&.include?('Open Babel')
svg = Molecule.svg_reprocess(svg, molecule.molfile)
svg_process = SVG::Processor.new.structure_svg('ketcher', svg, svg_digest, true)
else
FileUtils.cp(svg_file_src, svg_process[:svg_file_path])
end
end
end
molecule.attributes.merge(temp_svg: File.exist?(svg_process[:svg_file_path]) && svg_process[:svg_file_name], ob_log: babel_info[:ob_log])

present molecule, with: Entities::MoleculeEntity
Entities::MoleculeEntity.represent(molecule, temp_svg: result[:temp_svg], ob_log: result[:ob_log])
end
end

Expand Down Expand Up @@ -185,23 +137,21 @@ class MoleculeAPI < Grape::API
svg = params[:svg_file]
molfile = params[:molfile]
decoupled = params[:decoupled]
molecule = decoupled ? Molecule.find_or_create_dummy : Molecule.find_or_create_by_molfile(molfile)
molecule = Molecule.find_or_create_dummy if molecule.blank?
molecule = if decoupled
Molecule.find_or_create_dummy
else
Molecule.find_or_create_by_molfile(molfile) || Molecule.find_or_create_dummy
end
if molfile.present?
molecule.molfile = molfile
molecule.save! if molecule.changed?
end
ob = molecule&.ob_log
if svg.present?
svg_process = SVG::Processor.new.structure_svg(params[:editor], svg, molfile)
mol = molecule.molfile.lines.first(2)
if mol[1]&.strip&.match?('OpenBabel')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/ConditionalAssignment: Use the return of the conditional for variable assignment and comparison.

svg_process = SVG::Processor.new(svg, 'openbabel', molecule).process
else
svg_file_src = Rails.public_path.join('images', 'molecules', molecule.molecule_svg_file)
if File.exist?(svg_file_src)
mol = molecule.molfile.lines.first(2)
if mol[1]&.strip&.match?('OpenBabel')
svg = File.read(svg_file_src)
svg_process = SVG::Processor.new.structure_svg('openbabel', svg, molfile)
else
svg_process = SVG::Processor.new.generate_svg_info('samples', molfile)
FileUtils.cp(svg_file_src, svg_process[:svg_file_path])
end
end
svg_process = SVG::Processor.new(svg, params[:editor], molecule).process
end
molecule&.attributes&.merge(temp_svg: svg_process[:svg_file_name], ob_log: ob)
Entities::MoleculeEntity.represent(molecule, temp_svg: svg_process[:svg_file_name], ob_log: ob)
Expand Down
2 changes: 1 addition & 1 deletion app/packs/src/models/Sample.js
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,7 @@ export default class Sample extends Element {

set molecule(molecule) {
this._molecule = new Molecule(molecule);
if (molecule.temp_svg) { this.sample_svg_file = molecule.temp_svg; }
if (molecule.temp_svg && !molecule.molecule_svg_file) { this.sample_svg_file = molecule.temp_svg; }
}

get polymer_formula() {
Expand Down
83 changes: 83 additions & 0 deletions lib/chemotion/molecule_fetcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

module Chemotion
class MoleculeFetcher
def initialize(smiles, babel_info)
@smiles = smiles
@babel_info = babel_info
end

def fetch_or_create
find_existing || create_molecule
end

private

def find_existing
Molecule.find_by(inchikey: molecule_inchikey, is_partial: false)
end

def create_molecule
molfile = fetch_molfile
return unless molfile

Molecule.find_or_create_by_molfile(molfile, @babel_info) || Molecule.find_or_create_dummy
end

def fetch_molfile
@babel_info&.dig(:molfile) || rdkit_molfile || pubchem_molfile
end

def rdkit_molfile
rw_mol = RDKitChem::RWMol.mol_from_smiles(@smiles)
begin
rw_mol&.mol_to_mol_block
rescue StandardError => e
handle_rdkit_error(e, rw_mol)
end
end

def handle_rdkit_error(error, rw_mol)
log_error(error)
rw_mol&.mol_to_mol_block(true, -1, false)
end

def pubchem_molfile
pc_mol = Chemotion::PubchemService.molfile_from_smiles(@smiles)
validate_and_clear_molfile(pc_mol)
rescue StandardError => e
log_error(e)
nil
end

def validate_and_clear_molfile(pc_mol)
return unless validate_molfile(pc_mol)

Chemotion::OpenBabelService.molfile_clear_hydrogens(pc_mol)
end

def validate_molfile(molfile)
parsed = parse_to_hash(molfile)
parsed['Status'] != '400'
end

def parse_to_hash(input)
# Split the string into key-value pairs based on known patterns
input.each_line.with_object({}) do |line, hash|
next unless line =~ /^(.*?):\s*(.*)$/

key = Regexp.last_match(1).strip
value = Regexp.last_match(2).strip
hash[key] = value
end
end

def molecule_inchikey
@babel_info[:inchikey]
end

def log_error(error)
Rails.logger.error ["with smiles: #{@smiles}", error.message, *error.backtrace].join($INPUT_RECORD_SEPARATOR)
end
end
end
35 changes: 35 additions & 0 deletions lib/chemotion/smiles_processor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Chemotion
class SmilesProcessor
def initialize(params)
@smiles = params[:smiles]
@svg = params[:svg_file]
@editor = params[:editor]
end

def process
return unless valid_smiles? && babel_info.present?

molecule = Chemotion::MoleculeFetcher.new(@smiles, babel_info).fetch_or_create
return unless molecule

svg_result = SVG::Processor.new(@svg, @editor, molecule).process
build_result(molecule, svg_result)
end

private

def valid_smiles?
@smiles.present?
end

def babel_info
@babel_info ||= Chemotion::OpenBabelService.molecule_info_from_structure(@smiles, 'smi')
end

def build_result(molecule, svg_result)
{ molecule: molecule, temp_svg: svg_result[:svg_file_name], ob_log: babel_info[:ob_log] }
end
end
end
66 changes: 61 additions & 5 deletions lib/svg/processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@
module SVG
# SVG Processor
class Processor
def initialize(svg, editor, molecule)
@svg = svg
@editor = editor
@molecule = molecule
end

def process
svg_digest = generate_svg_digest(@molecule)
if @svg.present?
structure_svg(@editor, @svg, svg_digest)
else
regenerate_or_copy_svg(svg_digest)
end
end

def structure_svg(editor, svg, hexdigest, is_centered = false)
processor = case editor
when /marvinjs/i
Expand All @@ -18,19 +33,60 @@ def structure_svg(editor, svg, hexdigest, is_centered = false)
Chemotion::OpenBabelSvgProcessor.new(svg)
end
svg = processor.centered_and_scaled_svg unless is_centered == true
info = generate_svg_info('samples', hexdigest)
svg_file = File.new(info[:svg_file_path], 'w+')
svg_file.write(svg)
svg_file.close
{ svg_file_path: info[:svg_file_path], svg_file_name: info[:svg_file_name] }
save_svg_to_file(svg, generate_svg_info('samples', hexdigest))
end

private

def generate_svg_info(type, hexdigest)
digest = Digest::SHA256.hexdigest hexdigest
digest = Digest::SHA256.hexdigest digest
svg_file_name = "TMPFILE#{digest}.svg"
svg_file_path = File.join('public', 'images', type, svg_file_name)
{ svg_file_path: svg_file_path, svg_file_name: svg_file_name }
end

def regenerate_or_copy_svg(hexdigest)
svg_file_src = generate_svg_file_src
if svg_file_src && svg_file_exists?(svg_file_src)
info = generate_svg_info('samples', hexdigest)
needs_reprocessing? ? regenerate_svg(hexdigest) : copy_svg_file(svg_file_src, info)
else
generate_svg_info('samples', hexdigest)
end
end

def generate_svg_file_src
return if @molecule.molecule_svg_file.blank?

Rails.public_path.join('images', 'molecules', @molecule.molecule_svg_file)
end

def svg_file_exists?(svg_file_src)
File.exist?(svg_file_src)
end

def needs_reprocessing?
@svg.nil? || @svg.include?('Open Babel')
end

def save_svg_to_file(svg, info)
File.write(info[:svg_file_path], svg)
{ svg_file_path: info[:svg_file_path], svg_file_name: info[:svg_file_name] }
end

def regenerate_svg(hexdigest)
svg = Molecule.svg_reprocess(@svg, @molecule.molfile)
structure_svg(@editor, svg, hexdigest, true)
end

def copy_svg_file(src, info)
FileUtils.cp(src, info[:svg_file_path])
info
end

def generate_svg_digest(molecule)
"#{molecule.inchikey}#{Time.current}"
end
end
end
Loading
Loading