Class: HeatingSystemCalculator::ElementFinderByArea

Inherits:
BaseElementFinder show all
Defined in:
app/services/heating_system_calculator/element_finder_by_area.rb

Constant Summary collapse

MAX_SOLUTIONS =

Limit solutions to avoid explosive memory allocation

50

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from BaseElementFinder

#consolidate_elements, #find_best_solution

Methods inherited from BaseItemFinder

#get_number_of_poles_from_consolidated_elements, #get_total_from_solution

Constructor Details

#initialize(options) ⇒ ElementFinderByArea

Returns a new instance of ElementFinderByArea.



8
9
10
# File 'app/services/heating_system_calculator/element_finder_by_area.rb', line 8

def initialize(options)
  @element_set = options[:element_set]
end

Instance Attribute Details

#coverage_percentageObject (readonly)

Returns the value of attribute coverage_percentage.



3
4
5
# File 'app/services/heating_system_calculator/element_finder_by_area.rb', line 3

def coverage_percentage
  @coverage_percentage
end

#elementsObject (readonly)

Returns the value of attribute elements.



3
4
5
# File 'app/services/heating_system_calculator/element_finder_by_area.rb', line 3

def elements
  @elements
end

#errorObject (readonly)

Returns the value of attribute error.



3
4
5
# File 'app/services/heating_system_calculator/element_finder_by_area.rb', line 3

def error
  @error
end

#target_areaObject (readonly)

Returns the value of attribute target_area.



3
4
5
# File 'app/services/heating_system_calculator/element_finder_by_area.rb', line 3

def target_area
  @target_area
end

Instance Method Details

#calculate_elements_for_heated_area_sqft(heated_area_sqft, insulation_surface = nil) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'app/services/heating_system_calculator/element_finder_by_area.rb', line 12

def calculate_elements_for_heated_area_sqft(heated_area_sqft, insulation_surface = nil)
  # Priority: Find solutions that MEET OR EXCEED the heated area first
  # Only fall back to undersized solutions if nothing adequate can be found
  insulation_surface ||= heated_area_sqft

  # Phase 1: Try to find solutions that meet or exceed the target (up to insulation surface)
  # tcmin = target area, tcmax = insulation surface (allows slight overage)
  solutionset = find_elements(heated_area_sqft, insulation_surface)

  # Phase 2: If no adequate solutions, expand search to slightly over insulation surface
  if solutionset.length < MAX_SOLUTIONS
    solutionset.concat(find_elements(heated_area_sqft, 1.1 * insulation_surface))
  end

  # Phase 3: If still no solutions, allow undersized (fallback only)
  if solutionset.empty?
    # For small areas, allow down to 46-90% coverage on sliding scale
    # For larger areas, allow down to 85% coverage
    if heated_area_sqft < 120.0
      min_coverage = (0.46 + (0.44 * (heated_area_sqft / 120.0))) * heated_area_sqft
    else
      min_coverage = 0.85 * heated_area_sqft
    end
    solutionset = find_elements(min_coverage, heated_area_sqft)
  end

  foundsolution = false
  if solutionset.length > 1
    # if we get more than a single solution, find the best based on closeness to target and price
    solution = find_best_solution(solutionset, heated_area_sqft)
    foundsolution = true
  elsif solutionset.length == 1
    # if we get a single solution, we are done
    solution = solutionset[0]
    foundsolution = true
  end

  if foundsolution
    # get consolidated elements list
    @elements = consolidate_elements(solution)
    @coverage_percentage = [(100.0 * get_total_from_solution(solution, 'area').to_f / heated_area_sqft).round, 100].min
  else
    @error = { error_status: :no_solutions_found, error_message: 'No matching heating elements found.' }
  end
  @target_area = heated_area_sqft
  self
end

#find_elements(tcmin, tcmax) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'app/services/heating_system_calculator/element_finder_by_area.rb', line 60

def find_elements(tcmin, tcmax)
  # try to find a set of elements that at least covers TCMIN but does not exceed TCMAX
  # assume no set was found
  foundsolution = false
  # initialize the number of rolls to try in the solution set, guess a smaller than necessary but good starting point
  numrollstotry = [0, ((tcmin / @element_set.last['area']).floor - 1)].max
  # initialize the solution set
  solutionset = []
  # Track total solutions found across all iterations
  @total_solutions_found = 0
  # loop while no solution is found
  until foundsolution
    # increment the number of rolls to try
    numrollstotry += 1
    # test to see if smallest roll times the number of rolls to try exceeds TCMAX
    if @element_set[0]['area'] * numrollstotry > tcmax
      # yes we are done, no more solutions possible
      break
    elsif numrollstotry > 100
      # here we have a large area compared to our elements set so just set the solution to the correct set of the largest
      numrolls = ((tcmax / @element_set.last['area']).ceil - 1)
      soln = []
      numrolls.times do
        soln << @element_set.last
      end
      solutionset << soln
      foundsolution = true
    else
      # keep looking for solutions using this number of rolls
      @recurse_count = 0
      solutionset = find_solutions_for(numrollstotry, tcmin, tcmax)
      # trace("solutionset.length: "+String(solutionset.length));
      foundsolution = true if solutionset.length >= 1
    end
  end
  solutionset
end

#find_solutions_for(num, amin, amax) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/services/heating_system_calculator/element_finder_by_area.rb', line 98

def find_solutions_for(num, amin, amax)
  # this is a recursive function that enumerates all combinations of num rolls whose coverage
  # sums up to a value between amin and amax
  # set up an array of solutions
  solutionset = []
  # Early termination if we have enough solutions globally
  return solutionset if @total_solutions_found >= MAX_SOLUTIONS

  if @recurse_count <= 100
    if num == 1
      # we are looking for a single roll solution
      # loop over all elements in the product set
      @element_set.each do |elem|
        break if @total_solutions_found >= MAX_SOLUTIONS
        # test that area of the roll is within range
        if elem['area'] >= amin && elem['area'] <= amax
          # save this roll as a solution
          solutionset << [elem]
          @total_solutions_found += 1
        elsif elem['area'] > amax
          # we have reached a roll whose area exceeds TCMAX, we can stop looping
          # since the elements array is sorted ascending on area
          break
        end
      end
    elsif num > 1
      # we are looking for more than a single roll solution
      @recurse_count += 1
      # do a sanity check
      if @element_set[0]['area'] * num <= amax
        # loop over all elements
        @element_set.reverse_each do |elem|
          break if @total_solutions_found >= MAX_SOLUTIONS
          # Skip elements that are too large for the remaining area
          next if elem['area'] > amax

          # recursively call this function with a starting area of the current element roll
          # and num-1 rolls required
          tmpsolnset = find_solutions_for(num - 1, amin - elem['area'], amax - elem['area'])
          next if tmpsolnset.empty?

          # if the resulting solution set is non empty then add the current element roll to each solution in the set
          # Optimize: avoid creating intermediate arrays
          tmpsolnset.each do |tmpsol|
            break if @total_solutions_found >= MAX_SOLUTIONS
            solutionset << [elem, *tmpsol]
            @total_solutions_found += 1
          end
        end
      end
    end
  end
  # return the solution set
  solutionset
end