newmen/versatile-diamond

View on GitHub
analyzer/lib/concepts/reaction.rb

Summary

Maintainability
B
4 hrs
Test Coverage
module VersatileDiamond
  using Patches::RichArray

  module Concepts

    # Also contained positions between the reactants
    class Reaction < UbiquitousReaction
      include Modules::GraphDupper
      include Modules::SpecAtomSwapper
      include Linker
      include SurfaceLinker
      include PositionsComparer
      extend Forwardable

      # type of reaction could be only for not ubiquitous reaction
      attr_reader :type, :links, :children

      # Among super, keeps the atom map
      # @param [Array] super_args the arguments of super method
      # @param [Mcs::MappingResult] mapping the atom-mapping result
      def initialize(*super_args, mapping)
        super(*super_args)
        @mapping = mapping
        @links = {} # contain positions between atoms of different reactants

        @parent = nil
        @children = []

        @mapping.find_positions_for(self)

        reset_changes_caches!
      end

      def_delegator :mapping, :complex_source_spec_and_atom

      # Gets an appropriate representation of the reaction
      # @param [Symbol] type the type of parent reaction
      def as(type)
        @type == type ? self : reverse
      end

      # Store position relation between first and second atoms
      # @param [Array] first the array with first spec and atom
      # @param [Array] second the array with second spec and atom
      # @param [Position] position the position relation between atoms of both
      #   species
      # @option [Boolean] :check_possible is flag that another positions should be
      #   checked
      # @raise [RuntimeError] if linking atoms belongs to one spec
      # @raise [Lattices::Base::UndefinedRelation] if used relation instance is
      #   wrong for current lattice
      # @raise [Position::Duplicate] if same position already exist
      # @raise [Position::UnspecifiedAtoms] if not all atoms belongs to crystal
      #   lattice
      def position_between(first, second, position, check_possible: true)
        if mapping.reaction_type == :dissociation
          raise 'Cannot link atoms of single structure'
        end

        link_together(first, second, position, check_possible: check_possible)
        return if mapping.reaction_type == :association

        first = mapping.other_side(*first)
        second = mapping.other_side(*second)
        reverse.link_together(first, second, position)
      end

      # Duplicates current instance with each source and product specs and
      # store it to children array
      #
      # @param [String] name_tail the tail of reaction name
      # @yield [Symbol, Hash] do for each specs mirror of source and products
      # @return [Reaction] the duplicated reaction with changed name
      def duplicate(name_tail, &block)
        duplicate_by(self.class, name_tail) do |mirrors|
          mirrors.each(&block)
        end
      end

      # Duplicates current instance and creates lateral reaction instance with
      # setted theres
      #
      # @param [String] name_tail see at #duplicate same argument
      # @param [Array] theres the array of there objects
      # @yield see at #duplicate same argument
      def lateral_duplicate(name_tail, theres, &block)
        theres = theres.map(&:dup) # because each there will be changed

        duplicate_by(LateralReaction, name_tail, theres) do |mirrors|
          mirrors.each(&block)

          mirror = mirrors[:source]
          # here there objects which were used for creating a duplicate, will be
          # changed by swapping target species
          theres.each do |there|
            there.target_specs.each do |spec|
              there.swap_target(spec, mirror[spec])
            end
          end
        end
      end

      # Also remember reversed children
      # @return [Reaction] the reversed reaction
      # @override
      def reverse
        return @reverse if @reverse
        result = super
        @parent.reverse.children << result if @parent && @parent.has_reverse?
        children.each { |child| result.children << child.reverse }
        result
      end

      # Also changes atom mapping result
      # @param [Symbol] target the type of swapping species
      # @param [TerminationSpec | SpecificSpec] from which spec will be deleted
      # @param [TerminationSpec | SpecificSpec] to which spec will be added
      # @override
      def swap_on(target, from, to, **)
        super
        @links = swap_in_links(:swap, @links, from, to)
        mapping.swap(target, from, to)
        reset_changes_caches!
      end

      # Swaps using atoms of passed spec
      # @param [Spec | SpecificSpec] spec for which the atoms will be swapped
      # @param [Atom | AtomReference| SpecificAtom] from the atom which will be swapped
      # @param [Atom | AtomReference| SpecificAtom] to the atom to which will be
      #   swapped
      def swap_atom(spec, from, to)
        swap_atom_in_positions(spec, from, to)
        mapping.swap_atom(spec, from, to)
        reset_changes_caches!
      end

      # Applies relevant states for other side atom
      # @param [SpecificSpec] spec
      # @param [Atom | SpecificAtom] old_atom the atom which will be replaced
      # @param [SpecificAtom] new_atom the atom to which will be changed
      # @option [Boolean] :to_reverse_too applies relevant state to reverse
      #   reaction if set to true
      def apply_relevants(spec, old_atom, new_atom, to_reverse_too: true)
        is_source = source.include?(spec)

        if to_reverse_too
          os_s, os_old_a = mapping.other_side(spec, old_atom)
          mapping.apply_relevants(spec, old_atom, new_atom)
          _, os_new_a = mapping.other_side(spec, new_atom)

          if is_source
            reverse.apply_relevants(os_s, os_old_a, os_new_a, to_reverse_too: false)
          else
            apply_relevants(os_s, os_old_a, os_new_a, to_reverse_too: false)
            reverse.apply_relevants(spec, old_atom, new_atom, to_reverse_too: false)
          end
        end

        swap_atom_in_positions(spec, old_atom, new_atom) if is_source
      end

      # Gets all atoms of passed spec which used in reaction
      # @param [Spec | SpecificSpec] spec the one of reactant
      # @return [Array] the array of using atoms
      def used_atoms_of(spec)
        pos_atoms = @links.keys.select { |s, _| s == spec }.map(&:last)
        (pos_atoms + changed_atoms_of(spec)).uniq
      end

      # Also compares positions in both reactions
      # @param [UbiquitousReaction] see at #super same argument
      # @override
      def same?(other)
        super && same_positions?(other)
      end

      # Typical reaction isn't lateral
      # @return [Boolean] false
      def lateral?
        false
      end

      # Also checks that children are presented and they are significant too
      # @return [Boolean] is significant or not
      # @override
      def significant?
        return true if super
        significant_children = children.select(&:significant?)
        !significant_children.empty? && significant_children.all?(&:lateral?)
      end

      # Checks that all atoms belongs to lattice
      # @return [Array] atoms the array of checking atoms
      # @return [Boolean] all or not
      def all_latticed?(*atoms)
        a, b = atoms.map(&:lattice)
        a && a == b
      end

      # Gets full atom changes list
      # @return [Hash] the hash of changes where keys are spec-atom of source and
      #   values are spec-atom of products
      def full_mapping
        @_full_mapping ||=
          MappingResult.rezip(mapping.full).select { |(_, sa), (_, pa)| sa && pa }.to_h
      end

      # Gets atom changes list
      # @return [Hash] the hash of changes where keys are spec-atom of source and
      #   values are spec-atom of products
      def changes
        @_changes ||=
          (MappingResult.rezip(mapping.changes) + gas_mapping.to_a).uniq.to_h
      end

      # Gets number of changed atoms
      # @return [Integer] the number of changed atoms
      # @override
      def changes_num
        changes.size
      end

      # Reorganizes the specs of children reactions
      def reorganize_children_specs!
        children.each do |child|
          if same_specs?(child) && same_positions?(child)
            [:source, :products].each do |target|
              swap_by_map!(target, child, map_to_specs_of(child, target))
            end
          end
        end
      end

    protected

      attr_writer :parent, :links

      # Links together two structures by it atoms
      # @param [Array] first see at #position_between same argument
      # @param [Array] second see at #position_between same argument
      # @param [Position] position see at #position_between same argument
      # @raise [Position::UnspecifiedAtoms] if first or second atom isn't
      #   specified by lattice
      # @override
      def link_together(first, second, position, **)
        raise Position::UnspecifiedAtoms unless all_latticed?(first.last, second.last)

        @links[first] ||= []
        @links[second] ||= []

        super
      end

    private

      attr_reader :mapping

      # @return [Hash]
      def gas_mapping
        full_mapping.select do |(s, _), (p, _)|
          (s.gas? && !p.gas?) || (!s.gas? && p.gas?)
        end
      end

      # Makes the mirror of current specs to specs of child
      # @param [Reaction] child to which specs the map will built
      # @param [Symbol] method for get a list of specs
      # @return [Array] the map of specs
      def map_to_specs_of(child, method)
        child_specs = child.public_send(method).dup
        public_send(method).reduce([]) do |acc, self_spec|
          child_spec = child_specs.find { |s| self_spec.same?(s) }
          acc << [self_spec, child_specs.delete_one(child_spec)]
        end
      end

      # Swaps specs of child reaction by passed map
      # @param [Symbol] target the type of swapping species
      # @param [Reaction] child which specs will be swapped
      # @param [Hash] specs_map which will used for get correspond specs of current
      #   reaction
      def swap_by_map!(target, child, specs_map)
        specs_map.each do |self_spec, child_spec|
          child.swap_on(target, child_spec, self_spec)
        end
      end

      # Gets opposite relation between first and second atoms for passed
      # relation instance
      #
      # @param [Array] first see at #position_between same argument
      # @param [Array] second see at #position_between same argument
      # @param [Position] position see at #position_between same argument
      # @raise [Lattices::Base::UndefinedRelation] when passed relation is
      #   undefined
      # @return [Position] the opposite position relation
      def opposite_relation(first, second, relation)
        _, first_atom = first
        _, second_atom = second
        first_atom.lattice.opposite_relation(second_atom.lattice, relation)
      end

      # Updates attribute for current instance, or setup each child if they
      # exists
      #
      # @param [Symbol] attribute see at #super same argument
      # @param [Float] value see at #super same argument
      # @override
      def update_attribute(attribute, value)
        if children.empty?
          super
        else
          children.each do |child|
            child.send(:"#{attribute}=", value)
          end
        end
      end

      # Gets changed atoms of passed spec
      # @param [Spec | SpecificSpec] spec the one of reactant
      # @return [Array] the array of using atoms
      def changed_atoms_of(spec)
        mapping.used_atoms_of(spec)
      end

      # Reverse params for creating reverse reaction with reversing of atom
      # mapping result
      #
      # @return [Array] reversed parameters for creating reverse reaction
      # @override
      def reverse_params
        [*super, mapping.reverse]
      end

      # Duplicates current instance
      # @param [Class] klass the class instance of which will be returned
      # @param [String] name_tail see at #duplicate same argument
      # @param [Array] add_params the additional parameters of duplication
      # @yield [Symbol, Hash] does anything with obtained mirrors
      # @return [Reaction] duplicate of current reaction
      def duplicate_by(klass, name_tail, *add_params, &block)
        mirrors, *params = duplicate_params(name_tail)
        block[mirrors]

        duplication = klass.new(*(params + add_params))
        setup_duplication(duplication, mirrors)
      end

      # Duplicates internal properties of reaction such as specs and atom
      # mapping result
      #
      # @param [String] name_tail see at #duplicate same argument
      # @return [Array] the array of duplicated properties
      def duplicate_params(name_tail)
        mirrors = {}
        dup_and_save = -> type, specs do
          mirrors[type] = {}
          specs.map { |spec| mirrors[type][spec] = spec.dup }
        end

        source_dup = dup_and_save[:source, @source]
        products_dup = dup_and_save[:products, @products]

        mapping = @mapping.duplicate(mirrors)
        [mirrors, @type, "#{@name} #{name_tail}", source_dup, products_dup, mapping]
      end

      # Setups duplicated reaction
      # @param [Reaction] duplication the setuping duplicated reaction
      # @param [Hash] mirrors the mirrors of currents specs to specs in
      #   duplication
      # @return [Reaction] setuped duplicated reaction
      def setup_duplication(duplication, mirrors)
        old_to_dup = mirrors.values.reduce(&:merge)
        duplication.links = dup_graph(@links) do |old_spec, old_atom|
          dup_spec = old_to_dup[old_spec]
          [dup_spec, dup_spec.atom(old_spec.keyname(old_atom))]
        end

        duplication.parent = self
        @children << duplication

        duplication
      end

      # Swaps used specific spec atom to new atom (used only when atom was
      # changed for some specific spec and not chaned for current reaction)
      #
      # @param [SpecificSpec] spec the specific spec the atom of which will be swapped
      # @param [Atom] from the used atom
      # @param [Atom] to the new atom
      def swap_atom_in_positions(spec, from, to)
        return if from == to
        @links = swap_in_links(:swap_only_atoms, @links, spec, from, to)
      end

      def reset_changes_caches!
        @_full_mapping, @_changes = nil
      end
    end

  end
end