newmen/versatile-diamond

View on GitHub
analyzer/lib/organizers/atom_properties.rb

Summary

Maintainability
F
5 days
Test Coverage
module VersatileDiamond
  using Patches::RichArray
  using Patches::RichString

  module Organizers

    # Accumulates information about atom
    class AtomProperties
      include Modules::ListsComparer
      include Modules::OrderProvider
      include Lattices::BasicRelations

      extend Lattices::BasicRelations::Amorph
      UNDIRECTED_BONDS = [undirected_bond, double_bond, triple_bond].freeze
      INCOHERENT = Concepts::Incoherent.property.freeze
      UNFIXED = Concepts::Unfixed.property.freeze
      RELATIVE_PROPERTIES = [UNFIXED, INCOHERENT].freeze

      ACTIVE_BOND = Concepts::ActiveBond.property.freeze
      HYDROGEN = Concepts::AtomicSpec.new(Concepts::Atom.hydrogen).freeze

      UNIT_STATES = %i(atom_name valence).freeze
      STATIC_STATES = (UNIT_STATES + [:lattice]).freeze
      DYNAMIC_STATES = %i(relations danglings nbr_lattices relevants).freeze
      ALL_STATES = (STATIC_STATES + DYNAMIC_STATES).freeze

      class << self
        # Makes
        def raw(atom, **opts)
          atom_props = %i(name original_valence lattice)
          props = atom_props.map { |method_name| atom.public_send(method_name) } +
            DYNAMIC_STATES.map { |state| opts[state] || [] }

          new(props)
        end
      end

      # Fills under AtomClassifier#organize_properties
      attr_reader :smallests, :sames, :children

      # Stores all properties of atom
      # @overload new(props)
      #   @param [Array] props the array of default properties
      # @overload new(props)
      #   @param [Hash] props the hash of properties where each key is property method
      # @overload new(spec, atom)
      #   @param [DependentSpec | SpecResidual] spec in which atom will find properties
      #   @param [Concepts::Atom | Concepts::AtomReference | Concepts::SpecificAtom]
      #     atom the atom for which properties will be stored
      def initialize(*args)
        if args.one?
          arg = args.first
          if arg.is_a?(Array)
            @props = arg
          elsif arg.is_a?(Hash)
            @props = [
              arg[:atom_name] || raise(ArgumentError, 'Undefined atom name'),
              arg[:valence] || raise(ArgumentError, 'Undefined valence'),
              arg[:lattice] || raise(ArgumentError, 'Undefined lattice'),
              arg[:relations] || raise(ArgumentError, 'Undefined relations'),
              arg[:danglings] || [],
              arg[:nbr_lattices] || [],
              arg[:relevants] ? check_and_dup_relevants_from(arg[:relevants]) : []
            ]
          else
            raise ArgumentError, 'Wrong type of argument'
          end
        elsif args.size == 2
          spec, atom = args
          @props = [
            atom.name,
            atom.original_valence,
            atom.lattice,
            relations_for(spec, atom),
            danglings_for(spec, atom),
            nbr_lattices_for(spec, atom),
            check_and_dup_relevants_from(atom.relevants)
          ]
        else
          raise ArgumentError, 'Wrong number of arguments'
        end

        @smallests = Set.new
        @sames = Set.new
        @children = Set.new

        @_is_incoherent, @_is_unfixed, @_is_unfixed_from_nbrs = nil
        @_estab_bond_num, @_actives_num, @_dangling_hydrogens_num = nil
        @_hash, @_to_s = nil
      end

      # Deep compares two properties by all properties
      # @param [AtomProperties] other an other atom properties
      # @return [Boolean] equal or not
      def ==(other)
        same_basic_values?(other) && DYNAMIC_STATES.all? { |stn| eq_by?(other, &stn) }
      end
      alias :eql? :==

      # Compares two atom properties
      # @param [AtomProperties] other comparing atom properties
      # @return [Integer] the comparing result
      def <=>(other)
        if self == other
          0
        elsif include?(other)
          1
        elsif other.include?(self)
          -1
        else
          typed_order(self, other, :atom_name) do
            order(self, other, :valence) do
              typed_order(self, other, :lattice) do
                order(self, other, :estab_bonds_num) do
                  order(self, other, :crystal_relatons, :sort) do
                    order(self, other, :danglings, :sort, :reverse) do
                      order(self, other, :actives_num) do
                        order(self, other, :nbr_lattices_num) do
                          typed_order(self, other, :unfixed?) do
                            typed_order(self, other, :incoherent?)
                          end
                        end
                      end
                    end
                  end
                end
              end
            end
          end
        end
      end

      # Gets new atom properties instance from two instances
      # @param [AtomProperties] other adding atom properties
      # @return [AtomProperties] the extended instance of atom properties or nil
      def +(other)
        common_plus(other, merge_props(other, &:+))
      end

      # Gets the difference between two atom properties
      # @param [AtomProperties] other atom properties which will be erased from current
      # @return [AtomProperties] the difference result or nil
      def -(other)
        diff_props = merge_props(other) do |self_state, other_state|
          other_state.all?(&self_state.public_method(:include?)) &&
            self_state.accurate_diff(other_state)
        end

        if diff_props && DYNAMIC_STATES.all? { |sn| diff_props[sn] }
          self.class.new(state_values(diff_props))
        else
          nil
        end
      end

      # Accurate combines two atom properties
      # @param [AtomProperties] other atom properties which will be accurate added
      # @return [AtomProperties] the sum or nil
      def safe_plus(other)
        both = [self, other]
        smallests_of_both = both.map do |props|
          props.smallests.empty? ? Set[props] : props.smallests
        end

        # is additional cast to arrays requires due bug?
        max_root = smallests_of_both.map(&:to_a).reduce(:&).max
        return nil unless max_root

        diffs = both.map { |x| x - max_root }
        return nil unless diffs.all?

        is_zero_diffs = diffs.all?(&:zero?)
        right_diffs = is_zero_diffs ? both : diffs
        rests_sum = right_diffs.reduce(:+)
        return nil unless rests_sum

        is_zero_diffs ? rests_sum : max_root + rests_sum
      end

      # @return [AtomProperties] the result properties or nil
      def limited_plus(other, limitations)
        total_props = merge_props(other, &:+)
        return unless total_props

        key = [total_props[:atom_name], total_props[:valence], total_props[:lattice]]
        target_limits = limitations[key]

        groups = total_props[:relations].group_by(&:params)
        return if groups.any? { |(rp, rels)| target_limits[rp] < rels.size }

        lattices_num = total_props[:nbr_lattices].select(&:last).size
        both_wo_lattice = !lattice && !other.lattice
        return if both_wo_lattice && target_limits[:nbr_lts_num] < lattices_num

        common_plus(other, total_props)
      end

      # Calculates the hash of current instance for using it as key values in Hashes
      # @return [Integer] the hash of current instance
      def hash
        @_hash ||= ([atom_name, valence, lattice && lattice.instance] +
                          relations.sort + danglings.sort + relevants.sort +
                          nbr_lattices.map { |r, l| [r, l && l.instance] }.sort).hash
      end

      # Gets the major values of atom properties
      # @return [Array]
      def key
        [atom_name, valence, lattice]
      end

      # Gets idempotent value of atom properties group
      # @return [AtomProperties] zero
      def zero
        self - self
      end

      # @return [Boolean]
      def zero?
        self == zero
      end

      # Checks that current properties includes another properties
      # @param [AtomProperties] other probably child atom properties
      # @return [Boolean] includes or not
      def include?(other)
        other.contained_in?(self) || same_incoherent?(other) || same_unfixed?(other)
      end

      # Checks that current properties contained in another properties
      # @param [AtomProperties] other probably parent atom properties
      # @return [Boolean] contained or not
      def contained_in?(other)
        return false unless same_basic_values?(other)
        return false unless other.contain_all_relevants?(self)
        if incoherent? && other.relevant?
          other.eq_relevant_bonds?(self)
        else
          other.contain_all_bonds?(self)
        end
      end

      # Checks that some atom can have both properties: self and other
      # @param [AtomProperties] other checking atom properties
      # @return [Boolean] are self properties like other or not
      def like?(other)
        include?(other) || other.include?(self) || !!safe_plus(other)
      end

      # Checks that both properties have same states by hydrogen atoms
      # @param [AtomProperties] other properties which will be checked
      # @return [Boolean] same or not
      def same_hydrogens?(other)
        total_hydrogens_num == other.total_hydrogens_num
      end

      # Checks that other properties have same incoherent state
      # @param [AtomProperties] other probably same properties by incoherent state
      # @return [Boolean] same or not
      def same_incoherent?(other)
        same_basic_values?(other) && (incoherent? || maximal?) &&
          (maximal? || !other.relevant?) && same_dynamics?(other)
      end

      # Checks that other properties have same unfixed state
      # @param [AtomProperties] other probably same properties by unfixed state
      # @return [Boolean] same or not
      def same_unfixed?(other)
        same_basic_values?(other) && (unfixed? || maximal?) &&
          (maximal? || !other.unfixed?) && other.unfixed_by_nbrs? &&
          same_dynamics?(other)
      end

      # Checks that the inernal state of bonds are equal for comparing properties
      # @param [AtomProperties] other comparing properties
      # @return [Boolean] same or not
      def same_internal?(other)
        actives_num == other.actives_num && estab_bonds_num == other.estab_bonds_num
      end

      # Checks that current properties correspond to atom, lattice and have same
      # relations pack
      #
      # @param [Hash] info about checkable properties
      # @return [Boolean] is correspond or not
      def correspond?(info)
        info.all? do |key, value|
          internal_value = send(key)
          if internal_value.is_a?(Array)
            lists_are_identical?(internal_value, value)
          else
            internal_value == value
          end
        end
      end

      # Adds dependency from smallest atom properties
      # @param [AtomProperties] prop from which the current atom properties depends
      def add_smallest(prop)
        prop.add_children(self)
        @smallests = @smallests.subtract(prop.smallests)
        @smallests << prop
      end

      # Adds dependency from same atom properties
      # @param [AtomProperties] prop from which the current atom properties depends
      def add_same(prop)
        prop.add_children(self)
        @sames = @sames.subtract(prop.sames)
        @sames << prop
      end

      # Makes unrelevanted copy of self
      # @return [AtomProperties] unrelevanted atom properties
      def unrelevanted
        self.class.new(without_relevants)
      end

      # Has any relevant state or not
      # @return [Boolean]
      def relevant?
        !relevants.empty?
      end

      # Has unfixed state or not
      # @return [Boolean] contain or not
      def unfixed?
        return @_is_unfixed unless @_is_unfixed.nil?
        @_is_unfixed = relevants.include?(UNFIXED)
      end

      # Has incoherent state or not
      # @return [Boolean] contain or not
      def incoherent?
        return @_is_incoherent unless @_is_incoherent.nil?
        @_is_incoherent = relevants.include?(INCOHERENT)
      end

      # Makes incoherent copy of self
      # @return [AtomProperties] incoherented atom properties or nil
      def incoherent
        if valence > bonds_num && !incoherent?
          props = without_relevants
          props[-1] = [INCOHERENT]
          self.class.new(props)
        else
          nil
        end
      end

      # Gets property same as current but activated
      # @param [DependentTermination] atomic_dangling
      # @return [AtomProperties] activated properties or nil
      def activated(atomic_dangling = nil)
        fixed_dangling_props(:remove_dangling, atomic_dangling, &:add_dangling)
      end

      # Gets property same as current but deactivated
      # @param [DependentTermination] atomic_dangling
      # @return [AtomProperties] deactivated properties or nil
      def deactivated(atomic_dangling = nil)
        fixed_dangling_props(:add_dangling, atomic_dangling, &:remove_dangling)
      end

      # Gets property same as current but with added dangling property
      # @return [AtomProperties] extended properties or nil
      def add_dangling(dangling)
        if valence > bonds_num
          ext_dangs = danglings + [dangling]
          props = [*static_states, relations, ext_dangs, nbr_lattices]
          props << (valence > bonds_num + 1 ? relevants : [])
          self.class.new(props)
        else
          nil
        end
      end

      # Gets property same as current but witout dangling property
      # @return [AtomProperties] reduced properties or nil
      def remove_dangling(dangling)
        dgs = danglings.dup
        if dgs.delete_one(dangling)
          props = [*static_states, relations, dgs, nbr_lattices, relevants]
          self.class.new(props)
        else
          nil
        end
      end

      # Counts of dangling instances
      # @param [Symbol] state the counting state
      # @return [Integer] number of instances
      def count_danglings(state)
        danglings.select { |r| r == state }.size
      end

      # Gets number of active bonds
      # @return [Integer] number of active bonds
      def actives_num
        @_actives_num ||= count_danglings(ACTIVE_BOND)
      end

      # Gets the number of actives when each established bond replaced to active bond
      # @return [Integer] number of unbonded actives
      def unbonded_actives_num
        estab_bonds_num + actives_num
      end

      # Gets number of hydrogen atoms
      # @return [Integer] number of active bonds
      def dangling_hydrogens_num
        @_dangling_hydrogens_num ||= count_danglings(HYDROGEN)
      end

      # Counts total number of hydrogen atoms
      # @return [Integer] the number of total number of hydrogen atoms
      def total_hydrogens_num
        valence - bonds_num + dangling_hydrogens_num
      end

      # @return [Integer]
      def undir_bonds_num
        undir_bonds_num_in(relations)
      end

      # @return [Integer]
      def double_bonds_num
        relations.count(double_bond)
      end

      # @return [Integer]
      def triple_bonds_num
        relations.count(triple_bond)
      end

      # Gets the number of neighbour lattices of current atom properties
      # @return [Integer]
      def nbr_lattices_num
        nbr_lattices.select(&:last).size
      end

      # Checks has or not free bonds?
      # @return [Boolean] can form additional bond or not
      def has_free_bonds?
        return false if incoherent?
        num = unbonded_actives_num + dangling_hydrogens_num
        raise 'Wrong valence' if num > valence
        num < valence
      end

      # @return [Boolean] are all valence atoms has special state?
      def maximal?
        valence == bonds_num
      end

      # Convert properties to string representation
      # @return [String] the string representaion of properties
      def to_s
        return @_to_s if @_to_s

        name = atom_name.to_s

        dg = danglings.dup
        name = "*#{name}" while dg.delete_one(ACTIVE_BOND)

        while (monovalent_atom = dg.pop)
          name = "#{monovalent_atom}#{name}"
        end

        if relevant?
          relevants.each do |suffix|
            name = "#{name}:#{suffix}"
          end
        end

        name = "#{name}%#{lattice.name}" if lattice

        all_nls_groups = nbr_lattices.groups(&:last)
        many_nls_groups = all_nls_groups.reject(&:one?)
        mono_nls_groups = all_nls_groups.select(&:one?).reduce(:+)
        lattice_symbol = -> rel do
          if rel.multi?
            g = many_nls_groups.find { |g| g.size == rel.arity }
            frl = g && g.pop
            frl && (rel.arity - 1).times(&g.public_method(:pop))
          else
            olc = mono_nls_groups && mono_nls_groups.pop
            mlc = many_nls_groups.reject(&:empty?).first
            frl = olc || (mlc && mlc.pop)
          end
          (frl && frl.last) || '_'
        end

        rl = relations.dup
        down1 = rl.delete_one(bond_cross_110)
        down2 = rl.delete_one(bond_cross_110)
        if down1 && down2
          name = "#{name}<"
        elsif down1 || down2
          name = "#{name}/"
        elsif rl.delete_one(triple_bond)
          name = "#{name}≡#{lattice_symbol[triple_bond]}"
        elsif rl.delete_one(double_bond)
          name = "#{name}=#{lattice_symbol[double_bond]}"
        elsif rl.delete_one(undirected_bond)
          name = "#{name}~#{lattice_symbol[undirected_bond]}"
        end

        up1 = rl.delete_one(bond_front_110)
        up2 = rl.delete_one(bond_front_110)
        if up1 && up2
          name = ">#{name}"
        elsif up1 || up2
          name = "^#{name}"
        elsif rl.delete_one(double_bond)
          name = "#{lattice_symbol[double_bond]}=#{name}"
        end

        name = "-#{name}" if rl.delete_one(bond_front_100)
        name = "#{name}-" if rl.delete_one(bond_front_100)
        while rl.delete_one(undirected_bond)
          name = "#{lattice_symbol[undirected_bond]}~#{name}"
        end

        @_to_s = name
      end

      def inspect
        to_s
      end

    protected

      attr_reader :props

      # Define human named methods for accessing to props
      ALL_STATES.each_with_index do |name, i|
        define_method(name) { props[i] }
      end
      public :atom_name, :lattice, :relations, :danglings, :nbr_lattices, :relevants

      # The static (not arrayed) states of properties
      # @return [Array] the array of static states
      def static_states
        STATIC_STATES.map(&method(:send))
      end

      # Adds dependency from child atom properties
      # @param [AtomProperties] prop from which the current atom properties depends
      def add_children(prop)
        @children = @children.subtract(prop.children)
        @children << prop
      end

      # Gets number of established bond relations
      # @return [Integer] the number of established bond relations
      def estab_bonds_num
        @_estab_bond_num ||= estab_bonds_num_in(relations)
      end

      # Checks that properties have unfixed state by neighbour lattices set
      # @return [Boolean] unfixed or not?
      def unfixed_by_nbrs?
        return @_is_unfixed_from_nbrs unless @_is_unfixed_from_nbrs.nil?
        nbub = nbr_lattices.select { |r, _| r == undirected_bond }
        @_is_unfixed_from_nbrs =
          nbub.reduce(0) { |acc, (_, l)| acc + (l ? 1 : 0) } == 1
      end

      # Checks that other properties contain all bonds from current properties
      # @param [AtomProperties] other the checking properties
      # @return [Boolean] contain or not
      def contain_all_bonds?(other)
        [:relations, :danglings, :nbr_lattices].all? do |name|
          contain_all_by?(other, &name)
        end
      end

      # Checks that other properties have equal relations and neighbour lattices but
      #   contains all dangling properties
      # @param [AtomProperties] other the checking properties
      # @return [Boolean] equal or not
      def eq_relevant_bonds?(other)
        eq_relations?(other) && eq_nbr_lattices?(other) &&
          contain_all_danglings?(other)
      end

    private

      # Transforms passed hash to flatten sequence of states
      # @param [Hash] props_hash
      # @return [Array] the array of states
      def state_values(props_hash)
        ALL_STATES.map { |state| props_hash[state] }
      end

      # Concates two atom properties
      # @param [AtomProperties] other
      # @param [Hash] total_props
      # @return [AtomProperties] the result properties or nil
      def common_plus(other, total_props)
        lattices_num = total_props[:nbr_lattices].select(&:last).size
        total_unfixed = total_props[:relevants].include?(UNFIXED)
        return if total_unfixed && lattices_num > 1

        any_without_lattices = ![self, other].all?(&:lattice)
        return if total_unfixed && !any_without_lattices

        total_incoherent = total_props[:relevants].include?(INCOHERENT)
        any_without_bonds = [self, other].any? { |x| x.relations.empty? }
        return if total_incoherent && !any_without_bonds

        bonds_num = estab_bonds_num_in(total_props[:relations])
        dang_num = total_props[:danglings].size
        ext_num = bonds_num + dang_num

        is_incoherent = bonds_num < valence && total_incoherent
        is_unfixed = lattices_num == 1 && total_unfixed
        is_complete = (ext_num == valence)
        is_relevant = is_incoherent || is_unfixed
        is_unrelevanted = is_complete && is_relevant

        total_props[:lattice] = nil if lattices_num > 0
        if is_unrelevanted
          total_props[:relevants] = []
        elsif total_incoherent
          total_props[:relevants] = [INCOHERENT]
        end

        are_correct_props =
          (valid_relations?(total_props[:lattice], total_props[:relations])) &&
          (is_unrelevanted || ext_num < valence ||
            (is_complete && total_props[:relevants].empty?))

        are_correct_props ? self.class.new(state_values(total_props)) : nil
      end

      # @param [Symbol] atomic_op which will be called with atomic dangling if it is
      #   existed
      # @param [DependentTermination] atomic_dangling which will be additional applied
      #   if it need
      # @yield [AtomProperties] will be called with active bond
      # @return [AtomProperties] which danglings will be correctly exetnded if need
      def fixed_dangling_props(atomic_op, atomic_dangling = nil, &block)
        smart_proc = -> prps { smart_dangling_from(prps, atomic_dangling, &atomic_op) }
        result = atomic_op == :remove_dangling ? smart_proc[self] : self
        result = block[result || self, ACTIVE_BOND]
        atomic_op == :add_dangling ? smart_proc[result] : result
      end

      # @param [AtomProperties] props from which the atomic dangling will be remove if
      #   it is maximal
      # @param [DependentTermination] atomic_dangling which will be additional applied
      #   if it need
      # @yield [AtomProperties] by which the new property will be produced
      # @return [AtomProperties]
      def smart_dangling_from(props, atomic_dangling, &block)
        props && atomic_dangling ? block[props, atomic_dangling] : props
      end

      # Makes merged props hash with other instance by passed operation
      # @param [AtomProperties] other provider of properties
      # @yield binary operation which applies the dynamic states of both instances
      # @return [Array] the list of the merged properties hash
      def produce_props(other, &block)
        unit_props = UNIT_STATES.zip([atom_name, valence]).to_h
        all_lattices = [self, other].map(&:lattice).uniq
        any_unfixed = [self, other].any?(&:unfixed?)
        iterating_lattices =
          all_lattices == [nil] || any_unfixed ? [nil] : all_lattices.select(&:itself)

        iterating_lattices.each_with_object(unit_props) do |lts, result|
          result[:lattice] = lts
          DYNAMIC_STATES.each do |state_name|
            self_state, other_state = [self, other].map { |x| x.send(state_name) }
            result[state_name] = block[self_state, other_state]
          end

          unless result[:relevants]
            if relevants.include?(UNFIXED) && other.relevants.include?(INCOHERENT)
              result[:relevants] = [UNFIXED]
            end
          end
        end
      end

      # Merges own properties with properties of an other passed instance
      # @param [AtomProperties] other provider of properties
      # @yield binary operation which applies the dynamic states of both instances
      # @return [Hash] the merged properties hash or nil if properties cannot be merged
      def merge_props(other, &block)
        same_unit_states = UNIT_STATES.all? { |mn| send(mn) == other.send(mn) }
        is_valid_lts = lattice == other.lattice ||
                  (!lattice && other.lattice) || (lattice && !other.lattice)

        if same_unit_states && is_valid_lts
          mps = produce_props(other, &block)
          mps[:relevants] = mps[:relevants] ? mps[:relevants].uniq : []
          lattices_num = mps[:nbr_lattices] ? mps[:nbr_lattices].size : 0
          undir_bonds_num = mps[:relations] ? undir_bonds_num_in(mps[:relations]) : 0
          (undir_bonds_num == lattices_num ||
            (mps[:relations] && mps[:relations].any?(&:multi?))) &&
              mps
        else
          nil
        end
      end

      # Compares dynamic states of two properties
      # @peram [AtomProperties] other the comparing properties
      # @return [Boolean] equal or not
      def same_dynamics?(other)
        eq_nbr_lattices?(other) && eq_relations?(other) && same_danglings?(other)
      end

      # Compares with other properties by some method which returns list
      # @param [AtomProperties] other the comparing properties
      # @yield method by which will be comparing
      # @return [Boolean] lists are equal or not
      def eq_by?(other, &block)
        lists_are_identical?(block[self], block[other])
      end

      DYNAMIC_STATES.each do |method_name|
        # Compares current #{method_name} with other #{method_name}
        # @param [AtomProperties] other the comparing properties
        # @return [Boolean] lists are equal or not
        define_method(:"eq_#{method_name}?") do |other|
          eq_by?(other, &method_name)
        end

        # Checks that other properties contain all current #{method_name}
        # @param [AtomProperties] other the checking properties
        # @return [Boolean] contain or not
        define_method(:"contain_all_#{method_name}?") do |other|
          contain_all_by?(other, &method_name)
        end
      end
      protected :contain_all_relevants?

      # Checks that other properties contain all current states that go by some
      # method
      #
      # @param [AtomProperties] other the checking properties
      # @yield method by which will be comparing
      # @return [Boolean] contain or not
      def contain_all_by?(other, &block)
        stats = block[self].dup
        block[other].all? { |rel| stats.delete_one(rel) }
      end

      # Compares dynamic states of two properties
      # @peram [AtomProperties] other the comparing properties
      # @return [Boolean] equal or not
      def same_dynamics?(other)
        eq_nbr_lattices?(other) && eq_relations?(other) && same_danglings?(other)
      end

      # Compares dangling states of two properties
      # @peram [AtomProperties] other the comparing properties
      # @return [Boolean] similar danglings or not
      def same_danglings?(other)
        maximal? ? contain_all_danglings?(other) : eq_danglings?(other)
      end

      # Compares basic values of two properties
      # @peram [AtomProperties] other the comparing properties
      # @return [Boolean] same or not
      def same_basic_values?(other)
        static_states == other.static_states
      end

      # Harvest relations of atom in spec
      # @param [DependentSpec | SpecResidual] spec see at #new same argument
      # @param [Concepts::Atom | Concepts::AtomReference | Concepts::SpecificAtom]
      #   spec see at #new same argument
      # @return [Array] relations array
      def relations_for(spec, atom)
        remake_relations(spec, atom).map(&:last)
      end

      # Gets the relations of atom in spec, but drop positions and replace many
      # single undirected bonds to correspond values of multi bounds
      #
      # @param [DependentSpec | SpecResidual] spec see at #new same argument
      # @param [Concepts::Atom | Concepts::AtomReference | Concepts::SpecificAtom]
      #   spec see at #new same argument
      # @return [Array] the array of pairs of atoms and replaced relations
      def remake_relations(spec, atom)
        # only bonds without relevat states and positions
        bonds = spec.relations_of(atom, with_atoms: true).select { |_, r| r.bond? }

        result = []
        until bonds.empty?
          atwrel = bonds.pop
          nbr, rel = atwrel
          if rel.belongs_to_crystal?
            result << atwrel
            next
          end

          same = bonds.select { |pair| pair == atwrel }
          new_relation =
            if same.empty?
              rel
            else
              bonds.delete_one(atwrel)
              if same.size == 3 && same.size != 4
                bonds.delete_one(atwrel)
                triple_bond
              else
                double_bond
              end
            end

          result << [nbr, new_relation]
        end

        result
      end

      # Harvest dangling bonds of atom in spec
      # @param [MinuendSpec] spec see at #new same argument
      # @param [Concepts::Atom | Concepts::AtomReference | Concepts::SpecificAtom]
      #   spec see at #new same argument
      # @return [Array] dangling states array
      def danglings_for(spec, atom)
        dang_rels = spec.relations_of(atom).reject(&:relation?)
        RELATIVE_PROPERTIES.each_with_object(dang_rels) do |rel_prop, acc|
          acc.delete(rel_prop)
        end
      end

      # Collects only lattices which are reacheble through each undirected bond
      # @param [MinuendSpec] spec see at #new same argument
      # @param [Concepts::Atom | Concepts::AtomReference | Concepts::SpecificAtom]
      #   spec see at #new same argument
      # @return [Array] the array of achieving lattices and correspond relations
      def nbr_lattices_for(spec, atom)
        remake_relations(spec, atom).reduce([]) do |acc, (atom, relation)|
          if UNDIRECTED_BONDS.include?(relation)
            acc + [[undirected_bond, atom.lattice]] * relation.arity
          else
            acc
          end
        end
      end

      # Collects and checks that relevant states are correct
      def check_and_dup_relevants_from(incoming_relevant_states)
        if incoming_relevant_states.size < 2 # just incoherent or unfixed
          incoming_relevant_states.dup
        else
          msg = "Incorrect pack of relevant states: #{incoming_relevant_states}"
          raise ArgumentError, msg
        end
      end

      # Checks that numbers of passed relations is correct
      # @param [Concepts::Lattice] lts
      # @param [Array] rels the array of relation states
      # @return [Boolean] is valid or not
      def valid_relations?(lts, rels)
        if lts
          limits = lts.instance.relations_limit
          rels.group_by(&:params).all? { |pr, rs| limits[pr] >= rs.size }
        else
          rels.all?(&UNDIRECTED_BONDS.public_method(:include?))
        end
      end

      # Drops relevants properties if it exists
      # @return [Array] properties without relevants
      def without_relevants
        wr = props.dup
        wr[-1] = [] if relevant?
        wr
      end

      # Gets number of undirected bonds
      # @param [Array] relations where bonds will be counted
      # @return [Integer] the number of undirected bond relations
      def undir_bonds_num_in(relations)
        UNDIRECTED_BONDS.map { |bond| relations.count(bond) * bond.arity }.reduce(:+)
      end

      # Gets number of established bond relations
      # @param [Array] relations where bonds will be counted
      # @return [Integer] the number of established bond relations
      def estab_bonds_num_in(relations)
        bonds = relations.select(&:bond?)
        bonds.reject(&:multi?).size +
          2 * bonds.count(double_bond) +
          3 * bonds.count(triple_bond)
      end

      # Gets list of relations which belongs to crystal
      # @return [Array] the list of crystal relations
      def crystal_relatons
        relations.select(&:belongs_to_crystal?)
      end

      # Gets number of established and dangling bond relations
      # @return [Integer] the total number of bonds
      def bonds_num
        estab_bonds_num + danglings.size
      end
    end

  end
end