reamberPy Help

Patterns

Theory

To find patterns, we must:

  • Cluster Notes. Grouping

  • Find association between clusters. [Combinations ](#combinations

  • Remove unwanted associations. Filtering

Visualizing how it works

+---------------+ | C | | D | | | A | B | | | +---------------+ 0 1 2 3 Column

We'll walk through how our algorithm finds the A -> C Jack pattern.

  1. Firstly, we group the notes into clusters. This is done by checking which notes have the same offset.

Group
Group
C
D
A
B

We see that A and B are on the same offset, thus they are grouped together.

  1. Secondly, we find combinations between the groups. This is done by taking the cartesian product between groups. A <--> B is not a combination as they are in the same group.

Jack
Stair
Stair
Stair
A
C
D
B
  1. Finally, we filter out unwanted combinations. In this case, we want to filter out all combinations that are not Jacks.

Jack
A
C

Input

To start, we initialize Pattern in 2 ways:

from reamber.algorithms.pattern import Pattern from reamber.osu.OsuHit import OsuHit # Method 1 p = Pattern([0, 1, 2, 3], [1000, 2000, 3000, 4000], [OsuHit, OsuHit, OsuHit, OsuHit]) from reamber.osu import OsuMap # Method 2 m = OsuMap.read_file("...") p = Pattern.from_note_lists([m.hits, m.holds], include_tails=True)

Grouping

  • Horizontally: Grouping across columns of the same offset

  • Vertically: Grouping across offsets of the same column

from reamber.algorithms.pattern import Pattern from reamber.osu.OsuMap import OsuMap m = OsuMap.read_file("...") p = Pattern.from_note_lists([m.hits, m.holds], include_tails=True) g = p.group(v_window=50, h_window=None, avoid_jack=True)

Vertical & Horizontal Window

Windows define how far forward/sideways a note should look to group.

Consider the following marked X

[20ms] | _ O _ _ | ^ [10ms] | _ _ _ _ | | Vertical [0ms] | X _ O _ | v <---> Horizontal

If h_window=2, v_window=20. All notes will be grouped as one, anything smaller will split them.

Avoid Jack

When grouping, you may want to avoid grouping jacks together

[20ms] | O _ _ _ | ^ [10ms] | _ _ _ _ | | Vertical [0ms] | X _ _ _ | v

avoid_jack=True prevents that, forcing the next note to another group.

Large Horizontal Window with Jack Avoidance

For Example

Unlabelled Labelled =========== =========== | O O _ O | | 6 7 _ 8 | | _ _ _ _ | | _ _ _ _ | ^ | O _ _ O | == | 4 _ _ 5 | | Vertical | _ _ O _ | | _ _ 3 _ | | Window | _ O O _ | | _ 1 2 _ | v =========== =========== ! Notes are labelled from 1 to 8 Group 1 Group 2 Labelled Labelled =========== =========== | _ _ _ _ | | 6 7 _ 8 | ^ | _ _ _ _ | ^ | _ _ _ _ | | | 4 _ _ 5 | | | _ _ _ _ | | | _ _ _ _ | | | _ _ 3 _ | v | _ 1 2 _ | v | _ _ _ _ | =========== ===========

Notice that 3 was rejected from Group 1 because avoid_jack=True. Thus moved to Group 2

Examples

Let's say we want to group with the parameters

vwindow = 0, hwindow = None

[4s] _5__ _5__ _5__ _5__ _X__ [3s] ___4 GROUP 1 ___4 GROUP 2 ___4 GROUP 3 ___X GROUP 4 ___X [2s] _2_3 ------> _2_3 ------> _X_X ------> _X_X ------> _X_X [1s] ____ [1] ____ [2,3] ____ [4] ____ [5] ____ [0s] 1___ X___ X___ X___ X___ Output: [1][2,3][4][5]

vwindow = 1000, hwindow = None

[4s] _5__ _5__ _5__ _X__ [3s] ___4 GROUP 1 ___4 GROUP 2 ___X GROUP 3 ___X [2s] _2_3 ------> _2_3 ------> _X_X ------> _X_X [1s] ____ [1] ____ [2,3,4] ____ [5] ____ [0s] 1___ X___ X___ X___ Output: [1][2,3,4][5]

2, 3 and 4 are together as 4 is within the vwindow of 2;

vwindow = 1000, hwindow = 1

[4s] _5__ _5__ _5__ _5__ _X__ [3s] ___4 GROUP 1 ___4 GROUP 2 ___4 GROUP 3 ___X GROUP 4 ___X [2s] _2_3 ------> _2_3 ------> _X_3 ------> _X_X ------> _X_X [1s] ____ [1] ____ [2] ____ [3,4] ____ [5] ____ [0s] 1___ X___ X___ X___ X___ Output: [1][2][3,4][5]

2 and 3 aren't together as they are > 1 column apart, due to hwindow

(combinations)=

Combinations

Cartesian Product of [0, 1, 2] [A, B, C] A B C +--------- 0 | A0 B0 C0 1 | A1 B1 C1 2 | A2 B2 C2 = A0 B0 C0 A1 B1 C1 A2 B2 C2

For example:

If Group B comes after A, Group A = [0, 1] Group B = [3, 4] Combinations = [0, 3] [0, 4] [1, 3] [1, 4]
from reamber.algorithms.pattern.Pattern import Pattern from reamber.algorithms.pattern.combos.PtnCombo import PtnCombo from reamber.osu.OsuMap import OsuMap m = OsuMap.read_file("...") p = Pattern.from_note_lists([m.hits, m.holds], include_tails=True) g = p.group(v_window=50, h_window=None, avoid_jack=True) c = PtnCombo(g).combinations( size=2, make_size2=False, # We'll talk about Filters later. chord_filter=None, combo_filter=None, type_filter=None )

Size

With 3 Groups, we yield 2 Cartesian products GRP 1 GRP 2 GRP 3 [1, 2] [0, 3] [0, 2] GRP 1 x GRP 2 = [1, 0], [1, 3], [2, 0], [2, 3] GRP 2 x GRP 3 = [0, 0], [0, 2], [3, 0], [3, 2]
GRP 1 GRP 2 GRP 3 [1, 2] [0, 3] [0, 2] GRP 1 x GRP 2 x GRP 3 = [1, 0, 0], [1, 0, 2], [1, 3, 0], ..., [2, 3, 2]

Flatten & Make Size 2

By default, the returned structure is List[List[List[int]]].

You may flatten it.

If we are combining Group A = [0, 1] Group B = [3, 4] size = 2 Combination: [[[0, 3], [0, 4]], [[1, 3], [1, 4]]] Combination with Flatten: [[0, 3], [0, 4], [1, 3], [1, 4]]

make_size2 splits size>3 into pairs.

If we are combining Group A = [0, 1] Group B = [3, 4] Group C = [2] size = 3 Combination: [[[0, 3, 2], [0, 4, 2]], [[1, 3, 2], [1, 4, 2]]] Combination with Make Size 2: [0, 3], [3, 2], ..., [1, 4], [4, 2] [0, 3, 2] -> [0, 3], [3, 2] [0, 4, 2] -> [0, 4], [4, 2] [1, 3, 2] -> [1, 3], [3, 2] [1, 4, 2] -> [1, 4], [4, 2]

Filters

For each ..._filter, we expect a Callable / lambda.

You can use custom filters, however, I recommend our in-house lambdas for this.

from reamber.algorithms.pattern.filters import ( PtnFilterChord, PtnFilterType, PtnFilterCombo ) from reamber.algorithms.pattern.Pattern import Pattern from reamber.algorithms.pattern.combos.PtnCombo import PtnCombo from reamber.osu.OsuMap import OsuMap m = OsuMap.read_file("...") p = Pattern.from_note_lists([m.hits, m.holds], include_tails=True) g = p.group(v_window=50, h_window=None, avoid_jack=True) c = PtnCombo(g).combinations( size=2, make_size2=False, # We'll talk about Filters later. chord_filter=PtnFilterChord.create(...).filter, combo_filter=PtnFilterCombo.create(...).filter, type_filter=PtnFilterType.create(...).filter )

Filter Chord

For example, if we only wanted 2-sized chords followed by 2-sized chords or lower sized chords, we can do the following:

from reamber.algorithms.pattern.filters import PtnFilterChord chord_filter = PtnFilterChord.create( [[2, 2]], 4, options=PtnFilterChord.Option.AND_LOWER, exclude=False ).filter,
=========== | O O _ O | Group C | _ _ _ _ | | O _ _ O | Group B | _ _ _ _ | | _ O O _ | Group A =========== Combination Size = 2 Group A -> B -> C Chord [1, 2] -> [0, 3] -> [0, 1, 3] Size 2 -> 2 -> 3 ^----------^ [2, 2] ^----------^ Combination Size = 2 | [2, 3] | | v v +---------------------+ [2, 2] not in [[3, 2], [2, 3]] Filter | [[2, 3], [3, 2]] | +---------------------+ [2, 3] in [[3, 2], [2, 3]] | | x | V Output

Filter Chord controls which group combinations pass through.

Filter Chord Options

  • ANY_ORDER

    • Make Additional Filters of any order

      [A, B, C] -> Any Order -> [A, B, C], [A, C, B], ..., [C, B, A]

  • AND_LOWER

    • Make Additional Filters of any lower combination (Down to 1)

      [2, 3] -> And Lower -> [2, 3], [2, 2], ... , [1, 2], [1, 1]

  • AND_HIGHER

    • Make Additional Filters of any higher combination (Up to Keys)

      [1, 2] -> And Higher -> [1, 2], [2, 2], ... , [3, 4], [4, 4]

Filter Combo

from reamber.algorithms.pattern.filters import PtnFilterCombo combo_filter = PtnFilterCombo.create( [[0, 1, 2, 3]], 4, options=PtnFilterCombo.Option.HMIRROR, exclude=False ).filter,

The above example includes occurrences of [0, 1, 2, 3] and its horizontal mirror: [3, 2, 1, 0]

=========== | O _ _ O | Group B | _ _ _ _ | | _ O O _ | Group A =========== Combination Size = 2 Group A -> B Chord [1, 2] -> [0, 3] Cartesian [1, 0], [1, 3], [2, 0], [2, 3] Combination Size = 2 | | | | v v v v +-------------------------------+ Filter | [[1, 3], [2, 3]] | +-------------------------------+ | | | | x | x | v v Output Output

Filter Combo Options

  • REPEAT

    • Repeats the filter that are within bounds

      For example, a [1, 2] filter =========== | _ _ O _ | | _ O _ _ | -> Repeat -> =========== =========== =========== =========== | _ O _ _ | | _ _ O _ | | _ _ _ O | | O _ _ _ | | _ O _ _ | | _ _ O _ | =========== =========== =========== [0, 1] [1, 2] [2, 3] [1, 2] -> Repeat -> [0, 1], [1, 2], [2, 3]
  • HMIRROR

    • Mirrors the filter Horizontally

      For example, a [1, 3] filter Mirror | =========== =====|===== | _ _ _ O | | _ _|_ O | | _ _ _ _ | | _ _|_ _ | | _ O _ _ | -> H Mirror -> | _ O|_ _ | =========== =====|===== | Mirror =========== =========== | _ _ _ O | | O _ _ _ | | _ _ _ _ | | _ _ _ _ | | _ O _ _ | | _ _ O _ | =========== =========== [1, 3] [2, 0] [1, 3] -> H Mirror -> [1, 3], [2, 0]
  • VMIRROR

    • Mirrors the filter Vertically

      For example, a [1, 3] filter =========== =========== | _ _ _ O | | _ _ _ O | | _ _ _ _ | Mirror --------------- Mirror | _ O _ _ | -> V Mirror -> | _ O _ _ | =========== =========== =========== =========== | _ _ _ O | | _ O _ _ | | _ _ _ _ | | _ _ _ _ | | _ O _ _ | | _ _ _ O | =========== =========== [1, 3] [3, 1] [1, 3] -> V Mirror -> [1, 3], [3, 1]

Filter Type

from reamber.algorithms.pattern.filters import PtnFilterType from reamber.osu.OsuHold import OsuHold from reamber.osu.OsuHit import OsuHit combo_filter = PtnFilterType.create( [[OsuHit, OsuHit, OsuHold]], 4, options=PtnFilterType.Option.ANY_ORDER, exclude=False ).filter,

The above example includes occurrences of [OsuHit, OsuHit, OsuHold] and any other order: [OsuHit, OsuHold, OsuHit], [OsuHold, OsuHit, OsuHit]

For example, if we want to match only LN Heads/Hits, excluding LN Tails.

=========== | H | _ H | Group B | _ | _ _ | | _ L H _ | Group A =========== L: LN Head H: Hit Combination Size = 2 Group A -> B Chord [L, H] -> [H, H] Cartesian [L, H], [L, H], [H, H], [H, H] Combination Size = 2 | | | | v v v v +-------------------------------+ Filter | [[H, H]] | +-------------------------------+ | | | | x x | | v v Output Output

Options

  • ANY_ORDER

    • Make Additional Filters of any order [A, B, C] -> Any Order -> [A, B, C], [A, C, B], ..., [C, B, A]

  • VMIRROR

    • Mirrors the filter

      [A, B] -> Mirror -> [A, B], [B, A]

Custom Filters

As long as they fit the signature, you can make it work.

combinations( ..., chord_filter: Callable[[np.ndarray], bool] = None, combo_filter: Callable[[np.ndarray], np.ndarray[bool]] = None, type_filter: Callable[[np.ndarray], np.ndarray[bool]] = None )

All of them are functions accepting a certain data structure and return the boolean filter verdict.

Chord Filter

Input

np.ndarray[int] of chord sizes

Output

bool verdict

Example I/O

Input: np.ndarray([1, 3]) Output: False

Implement a function that accepts if the first chord size is greater than 1.

def chord_filter(ar: np.ndarray): return ar[0] > 1 combos = combinations(..., chord_filter=chord_filter)

Combo Filter

Input

np.ndarray[int] of combos

Output

np.ndarray[bool] verdict

Example I/O

Input: np.ndarray([[1, 2], [2, 3], [3, 4]]) Output: np.ndarray([True, True, False])

Implement a function that accepts if the first column is less than 3.

def combo_filter(ar: np.ndarray): return ar[:, 0] < 3 combos = combinations(..., combo_filter=combo_filter)

Type Filter

Input

np.ndarray[type] of combos

Output

np.ndarray[bool] verdict

Example I/O

Input: np.ndarray([[Hit, HoldTail], [Hit, Hit], [Hold, HoldTail]]) Output: np.ndarray([True, False, True])

Implement a function that accepts if the second column is a subclass of HoldTail.

def type_filter(ar: np.ndarray): return [isssubclass(t, HoldTail) for t in ar[:, 1]] combos = combinations(..., type_filter=type_filter)
Last modified: 23 March 2024