analyzer/lib/concepts/specific_spec.rb
module VersatileDiamond
module Concepts
# Instance of it class represents usual specific spec that is most commonly
# used in reactions
class SpecificSpec
include Modules::RelationBetweenChecker
include Modules::GraphDupper
include BondsCounter
extend Forwardable
attr_reader :spec, :specific_atoms
def_delegators :spec, :extendable?, :termination?, :gas?, :simple?
# Initialize specific spec instalce. Checks specified atom for correct
# valence value
#
# @param [Spec] spec the base spec instance
# @param [Hash] specific_atoms references to specific atoms. Uses only for easy
# setup from rspec tests
def initialize(spec, specific_atoms = {})
spatoms = specific_atoms.map do |atom_keyname, specific_atom|
atom = spec.atom(atom_keyname)
unused_bonds = spec.external_bonds_for(atom) -
specific_atom.actives - specific_atom.monovalents.size
if unused_bonds < 0
raise Atom::IncorrectValence.new(atom_keyname)
end
correct_atom =
# references should have same incident relations
if atom.reference? && !specific_atom.reference?
SpecificAtom.new(atom, ancestor: specific_atom)
else
specific_atom
end
[atom_keyname, correct_atom]
end
@specific_atoms = Hash[spatoms]
@spec = spec
@original_name = spec.name
@extended_spec, @reduced, @correct_reduced = nil
@_name, @_external_bonds_after_extend = nil
end
# Makes a copy of other specific spec by dup each specific atom from it
# @param [SpecificSpec] other the duplicating spec
def initialize_copy(other)
@spec = other.spec
@specific_atoms = Hash[other.specific_atoms.map { |k, a| [k, a.dup] }]
reset_caches!
end
# @return [Boolean]
def specific?
true
end
# Updates base spec from which dependent current specific spec
# @param [Spec] new_spec the new base spec
def replace_base_spec(new_spec)
rename_used_keynames_and_update_links(new_spec)
@spec = new_spec
@_name = nil
end
# Finds positions between atoms in base structure.
# Can be used only for specified *surface* spec.
#
# @param [Atom] atom1 the first atom
# @param [Atom] atom2 the second atom
# @return [Position] nil or positions between atoms in both directions
def position_between(atom1, atom2)
spec.position_between(base_atom(atom1), base_atom(atom2))
end
# Builds the full name of specific spec (with specificied atom info)
# @return [Symbol] the full name of specific spec
def name
return @_name if @_name
sorted_atoms = @specific_atoms.to_a.sort { |(k1, _), (k2, _)| k1 <=> k2 }
args = sorted_atoms.reduce([]) do |arr, (keyname, atom)|
atom.actives.times { arr << "#{keyname}: *" }
arr + relevants_for(atom) + monovalents_for(atom)
end
@_name = :"#{@original_name}(#{args.join(', ')})"
end
# Gets corresponding atom, because it can be specific atom
# @param [Symbol] keyname the atom keyname
# @return [Atom | SpecificAtom] the corresponding atom
def atom(keyname)
@specific_atoms[keyname] || spec.atom(keyname)
end
# Returns a keyname of passed atom
# @param [Atom] atom the atom for which keyname will be found
# @return [Symbol] the keyname of atom
def keyname(atom)
@specific_atoms.invert[atom] || spec.keyname(atom)
end
# Describes atom by storing it to specific atoms hash
# @param [Symbol] keyname the keyname of new specified atom
# @param [SpecificAtom] atom the specified atom which will be stored
# @raise [ArgumentError] when keyname is undefined, or keyname already
# specified, or atom is not specified
def describe_atom(keyname, atom)
unless spec.atom(keyname)
raise ArgumentError, "Undefined atom #{keyname} for #{name}!"
end
if @specific_atoms[keyname]
raise ArgumentError,
"Atom #{keyname} for specific #{name} already described!"
end
unless atom.specific?
raise ArgumentError,
"Described atom #{keyname} for specific #{name} cannot be unspecified"
end
@specific_atoms[keyname] = atom
reset_caches!
end
# Swaps from own to new
# @param [Atom | SpecificAtom | AtomReference] from
# @param [Atom | SpecificAtom | AtomReference] to
def swap_atom(from, to)
original_links = dup_graph(links, &:itself)
find_proc = -> atom { @specific_atoms.find { |_, a| a == atom } }
pair = find_proc[from]
if pair
raise ArgumentError, 'Incorrect swapping' if find_proc[to]
@specific_atoms[pair.first] = to
elsif to.specific?
@specific_atoms[spec.keyname(from)] = to
else
spec.swap_atom(from, to)
end
reset_caches!
@links = dup_graph(original_links) { |atom| atom == from ? to : atom }
end
# Returns original links of base spec but exchange correspond atoms to
# specific atoms
#
# @return [Hash] cached hash of all links between atoms
def links
@links ||= spec.links_with_replace_by(@specific_atoms)
end
%w(incoherent unfixed).each do |state|
method_name = :"#{state}!"
# Defines #{state} method which change a state of atom selected by
# keyname
#
# @param [Symbol] atom_keyname the keyname of selecting atom
# @raise [Errors::SyntaxError] if atom already has setuping state
define_method(method_name) do |atom_keyname|
atom = @specific_atoms[atom_keyname]
unless atom
atom = SpecificAtom.new(spec.atom(atom_keyname))
@specific_atoms[atom_keyname] = atom
reset_caches!
end
atom.send(method_name)
end
end
# Counts number of external bonds
# @return [Integer] the number of external bonds
def external_bonds
# TODO: incorrect counting because material balance matcher depends from it
# TODO: replace external bonds logic to directly using number of atoms
spec.external_bonds - active_bonds_num #- monovalents_num
end
# Extends originial spec by atom-references and store it to temp variable
# after that counts bonds for extended spec
#
# @return [Integer] the number of external bonds for extended spec
def external_bonds_after_extend
return @_external_bonds_after_extend if @_external_bonds_after_extend
@extended_spec = spec.extend_by_references
@_external_bonds_after_extend =
@extended_spec.external_bonds - active_bonds_num
end
# Makes a new specific spec by extended base spec
# @return [SpecificSpec] the extended spec
def extended
external_bonds_after_extend unless @extended_spec
spec = self.class.new(@extended_spec)
spec.reduced = self # @reduced assign
@specific_atoms.each do |keyname, old_atom|
spec.specific_atoms[keyname] =
SpecificAtom.new(@extended_spec.atom(keyname), ancestor: old_atom)
end
spec
end
# Is extended or not
# @return [Boolean] extended or not
def extended?
!!@reduced
end
# Makes a correct reduced spec by applying specific atoms from current
# spec to reduced spec
#
# @return [SpecificSpec] correct reduced spec or nil
def reduced
return unless extended?
return @correct_reduced if @correct_reduced
@correct_reduced = @reduced.dup
@specific_atoms.each do |keyname, atom|
rd_atom = @correct_reduced.atom(keyname)
is_specific = @correct_reduced.specific_atoms[keyname]
df = atom.diff(rd_atom)
if is_specific
rd_atom.apply_diff(df)
else
@correct_reduced.
describe_atom(keyname, SpecificAtom.new(rd_atom, ancestor: atom))
end
end
@correct_reduced
end
# Checks that specific spec could be reduced
# @return [Boolean] could or not
def could_be_reduced?
extended? && Spec.good_for_reduce?(@specific_atoms.keys)
end
# Compares two specific specs
# @param [TerminationSpec | SpecificSpec] other with which comparison
# @return [Boolean] the same or not
def same?(other)
self.class == other.class ? correspond?(other) : other.same?(self)
end
# Checks termination atom at the inner atom which belongs to current spec
# @param [Atom | SpecificAtom] internal_atom the atom which belongs to
# current spec
# @param [AtomicSpec] term_spec the termination specie with monovalent atom
# @return [Boolean] has termination atom or not
def has_termination?(internal_atom, term_spec)
(term_spec.hydrogen? && external_bonds_for(internal_atom) > 0) ||
internal_atom.monovalents.include?(term_spec)
end
# Counts the sum of active bonds
# @return [Integer] sum of active bonds
def active_bonds_num
specific_atoms.reduce(0) { |acc, (_, atom)| acc + atom.actives }
end
# Counts the sum of monovalent atoms at specific atoms
# @return [Integer] sum of monovalent atoms
def monovalents_num
specific_atoms.reduce(0) { |acc, (_, atom)| acc + atom.monovalents.size }
end
def to_s
spec.to_s
end
def inspect
name.to_s
end
protected
attr_writer :reduced
private
# Gets analog atom from base spec
# @param [Atom] atom from current or from base spec
# @return [Atom] the atom from base spec
def base_atom(atom)
spec.atom(keyname(atom))
end
# Renames internal used keynames to new keynames from another base spec
# @param [Spec] other the base spec from which keynames will gotten
def rename_used_keynames_and_update_links(other)
mirror = Mcs::SpeciesComparator.make_mirror(spec, other)
new_specific_atoms = {}
@specific_atoms.each do |old_keyname, atom|
base_atom = spec.atom(old_keyname)
other_atom = mirror[base_atom]
new_keyname = other_atom ? other.keyname(other_atom) : old_keyname
# raise could be when other base spec contain keynames same as residual
# atom keynames which are present if other base spec atoms size less than
# previous base spec atoms size
raise 'Keyname is duplicated' if new_specific_atoms[new_keyname]
new_specific_atoms[new_keyname] = atom
end
update_links(mirror)
@specific_atoms = new_specific_atoms
end
# Updates current links to correct atoms from some other base spec
# @param [Hash] mirror of atoms from prev base to some other new base spec
def update_links(mirror)
@links = dup_graph(links) { |a| mirror[a] || a }
end
# Collect all relevant states for passed atom
# @param [SpecificAtom] atom see at #collect_states same argument
# @return [Array] the array of relevant states
def relevants_for(atom)
collect_states(atom, :relevants, 'to_s[0]')
end
# Collect all monovalent atoms for passed atom
# @param [SpecificAtom] atom see at #collect_states same argument
# @return [Array] the array of monovalent atoms
def monovalents_for(atom)
collect_states(atom, :monovalents)
end
# Collects all states of atom by passed for each of them
# @param [SpecificAtom] atom the atom for which states will be got
# @param [Symbol] atom_method the method states to take
# @param [String] state_method the additional method that applied to each
# state if has
# @return [Array] the array where each element is key-value string with
# atom keyname and some state
def collect_states(atom, atom_method, state_method = nil)
atom_keyname = keyname(atom)
states = atom.send(atom_method)
states.empty? ?
[] :
states.map do |state|
state_str = state_method ? eval("state.#{state_method}") : state
"#{atom_keyname}: #{state_str}"
end
end
# Verifies that the passed instance is correspond to the current, by
# using the Hanser's algorithm
#
# @param [SpecificSpec] other see at #same? same argument
# @return [Boolean] the result of Hanser's algorithm
def correspond?(other)
equal?(other) || (links.size == other.links.size &&
Mcs::SpeciesComparator.contain?(self, other))
end
# Resets internal caches
def reset_caches!
@_name = nil
@links = nil
@_external_bonds_after_extend = nil
end
end
end
end