newmen/versatile-diamond

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

Summary

Maintainability
C
1 day
Test Coverage
module VersatileDiamond
  module Organizers

    # Classifies atoms in specs and associate each atom type with some value,
    # which will be used for detecting overlapping specs between each other
    # and generates optimal specs search algorithm (after some reaction has
    # been realised) for updating of real specs set
    class AtomClassifier

      # Initialize a classifier by empty sets of properties
      # @param [Array] danglings the list of termination species
      def initialize(danglings = [])
        @danglings = danglings

        @described_props = Set.new
        @over_danglings_props = Set.new
        @over_relevants_props = Set.new
        @unrelevanted_props = Set.new

        @_default_latticed_atoms = nil
        @_atomic_dangling = nil
        reset_caches!
      end

      # Provides the hash of all analyzed atom properties
      # @return [Hash] the hash with set of atom properties
      def props_hash
        @_props_hash ||= Hash[props_with_indexes]
      end

      # Provides the array of all analyzed atom properties
      # @return [Array] the array of all presented properties
      def props
        @_props ||= all_props.to_a.sort
      end

      # Analyze spec and store all uniq properties
      # @param [DependentBaseSpec | DependentSpecificSpec] spec the analyzing spec
      def analyze!(spec)
        raise 'Cache of properties already created' if @_props_with_indexes

        avail_props = spec.links.keys.map { |atom| AtomProperties.new(spec, atom) }
        avail_props.each do |prop|
          store_prop(@described_props, prop)
          append_danglings!(prop) unless @danglings.empty? || prop.relevant?
        end
      end

      # Organizes dependencies between properties
      def organize_properties!
        add_default_latticed_atoms
        organize_by_inclusion!(props)
        organize_by_relatives!(props)
      end

      # Classify spec and return the hash where keys are order numbers of properties
      # and values are numbers of atoms in spec with same properties
      #
      # @param [DependentSpec | SpecResidual] spec the analyzing spec
      # @option [DependentWrappedSpec] :without do not classify atoms like as from
      #   passed spec (not using when spec is termination spec)
      # @return [Hash] result of classification
      def classify(spec)
        append_to = -> acc, prop, num do
          idx = index(prop)
          acc[idx] ||= [prop, 0]
          acc[idx][1] += num
        end

        if spec.is_a?(DependentTermination)
          current_props = props.select { |prop| spec.terminations_num(prop) > 0 }
          current_props.each_with_object({}) do |prop, acc|
            append_to[acc, prop, spec.terminations_num(prop)]
          end
        else
          spec.anchors.each_with_object({}) do |atom, acc|
            append_to[acc, AtomProperties.new(spec, atom), 1]
          end
        end
      end

      # Finds index of passed property in collection
      # @overloaded index(collection, prop)
      #   @param [AtomProperties] prop the property index of which will be found
      # @overloaded index(collection, spec, atom)
      #   @param [DependentSpec | SpecResidual] spec the spec for which properties of
      #     atom will be found
      #   @param [Concepts::Atom | Concepts::AtomReference | Concepts::SpecificAtom]
      #     atom for which properties will be found
      # @return [Integer] the index of properties or nil
      def index(*args)
        prop =
          if args.one?
            args.first
          elsif args.size == 2
            AtomProperties.new(*args)
          else
            raise ArgumentError, "Wrong number of arguments: #{args.inspect}"
          end
        props_hash.key(prop)
      end

      # Checks that properties by index has relevants
      # @param [Integer] index the index of properties
      # @return [Boolean] has or not
      def has_relevants?(index)
        !!props_hash[index].relevant?
      end

      # Gets matrix of transitive closure for atom properties dependencies
      # @return [TransitiveMatrix] the general transitive closure matrix
      def general_transitive_matrix
        @_tmatrix ||= TransitiveMatrix.new(self, :smallests, :sames)
      end

      # @param [AtomProperties] prop which will be specified
      # @return [AtomProperties] the end point fo atom properties dependencies graph
      def specificate(prop)
        checking_prop = detect_prop(prop)
        small_next = smallests_transitive_matrix.specification_for(checking_prop)
        gen_next = general_transitive_matrix.specification_for(checking_prop)
        [small_next, gen_next].sort.last
      end

      # Gets array where each element is index of much more specific atom properties
      # @return [Array] the specification array
      def specification
        props.map(&method(:specificate)).map(&method(:index))
      end

      # Gets transitions array of actives atoms to notactives
      # @return [Array] the transitions array
      def actives_to_deactives
        collect_transitions(&:deactivated)
      end

      # Gets transitions array of notactives atoms to actives
      # @return [Array] the transitions array
      def deactives_to_actives
        collect_transitions(&:activated)
      end

      # Gets all used lattices in analysed results
      # @return [Array] the array of lattice instances
      def used_lattices
        props.map(&:lattice).uniq
      end

      # Gets the list of default atoms which are presented on lattice
      # @return [Array] the list of default latticed atoms
      def default_latticed_atoms
        @_default_latticed_atoms ||=
          used_lattices.compact.flat_map do |lattice|
            %w(major surface).map do |name|
              atom_hash = lattice.instance.send(:"#{name}_crystal_atom")
              props_hash = atom_hash.merge(lattice: lattice)
              AtomProperties.new(props_hash)
            end
          end
      end

      # Checks that the first atom properties are the second atom properties
      # @param [AtomProperties] bigger
      # @param [AtomProperties] smallest
      # @return [Boolean] is or not
      def is?(bigger, smallest)
        general_transitive_matrix[detect_prop(bigger), detect_prop(smallest)]
      end

      # Selects the children of passed atom properties
      # @param [AtomProperties] bigger for which the children properties will be
      #   gotten
      # @return [Array] the list of children atom properties
      def children_of(bigger)
        result = detect_prop(bigger).children.flat_map do |child|
          [child] + (child.children.empty? ? [] : children_of(child))
        end
        result.uniq.sort
      end

      # @return [Hash]
      def limitations
        @_limitations ||= props.reduce({}) do |acc, prop|
          groups = prop.relations.group_by(&:params)
          limits = groups.map { |rp, rels| [rp, rels.size] }.to_h
          merge_limits(acc, prop, limits)
        end
      end

      # @return [String]
      def inspect
        props_with_index = props.map.with_index
        items = props_with_index.map { |pr, i| "#{i.to_s.rjust(7)}:  #{pr}\n" }
        "{\n#{items.join}}"
      end

    private

      ACTIVE_BOND = AtomProperties::ACTIVE_BOND

      # @param [Hash] acc
      # @param [AtomProperties] prop
      # @param [Hash] groups
      # @return [Hash]
      def merge_limits(acc, prop, groups)
        key = prop.key
        slice = (acc[key] ||= {})
        slice[:nbr_lts_num] ||= 0
        slice[:nbr_lts_num] = [slice[:nbr_lts_num], prop.nbr_lattices_num].max
        groups.each_with_object(acc) do |(rp, num), _|
          slice[rp] ||= 0
          slice[rp] = [slice[rp], num].max
        end
      end

      # Checks that passed atom properties haven't most bigger
      # @param [AtomProperties] bigger atom properties which will be checked
      # @return [Boolean] is most bigger or not
      def most_bigger?(bigger)
        !props.any? { |prop| prop != bigger && is?(prop, bigger) }
      end

      # @return [Boolean] is any stored properties defined or not?
      def incoherent_defined?
        return @_is_incoherent_defined unless @_is_incoherent_defined.nil?
        @_is_incoherent_defined = props.any?(&:incoherent?)
      end

      # @return [Boolean] are required relative states or not
      def relatives_required?
        @danglings.empty? && incoherent_defined?
      end

      # Gets array of all using props with their indexes
      # @return [Array] zip of real properties to their indexes
      def props_with_indexes
        @_props_with_indexes ||= props.map.with_index { |prop, i| [i, prop] }
      end

      # @return [Set]
      def all_props
        @_all_props ||= @described_props + @unrelevanted_props +
          @over_danglings_props + @over_relevants_props
      end

      # Adds default atoms of all used lattices
      def add_default_latticed_atoms
        default_latticed_atoms.each { |prop| store_prop(@described_props, prop) }
      end

      # Iterates available dangling species and extends the total set of properties
      # @param [AtomProperties] prop which extended analogies will be stored too
      def append_danglings!(prop)
        observed_props = Set[prop]
        checking_props = [prop]
        until checking_props.empty?
          cp = checking_props.shift
          @danglings.each do |termination|
            storing_props = store_all(cp, termination, &:add_dangling) +
                            store_all(cp, termination, &:remove_dangling)
            new_props = storing_props.reject(&observed_props.public_method(:include?))
            checking_props = (checking_props + new_props).uniq
            observed_props = observed_props + new_props.to_set
          end
        end
      end

      # Organizes dependencies between atom properties by checking inclusion
      # @param [Array] props_list the observing atom properties
      def organize_by_inclusion!(props_list)
        group_props_list(props_list, &:contained_in?).each do |big, smallests|
          smallests.each do |small|
            big.add_smallest(small) unless deep_same?(big, small)
          end
        end
      end

      # Organizes dependencies between atom properties by checking relative states
      # @param [Array] props_list the observing atom properties
      def organize_by_relatives!(props_list)
        group_props_list(props_list, &method(:relevant_same?)).each do |big, smallests|
          smallests.each do |small|
            big.add_same(small) unless deep_same?(big, small)
          end
        end
      end

      # @param [AtomProperties] small
      # @param [AtomProperties] big
      # @return [Boolean] is big relevant states are same as in small
      def relevant_same?(small, big)
        !small.contained_in?(big) &&
          (big.same_incoherent?(small) || big.same_unfixed?(small))
      end

      # @param [AtomProperties] big atom properties which children will be checked
      # @param [AtomProperties] small atom properties which will be compared with each
      #   child properties of big
      # @return [Boolean] is big already has small as same
      def deep_same?(big, small)
        children = big.sames + big.smallests
        !children.empty? &&
          children.any? { |child| child == small || deep_same?(child, small) }
      end

      # Groups passed list of atom properties
      # @param [Array] props_list the iterating atom properties
      # @yield [AtomProperties, AtomProperties] do with bigger and smallest atom
      #   properties which should be compared by
      def group_props_list(props_list, &block)
        result = []
        props_list_dup = props_list.sort
        until props_list_dup.empty?
          big = props_list_dup.pop
          smallests = props_list_dup.select { |small| block[small, big] }
          result << [big, smallests.sort.reverse]
        end
        result.sort_by(&:first)
      end


      # Stores all derived properties which can be gotten by passed method name
      # @param [AtomProperties] prop which extended copies will be stored
      # @param [Concepts::TerminationSpec] termination spec by which the property
      #   copies will be created
      # @yield [AtomProperties] by which the derived props can be gotten
      # @return [Array] the list of added nodes
      def store_all(prop, termination, &block)
        result = []
        next_prop = prop
        loop do
          next_prop = block[next_prop, termination]
          return result unless next_prop
          unless next_prop.relevant? && @over_danglings_props.include?(next_prop)
            store_prop(@over_danglings_props, next_prop)
            result << next_prop
          end
        end
      end

      # Stores passed prop and it over props to internal sets
      # @param [Set] props_set the target set where props will be stored
      # @param [AtomProperties] prop the storing properties
      def store_prop(props_set, prop)
        return if props_set.include?(prop)
        store_and_drop_cache(props_set, prop)
        store_to(@unrelevanted_props, prop.unrelevanted)
        if !prop.incoherent? && relatives_required?
          store_to(@over_relevants_props, prop.incoherent)
        end
      end

      # Stores atom properties and drops the internal cache of all properties
      # @param [AtomProperties] prop the storing properties
      # @param [Set] props_set the target set where props will be stored
      def store_and_drop_cache(props_set, prop)
        props_set << detect_prop(prop)
        reset_caches!
      end

      # Stores passed prop and it unrelevanted analog to some set of properties
      # @param [AtomProperties] prop the storing properties
      # @param [Set] props_set the target set where props will be stored
      def store_to(props_set, prop)
        store_and_drop_cache(props_set, prop) unless not_nil_stored?(prop)
      end

      # Detects that passing atom properties already add
      # @param [AtomProperties] prop the  atom properties search patern or nil
      # @return [Boolean] was added or not
      def not_nil_stored?(prop)
        !prop || over_all_props(prop, &:any?)
      end

      # Detects analogies atom properties
      # @param [AtomProperties] prop the  atom properties search patern or nil
      # @return [AtomProperties] analogies atom properties from internal value or nil
      def detect_prop(prop)
        over_all_props(prop, &:find) || prop
      end

      # Applies passed method for detect some value on all properties set
      # @param [AtomProperties] prop the  atom properties search patern
      # @yield [Array] applies to all atom properties list
      # @return [Object] the result of passing method
      def over_all_props(prop, &block)
        block.call(all_props) { |x| prop == x }
      end

      # Collects transitions array by passed method name
      # @yield [AtomProperties] produce new properties
      # @return [Array] collected array
      def collect_transitions(&block)
        props.map do |prop|
          trans_prop = block[prop, atomic_dangling]
          target_prop = trans_prop && not_nil_stored?(trans_prop) ? trans_prop : prop
          index(specificate(target_prop))
        end
      end

      # Gets the atomic dangling if there is just one this dangling
      # @return [DependentTermination] another used termiation dangling atomic spec
      def atomic_dangling
        if @_atomic_dangling.nil?
          another_danglings = props.flat_map(&:danglings).uniq - [ACTIVE_BOND]
          @_atomic_dangling = another_danglings.one? ? another_danglings.first : false
        else
          @_atomic_dangling == false ? nil : @_atomic_dangling
        end
      end

      # Gets matrix of transitive closure for smallests atom properties
      # dependencies
      #
      # @return [TransitiveMatrix] the transitive closure matrix of smallests
      #   dependencies
      def smallests_transitive_matrix
        @_st_matrix ||= TransitiveMatrix.new(self, :smallests)
      end

      def reset_caches!
        @_limitations = nil
        @_all_props, @_props, @_props_with_indexes, @_props_hash = nil
        @_is_incoherent_defined = nil
        @_tmatrix, @_st_matrix = nil
      end
    end

  end
end