Generate Problem Instances

Ecole contains a number of problem InstanceGenerator in the ecole.instance module. They generate instances as ecole.scip.Model. To generate instances, first instantiate a generator. All generators are constructed with different parameters depending on the problem type. An InstanceGenerator is infinite Python iterators so we can iterate over them using any of Python iterating mechnisms.

For instance, to generate set covering problems, one would use SetCoverGenerator in the following fashion:

from ecole.instance import SetCoverGenerator


generator = SetCoverGenerator(n_rows=100, n_cols=200, density=0.1)

for i in range(50):
    instance = next(generator)

    # Do anything with the ecole.scip.Model
    instance.write_problem("some-folder/set-cover-{i:04}.lp")

Note how we are iterating over a range(50) and calling next on the generator. This is because iterating directly over the iterator would produce an infinte loop. For users more comfortable with iterators, other possibilities exists, such as islice.

Generators Random States

An InstanceGenerator holds a random state to generate instance. This can be better understood when using the seed() method of the generator.

generator_a = SetCoverGenerator(n_rows=100, n_cols=200, density=0.1)
generator_b = SetCoverGenerator(n_rows=100, n_cols=200, density=0.1)

# These are not the same instance
instance_a = next(generator_a)
instance_b = next(generator_b)

generator_a.seed(809)
generator_b.seed(809)

# These are exactly the same instances
instance_a = next(generator_a)
instance_b = next(generator_b)

With an Environment

The environment reset() accepts problem instance as ecole.scip.Model, so there is no need to write generated instances to file.

A typical example training over 1000 instances/episodes would look like:

import ecole


env = ecole.environment.Branching()
gen = ecole.instance.SetCoverGenerator(n_rows=100, n_cols=200)

for _ in range(1000):
    observation, action_set, reward_offset, done, info = env.reset(next(gen))
    while not done:
        observation, action_set, reward, done, info = env.step(action_set[0])

Note

While it is possible to modify the instance before passing it to reset(), it is not considered a good practice, as it obscure what what task is being learned (which is not self contained by the environment class anymore). A better alternative is to create a new environment to perfom such changes.

Adapt Instance Generators

An InstanceGenerator only create instances for users to consume. Therefore, there is no constraints on how iterating over instance should be done, it is entirely up to the user. Using different data structure, such as lists, dictionaries, etc. is completely valid because environments never “see” generators, only the instances. Here we illustrate some possibilities to adapt Ecole instance generators. Python’s yield keyword can make it very compact to create iterators.

Combine Multiple Generators

To learn over multiple problem types, one could build a generator that, for every instance to generate, chooses a a problem type at random, and returns it.

import random


def CombineGenerators(*generators):
    # A random state for choice
    random_engine = random.Random()
    while True:
        # Randomly pick a generator
        gen = random_engine.choice(generators)
        # And yield the instance it generates
        yield next(gen)

This generator does not have a seed method. If we want to implement it, we have to write the same generator as the equilvalent class.

class CombineGenerators:
    def __init__(self, *generators):
        self.generators = generators
        self.random_engine = random.Random()

    def __next__(self):
        return next(self.random_engine.choice(self.generators))

    def __iter__(self):
        return self

    def seed(self, val):
        self.random_engine.seed(val)
        for gen in self.generators:
            gen.seed(val)

Generator Random Parameters

Another useful case it to generate instances of a same problem type but with different parameters. If there are few different set of parameter to choose from, then we could use the same technique as above. However, with more set of parameters (or even infinite), this becomes wasteful (or impossible).

To do this, we can use the generator’s generate_instance() static function and manually pass a RandomEngine. For instance, to randomly choose the n_cols and n_rows parameters from SetCoverGenerator, one could use

import random
import ecole


class VariableSizeSetCoverGenerator:
    def __init__(self, n_cols_range, n_rows_range):
        self.n_cols_range = n_cols_range
        self.n_rows_range = n_rows_range
        # A Python radnom state for randint
        self.py_random_engine = random.Random()
        # An Ecole random state to pass to generating functions.
        # This function returns a random state whose seed depends on Ecole global random state
        self.ecole_random_engine = ecole.spawn_random_engine()

    def __next__(self):
        return ecole.instance.SetCoverGenerator(
            n_cols=self.py_random_engine.randint(*self.n_cols_range),
            n_rows=self.py_random_engine.randint(*self.n_rows_range),
            random_engine=self.ecole_random_engine,
        )

    def __iter__(self):
        return self

    def seed(self, val):
        self.py_random_engine.seed(val)
        self.ecole_random_engine.seed(val)

See the discussion on seeding for an explanation of ecole.spawn_random_engine().