newmen/versatile-diamond

View on GitHub
analyzer/lib/generators/engine_code.rb

Summary

Maintainability
C
7 hrs
Test Coverage
module VersatileDiamond
  using Patches::RichArray
  using Patches::RichString

  module Generators

    # Generates program code based on engine framework for each interpreted entities
    class EngineCode < Base

      MAIN_CODE_INST_NAMES =
        %w(atom_builder config env finder handbook names rates_reader).freeze

      attr_reader :sequences_cacher, :detectors_cacher

      # Initializes code generator
      # @param [Organizers::AnalysisResult] analysis_result see at super same argument
      # @param [String] out_path the path where result files will be placed
      # @param [Hash] opts
      def initialize(analysis_result, out_path, **opts)
        super(analysis_result, **opts)
        @out_path = out_path

        @sequences_cacher = Code::SequencesCacher.new
        @detectors_cacher = Code::DetectorsCacher.new(self)

        @lattices = collect_code_lattices
        @species = collect_code_species
        @reactions = collect_code_reactions

        reset_caches!
        surface_reactants.each(&:find_symmetries!)
      end

      # provides methods from base generator class
      public :classifier, :base_surface_specs, :specific_surface_specs, :term_specs,
        :ubiquitous_reactions, :spec_reactions

      # Generates source code and configuration files
      def generate(**)
        common_files = generating_instances.flat_map { |cc| cc.generate(@out_path) }
        (common_files.uniq + [common_main_file]).each { |f| f.copy_to(@out_path) }
      end

      MAIN_CODE_INST_NAMES.each do |name|
        var_name = :"@_#{name}"
        # Gets #{name} class code generator
        # @return [#{name.classify}] the #{name} class code generator instance
        define_method(name.to_sym) do
          instance_variable_get(var_name) ||
            instance_variable_set(var_name, Code.const_get(name.classify).new(self))
        end
      end

      # Collects only unique base atom instances
      # @return [Array] the array of pure unique atom instances
      def unique_pure_atoms
        pure_atoms = base_specs.reduce([]) do |acc, spec|
          all_atoms = spec.links.keys
          base_atoms = all_atoms.reject { |atom| atom.reference? || atom.specific? }
          multivalents = base_atoms.select { |atom| atom.valence > 1 }
          unificate(acc + multivalents) { |a, b| a.name == b.name }
        end

        raise 'No unique atoms found!' if pure_atoms.empty?
        pure_atoms.map { |atom| Code::Atom.new(atom) }
      end

      # Gets all reactions which were collected
      # @return [Array] the list of reaction code generators
      def reactions
        @reactions.values
      end

      # Gets all used lattices
      # @return [Array] the array of used lattices
      def lattices
        @lattices.values
      end

      # Gets lattice source classes code generator
      # @param [Concepts::Lattice] lattice by which code generator will be got
      # @return [Code::Lattice] the lattice code generator
      def lattice_class(lattice)
        @lattices[lattice.name]
      end

      # Gets specie source files generator by some specie name
      # @param [Symbol] spec_name by which code generator will be got
      # @return [Code::Specie] the correspond code generator instance
      def specie_class(spec_name)
        @species[spec_name]
      end

      # Gets the reaction code generator
      # @param [Symbol] reaction_name the name of reaction which will be returned
      # @return [Code::BaseReaction] the reaction code generator instance
      def reaction_class(reaction_name)
        @reactions[reaction_name]
      end

      # Gets root species
      # @return [Array] the array of root specie class code generators
      def root_species
        @_root_species ||= surface_reactants.select(&:find_root?)
      end

      # Gets non simple and non gas collected species
      # @return [Set] the array of collected specie class code generators
      def surface_reactants
        @_surface_reactants ||= surface_reactants_hash.values.to_set
      end

      # Gets the list of specific species which are gas molecules
      # @return [Array] the list of dependent specific gas species
      def specific_gas_specs
        @_gas_specs ||=
          dependent_specs.values.select { |s| s.gas? && (s.simple? || s.specific?) }
      end

      # Gets number of usages of passed atom in all major species
      # @param [Organizers::DependentWrappedSpec] spec which atom props will be checked
      # @param [Concepts::Atom | Concepts::AtomRelation | Concepts::SpecificAtom]
      #   atom which number of usages will be counted
      # @option [Boolean] :latticed_too if is true then default latticed atoms also
      #   will be checked of several times usation for source species
      # @return [Integer] how many times passed atoms uses in all major species
      def usages_num(spec, atom, **kwargs)
        slice = all_classifications(**kwargs)[spec.name]
        (slice && slice[atom_properties(spec, atom)]) ||
          (spec.links.keys.include?(atom) ? 1 : 0)
      end

      # Checks that atom can contain several references to specie under simulation do
      # @param [Organizers::DependentWrappedSpec] spec the use of which will be checked
      #   for passed atom
      # @param [Concepts::Atom | Concepts::AtomRelation | Concepts::SpecificAtom]
      #   atom which number possible references to correspond specie will be checked
      # @option [Boolean] :latticed_too if is true then default latticed atoms also
      #   will be checked of several times usation for source species
      # @return [Boolean] is atom can be used in several species by the role which it
      #   plays in passed specie
      def many_times?(spec, atom, **kwargs)
        usages_num(spec, atom, **kwargs) > 1
      end

      def inspect
        '✾'
      end

    private

      # Gets the instances of major code elements
      # @return [Array] the array of instances
      def major_code_instances
        MAIN_CODE_INST_NAMES.map { |name| send(name.to_sym) }
      end

      # @return [CommonFile]
      def common_main_file
        CommonFile.new('main.cpp')
      end

      # @return [Array] the flatten list of all generating instances
      def generating_instances
        [
          major_code_instances,
          unique_pure_atoms,
          lattices.compact,
          surface_reactants,
          reactions
        ].flat_map(&:to_a) + [Makefile.new]
      end

      # Finds and drops not unique items which are compares by block
      # @param [Array] list of unificable elements
      # @yield [Object, Object] compares two elements
      # @return [Array] the array of unique elements
      def unificate(list, &block)
        ldp = list.dup
        result = []
        until ldp.empty?
          first = ldp.pop
          ldp.reject! { |item| block[first, item] }
          result << first
        end
        result
      end

      # Collects all used lattices and wraps it by source code generator
      # @return [Hash] the mirror of lattices to code generator
      def collect_code_lattices
        classifier.used_lattices.each_with_object({}) do |concept, acc|
          if concept
            acc[concept.name] = Code::Lattice.new(self, concept)
          else
            acc[nil] = nil
          end
        end
      end

      # Gets the list of all available organized dependent species
      # @return [Array] the list with dependent species from analysis result
      def all_avail_specs
        (base_specs || []) + (specific_specs || [])
      end

      # Collects all used species from analysis results
      # @return [Hash] the mirror of specs names to dependent species
      def dependent_specs
        @_dependent_specs ||= Hash[all_avail_specs.map { |spec| [spec.name, spec] }]
      end

      # Wraps all collected species from analysis results. Makes the mirror of specie
      # names to specie code generator instances.
      def collect_code_species
        dependent_specs.each_with_object({}) do |(name, spec), acc|
          acc[name] = Code::Specie.new(self, spec)
        end
      end

      # Gets the list of reactions with code generator klass for each reactions list
      # @return [Array] list of pairs where first item is type of reaction and the
      #   second is list of reactions of it type
      def reactions_with_types
        local_reactions = spec_reactions.select(&:local?)
        lateral_reactions = spec_reactions.select(&:lateral?)
        typical_reactions = spec_reactions - local_reactions - lateral_reactions

        [[Code::LateralReaction, lateral_reactions]] +
          if ubiquitous_reactions.empty?
            # if ubiquitous reactions are not presented then all local reactions
            # interpreted as typical reaction
            [[Code::TypicalReaction, local_reactions + typical_reactions]]
          else
            %w(ubiquitous local typical).map do |rtype|
              [Code.const_get("#{rtype.classify}Reaction"), eval("#{rtype}_reactions")]
            end
          end
      end

      # Wraps all analyzed reactions
      # Makes the mirror of reaction names to reaction code generator instances
      # @return [Hash] the mirror of reactions names to dependent reactions
      def collect_code_reactions
        reactions_with_types.reduce({}) do |acc, args|
          acc.merge(wrap_reactions(*args))
        end
      end

      # Wraps one reaction by passed code generator class and store it to interal
      # cache
      #
      # @param [Class] klass by which the passed reaction will be wrapped
      # @param [Array] reactions which will be wrapped
      # @return [Hash] the mirror of reaction names to the wrapped instances
      def wrap_reactions(klass, reactions)
        Hash[reactions.map { |reaction| [reaction.name, klass.new(self, reaction)] }]
      end

      # Provides the hash of surface specie class generators with significant species
      # @return [Hash] the hash where keys are concept names of species and the values
      #   are specie class generators
      def surface_reactants_hash
        @species.select do |name, _|
          spec = dependent_specs[name]
          !skipping?(spec) && spec.deep_reactant?
        end
      end

      # Checks that passed spec is skipping
      # @param [Organizers::DependentWrappedSpec] spec which state will be checked
      # @return [Boolean] is interesed spec or not
      def skipping?(spec)
        spec.simple? || spec.gas? || spec.termination?
      end

      # Provides general classification of anchors of all species
      # @option [Boolean] :latticed_too if is true then default latticed atoms also
      #   will be checked of several times usation for source species
      # @return [Hash] the classification hash, where keys are specie names and values
      #   are hashes, which keys are anchors as atom properties and values are numbers,
      #   of usage these properties in all another major species
      # @example
      #   {
      #     :bridge => {
      #       (C%d<) => 1,
      #       (^C%d<) => 2
      #     },
      #     :methyl_on_bridge => {
      #       (_~C%d<) => 1,
      #       (C~%d) => 2 # if there is cross_bridge_on_bridges
      #     }
      #   }
      def all_classifications(latticed_too: true)
        @_all_classifications[latticed_too] ||=
          major_dept_specs.reduce({}) do |all, spec|
            acc = classificate_parents(all, spec)
            latticed_too ? inject_latticed_props(acc, spec) : acc
          end
      end

      # Gets a list of structured dependent species
      # @return [Array] the list of species which can be classified
      def major_dept_specs
        dependent_specs.values.reject(&method(:skipping?))
      end

      # Counts how many times latticed atom props contains the passed atom props
      # @param [Organizers::AtomProperties] latticed_props which will used as context
      # @param [Organizers::AtomProperties] props which will combined num times
      # @return [Integer] how many times making combination of atom properties can
      #   contains in latticed atom properties
      def contain_times(latticed_props, props)
        diff = latticed_props - props
        if diff
          num = 1
          if !diff.zero? && props.include?(diff)
            loop do
              sum = props + diff
              break unless sum && latticed_props.include?(sum)
              num += 1
              break if sum == latticed_props
            end
          end
          num
        else
          0
        end
      end

      # Counts number of usages pf passed atom props in each latticed atom props
      # @param [Organizers::AtomProperties] props which will combination will check
      # @return [Array] numbers of usages of making atom properties combination in
      #   latticed atom properties
      def latticed_contains_times(props)
        latticed_props = classifier.default_latticed_atoms
        undangled_props = latticed_props.select { |ap| ap.danglings.empty? }
        undangled_props.map { |def_ap| contain_times(def_ap, props) }
      end

      # Gets maximal number of usages pf passed atom props in latticed atom props
      # @param [Organizers::AtomProperties] props which will combination will check
      # @return [Integer] maximal number of usages of making atom properties
      #   combination in latticed atom properties
      def max_latticed_contains_times(props)
        @_max_latticed_contains_times[props] ||= latticed_contains_times(props).max
      end

      # Extend passed classification hash when atoms of passed spec like latticed atoms
      # @param [Hash] all the general classification of all species
      # @param [Organizers::DependentWrappedSpec] spec which anchors will be classified
      # @yield [Organizers::AtomProperties] counts a number of usages
      # @return [Hash] the extended classification hash with values for latticed atoms
      def inject_latticed_props(all, spec)
        inject_classification(all, spec, &method(:max_latticed_contains_times))
      end

      # Extends passed classification hash for passed spec
      # @param [Hash] all the general classification of all species
      # @param [Organizers::DependentWrappedSpec] spec which anchors will be classified
      # @yield [Organizers::AtomProperties] counts a number of usages
      # @return [Hash] the extended classification hash with values for anchors of spec
      def inject_classification(all, spec, &block)
        all[spec.name] ||= {}
        avail_props = spec.anchors.map { |atom| atom_properties(spec, atom) }
        avail_props.each_with_object(all.dup) do |ap, acc|
          inner = acc[spec.name]
          stored_value = inner[ap]
          usage_times = block[ap]
          inner[ap] = usage_times if !stored_value || usage_times > stored_value
        end
      end

      # Extends passed classification hash by parents of passed spec
      # @param [Hash] all the general classification of all species
      # @param [Organizers::DependentWrappedSpec] spec which parents will be classified
      # @return [Hash] the extended classification hash with usages number for each
      #   anchor of parent species which are presented in passed spec several times
      def classificate_parents(all, spec)
        ppns = same_pwts(spec).map { |pr, tw, n| [pr, atom_properties(pr, tw), n] }
        pr_with_ap_num = ppns.uniq { |pr, ap, n| [pr.original, ap, n] }
        triples = pr_with_ap_num + self_swapns(spec)
        triples.each_with_object(all) do |(parent, twin_props, num), acc|
          inject_classification(acc, parent) { |ap| twin_props == ap ? num : 1 }
        end
      end

      # Getsh list of triples just for passed specie
      # @param [Organizers::DependentWrappedSpec] spec which will be pseudo classified
      # @return [Array] the list of self triples for passed specie
      def self_swapns(spec)
        pss = spec.links.keys.map { |atom| atom_properties(spec, atom) }.uniq
        pss.map { |ap| [spec, ap, 1] }
      end

      # Gets same parents with twins for passed spec and atom
      # @param [Organizers::DependentWrappedSpec] spec which parents will be gotten
      # @return [Array] the list of unique parents with twins which several times uses
      #   by passed spec and atom
      def same_pwts(spec)
        groups = all_pps(spec).groups do |pr, (tw, a)|
          [pr.original, atom_properties(pr.original, tw), a]
        end

        groups.reject(&:one?).map do |group|
          pr, (tw, _) = group.first
          [pr, tw, group.map(&:first).uniq.size]
        end
      end

      # Gets the list of all possible structures where for each this structure the
      # parent, their twin and corresponding passed spec atom are present
      #
      # @param [Organizers::DependentWrappedSpec] spec which parents will be gotten
      # @return [Array] the list of specific pps structures
      def all_pps(spec)
        spec.links.keys.flat_map { |atom| pps_for(spec, atom) }
      end

      # Gets all possible parents with twin and correspond atom of spec for passed spec
      # and atom
      #
      # @param [Organizers::DependentWrappedSpec] spec which parents will be gotten
      # @param [Concepts::Atom | Concepts::AtomRelation | Concepts::SpecificAtom]
      #   atom for which the twins also will be gotten
      # @return [Array] the list with all available parents with twin and correspond
      #   atom of spec
      def pps_for(spec, atom)
        pwts = spec.parents_with_twins_for(atom)
        pwts.map { |pr, tw| [pr, [tw, pr.atom_by(tw)]] } +
          pwts.flat_map do |pr, tw|
            pps_for(pr, tw).map do |sub_pr, (sub_tw, a)|
              [sub_pr, [sub_tw, pr.atom_by(a)]]
            end
          end
      end

      # Resets the internal caches
      def reset_caches!
        @_atom_builder, @_env, @_finder, @_handbook = nil
        @_dependent_specs, @_root_species, @_surface_reactants, @_gas_specs = nil
        @_all_classifications = {}
        @_max_latticed_contains_times = {}
      end
    end

  end
end