subdgtl / Monoceros

Wave Function Collapse plug-in for Grasshopper
MIT License
63 stars 13 forks source link

Implement weights #33

Open janper opened 3 years ago

janper commented 3 years ago

Note: Relates to https://github.com/subdgtl/WFC as well.

The WFC Solver could prefer choosing certain Modules to others during the observation. This preference will be called Weight from here on. Modules with minimum weight will never by chosen by an observation (they will have to remain as the last option) - this needs to be tested as it possibly may prevent finding any solution.

The weights could be applied in several ways, out of which the following seem to make sense to me. I prefer choosing one of the the mentioned options, not a combination of more. The implementation in https://github.com/subdgtl/WFC may differ from the implementation in Monoceros, where the major requirement is comfort of use.

  1. Weight per Module - each Module will be assigned a certain weight when constructed.
  2. Weight of each Module per Slot - a Module will be assigned a weight specific for each Slot that allows its placement. Even if this is the implementation in https://github.com/subdgtl/WFC, the behavior in Monoceros could look like option 1.
  3. Weight per Rule - instead of the Modules, Rules will be assigned a weight.
  4. Weight of each Rule per Slot - a Rule will be assigned a weight specific for each Slot that allows placement of a Module described by the Rule. Even if this is the implementation in https://github.com/subdgtl/WFC, the behavior in Monoceros could look like option 3.

The most comfortable options in regard to the Monoceros UI would be 1, 3 and partly also 2. Option 4 would be tricky.

yanchith commented 3 years ago

The implementation in the WFC repo is already approach 1). This isn't currently surfaced, and is only used for Shannon entropy (to prevent slowdowns if Shannon entropy isn't used). If we make weights optional, we can still prevent the slowdowns this will incur. Implementing this would enable us to remove the current hacky Shannon Entropy option - Shannon Entropy would be implicitly used if weights are manually set.

Of your approaches, 1) seems to give the mot bang for the buck. 2) and 4) would require more work from the user.

Also, 3) and 4) need more specification if we decide to go that way. What is the behavior of weighted rules? We need to weighted modules for module selection, so do we somehow compute that from weighted rules? Using what strategy? Would it be configurable?

janper commented 3 years ago

Options 3 and 4 are also quite unclear to me. I just thought they are worth mentioning.

I prefer Option 2 though - it is not only "backwards compatible" with Option 1, it also allows to distribute the probability over the envelope. It requires a redesign of Slot constructors - specifically a Slot that allows placement of all would become way less comfortable or wouldn't support the weights at all.

Let's break down the UI implementation:

Option 1. only requires one more input for Module constructor - a Module Weight, by default set to 100% (and 0% for an Empty Module). The Modules will have the same weight everywhere.

In Option 2.a there will be no weight in the definition of a Module itself. There are currently two Slot constructors: "allow all Modules" and "allow listed Modules". The first one is not aware of Modules at creation, so there can be no weights. For the second one it is easy, because one of the input pins requires a list of Modules to be allowed in the Slot. Along with it there can be another input pin with a parallel list of Module weights.

There could be also another option, where the weights could be defined for both, the Modules and Slots. The "allow all" Slot infers the weights from the Module definition. The "allow listed" Slot (Option 2.b) overrides the per-Module-weights or (Option 2.c) multiplies them (what about 0%?) or (Option 2.d) has a switch between inferring / overriding / multiplying.

yanchith commented 3 years ago

Oh no. I just had a look in the code and found out I misremembered. Module weights are present in the solver, but only to compute entropy of a slot. To use them for selecting modules, we'd have to re-normalize the weights to only take into account modules that are still possible to place for the slot in question, meaning more slowdowns (although probably not very drastic ones).

a Module Weight, by default set to 100% (and 0% for an Empty Module)

Note that even 0% becomes 100% if the module is the last legal module to place in a slot.

Yeah, option 2) will be strictly more powerful than option 1), but also more computationally expensive, meaning even more slowdowns. I unfortunately can't speak towards the UI part of your comment (I don't remember the Monoceros UI that well), but from your explanation option 2) and its variants, it does seem tedious.

janper commented 3 years ago

Note that even 0% becomes 100% if the module is the last legal module to place in a slot.

Technically, if the Module is the last to be placed, it never goes through an observation and therefore there is no weight considered. As a result, 0% Module is only placed when there is no better option and in my understanding that is exactly what 0% should mean.

janper commented 3 years ago

we'd have to re-normalize the weights

Are you sure? BTW, what would be a pseudocode of the decision based on weights? My naïve approach would be either the roulette or dividing the random number interval into smaller, differently sized intervals respective to the weighted options.

janper commented 3 years ago

unfortunately can't speak towards the UI part of your comment I don't remember the Monoceros UI that well)

That's sad!!!

Long story short - I'd go for Option 2.d regardless of the complications. It seems like the usual grasshopper way. For you (the Rust part) it would mean Option 2: Weight of each Module per Slot. The rest would be done in C#.

yanchith commented 3 years ago

Technically, if the Module is the last to be placed, it never goes through an observation and therefore there is no weight considered. As a result, 0% Module is only placed when there is no better option and in my understanding that is exactly what 0% should mean.

Disagree. You still have to observe the module, even if there is only a single choice left.

Also, the 0% makes less sense the more I think about it. Perhaps it should be a small number instead that gets to fill the entire 100% when there are no other choices left?

yanchith commented 3 years ago

we'd have to re-normalize the weights

Are you sure? BTW, what would be a pseudocode of the decision based on weights? My naïve approach would be either the roulette or dividing the random number interval into smaller, differently sized intervals respective to the weighted options.

No concrete idea yet. I'd probably compute re-normalized weights after the illegal choices are subtracted away. That way, a single roll of the RNG suffices to make the decision.

Doing a roulette means taking a variable amount of random numbers from the RNG. That's in direct opposition of providing any kind of stable results we'd like to achieve with https://github.com/subdgtl/WFC/issues/20