3

Suppose I have the following 5x5x5 3D array, consisting of binary values:

space = [
[[0,1,0,0,1], [1,0,0,1,0], [0,1,0,1,1], [0,0,0,1,1], [0,1,1,0,1]],
[[1,1,1,0,1], [0,0,0,1,0], [0,0,1,1,1], [0,0,0,1,1], [0,1,0,0,0]],
[[0,1,0,1,0], [1,1,0,0,0], [1,0,0,1,0], [0,1,1,1,0], [0,1,1,1,1]],
[[0,1,0,1,0], [0,1,0,1,1], [1,1,0,1,0], [1,0,0,1,0], [0,0,0,0,0]],
[[1,0,0,1,1], [0,1,1,0,1], [0,1,0,1,1], [0,1,1,0,1], [1,0,1,0,0]],
]

and a function measure(space) which takes this 3D array as the input, and returns a real value. My goal is to find the best space configuration that returns the minimum measure() output.

How may I use scipy.optimize.minimize which takes a 1D-array as input (or any other function/library you might think is more appropriate for this problem) to solve this optimization problem?


EDIT: To clarify, the measure() function converts the 3D array into a CAD model (where 1: solid; 0: void), and passes the 3D geometry into an electromagnetic solver (antenna simulator) to get a result describing the "efficiency" of the antenna (sort of what the metric describes, except the lower the value is, the better the performance of the antenna).

Coto TheArcher
  • 367
  • 3
  • 13
  • Can you provide your `measure` function? Since your `space` array is binary-valued, you have an integer programming problem. This kind of problem can't be solved directly by `scipy.optimize.minimize`. You can either use a penalty approach or another library like `pulp` or `pyscipopt`, assumed your `measure` function is linear. – joni May 04 '22 at 13:23
  • @joni Added a brief description on what the `measure` function does. – Coto TheArcher May 04 '22 at 15:26

1 Answers1

1

Don't use a 1d optimization function, there are at least three (surely more) approaches you can take:

  • Brute force, in your case that would be trying 2**125, which seems a bit too much.

  • Using MonteCarlo, i.e generating random solutions till finding the best, or at least one that is good enough

  • Using genetic algorithms, which will be probably the best you can get for this problem. You can use PyGAD for instance, and it won't take much time to get a good solution if not the best.

Here I put an example working where you only need to specify your fitness_function, in this case it will likely find the best solution.

import pygad
import numpy as np

space = [
[[0,1,0,0,1], [1,0,0,1,0], [0,1,0,1,1], [0,0,0,1,1], [0,1,1,0,1]],
[[1,1,1,0,1], [0,0,0,1,0], [0,0,1,1,1], [0,0,0,1,1], [0,1,0,0,0]],
[[0,1,0,1,0], [1,1,0,0,0], [1,0,0,1,0], [0,1,1,1,0], [0,1,1,1,1]],
[[0,1,0,1,0], [0,1,0,1,1], [1,1,0,1,0], [1,0,0,1,0], [0,0,0,0,0]],
[[1,0,0,1,1], [0,1,1,0,1], [0,1,0,1,1], [0,1,1,0,1], [1,0,1,0,0]],
]
space = np.array(space)

# I create a reference binary matrix to create a objective solution
i = np.identity(5)
ref = np.dstack([i]*5)

# flat your array to do it gen-like
space= space.flatten()
ref = ref.flatten()

def fitness_func(solution, solution_idx):
    # write here your fitness function, in my case i just compare how different two matrix are.
    fitness = np.sum(ref == solution)
    return fitness
    
fitness_function = fitness_func

num_generations = 400
num_parents_mating = 10

sol_per_pop = 14
num_genes = len(space)
init_range_low = 0
init_range_high = 1
gene_space=[0,1] # only binary solutions
parent_selection_type = "sss"
keep_parents = 8

crossover_type = "single_point" #"scattered" #
mutation_type = "random"
mutation_percent_genes = 1

ga_instance = pygad.GA(num_generations=num_generations,
                       num_parents_mating=num_parents_mating,
                       fitness_func=fitness_function,
                       sol_per_pop=sol_per_pop,
                       num_genes=num_genes,
                       init_range_low=init_range_low,
                       init_range_high=init_range_high,
                       gene_space=gene_space,
                       parent_selection_type=parent_selection_type,
                       keep_parents=keep_parents,
                       crossover_type=crossover_type,
                       mutation_type=mutation_type,
                       mutation_percent_genes=mutation_percent_genes)
                       
ga_instance.run()

solution, solution_fitness, solution_idx = ga_instance.best_solution()
print(f"Parameters of the best solution : {solution}")
print(f"Fitness value of the best solution = {solution_fitness}")

# reshape the solution 
solution = solution.reshape([5,5,5])
print(solution)

In general without knowing how "measure" the only approach that guarantee the best solution is brute force. If you know how "measure" looks like, using "maths" could be a fourth approach. But for most cases the genetic algorithm is a good enough solution for this optimization problem.

Ziur Olpa
  • 1,839
  • 1
  • 12
  • 27
  • Thank you, but where exactly does the `measure` function get called in the code? Also, what should I set for `function_inputs`? By the way I added a bit more detail on the post to clarify what the `measure()` function does exactly. – Coto TheArcher May 04 '22 at 15:28
  • In my code fitness_func() is your measure() and the return (fitness) in your case will be the efficiency of your antenna. Your function should look like "def measure(solution, solution_idx)". – Ziur Olpa May 04 '22 at 15:38
  • @CotoTheArcher function_inputs was a typo, now is corrected, is just your input (space) – Ziur Olpa May 04 '22 at 15:46
  • By the way, how would the code change if my `space` array was not square (e.g. `a x b x c`)? How would `i` and `ref` change for example? – Coto TheArcher May 05 '22 at 14:41
  • @CotoTheArcherit the code won't change, because the matrix space is flatten "space.flatten() ", only at the end you change the reshape to solution.reshape([a,b,c]). i and ref won't exist, becase is creating a ref matrix using a identity matrix that is squared by default, just hardcode a matrix of the shape a,b,c in the best way you can :) – Ziur Olpa May 06 '22 at 07:31
  • To extend a bit more on that, "i" and "ref" belong to the my "measure - fitness_func" function, so for you case, you wont need them, the genetic algorithm does not care about the dimensions of the problem as the matrix is converted to a 1D array. – Ziur Olpa May 06 '22 at 07:38
  • moreover, notice that "space" is not really used (only for getting the shape) so you can use the matrix of space as the reference matrix, but again, this code is solely a demonstration, in your case you won't need nor space nor ref, just the shape and the measure function. – Ziur Olpa May 06 '22 at 07:41
  • I see, makes sense, thanks! And in regard to my `measure` function, what I currently output is a value, which I wish to minimize. Have we specified anywhere that the goal is to minimize the function, instead of e.g. maximizing it? – Coto TheArcher May 06 '22 at 16:12
  • @CotoTheArcher we don't, but it is specified in the documentation of the link I shared, but https://pygad.readthedocs.io/en/latest/README_pygad_ReadTheDocs.html there you will read that it is a maximizing function which usually makes sense to maximize efficiencies, but if you need to minimize just wrap your original function into another function with inverse sign to minimize. – Ziur Olpa May 07 '22 at 20:26
  • @CotoTheArcher did the answer solved your question? – Ziur Olpa May 28 '22 at 22:05