Add chunk utils

Fix for:

#8

This commit builds upon #11, and aims to break down the dictionary of patterns into chunks.

def get_pattern(all_dictionary, guess_word):
    chunk_no = find_chunk_no(all_dictionary, guess_word)
    pattern_dict = load_pattern_dict(chunk_no)
    return pattern_dict[guess_word]

Would this line: pattern_dict = load_pattern_dict(chunk_no) not occur more overhead than it would gain? Loading the pickle from disk for every pattern calculation seems to be very expensive. Perhaps these chunks of the pattern_dict need to be stored in memory?

I created a small benchmark script based on an older version of the code. I think we should re-run this on every branch and check the (avg/max) number of guesses and time required for 100 randomly sampled words (with a fixed seed).
import os
import itertools
import random
import pickle
import time
from tqdm import tqdm
from scipy.stats import entropy
from collections import defaultdict, Counter

import numpy as np
np.random.seed(42)

N_GUESSES = 10
DICT_FILE_all = 'all_words.txt'
DICT_FILE = 'words.txt'
SAVE_TIME = True

def calculate_pattern(guess, true):
    """Generate a pattern list that Wordle would return if you guessed
    `guess` and the true word is `true`
    Thanks to MarkCBell, Jeroen van der Hooft and gbotev1
    >>> calculate_pattern('weary', 'crane')
    (0, 1, 2, 1, 0)
    >>> calculate_pattern('meets', 'weary')
    (0, 2, 0, 0, 0)
    >>> calculate_pattern('rower', 'goner')
    (0, 2, 0, 2, 2)
    """
    wrong = [i for i, v in enumerate(guess) if v != true[i]]
    counts = Counter(true[i] for i in wrong)
    pattern = [2] * 5
    for i in wrong:
        v = guess[i]
        if counts[v] > 0:
            pattern[i] = 1
            counts[v] -= 1
        else:
            pattern[i] = 0
    return tuple(pattern)

def generate_pattern_dict(dictionary):
    """For each word and possible information returned, store a list
    of candidate words
    >>> pattern_dict = generate_pattern_dict(['weary', 'bears', 'crane'])
    >>> pattern_dict['crane'][(2, 2, 2, 2, 2)]
    {'crane'}
    >>> sorted(pattern_dict['crane'][(0, 1, 2, 0, 1)])
    ['bears', 'weary']
    """
    pattern_dict = defaultdict(lambda: defaultdict(set))
    for word in tqdm(dictionary):
        for word2 in dictionary:
            pattern = calculate_pattern(word, word2)
            pattern_dict[word][pattern].add(word2)
    return dict(pattern_dict)

def calculate_entropies(words, possible_words, pattern_dict, all_patterns):
    """Calculate the entropy for every word in `words`, taking into account
    the remaining `possible_words`"""
    entropies = {}
    for word in words:
        counts = []
        for pattern in all_patterns:
            matches = pattern_dict[word][pattern]
            matches = matches.intersection(possible_words)
            counts.append(len(matches))
        entropies[word] = entropy(counts)
    return entropies

def main():
    # load all 5-letter-words for making patterns
    with open(DICT_FILE_all) as ifp:
        all_dictionary = list(map(lambda x: x.strip(), ifp.readlines()))

    # Load 2315 words for solutions
    with open(DICT_FILE) as ifp:
        dictionary = list(map(lambda x: x.strip(), ifp.readlines()))

    error_msg = 'Dictionary contains different length words.'
    assert len({len(x) for x in all_dictionary}) == 1, error_msg
    print(f'Loaded dictionary with {len(all_dictionary)} words...')
    WORD_LEN = len(all_dictionary[0]) # 5-letters

    # Generate the possible patterns of information we can get
    all_patterns = list(itertools.product([0, 1, 2], repeat=WORD_LEN))

    # Calculate the pattern_dict and cache it, or load the cache.
    if 'pattern_dict.p' in os.listdir('.'):
        pattern_dict = pickle.load(open('pattern_dict.p', 'rb'))
    else:
        pattern_dict = generate_pattern_dict(all_dictionary)
        pickle.dump(pattern_dict, open('pattern_dict.p', 'wb+'))

    # Simulate games
    stats = defaultdict(list)
    words_to_guess = np.random.choice(
        dictionary,
        replace=False,
        size=100
    )
    for WORD_TO_GUESS in tqdm(words_to_guess):

        if SAVE_TIME:
            guess_word = 'tares'
            all_words = set(all_dictionary)
            info = calculate_pattern(guess_word, WORD_TO_GUESS)
            words = pattern_dict[guess_word][info]
            all_words = all_words.intersection(words)
            init_round = 1
        else:
            all_words = set(all_dictionary)
            init_round = 0

        for n_round in range(init_round, N_GUESSES):

            candidates = all_dictionary
            entropies = calculate_entropies(candidates, all_words, pattern_dict, all_patterns)

            if max(entropies.values()) < 0.1:
                candidates = all_words
                entropies = calculate_entropies(candidates, all_words, pattern_dict, all_patterns)

            # Guess the candiate with highest entropy
            guess_word = max(entropies.items(), key=lambda x: x[1])[0]
            info = calculate_pattern(guess_word, WORD_TO_GUESS)

            # Print round information
            if guess_word == WORD_TO_GUESS:
                # print(f'WIN IN {n_round + 1} GUESSES!\n\n\n')
                stats[WORD_TO_GUESS] = n_round + 1
                break

            # Filter our list of remaining possible words
            words = pattern_dict[guess_word][info]
            all_words = all_words.intersection(words)

        if WORD_TO_GUESS not in stats:
            stats[WORD_TO_GUESS] = -1

    return stats

if __name__ == "__main__":
    start = time.time()
    stats = main()
    end = time.time()
    print(f'Words not guessed: {sum(x < 0 for x in stats.values())}')
    print(f'Average number of guesses: {np.mean(list(stats.values()))}')
    print(f'Maximum number of guesses: {np.max(list(stats.values()))}')
    print(f'Time needed: {end - start}')

def get_pattern(all_dictionary, guess_word):
    chunk_no = find_chunk_no(all_dictionary, guess_word)
    pattern_dict = load_pattern_dict(chunk_no)
    return pattern_dict[guess_word]

Would this line: pattern_dict = load_pattern_dict(chunk_no) not occur more overhead than it would gain? Loading the pickle from disk for every pattern calculation seems to be very expensive. Perhaps these chunks of the pattern_dict need to be stored in memory?

Yes, I don't think my change is such a good idea: the files stored on disk are really big, and the script is really slow. The only advantage is that there is no crash due to a high usage of RAM.
I have made many other refactoring changes in my fork, but I don't think it would make sense to incorporte the chunk utils, unless the data is stored in a more compact way so that the script runs fast.
By the way, I have seen that there is some (very fast) supplementary code to the video mentioned in your README:

3Blue1Brown, Solving Wordle using information theory, posted on Youtube on February 6, 2022,
3b1b/videos: supplementary code (in Python) accompanying the aforementioned video,

For reference, this is the result of the clean-up of the supplementary code: https://github.com/woctezuma/3b1b-wordle-solver


Flexget unable to communicate with Trakt.TV API

Expected behaviour:
Flexget should be able to authenticate with the Trakt.tv API
Actual behaviour:
When trying to authenticate with trakt
Steps to reproduce:

Run flexget --loglevel TRACE  -c /config/config.yml trakt auth carlba

Config:
I don't think the config is relevant here as the authentication with trakt is done irregardless of the config. If I am in the wrong here please tell me and I will supply the config.
Log:

(click to expand)
2024-12-27 14:48:47 DEBUG    manager                       Config file /config/config.yml selected
2024-12-27 14:48:47 DEBUG    manager                       sys.defaultencoding: utf-8
2024-12-27 14:48:47 DEBUG    manager                       sys.getfilesystemencoding: utf-8
2024-12-27 14:48:47 DEBUG    manager                       flexget detected io encoding: utf-8
2024-12-27 14:48:47 DEBUG    manager                       os.path.supports_unicode_filenames: False
There is a FlexGet process already running for this config, sending execution there.
2024-12-27 14:48:47 DEBUG    manager                       Sending command to running FlexGet process: ['--loglevel', 'TRACE', '-c', '/config/config.yml', 'trakt', 'auth', 'carlba']
2024-12-27 14:48:47 DEBUG    api_trakt                     No pin specified for an unknown account carlba. Attempting to authorize device.
2024-12-27 14:48:47 DEBUG    utils.requests                 POSTing URL https://api.trakt.tv/oauth/device/code with args () and kwargs {'data': {'client_id': '57e188bcb9750c79ed452e1674925bc6848bd126e02bb15350211be74c6547af'}, 'timeout': 30}
Authorization failed: Device authorization with Trakt.tv failed: HTTPSConnectionPool(host='api.trakt.tv', port=443): Max retries exceeded with url: /oauth/device/code (Caused by
NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f01bbdca650>: Failed to establish a new connection: [Errno -3] Try again'))

Additional information:

FlexGet version: 3.13.4
Python version: Python 3.11.11
Installation method: Docker
Using daemon (yes/no): Yes
OS and version: Ubuntu 20.04.6 LTS

I have also tried obvious fixes such as deleting my Flexget trakt account config by doing flexget  -c /config/config.yml trakt delete carlba. The deletion works but after that it is not possible to reauthenticate.

Closing this as it was a DNS issue.


Relax bounds on fold_many* functions to accept FnMut for accum func

This PR relaxes the requirement on the accumulator function to the fold_many* family of functions in order to allow FnMuts to be passed. This seems in line with the stated goals of the version 6 bump, and is strictly additive.
It would be nice to also remove the Clone bound from the init value, however that doesn't seem possible for now.
If this seems pretty weird, it is because I'm trying to use fold_many to accumulate on a value that does not implement Clone. So I have developed the following pattern for an expression list, which I find to be quite pleasant:
pub fn expr_list<'a>(input: &'a str) -> IResult<&'a str, Vec<Expr<'a>>> {
    expr(input).and_then(
        |(i, e)| {
            let mut exprs = vec![e];
            fold_many0(
                preceded(tag(","), expr),
                (),
                |_, e| {
                    exprs.push(e);
                }
            )(i).map(|(i, _)| (i, exprs))
        }
    )
}

In this example, Expr does not implement Clone.
Edit: fixed example so that it compiles.

Coverage remained the same at 82.808% when pulling 0ae09645c2cf62877b62fb498e24ad436b4ed0dc on maplant:master into e7b080877adf0f021c4b2d53044fad7d3a88d18b on Geal:master.

Right, this should be fine


DBeaver cannot start in Ubuntu 22.04

Description
I'm currently using a Mac Air M2, I run Ubuntu 22.04 through VMware Fusion
I installed beaver, when I launched it, it doesn't launched.
I tried to launch it from terminal using: dbeaver-ce &
It gave this error

DBeaver Version
Community 24.1.2
Operating System
MacOS 14.5
Database and driver
No response
Steps to reproduce

I installed beaver
I launched dbeaver
Nothing happened

Additional context
No response

@Anlele1701 Hi, I understand you are using an ARM-based Mac. DBeaver snap is compiled for x86-64 architecture on Linux, so this might be a reason why you can't launch the client. Unfortunately, I don't have knowledge if there are ways to use x86 apps via the translation layer on Linux; if they are present, they should help.


bug: python/java code not exported on save

it's always robot karol code, overlooked probably

fixed


Add a rename_modules_dict arg to Envoy that allows renaming modules

After discussing with @cadentj some alternatives to transformer_lens (which sometimes has a significant implementation gap with HuggingFace), he suggested adding a renaming feature to nnsight itself, as it might be useful not only for transformers.
The basic usage would be to convert module names from, e.g., GPT-2 to the standard LLaMA / Gemma format using:
The basic usage would be to convert module names from e.g. gpt2 to the standard llama / gemma format using
dict_change = {
    "transformer": "model",
    "self_attention": "self_attn",
    "attn": "self_attn",
    "h": "layers",
}
patched_model = LanguageModel("gpt2", rename_modules_dict=dict_change)
with patched_model.trace("hello"):
    out = patched_model.model.layers[0].output[0].save()

@JadenFiotto-Kaufman I can try to add tests if needed but I wanted to first check if you were ok with adding this feature to the nnsight core.

@JadenFiotto-Kaufman I can try to add tests if needed but I wanted to first check if you were ok with adding this feature to the nnsight core.

Hey this is an awesome idea and something I've wanted to add to nnsight. Essentially we want aliasing right?
I think it should be a dictionary where the keys are valid regex strings that map to an alias. This dictionary would live at Envoy._aliases and would be checked on the Envoy.getattr method. If a key matches the reverse mapping, it returns the real underlying envoy for that alias.
Does that sound like a good solution?

Hmm, would you still pass the alias dict through the NNSight class init?
I guess one advantage of renaming over aliasing is that when you print the model, you actually see modules' new names.
What advantage do you see of using aliasing instead of renaming?

@JadenFiotto-Kaufman bump

@Butanium Does this work? https://github.com/ndif-team/nnsight/pull/298

Implemented in #298