Dynamically generate constrained solutions that meet multiple criteria

115 views Asked by At

I feel like there is an elegant Python solution for this problem that I've been trying to solve with VBA. Can someone please help?

How can I generate a list of numbers that meet the following criteria:

  1. Are all integers
  2. The total is first split between types.
  3. The numbers are further split into subtypes and optimized to meet the required percentages.
  4. The total sum does not exceed the specified Total.

Example of this problem:

  • You plan to build a maximum of 102 residential units.
  • Construction type: 40% of them will be Studios and 60% will be Townhouse construction type. (Could have more or fewer types)
  • There are two Priority lists for the future occupants for the apartments: Assisted and Unassisted
    • The unit allocation percentages in the Assisted(A) list are required to be fully met and the Unassisted(U) list is flexible. We'd like the Unassisted(U) list to be up to 20% of the Total units if possible, but we definitely need a minimum of 80% of the Total units to be Assisted(A) units.
  • The Assisted(A) units are a total of 102x80% = 81.6 units (already not an integer)
    • A minimum of 10% of the Assisted(A) list units must be for elderly individuals.
    • A minimum of 40% of the Assisted(A) list units must be for families.
    • The remaining 50% of the Assisted(A) list units are for aspiring data scientists.

The remaining 20% of the Total units are Unassisted(U) occupancy units.

So:

  • Total: 102 units.
  • Construction_type_categories=[.4, .6] (102 units split into Studios and Townhouses: 40% and 60%)
  • Assisted=[.1,.4,.5] (80%+ of the total units are Assisted, further categorized as elderly, family, other occupancy according to the percentages in the list)
  • Unassisted units are the remaining units that were not part of the 80%+ of the Assisted list. (up to 20% of total units)

Result:

[4,16,12,8,7,25,19,11]

Breakdown:

  • 4 Studio Units Assisted for elderly
  • 16 Studio Units Assisted for families
  • 12 Studio Units for other Assisted occupancy type
  • 8 Studio Units (Unassisted)
  • 7 Townhouse Units Assisted for elderly
  • 25 Townhouse Units Assisted for families
  • 19 Townhouse Units for other Assisted occupancy type
  • 11 Townhouse Units (Unassisted).

I thought of first generating a preliminary array of rounded numbers and then looping through and making adjustments. It looked so tedious I've started considering generating a large matrix of numbers using numpy and filtering by the outlined criteria.

It has been time consuming to generate and optimize the numbers manually so I appreciate any help with a better solution.

import math

def is_even(x):
    if x % 2 == 0:
        return True
    else:
        return False

total=102 
unassisted=.2
unassisted_unit_count= math.floor(total*unassisted)
assisted_unit_count=total- unassisted_unit_count

construction_type_categories=[.4, .6] #Must be even.
assisted_subcategories=[.1,.4,.5] #Last element here will be of least priority.


def Unit_Number_Getter(L):

    if L[1]=='total_constr_type_amounts':
        giventotal= total
    if L[1]=='assisted_constr_type_amounts':
        giventotal= assisted_unit_count

    #Spliting by construction type (preliminary).
    constr_type_amounts=[]
    for ct in construction_type_categories:
        constr_type_amounts.append(round(giventotal * ct))

    #Making the unit counts even for the total construction type amounts (need to).
    for p in constr_type_amounts:
        if not is_even(p):
            add_here=constr_type_amounts.index(p)
            for f in constr_type_amounts:
                if not is_even(f):
                    from_here= constr_type_amounts.index(f)
            constr_type_amounts[add_here] +=1
            constr_type_amounts[from_here] -=1
    assert sum(constr_type_amounts)==giventotal

    print L[1]
    print(constr_type_amounts)
    L[0]=constr_type_amounts


total_constr_type_amounts=0
assisted_constr_type_amounts=0

List_of_lists=[[total_constr_type_amounts,"total_constr_type_amounts"],[assisted_constr_type_amounts,"assisted_constr_type_amounts"]]

#Established the unit counts for each construction type (for Assisted and total units)
for L in List_of_lists:
    Unit_Number_Getter(L)


#Getting a list of the unit counts for each assisted subcategory in each constr type.
testlist=[]
for c in List_of_lists[1][0]:
    already_added=0
    for a in assisted_subcategories[:-1]:
        adding_now= math.ceil(c * a)
        testlist.append(adding_now)
        already_added+=adding_now
    #^Added the priority assisted units (all but the last element).   

    #Now will add the last of the assisted units.
    testlist.append(c-already_added)

    #Now will add he least prioritized unassisted units up to the max.
    Max_unassisted_units= List_of_lists[0][0][List_of_lists[1][0].index(c)]-c

    testlist.append(Max_unassisted_units)

    assert ((c+Max_unassisted_units)== List_of_lists[0][0][List_of_lists[1][0].index(c)])#all units present

print("Result: "+ "\n" + str(testlist))     
0

There are 0 answers