<commit_before>"""
This generates a single-tone audio clip for instrument classification.
"""

import argparse
from music21.chord import Chord
from music21.duration import Duration
from music21.instrument import Instrument
from music21.note import Note, Rest
from music21.stream import Stream
from music21.tempo import MetronomeMark
from music21.volume import Volume
import numpy as np
import os
import pandas as pd
import soundfile as sf

from fluidsynth import FluidSynth
from instruments import midi_instruments

def make_instrument(id):
    i = Instrument()
    i.midiProgram = id
    return i

def chord_with_volume(chord, volume):
    chord.volume = Volume(velocityScalar=volume)
    return chord

def write_midi(stream, output_file):
    stream.write('midi', output_file)

def generate_single_note(midi_number, midi_instrument=0, volume=1.0, duration=1.0, tempo=120):
    """
    Generates a stream containing a single note with given parameters.
    midi_number - MIDI note number, 0 to 127
    midi_instrument - MIDI intrument number, 0 to 127
    duration - floating point number (in quarter note lengths)
    volume - 0.0 to 1.0
    tempo - number of quarter notes per minute (eg. 120)

    Note that there's a quarter note rest at the beginning and at the end.
    """
    return Stream([
        MetronomeMark(number=tempo),
        make_instrument(int(midi_instrument)),
        chord_with_volume(Chord([
            Note(midi=int(midi_number), duration=Duration(duration))
        ]), volume)
    ])

def generate_separete_notes(note_params_df, output_dir, audio_format='flac'):
    """
    Generates a batch of single note samples from the given table of parameters.

    `note_params_df` - a Pandas Dataframe with columns:
    `midi_number, midi_instrument, volume, duration, tempo`. Their meaning is the same as in generate_single_note.
    `output_dir` - output directory for the MIDI files

    Each sample goes to a single MIDI file named by the numeric index. Also each synthesized audio sample goes to a
    """
    os.makedirs(output_dir, exist_ok=True)

    fs = FluidSynth()
    for i, row in note_params_df.iterrows():
        midi_file = '{0}/{1:06d}.midi'.format(output_dir, i)
        audio_file = '{0}/{1:06d}.{2}'.format(output_dir, i, audio_format)

        print(row, midi_file, audio_file)

        stream = generate_single_note(**row)
        write_midi(stream, midi_file)
        fs.midi_to_audio(midi_file, audio_file)

def random_params(n, note_range=None, volume_range=(0.5, 1.0), duration=1.0, tempo=60, seed=None):
    """
    Generate note parameters randomly as a DataFrame.

    n - number of samples
    """

    if seed is not None:
        np.random.seed(seed)

    instruments = midi_instruments()

    def instrument_range(i):
        instr = instruments.ix[i]
        instr_range = np.array([instr['min_pitch'], instr['max_pitch']])
        if note_range is not None:
            instr_range = np.clip(instr_range, *note_range)
        return instr_range

    allowed_instruments = np.hstack([
            np.arange(0, 8), # piano
            np.arange(16, 32), # organ, guitar
            np.arange(40, 48), # strings
            np.arange(56, 80), # brass, reed, pipe
        ])

    def random_note_for_instrument(instr):
        instr_range = instrument_range(instr)
        return np.random.random_integers(low=instr_range[0], high=instr_range[1], size=1)[0]

    df = pd.DataFrame()
    df['midi_instrument'] = np.random.choice(allowed_instruments, size=n)
    df['midi_number'] = df['midi_instrument'].apply(random_note_for_instrument)
    df['volume'] = np.random.uniform(low=volume_range[0], high=volume_range[1], size=n)
    # TODO: allow varying duration while maintaining constant audio length
    df['duration'] = duration
    df['tempo'] = tempo

    return df

def generate_notes_in_batch(note_params_df, output_dir, audio_format='flac', sample_rate=44100):
    """
    Generates a batch of single note samples from the given table of parameters.

    `note_params_df` - a Pandas Dataframe with columns:
    `midi_number, midi_instrument, volume, duration, tempo`. Their meaning is the same as in generate_single_note.
    `output_dir` - output directory for the MIDI files

    Each sample goes to a single MIDI file named by the numeric index. Also each synthesized audio sample goes to a
    """
    os.makedirs(output_dir, exist_ok=True)

    fs = FluidSynth(sample_rate=sample_rate)

    stream = Stream()

    for i, row in note_params_df.iterrows():
        stream.append(MetronomeMark(number=row['tempo']))
        stream.append(make_instrument(int(row['midi_instrument'])))
        duration = row['duration']
        stream.append(chord_with_volume(Chord([
            Note(midi=int(row['midi_number']), duration=Duration(duration))
        ]), row['volume']))
        stream.append(Rest(duration=Duration(2 * duration)))

    midi_file = '{0}/all_samples.midi'.format(output_dir)
    audio_file_stereo = '{0}/all_samples_stereo.{1}'.format(output_dir, audio_format)
    audio_file = '{0}/all_samples.{1}'.format(output_dir, audio_format)
    audio_index_file = '{0}/all_samples_index.csv'.format(output_dir)

    # TODO: We currently assume some fixed duration and tempo (1.0, 120)!!!
    # The parts should be split according to an index.
    audio_index = make_audio_index(note_params_df, 3.0, 0.5, sample_rate)
    audio_index.to_csv(audio_index_file)

    write_midi(stream, midi_file)

    fs.midi_to_audio(midi_file, audio_file_stereo)

    convert_to_mono(audio_file_stereo, audio_file)
    os.remove(audio_file_stereo)

    x, sample_rate = sf.read(audio_file)

    parts = split_audio_to_parts(x, sample_rate, audio_index)
    store_parts_to_files(parts, sample_rate, output_dir, audio_format)

def convert_to_mono(stereo_file, mono_file):
    x, sample_rate = sf.read(stereo_file)
    x_mono = x.mean(axis=1) # convert to mono
    sf.write(mono_file, x_mono, sample_rate)

def make_audio_index(note_params_df, part_duration, margin_duration, sample_rate):
    sample_count = len(note_params_df)
    index = np.arange(sample_count)

    # let's have larger margin to prevent spilling the content
    # 1 second margin, 1 second note, 1 second margin
    # then cut the margin down a bit

    part_samples = int(part_duration * sample_rate)
    margin_samples = int(margin_duration * sample_rate)

    audio_index = pd.DataFrame()

    audio_index['start_samples'] = index * part_samples + margin_samples
    audio_index['start_time'] = audio_index['start_samples'] / sample_rate

    audio_index['end_samples'] = (index + 1) * part_samples - margin_samples
    audio_index['end_time'] = audio_index['end_samples'] / sample_rate

    return audio_index

def split_audio_to_parts(x, sample_rate, audio_index):
    for i, row in audio_index.iterrows():
        x_part = x[int(row['start_samples']):int(row['end_samples'])]
        yield x_part

def store_parts_to_files(parts, sample_rate, output_dir, audio_format):
    """
    Store the cut samples in separate files for easier human listening.
    """
    for i, x_part in enumerate(parts):
        audio_file = output_dir + '/sample_{0:06d}.{1}'.format(i, audio_format)
        print(audio_file)
        sf.write(audio_file, x_part, sample_rate)

def generate_random_samples(args):
    params_df = random_params(args.count, seed=args.seed)
    os.makedirs(args.output_dir, exist_ok=True)
    params_df.to_csv(args.output_dir + '/parameters.csv')
    # generate_separete_notes(params_df, output_dir, args.audio_format)
    generate_notes_in_batch(params_df, args.output_dir, args.audio_format)

class SingleToneDataset():
    """
    Represents the generated dataset, useful for loading the data for analysis and model training.
    """
    def __init__(self, path):
        self.path = path
        self.params_file = path + '/parameters.csv'
        self.params = pd.read_csv(self.params_file, index_col=0)
        self.audio_index_file = path + '/all_samples_index.csv'
        self.audio_index = pd.read_csv(self.audio_index_file, index_col=0)
        self.audio_file = path + '/all_samples.flac'
        audio, self.sample_rate = sf.read(self.audio_file)
        self.samples = np.array(list(split_audio_to_parts(audio, self.sample_rate, self.audio_index)))

    def __repr__(self):
        return '<SingleToneDataset at {} of shape {}>'.format(self.path, self.samples.shape)

def parse_args():
    parser = argparse.ArgumentParser(description='Generate random audio samples.')
    parser.add_argument('-c', '--count', type=int, help='number of samples')
    parser.add_argument('-s', '--seed', type=int, help='random seed')
    parser.add_argument('-o', '--output-dir', type=str, help='output directory')
    parser.add_argument('-f', '--audio-format', type=str, default='flac', help='audio format (flac, wav)')

    return parser.parse_args()

if __name__ == '__main__':
    generate_random_samples(parse_args())

# TODO: split into two parts with separate responsibilities:
# - randomly generate the parameters to a CSV file
# - synthesize sounds from a given CSV file
<commit_msg>Fix a typo in function name.<commit_after>"""
This generates a single-tone audio clip for instrument classification.
"""

import argparse
from music21.chord import Chord
from music21.duration import Duration
from music21.instrument import Instrument
from music21.note import Note, Rest
from music21.stream import Stream
from music21.tempo import MetronomeMark
from music21.volume import Volume
import numpy as np
import os
import pandas as pd
import soundfile as sf

from fluidsynth import FluidSynth
from instruments import midi_instruments

def make_instrument(id):
    i = Instrument()
    i.midiProgram = id
    return i

def chord_with_volume(chord, volume):
    chord.volume = Volume(velocityScalar=volume)
    return chord

def write_midi(stream, output_file):
    stream.write('midi', output_file)

def generate_single_note(midi_number, midi_instrument=0, volume=1.0, duration=1.0, tempo=120):
    """
    Generates a stream containing a single note with given parameters.
    midi_number - MIDI note number, 0 to 127
    midi_instrument - MIDI intrument number, 0 to 127
    duration - floating point number (in quarter note lengths)
    volume - 0.0 to 1.0
    tempo - number of quarter notes per minute (eg. 120)

    Note that there's a quarter note rest at the beginning and at the end.
    """
    return Stream([
        MetronomeMark(number=tempo),
        make_instrument(int(midi_instrument)),
        chord_with_volume(Chord([
            Note(midi=int(midi_number), duration=Duration(duration))
        ]), volume)
    ])

def generate_separate_notes(note_params_df, output_dir, audio_format='flac'):
    """
    Generates a batch of single note samples from the given table of parameters.

    `note_params_df` - a Pandas Dataframe with columns:
    `midi_number, midi_instrument, volume, duration, tempo`. Their meaning is the same as in generate_single_note.
    `output_dir` - output directory for the MIDI files

    Each sample goes to a single MIDI file named by the numeric index. Also each synthesized audio sample goes to a
    """
    os.makedirs(output_dir, exist_ok=True)

    fs = FluidSynth()
    for i, row in note_params_df.iterrows():
        midi_file = '{0}/{1:06d}.midi'.format(output_dir, i)
        audio_file = '{0}/{1:06d}.{2}'.format(output_dir, i, audio_format)

        print(row, midi_file, audio_file)

        stream = generate_single_note(**row)
        write_midi(stream, midi_file)
        fs.midi_to_audio(midi_file, audio_file)

def random_params(n, note_range=None, volume_range=(0.5, 1.0), duration=1.0, tempo=60, seed=None):
    """
    Generate note parameters randomly as a DataFrame.

    n - number of samples
    """

    if seed is not None:
        np.random.seed(seed)

    instruments = midi_instruments()

    def instrument_range(i):
        instr = instruments.ix[i]
        instr_range = np.array([instr['min_pitch'], instr['max_pitch']])
        if note_range is not None:
            instr_range = np.clip(instr_range, *note_range)
        return instr_range

    allowed_instruments = np.hstack([
            np.arange(0, 8), # piano
            np.arange(16, 32), # organ, guitar
            np.arange(40, 48), # strings
            np.arange(56, 80), # brass, reed, pipe
        ])

    def random_note_for_instrument(instr):
        instr_range = instrument_range(instr)
        return np.random.random_integers(low=instr_range[0], high=instr_range[1], size=1)[0]

    df = pd.DataFrame()
    df['midi_instrument'] = np.random.choice(allowed_instruments, size=n)
    df['midi_number'] = df['midi_instrument'].apply(random_note_for_instrument)
    df['volume'] = np.random.uniform(low=volume_range[0], high=volume_range[1], size=n)
    # TODO: allow varying duration while maintaining constant audio length
    df['duration'] = duration
    df['tempo'] = tempo

    return df

def generate_notes_in_batch(note_params_df, output_dir, audio_format='flac', sample_rate=44100):
    """
    Generates a batch of single note samples from the given table of parameters.

    `note_params_df` - a Pandas Dataframe with columns:
    `midi_number, midi_instrument, volume, duration, tempo`. Their meaning is the same as in generate_single_note.
    `output_dir` - output directory for the MIDI files

    Each sample goes to a single MIDI file named by the numeric index. Also each synthesized audio sample goes to a
    """
    os.makedirs(output_dir, exist_ok=True)

    fs = FluidSynth(sample_rate=sample_rate)

    stream = Stream()

    for i, row in note_params_df.iterrows():
        stream.append(MetronomeMark(number=row['tempo']))
        stream.append(make_instrument(int(row['midi_instrument'])))
        duration = row['duration']
        stream.append(chord_with_volume(Chord([
            Note(midi=int(row['midi_number']), duration=Duration(duration))
        ]), row['volume']))
        stream.append(Rest(duration=Duration(2 * duration)))

    midi_file = '{0}/all_samples.midi'.format(output_dir)
    audio_file_stereo = '{0}/all_samples_stereo.{1}'.format(output_dir, audio_format)
    audio_file = '{0}/all_samples.{1}'.format(output_dir, audio_format)
    audio_index_file = '{0}/all_samples_index.csv'.format(output_dir)

    # TODO: We currently assume some fixed duration and tempo (1.0, 120)!!!
    # The parts should be split according to an index.
    audio_index = make_audio_index(note_params_df, 3.0, 0.5, sample_rate)
    audio_index.to_csv(audio_index_file)

    write_midi(stream, midi_file)

    fs.midi_to_audio(midi_file, audio_file_stereo)

    convert_to_mono(audio_file_stereo, audio_file)
    os.remove(audio_file_stereo)

    x, sample_rate = sf.read(audio_file)

    parts = split_audio_to_parts(x, sample_rate, audio_index)
    store_parts_to_files(parts, sample_rate, output_dir, audio_format)

def convert_to_mono(stereo_file, mono_file):
    x, sample_rate = sf.read(stereo_file)
    x_mono = x.mean(axis=1) # convert to mono
    sf.write(mono_file, x_mono, sample_rate)

def make_audio_index(note_params_df, part_duration, margin_duration, sample_rate):
    sample_count = len(note_params_df)
    index = np.arange(sample_count)

    # let's have larger margin to prevent spilling the content
    # 1 second margin, 1 second note, 1 second margin
    # then cut the margin down a bit

    part_samples = int(part_duration * sample_rate)
    margin_samples = int(margin_duration * sample_rate)

    audio_index = pd.DataFrame()

    audio_index['start_samples'] = index * part_samples + margin_samples
    audio_index['start_time'] = audio_index['start_samples'] / sample_rate

    audio_index['end_samples'] = (index + 1) * part_samples - margin_samples
    audio_index['end_time'] = audio_index['end_samples'] / sample_rate

    return audio_index

def split_audio_to_parts(x, sample_rate, audio_index):
    for i, row in audio_index.iterrows():
        x_part = x[int(row['start_samples']):int(row['end_samples'])]
        yield x_part

def store_parts_to_files(parts, sample_rate, output_dir, audio_format):
    """
    Store the cut samples in separate files for easier human listening.
    """
    for i, x_part in enumerate(parts):
        audio_file = output_dir + '/sample_{0:06d}.{1}'.format(i, audio_format)
        print(audio_file)
        sf.write(audio_file, x_part, sample_rate)

def generate_random_samples(args):
    params_df = random_params(args.count, seed=args.seed)
    os.makedirs(args.output_dir, exist_ok=True)
    params_df.to_csv(args.output_dir + '/parameters.csv')
    # generate_separate_notes(params_df, output_dir, args.audio_format)
    generate_notes_in_batch(params_df, args.output_dir, args.audio_format)

class SingleToneDataset():
    """
    Represents the generated dataset, useful for loading the data for analysis and model training.
    """
    def __init__(self, path):
        self.path = path
        self.params_file = path + '/parameters.csv'
        self.params = pd.read_csv(self.params_file, index_col=0)
        self.audio_index_file = path + '/all_samples_index.csv'
        self.audio_index = pd.read_csv(self.audio_index_file, index_col=0)
        self.audio_file = path + '/all_samples.flac'
        audio, self.sample_rate = sf.read(self.audio_file)
        self.samples = np.array(list(split_audio_to_parts(audio, self.sample_rate, self.audio_index)))

    def __repr__(self):
        return '<SingleToneDataset at {} of shape {}>'.format(self.path, self.samples.shape)

def parse_args():
    parser = argparse.ArgumentParser(description='Generate random audio samples.')
    parser.add_argument('-c', '--count', type=int, help='number of samples')
    parser.add_argument('-s', '--seed', type=int, help='random seed')
    parser.add_argument('-o', '--output-dir', type=str, help='output directory')
    parser.add_argument('-f', '--audio-format', type=str, default='flac', help='audio format (flac, wav)')

    return parser.parse_args()

if __name__ == '__main__':
    generate_random_samples(parse_args())

# TODO: split into two parts with separate responsibilities:
# - randomly generate the parameters to a CSV file
# - synthesize sounds from a given CSV file
<|endoftext|>


<commit_before>from basic_graph import BasicGraph, Node
# class Node:
#     def __init__(self):
#         self.__init__('default')
#
#     def __init__(self, title):
#         self.title = title
#         self.parents = []
#         self.childs = []
#         self.coverage_score = 0
#         self.specificity_score = -1
#         self._coverage_computed = False
#         self.num_of_subjects = -1
#         self.path_specificity = -1
#         self.score = -1
#         self.depth = -1
#
#     def __str__(self):
#         return self.title
#
#     def __unicode__(self):
#         return self.title
#
#     def __repr__(self):
#         return self.title

class TypeGraph(BasicGraph):

    def break_cycles(self):
        for r in self.roots:
            self.dfs_break_cycle([r])

    def dfs_break_cycle(self, visited):
        node = visited[-1]
        for ch in node.childs:
            if ch in visited:  # there is a cycle
                self.remove_edge(node, ch)
            else:
                self.dfs_break_cycle(visited=visited+[ch])

    def draw_with_scores(self, multi=False):
        if not multi:
            print "not multi"
            from graphviz import Digraph
            dot = Digraph(comment='The Round Table')
            for n in self.cache:
                node = self.find_v(n)
                dot.node(clean_with_score(node))

            for n in self.cache:
                node = self.index[n]
                for ch in node.childs:
                    dot.edge(clean_with_score(node), clean_with_score(ch))

            dot.render('graph.gv', view=True)
        else:
            print "multi"
            self.draw_with_score_separate()

    def draw_with_score_separate(self):
        for idx, r in enumerate(self.roots):
            self.draw_score_for_root(r, "%d_graph.gv"%idx)
        # self.draw_score_for_root(self.roots[0], "1_graph.gv")

    def draw_score_for_root(self, root, file_name):
        from graphviz import Digraph
        dot = Digraph(comment='The Round Table')
        nodes = self.get_all_child_nodes(root, [])
        if len(nodes) <= 3:
            return
        for n in nodes:
            dot.node(clean_with_score(n))

        for n in nodes:
            for ch in n.childs:
                dot.edge(clean_with_score(n), clean_with_score(ch))

        dot.render(file_name, view=True)

    def get_scores(self):
        nodes = []
        for n in self.roots:
            nodes += self.get_score_for_node(n)
        nodes = list(set(nodes))
        return sorted(nodes, key=lambda node: node.score, reverse=True)

    def get_score_for_node(self, node):
        nodes = [node]
        for child in node.childs:
            nodes += self.get_score_for_node(child)
        return nodes

    def set_score_for_graph(self, coverage_weight=0.5, coverage_norm=1):
        """
        :param coverage_weight: the alpha
        :param coverage_norm: since coverage is actually increase when the number of entities increase, we need to
        normalize the coverage score by dividing it with the coverage_norm
        :return:
        """
        for n in self.roots:
            self.set_score_for_node(n, coverage_weight, coverage_norm)

    def set_score_for_node(self, node, coverage_weight, coverage_norm):
        if node.score != -1:
            return
        node.score = node.coverage_score * coverage_weight * coverage_norm + (1-coverage_weight) * -node.path_specificity / (node.depth + 1)
        for child in node.childs:
            self.set_score_for_node(child, coverage_weight, coverage_norm)

    def set_converage_score(self):
        for n in self.roots:
            print 'set coverage root: %s' % n.title
            self.compute_coverage_score_for_node(n)

    def compute_coverage_score_for_node(self, node):
        print 'enter in %s' % node.title
        if node._coverage_computed:
            return node.coverage_score
        for child in node.childs:
            node.coverage_score += self.compute_coverage_score_for_node(child)
        if len(node.childs) == 0:
            print 'leave score of %s: %g' % (node.title, node.coverage_score)
        else:
            print 'mid score of %s: %g' % (node.title, node.coverage_score)
        print 'leaving %s' % node.title
        node._coverage_computed = True
        return node.coverage_score

    def set_path_specificity(self):
        for n in self.get_leaves_from_graph():
            self.set_path_specificity_for_node(n)

    def set_path_specificity_for_node(self, node):  # solve bug #2
        if node.path_specificity == -1:
            if node.parents == []:
                node.path_specificity = 1
            else:
                node.path_specificity = min([self.set_path_specificity_for_node(p) for p in node.parents]) * node.specificity_score
        return node.path_specificity

    # iteration 8
    def set_nodes_subjects_counts(self, d):
        for n in self.roots:
            self.set_subjects_count_for_node(n, d)

    def set_subjects_count_for_node(self, node, d):
        if node.num_of_subjects != -1:  # it is already set
            return
        for child in node.childs:
            self.set_subjects_count_for_node(child, d)
        if node.title in d:
            node.num_of_subjects = int(d[node.title])
        else:
            node.num_of_subjects = 0
            raise Exception("in iteration 8 this should not happen as we are checking the childs as well")

    def set_specificity_score(self):
        for n in self.roots:
            self.compute_specificity_for_node(n)

    def compute_specificity_for_node(self, node):
        if node.specificity_score != -1:  # if specificity score is computed
            return

        if node.parents == []:
            node.specificity_score = 1
        else:
            node.specificity_score = node.num_of_subjects * 1.0 / max([p.num_of_subjects for p in node.parents])

        for child in node.childs:
            self.compute_specificity_for_node(child)

    def set_depth_for_graph(self):
        for n in self.roots:
            n.depth = 0
        for t in self.cache:
            n = self.find_v(t)
            self.set_depth_for_node(n)

    def set_depth_for_node(self, node):
        if node.depth == -1:  # depth is not set
            max_node = node.parents[0]
            self.set_depth_for_node(max_node)
            for p in node.parents[1:]:
                self.set_depth_for_node(p)
                if self.set_path_specificity_for_node(p) > max_node.path_specificity:
                    max_node = p
            node.depth = 1 + max_node.depth

        return node.depth

def clean_with_score(n):
    return "%s cove(%g) num(%d) depth(%d) pspec(%f) score(%f)" % (
        clean(n.title), n.coverage_score, n.num_of_subjects, n.depth, n.path_specificity, n.score)

def clean(s):
    return s.replace("http://", "")
<commit_msg>refactor fs<commit_after>from basic_graph import BasicGraph, Node
# class Node:
#     def __init__(self):
#         self.__init__('default')
#
#     def __init__(self, title):
#         self.title = title
#         self.parents = []
#         self.childs = []
#         self.coverage_score = 0
#         self.specificity_score = -1
#         self._coverage_computed = False
#         self.num_of_subjects = -1
#         self.path_specificity = -1
#         self.score = -1
#         self.depth = -1
#
#     def __str__(self):
#         return self.title
#
#     def __unicode__(self):
#         return self.title
#
#     def __repr__(self):
#         return self.title

class TypeGraph(BasicGraph):

    # new specificity function
    # def fs(self, node):
    #     """
    #     function that computes the specificity of a node
    #
    #                   1           1
    #     fs(x, d) = -------- - -----------
    #                 (x+1)^10    (d+1)^0.1
    #
    #     :param node: node
    #     :return: the specificity score
    #     """
    #     x = node.path_specificity
    #     d = node.depth
    #     fs_score = 1.0/(x+1)**10 - 1.0/(d+1)**0.1
    #     return fs_score

    def fs(self, node):
        return -node.path_specificity / (node.depth + 1)

    def fc(self, node):
        """
        actually exists in the annotator module.
        a function that compute the coverage score of a given node
        :param node: node
        :return: the coverage score
        """
        pass

    def break_cycles(self):
        for r in self.roots:
            self.dfs_break_cycle([r])

    def dfs_break_cycle(self, visited):
        node = visited[-1]
        for ch in node.childs:
            if ch in visited:  # there is a cycle
                self.remove_edge(node, ch)
            else:
                self.dfs_break_cycle(visited=visited+[ch])

    def draw_with_scores(self, multi=False):
        if not multi:
            print "not multi"
            from graphviz import Digraph
            dot = Digraph(comment='The Round Table')
            for n in self.cache:
                node = self.find_v(n)
                dot.node(clean_with_score(node))

            for n in self.cache:
                node = self.index[n]
                for ch in node.childs:
                    dot.edge(clean_with_score(node), clean_with_score(ch))

            dot.render('graph.gv', view=True)
        else:
            print "multi"
            self.draw_with_score_separate()

    def draw_with_score_separate(self):
        for idx, r in enumerate(self.roots):
            self.draw_score_for_root(r, "%d_graph.gv"%idx)
        # self.draw_score_for_root(self.roots[0], "1_graph.gv")

    def draw_score_for_root(self, root, file_name):
        from graphviz import Digraph
        dot = Digraph(comment='The Round Table')
        nodes = self.get_all_child_nodes(root, [])
        if len(nodes) <= 3:
            return
        for n in nodes:
            dot.node(clean_with_score(n))

        for n in nodes:
            for ch in n.childs:
                dot.edge(clean_with_score(n), clean_with_score(ch))

        dot.render(file_name, view=True)

    def get_scores(self):
        nodes = []
        for n in self.roots:
            nodes += self.get_score_for_node(n)
        nodes = list(set(nodes))
        return sorted(nodes, key=lambda node: node.score, reverse=True)

    def get_score_for_node(self, node):
        nodes = [node]
        for child in node.childs:
            nodes += self.get_score_for_node(child)
        return nodes

    def set_score_for_graph(self, coverage_weight=0.5, coverage_norm=1):
        """
        :param coverage_weight: the alpha
        :param coverage_norm: since coverage is actually increase when the number of entities increase, we need to
        normalize the coverage score by dividing it with the coverage_norm
        :return:
        """
        for n in self.roots:
            self.set_score_for_node(n, coverage_weight, coverage_norm)

    def set_score_for_node(self, node, coverage_weight, coverage_norm):
        if node.score != -1:
            return
        node.score = node.coverage_score * coverage_weight * coverage_norm + (1-coverage_weight) * -node.path_specificity / (node.depth + 1)
        for child in node.childs:
            self.set_score_for_node(child, coverage_weight, coverage_norm)

    def set_converage_score(self):
        for n in self.roots:
            print 'set coverage root: %s' % n.title
            self.compute_coverage_score_for_node(n)

    def compute_coverage_score_for_node(self, node):
        print 'enter in %s' % node.title
        if node._coverage_computed:
            return node.coverage_score
        for child in node.childs:
            node.coverage_score += self.compute_coverage_score_for_node(child)
        if len(node.childs) == 0:
            print 'leave score of %s: %g' % (node.title, node.coverage_score)
        else:
            print 'mid score of %s: %g' % (node.title, node.coverage_score)
        print 'leaving %s' % node.title
        node._coverage_computed = True
        return node.coverage_score

    def set_path_specificity(self):
        for n in self.get_leaves_from_graph():
            self.set_path_specificity_for_node(n)

    def set_path_specificity_for_node(self, node):  # solve bug #2
        if node.path_specificity == -1:
            if node.parents == []:
                node.path_specificity = 1
            else:
                node.path_specificity = min([self.set_path_specificity_for_node(p) for p in node.parents]) * node.specificity_score
        return node.path_specificity

    # iteration 8
    def set_nodes_subjects_counts(self, d):
        for n in self.roots:
            self.set_subjects_count_for_node(n, d)

    def set_subjects_count_for_node(self, node, d):
        if node.num_of_subjects != -1:  # it is already set
            return
        for child in node.childs:
            self.set_subjects_count_for_node(child, d)
        if node.title in d:
            node.num_of_subjects = int(d[node.title])
        else:
            node.num_of_subjects = 0
            raise Exception("in iteration 8 this should not happen as we are checking the childs as well")

    def set_specificity_score(self):
        for n in self.roots:
            self.compute_specificity_for_node(n)

    def compute_specificity_for_node(self, node):
        if node.specificity_score != -1:  # if specificity score is computed
            return

        if node.parents == []:
            node.specificity_score = 1
        else:
            node.specificity_score = node.num_of_subjects * 1.0 / max([p.num_of_subjects for p in node.parents])

        for child in node.childs:
            self.compute_specificity_for_node(child)

    def set_depth_for_graph(self):
        for n in self.roots:
            n.depth = 0
        for t in self.cache:
            n = self.find_v(t)
            self.set_depth_for_node(n)

    def set_depth_for_node(self, node):
        if node.depth == -1:  # depth is not set
            max_node = node.parents[0]
            self.set_depth_for_node(max_node)
            for p in node.parents[1:]:
                self.set_depth_for_node(p)
                if self.set_path_specificity_for_node(p) > max_node.path_specificity:
                    max_node = p
            node.depth = 1 + max_node.depth

        return node.depth

def clean_with_score(n):
    return "%s cove(%g) num(%d) depth(%d) pspec(%f) score(%f)" % (
        clean(n.title), n.coverage_score, n.num_of_subjects, n.depth, n.path_specificity, n.score)

def clean(s):
    return s.replace("http://", "")
<|endoftext|>


<commit_before>from __future__ import division
from collections import deque

import numpy as np

def sigmoid(x):
    """
    Computes the sigmoid function on x

    :param x: a numeric type or numpy-friendly array
    :returns: the sigmoid function applied to x, if x is numpy-friendly,
              then a numpy array with sigmoid applied to each element
    """
    return 1/(1 + np.exp(-x))

class FTRLProximal(object):
    """
    A pure python implementation of the FTRLProximal algorithm described in
    ["Ad Click Prediction: a View from the Trenches"][1] and
    ["Follow-the-Regularized-Leader and Mirror Descent: Equivalence Theorems and L1 Regularization"][2]

    This implementation relies on native python data-types with the intention of making the learned parameters
    human readable

    [1][http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/41159.pdf]
    [2][http://jmlr.org/proceedings/papers/v15/mcmahan11b/mcmahan11b.pdf]
    """
    def __init__(self, alpha=1., beta=1., l1=0., l2=0., history_len=None):
        """
        :param alpha: Parameter used to control scaling of per-weight learning rates. (Default 1.0)
                      Can be loosely thought of as a learning rate in Gradient Descent.
                      This should be tuned with validation.
        :param beta: Parameter used to stabalize the magnitude of early per-weight learning rates (Default 1.0)
                     Learning rates are approximately defined as alpha / (beta + stuff). At the beginning of training
                     if 'stuff' is small, the learning rate for parameters will be large, beta is used to stabalize the
                     denominator. Leaving this at 1.0 should be good enough.
        :param l1: The magnitude of L1 regularization (Default 0.0)
        :param l2: The magnitude of L2 regularization (Default 0.0)
        """
        self.alpha = alpha
        self.beta = beta
        self.l1 = l1
        self.l2 = l2
        self._z = {}
        self._n = {}
        self._round = 0
        self._history = deque(maxlen=history_len)
        self._bias = 0
        self._nbias = 0

    @property
    def bias(self):
        """
        :returns: The learned bias of the model
        """
        return self._bias

    @property
    def weights(self):
        """
        :returns: The learned coefficients of the model
        """
        return self._weights(self._z.keys())

    def _weights(self, keys):
        w = {}
        a = self.alpha
        b = self.beta
        n = self._n
        l1 = self.l1
        l2 = self.l2
        for k in keys:
            zi = self._z.get(k, 0)
            if np.abs(zi) <= l1:
                w[k] = 0
            else:
                w[k] = -(zi - l1 * np.sign(zi)) / (l2 + (b + np.sqrt(n[k])) / a)
        return w

    def _linear_predictor(self, x, w):
        return self._bias + sum(w.get(k, 0) * v for k, v in x.iteritems())

    def _predict(self, x, w):
        return sigmoid(self._linear_predictor(x, w))

    def predict(self, x):
        """
        Make a prediction on data x using the learned parameters of the model

        :param x: a dict or list of dicts. The dict is an individaul datapoint represented in a sparse format
                  if a list of dicts, each element should be datapoints represented in sparse formats
        :returns: if x is a singular dict, returns the models prediction on x,
                 if x is a list, returns a list of element-wise predictions
        """
        w = self.weights  # we only want to generate the weights once
        if type(x) is list:  # TODO: Allow any iterable to be passed for predictions
            return [self._predict(xi, w) for xi in x]
        else:
            return self._predict(x, w)

    def fit(self, x, y):
        """
        Fit the model on an x, y datapoint pair
        :param x: a sparse dict with format {'feature1_name': value, ...}
                  featureN_name is a human readable name, value is numeric
                  to save on computation, any zero-values in x should be omitted from the dict
        :param y: the binary classification label for x
        :returns: self
        """
        w = self._weights(x.keys())
        pred = self._predict(x, w)
        self._update(x, y, pred, w)
        self._history.append((pred, y))
        return self

    def _update(self, x, y, p, w):
        err = p - y
        for k, xi in x.iteritems():
            gi = err*xi
            gg = gi**2
            ni = self._n.get(k, 0)
            si = (np.sqrt(ni + gg) - np.sqrt(ni)) / self.alpha
            self._z[k] = self._z.get(k, 0) + gi - si * w.get(k, 0)
            self._n[k] = self._n.get(k, 0) + gg
        self._bias -= self.alpha * err / (self.beta + np.sqrt(self._nbias))
        self._nbias += err**2
        self._round += 1

    def uncertainty_score(self, x):  # This is a work in progress
        """
        Compute the uncertainty score described in Google's paper

        The goal of the uncertainty score is to be a heuristic way of quantifying uncertainty

        NOTE: This is still a work-in-progress and currently relies on the assumption that each value of x
              has magnitude <= 1.0

        :param x: a dict representation of the datapoint
        :returns: an uncertainty score of the prediction
        """
        return self.alpha * sum(np.abs(v) / np.sqrt(self._n.get(k, 0))
                                for k, v in x.iteritems())

class FTRLProximalSVM(FTRLProximal):
    """
    An implementation of the FTRLProximal algorithm using the L2-SVM as the objective function for training

    The only differences from the logistic implementation is the objective function used, and the ouput range of
    predictions

    Predictions, p, from this model are on the interval (-inf, inf), and sign(p) should be used as the classification
    label
    """
    def _predict(self, x, w):
        return self._linear_predictor(x, w)

    def _update(self, x, y, p, w):
        err = -np.max(1 - p*y, 0)*y
        for k, xi in x.iteritems():
            gi = err*xi
            gg = gi**2
            ni = self._n.get(k, 0)
            si = (np.sqrt(ni + gg) - np.sqrt(ni)) / self.alpha
            self._z[k] = self._z.get(k, 0) + gi - si * w.get(k, 0)
            self._n[k] = self._n.get(k, 0) + gg
        self._bias -= self.alpha * err / (self.beta + np.sqrt(self._nbias))
        self._nbias += err**2
        self._round += 1<commit_msg>Improved history maintenance and feature inclusion<commit_after>from __future__ import division
from collections import deque

import numpy as np

def sigmoid(x):
    """
    Computes the sigmoid function on x

    :param x: a numeric type or numpy-friendly array
    :returns: the sigmoid function applied to x, if x is numpy-friendly,
              then a numpy array with sigmoid applied to each element
    """
    return 1/(1 + np.exp(-x))

class HistoryWriter(object):
    def __init__(self, save_file=None, maxlen=None):
        self._save_file = save_file
        self._history = deque(maxlen=maxlen)

    def append(self, o):
        if self._save_file is not None:
            self._write_history(o)
        self._history.append(o)

    def _write_history(self, o):
        with open(self._save_file, 'aw') as outfile:
            try:
                outfile.write('\t'.join(str(oi) for oi in o) + '\n')
            except TypeError:
                outfile.write(str(o))

class FTRLProximal(object):
    """
    A pure python implementation of the FTRLProximal algorithm described in
    ["Ad Click Prediction: a View from the Trenches"][1] and
    ["Follow-the-Regularized-Leader and Mirror Descent: Equivalence Theorems and L1 Regularization"][2]

    This implementation relies on native python data-types with the intention of making the learned parameters
    human readable

    [1][http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/41159.pdf]
    [2][http://jmlr.org/proceedings/papers/v15/mcmahan11b/mcmahan11b.pdf]
    """
    def __init__(self, alpha=1., beta=1., l1=0., l2=0., lam=1., history_len=None, history_file=None):
        """
        :param alpha: Parameter used to control scaling of per-weight learning rates. (Default 1.0)
                      Can be loosely thought of as a learning rate in Gradient Descent.
                      This should be tuned with validation.
        :param beta: Parameter used to stabalize the magnitude of early per-weight learning rates (Default 1.0)
                     Learning rates are approximately defined as alpha / (beta + stuff). At the beginning of training
                     if 'stuff' is small, the learning rate for parameters will be large, beta is used to stabalize the
                     denominator. Leaving this at 1.0 should be good enough.
        :param l1: The magnitude of L1 regularization (Default 0.0)
        :param l2: The magnitude of L2 regularization (Default 0.0)
        :param lam: The probability that a new feature will be included when encountered
                    Concretely, if a new feature is observed, it is added to the model with probability lam
        :param history_len: what is the maximum length of history to maintain in memory
        :param history_file: if not None, will write update history to the filename provided
        """
        self.alpha = alpha
        self.beta = beta
        self._l1 = l1
        self._l2 = l2
        self.lam = lam
        self._z = {}
        self._n = {}
        self._round = 0
        self._history = HistoryWriter(save_file=history_file, maxlen=history_len)
        self._bias = 0
        self._nbias = 0
        self._use_cached_weights = False
        self._cached_weights = {}

    @property
    def l1(self):
        return self._l1

    @l1.setter
    def l1(self, new):
        self._l1 = new
        self._use_cached_weights = False

    @property
    def l2(self):
        return self._l2

    @l2.setter
    def l2(self, new):
        self._l2 = new
        self._use_cached_weights = False

    @property
    def bias(self):
        """
        :returns: The learned bias of the model
        """
        return self._bias

    @property
    def weights(self):
        """
        :returns: The learned coefficients of the model
        """
        if not self._use_cached_weights:
            self._cached_weights = self._weights(self._z.keys())
            self._use_cached_weights = True
        return self._cached_weights

    def _weights(self, keys):
        w = {}
        a = self.alpha
        b = self.beta
        n = self._n
        l1 = self.l1
        l2 = self.l2
        for k in keys:
            zi = self._z.get(k, 0)
            if np.abs(zi) > l1:
                w[k] = -(zi - l1 * np.sign(zi)) / (l2 + (b + np.sqrt(n[k])) / a)
        return w

    def _linear_predictor(self, x, w):
        return self._bias + sum(w.get(k, 0) * v for k, v in x.iteritems())

    def _predict(self, x, w):
        return sigmoid(self._linear_predictor(x, w))

    def predict(self, x):
        """
        Make a prediction on data x using the learned parameters of the model

        :param x: a dict or list of dicts. The dict is an individaul datapoint represented in a sparse format
                  if a list of dicts, each element should be datapoints represented in sparse formats
        :returns: if x is a singular dict, returns the models prediction on x,
                 if x is a list, returns a list of element-wise predictions
        """
        w = self.weights  # we only want to generate the weights once
        if type(x) is list:  # TODO: Allow any iterable to be passed for predictions
            return [self._predict(xi, w) for xi in x]
        else:
            return self._predict(x, w)

    def fit(self, x, y, pred=None, weights=None):
        """
        Fit the model on an x, y datapoint pair
        :param x: a sparse dict with format {'feature1_name': value, ...}
                  featureN_name is a human readable name, value is numeric
                  to save on computation, any zero-values in x should be omitted from the dict
        :param y: the binary classification label for x
        :returns: self
        """
        weights = self._weights(x.keys()) if weights is None else weights
        pred = self._predict(x, weights) if pred is None else pred
        score = self.uncertainty_score(x)

        self._update(x, y, pred, weights)
        self._history.append((x, pred, y, score))
        return self

    def _update(self, x, y, p, w):
        err = p - y
        for k, xi in x.iteritems():
            if k not in self._z and np.random.rand() > self.lam:
                continue
            gi = err*xi
            gg = gi**2
            ni = self._n.get(k, 0)
            si = (np.sqrt(ni + gg) - np.sqrt(ni)) / self.alpha
            self._z[k] = self._z.get(k, 0) + gi - si * w.get(k, 0)
            self._n[k] = self._n.get(k, 0) + gg
        self._bias -= self.alpha * err / (self.beta + np.sqrt(self._nbias))
        self._nbias += err**2
        self._round = 1
        self._use_cached_weights = False

    def uncertainty_score(self, x):  # This is a work in progress
        """
        Compute the uncertainty score described in Google's paper

        The goal of the uncertainty score is to be a heuristic way of quantifying uncertainty

        NOTE: This is still a work-in-progress and currently relies on the assumption that each value of x
              has magnitude <= 1.0

        :param x: a dict representation of the datapoint
        :returns: an uncertainty score of the prediction
        """

        return sum(np.abs(v) / np.sqrt(self._n.get(k, 0))
                   for k, v in x.iteritems() if k in self._n)

class FTRLProximalSVM(FTRLProximal):
    """
    An implementation of the FTRLProximal algorithm using the L2-SVM as the objective function for training

    The only differences from the logistic implementation is the objective function used, and the ouput range of
    predictions

    Predictions, p, from this model are on the interval (-inf, inf), and sign(p) should be used as the classification
    label
    """
    def _predict(self, x, w):
        return self._linear_predictor(x, w)

    def _update(self, x, y, p, w):
        err = -np.max(1 - p*y, 0)*y
        for k, xi in x.iteritems():
            gi = err*xi
            gg = gi**2
            ni = self._n.get(k, 0)
            si = (np.sqrt(ni + gg) - np.sqrt(ni)) / self.alpha
            self._z[k] = self._z.get(k, 0) + gi - si * w.get(k, 0)
            self._n[k] = self._n.get(k, 0) + gg
        self._bias -= self.alpha * err / (self.beta + np.sqrt(self._nbias))
        self._nbias += err**2
        self._round += 1
<|endoftext|>


<commit_before># Copyright 2015 Martin Olejar
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import logging
import collections
from time import time
from struct import pack, unpack_from
from .misc import atos

#os.environ['PYUSB_DEBUG'] = 'debug'
#os.environ['PYUSB_LOG_FILENAME'] = 'usb.log'

########################################################################################################################
# USB HID Interface Base Class
########################################################################################################################
class UsbHidBase(object):

    def __init__(self):
        self.vid = 0
        self.pid = 0
        self.vendor_name = ""
        self.product_name = ""

    def _encode_packet(self, report_id, data, pkglen=36):
        raw_data = pack('<BBH', report_id, 0x00, len(data))
        raw_data += data
        raw_data += bytes([0x00]*(pkglen - len(raw_data)))
        return raw_data

    def _decode_packet(self, raw_data):
        report_id, _, plen = unpack_from('<BBH', raw_data)
        data = raw_data[4:4 + plen]
        return (report_id, data)

    def open(self):
        raise NotImplementedError()

    def close(self):
        raise NotImplementedError()

    def getInfo(self):
        return "{0:s} (0x{1:04X}, 0x{2:04X})".format(self.product_name, self.vid, self.pid)

    def write(self, id, data, size):
        raise NotImplementedError()

    def read(self, timeout):
        raise NotImplementedError()

########################################################################################################################
# USB Interface Classes
########################################################################################################################
if os.name == "nt":
    try:
        import pywinusb.hid as hid
    except:
        raise Exception("PyWinUSB is required on a Windows Machine")

    class USBIF(UsbHidBase):
        """
        This class provides basic functions to access
        a USB HID device using pywinusb:
            - write/read an endpoint
        """
        def __init__(self):
            super().__init__()
            # Vendor page and usage_id = 2
            self.report = []
            # deque used here instead of synchronized Queue
            # since read speeds are ~10-30% faster and are
            # comprable to a based list implmentation.
            self.rcv_data = collections.deque()
            self.device = None
            return

        # handler called when a report is received
        def __rx_handler(self, data):
            # logging.debug("rcv: %s", data[1:])
            self.rcv_data.append(data)

        def open(self):
            """ open the interface """
            logging.debug("Opening USB interface")
            self.device.set_raw_data_handler(self.__rx_handler)
            self.device.open(shared=False)

        def close(self):
            """ close the interface """
            logging.debug("Closing USB interface")
            self.device.close()

        def write(self, id, data, size=36):
            """
            write data on the OUT endpoint associated to the HID interface
            """
            rawdata = self._encode_packet(id, data, size)
            logging.debug('USB-OUT[0x]: %s', atos(rawdata))
            self.report[id - 1].send(rawdata)

        def read(self, timeout=2000):
            """
            Read data on the IN endpoint associated to the HID interface
            :param timeout:
            """
            start = time()
            while len(self.rcv_data) == 0:
                if ((time() - start) * 1000) > timeout:
                    raise Exception("Read timed out")
            rawdata = self.rcv_data.popleft()
            logging.debug('USB-IN [0x]: %s', atos(rawdata))
            return self._decode_packet(rawdata)

        @staticmethod
        def enumerate(vid, pid):
            """
            returns all the connected devices which matches PyWinUSB.vid/PyWinUSB.pid.
            returns an array of PyWinUSB (Interface) objects
            :param vid:
            :param pid:
            """
            all_devices = hid.find_all_hid_devices()

            # find devices with good vid/pid
            all_kboot_devices = []
            for d in all_devices:
                if (d.vendor_id == vid) and (d.product_id == pid):
                    all_kboot_devices.append(d)

            targets = []
            for dev in all_kboot_devices:
                try:
                    dev.open(shared=False)
                    report = dev.find_output_reports()
                    dev.close()

                    if report:
                        new_target = USBIF()
                        new_target.report = report
                        new_target.vendor_name = dev.vendor_name
                        new_target.product_name = dev.product_name
                        new_target.vid = dev.vendor_id
                        new_target.pid = dev.product_id
                        new_target.device = dev
                        new_target.device.set_raw_data_handler(new_target.__rx_handler)
                        targets.append(new_target)
                except Exception as e:
                    logging.error("Receiving Exception: %s", e)
                    dev.close()

            return targets

elif os.name == "posix":
    try:
        import usb.core
        import usb.util
    except:
        raise Exception("PyUSB is required on a Linux Machine")

    class USBIF(UsbHidBase):
        """
        This class provides basic functions to access
        a USB HID device using pyusb:
            - write/read an endpoint
        """

        vid = 0
        pid = 0
        intf_number = 0

        def __init__(self):
            super().__init__()
            self.ep_out = None
            self.ep_in = None
            self.dev = None
            self.closed = False

        def open(self):
            """ open the interface """
            logging.debug("Opening USB interface")

        def close(self):
            """ close the interface """
            logging.debug("Close USB Interface")
            self.closed = True
            try:
                if self.dev: usb.util.dispose_resources(self.dev)
            except:
                pass

        def write(self, id, data, size=36):
            """
            write data on the OUT endpoint associated to the HID interface
            """
            rawdata = self._encode_packet(id, data, size)
            logging.debug('USB-OUT[0x]: %s', atos(rawdata))

            if self.ep_out:
                self.ep_out.write(rawdata)
            else:
                bmRequestType = 0x21       #Host to device request of type Class of Recipient Interface
                bmRequest = 0x09           #Set_REPORT (HID class-specific request for transferring data over EP0)
                wValue = 0x200             #Issuing an OUT report
                wIndex = self.intf_number  #Interface number for HID
                self.dev.ctrl_transfer(bmRequestType, bmRequest, wValue + id, wIndex, rawdata)

        def read(self, timeout=1000):
            """
            read data on the IN endpoint associated to the HID interface
            """
            #rawdata = self.ep_in.read(self.ep_in.wMaxPacketSize, timeout)
            rawdata = self.ep_in.read(36, timeout)
            logging.debug('USB-IN [0x]: %s', atos(rawdata))
            return self._decode_packet(rawdata)

        @staticmethod
        def enumerate(vid, pid):
            """
            returns all the connected devices which matches PyUSB.vid/PyUSB.pid.
            returns an array of PyUSB (Interface) objects
            :param vid:
            :param pid:
            """
            # find all devices matching the vid/pid specified
            all_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)

            if not all_devices:
                logging.debug("No device connected")
                return None

            targets = []

            # iterate on all devices found
            for dev in all_devices:
                interface = None
                interface_number = -1

                # get active config
                config = dev.get_active_configuration()

                # iterate on all interfaces:
                for interface in config:
                    if interface.bInterfaceClass == 0x03: # HID Interface
                        interface_number = interface.bInterfaceNumber
                        break

                if interface is None or interface_number == -1:
                    continue

                try:
                    if dev.is_kernel_driver_active(interface_number):
                        dev.detach_kernel_driver(interface_number)
                except Exception as e:
                    print(str(e))

                try:
                    dev.set_configuration()
                    dev.reset()
                except usb.core.USBError as e:
                    print("Cannot set configuration the device: %s" % str(e))

                ep_in, ep_out = None, None
                for ep in interface:
                    if ep.bEndpointAddress & 0x80:
                        ep_in = ep
                    else:
                        ep_out = ep

                if usb.__version__ == '1.0.0b1':
                    vendor_name = usb.util.get_string(dev, 64, 1)
                    product_name = usb.util.get_string(dev, 64, 2)
                else:
                    vendor_name = usb.util.get_string(dev, 1)
                    product_name = usb.util.get_string(dev, 2)

                if not ep_in:
                    logging.error('Endpoints not found')
                    return None

                new_target = USBIF()
                new_target.ep_in = ep_in
                new_target.ep_out = ep_out
                new_target.dev = dev
                new_target.vid = vid
                new_target.pid = pid
                new_target.intf_number = interface_number
                new_target.vendor_name = vendor_name
                new_target.product_name = product_name
                targets.append(new_target)

            return targets

else:
    raise Exception("No USB backend found")

<commit_msg>fixed read method in usbif class<commit_after># Copyright 2015 Martin Olejar
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import logging
import collections
from time import time
from struct import pack, unpack_from
from .misc import atos

#os.environ['PYUSB_DEBUG'] = 'debug'
#os.environ['PYUSB_LOG_FILENAME'] = 'usb.log'

########################################################################################################################
# USB HID Interface Base Class
########################################################################################################################
class UsbHidBase(object):

    def __init__(self):
        self.vid = 0
        self.pid = 0
        self.vendor_name = ""
        self.product_name = ""

    def _encode_packet(self, report_id, data, pkglen=36):
        raw_data = pack('<BBH', report_id, 0x00, len(data))
        raw_data += data
        raw_data += bytes([0x00]*(pkglen - len(raw_data)))
        return raw_data

    def _decode_packet(self, raw_data):
        report_id, _, plen = unpack_from('<BBH', raw_data)
        data = raw_data[4:4 + plen]
        return report_id, data

    def open(self):
        raise NotImplementedError()

    def close(self):
        raise NotImplementedError()

    def getInfo(self):
        return "{0:s} (0x{1:04X}, 0x{2:04X})".format(self.product_name, self.vid, self.pid)

    def write(self, id, data, size):
        raise NotImplementedError()

    def read(self, timeout):
        raise NotImplementedError()

########################################################################################################################
# USB Interface Classes
########################################################################################################################
if os.name == "nt":
    try:
        import pywinusb.hid as hid
    except:
        raise Exception("PyWinUSB is required on a Windows Machine")

    class USBIF(UsbHidBase):
        """
        This class provides basic functions to access
        a USB HID device using pywinusb:
            - write/read an endpoint
        """
        def __init__(self):
            super().__init__()
            # Vendor page and usage_id = 2
            self.report = []
            # deque used here instead of synchronized Queue
            # since read speeds are ~10-30% faster and are
            # comparable to a based list implementation.
            self.rcv_data = collections.deque()
            self.device = None
            return

        # handler called when a report is received
        def __rx_handler(self, data):
            # logging.debug("rcv: %s", data[1:])
            self.rcv_data.append(data)

        def open(self):
            """ open the interface """
            logging.debug("Opening USB interface")
            self.device.set_raw_data_handler(self.__rx_handler)
            self.device.open(shared=False)

        def close(self):
            """ close the interface """
            logging.debug("Closing USB interface")
            self.device.close()

        def write(self, id, data, size=36):
            """
            write data on the OUT endpoint associated to the HID interface
            """
            rawdata = self._encode_packet(id, data, size)
            logging.debug('USB-OUT[0x]: %s', atos(rawdata))
            self.report[id - 1].send(rawdata)

        def read(self, timeout=2000):
            """
            Read data on the IN endpoint associated to the HID interface
            :param timeout:
            """
            start = time()
            while len(self.rcv_data) == 0:
                if ((time() - start) * 1000) > timeout:
                    raise Exception("Read timed out")
            rawdata = self.rcv_data.popleft()
            logging.debug('USB-IN [0x]: %s', atos(rawdata))
            return self._decode_packet(bytes(rawdata))

        @staticmethod
        def enumerate(vid, pid):
            """
            returns all the connected devices which matches PyWinUSB.vid/PyWinUSB.pid.
            returns an array of PyWinUSB (Interface) objects
            :param vid:
            :param pid:
            """
            all_devices = hid.find_all_hid_devices()

            # find devices with good vid/pid
            all_kboot_devices = []
            for d in all_devices:
                if (d.vendor_id == vid) and (d.product_id == pid):
                    all_kboot_devices.append(d)

            targets = []
            for dev in all_kboot_devices:
                try:
                    dev.open(shared=False)
                    report = dev.find_output_reports()
                    dev.close()

                    if report:
                        new_target = USBIF()
                        new_target.report = report
                        new_target.vendor_name = dev.vendor_name
                        new_target.product_name = dev.product_name
                        new_target.vid = dev.vendor_id
                        new_target.pid = dev.product_id
                        new_target.device = dev
                        new_target.device.set_raw_data_handler(new_target.__rx_handler)
                        targets.append(new_target)
                except Exception as e:
                    logging.error("Receiving Exception: %s", e)
                    dev.close()

            return targets

elif os.name == "posix":
    try:
        import usb.core
        import usb.util
    except:
        raise Exception("PyUSB is required on a Linux Machine")

    class USBIF(UsbHidBase):
        """
        This class provides basic functions to access
        a USB HID device using pyusb:
            - write/read an endpoint
        """

        vid = 0
        pid = 0
        intf_number = 0

        def __init__(self):
            super().__init__()
            self.ep_out = None
            self.ep_in = None
            self.dev = None
            self.closed = False

        def open(self):
            """ open the interface """
            logging.debug("Opening USB interface")

        def close(self):
            """ close the interface """
            logging.debug("Close USB Interface")
            self.closed = True
            try:
                if self.dev: usb.util.dispose_resources(self.dev)
            except:
                pass

        def write(self, id, data, size=36):
            """
            write data on the OUT endpoint associated to the HID interface
            """
            rawdata = self._encode_packet(id, data, size)
            logging.debug('USB-OUT[0x]: %s', atos(rawdata))

            if self.ep_out:
                self.ep_out.write(rawdata)
            else:
                bmRequestType = 0x21       #Host to device request of type Class of Recipient Interface
                bmRequest = 0x09           #Set_REPORT (HID class-specific request for transferring data over EP0)
                wValue = 0x200             #Issuing an OUT report
                wIndex = self.intf_number  #Interface number for HID
                self.dev.ctrl_transfer(bmRequestType, bmRequest, wValue + id, wIndex, rawdata)

        def read(self, timeout=1000):
            """
            read data on the IN endpoint associated to the HID interface
            """
            #rawdata = self.ep_in.read(self.ep_in.wMaxPacketSize, timeout)
            rawdata = self.ep_in.read(36, timeout)
            logging.debug('USB-IN [0x]: %s', atos(rawdata))
            return self._decode_packet(rawdata)

        @staticmethod
        def enumerate(vid, pid):
            """
            returns all the connected devices which matches PyUSB.vid/PyUSB.pid.
            returns an array of PyUSB (Interface) objects
            :param vid:
            :param pid:
            """
            # find all devices matching the vid/pid specified
            all_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)

            if not all_devices:
                logging.debug("No device connected")
                return None

            targets = []

            # iterate on all devices found
            for dev in all_devices:
                interface = None
                interface_number = -1

                # get active config
                config = dev.get_active_configuration()

                # iterate on all interfaces:
                for interface in config:
                    if interface.bInterfaceClass == 0x03: # HID Interface
                        interface_number = interface.bInterfaceNumber
                        break

                if interface is None or interface_number == -1:
                    continue

                try:
                    if dev.is_kernel_driver_active(interface_number):
                        dev.detach_kernel_driver(interface_number)
                except Exception as e:
                    print(str(e))

                try:
                    dev.set_configuration()
                    dev.reset()
                except usb.core.USBError as e:
                    print("Cannot set configuration the device: %s" % str(e))

                ep_in, ep_out = None, None
                for ep in interface:
                    if ep.bEndpointAddress & 0x80:
                        ep_in = ep
                    else:
                        ep_out = ep

                if usb.__version__ == '1.0.0b1':
                    vendor_name = usb.util.get_string(dev, 64, 1)
                    product_name = usb.util.get_string(dev, 64, 2)
                else:
                    vendor_name = usb.util.get_string(dev, 1)
                    product_name = usb.util.get_string(dev, 2)

                if not ep_in:
                    logging.error('Endpoints not found')
                    return None

                new_target = USBIF()
                new_target.ep_in = ep_in
                new_target.ep_out = ep_out
                new_target.dev = dev
                new_target.vid = vid
                new_target.pid = pid
                new_target.intf_number = interface_number
                new_target.vendor_name = vendor_name
                new_target.product_name = product_name
                targets.append(new_target)

            return targets

else:
    raise Exception("No USB backend found")

<|endoftext|>


<commit_before>#!/usr/bin/python

#
# Script: metrochrome.py
# Author: Michael Stockman
#
# Purpose: A command line based tool for exploration and manipulation of color
# values.
#

import sys

class InvalidColorError(Exception):
    def __init__(self):
        self.value = "Invalid color error"
    def __str__(self):
        return repr(self.value)

class RgbColor:
    """A color represented in RGB color space by values of red green and blue"""
    def __init__(self, red, green, blue):
        self.red = red
        self.green = green
        self.blue = blue
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "%i %i %i" % (self.red, self.green, self.blue)

    def parseString(self, red, green, blue):
        try:
            self.red = int(red)
            self.green = int(green)
            self.blue = int(blue)
        except:
            raise InvalidColorError()
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        r = self.red
        g = self.green
        b = self.blue
        if r < 0 or r > 255 or g < 0 or g > 255 or b < 0 or b > 255:
            return True
        else:
            return False

class RgbHexColor:
    def __init__(self, invalue):
        self.value = invalue
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "#%0.6X" % self.value

    def parseString(self, string):
        if string.startswith("#"):
            string = string[1:]
        if len(string) != 6:
            raise InvalidColorError()
        try:
            val = int(string, 16)
        except:
            raise InvalidColorError()
        self.value = val
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        if self.value < 0 or self.value > 16777215:
            return True
        else:
            return False

class CmykColor:
    def __init__(self, cyan, magenta, yellow, key):
        self.cyan = cyan
        self.magenta = magenta
        self.yellow = yellow
        self.key = key
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "%.1f %.1f %.1f %.1f" % (self.cyan, self.magenta, self.yellow, self.key)

    def parseString(self, cyan, magenta, yellow, key):
        try:
            self.cyan = float(cyan)
            self.magenta = float(magenta)
            self.yellow = float(yellow)
            self.key = float(key)
        except:
            raise InvalidColorError()
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        c = self.cyan
        m = self.magenta
        y = self.yellow
        k = self.key
        if c < 0.0 or c > 100.0 or m < 0.0 or m > 100.0 or y < 0.0 or y > 100.0 or k < 0.0 or k > 100.0:
            return True
        else:
            return False

class CmykRatioColor:
    def __init__(self, cyan, magenta, yellow, key):
        self.cyan = cyan / 100.0
        self.magenta = magenta / 100.0
        self.yellow = yellow / 100.0
        self.key = key / 100.0
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "%.3g %.3g %.3g %.3g" % (self.cyan, self.magenta, self.yellow, self.key)

    def parseString(self, cyan, magenta, yellow, key):
        try:
            self.cyan = float(cyan)
            self.magenta = float(magenta)
            self.yellow = float(yellow)
            self.key = float(key)
        except:
            raise InvalidColorError()
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        c = self.cyan
        m = self.magenta
        y = self.yellow
        k = self.key
        if c < 0.0 or c > 1.0 or m < 0.0 or m > 1.0 or y < 0.0 or y > 1.0 or k < 0.0 or k > 1.0:
            return True
        else:
            return False

class HsvColor:
    def __init__(self, hue, saturation, value):
        self.hue = hue
        self.saturation = saturation
        self.value = value
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "%.1f %.3f %.3f" % (self.hue, self.saturation, self.value)

    def parseString(self, hue, saturation, lightness):
        try:
            self.hue = float(hue)
            self.saturation = float(saturation)
            self.value = float(value)
        except:
            raise InvalidColorError()
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        h = self.hue
        s = self.saturation
        v = self.value
        if h < 0.0 or h > 360.0 or s < 0.0 or s > 1.0 or v < 0.0 or v > 1.0:
            return True
        else:
            return False

def printHelp():
    print("""
metrochrome.py - a command line tool for exploring color

++ Convert color representations from one color space to another: RGB, RGB (hexadecimal), CMYK, etc.
++ Find variations on a color tint, hue, shade

    Usage Options
--------------------------------

* Display this help screen *
    metrochrome.py -help   OR   metrochrome.py -h

* Convert color space *
    metrochrome.py <in_color_space> <in_color> <out_color_space>

    color space flags:
    -rgb = RGB color space written as three numbers 0-255 separted by spaces
    -rgbh = RGB in hexadecimal format with a leading hash mark (e.g. #00FF30)
    -cmyk = CMYK color space 4 space separated percentages from 0 to 100 such as 0 100 95.5 82.7
    -cmykr = CMYK in percent format 4 space separated ratio values 0 to 1 such as 0 1 0.955 0.827

    examples:
    metrochrome.py -rgb 0 0 0 -cmyk        # converts RGB to CMYK and prints (0, 0, 0, 1)
    metrochrome.py -rgb 255 255 255 -rgbh  # converts RGB to hexadecimal and prints #FFFFFF
    metrochrome.py -cmyk (0, 1, 0.955, 0.827) -rgb  # converts CMYK to RGB and prints #FFFFFF
""")

def exitWithError():
    """If the inputs are unusable explain how to print the help screen"""
    print("Unusable parameters...\nRun 'python metrochrome.py -help' to display the help screen.")
    sys.exit(1)

def RGB_to_RGBhex(inputRgb):
    total = inputRgb.red*65536 + inputRgb.green*256 + inputRgb.blue
    return RgbHexColor(total)

def RGB_to_CMYK(inputRgb):
    redRatio = inputRgb.red / 255.0
    greenRatio = inputRgb.green / 255.0
    blueRatio = inputRgb.blue / 255.0

    key = 1.0 - max(redRatio, greenRatio, blueRatio)

    if key == 1.0:
        cyan = 0.0
        magenta = 0.0
        yellow = 0.0
    else:
        cyan = (1.0 - redRatio - key) / (1.0 - key)
        magenta = (1.0 - greenRatio - key) / (1.0 - key)
        yellow = (1.0 - blueRatio - key) / (1.0 - key)

    return CmykColor(100.0*cyan, 100.0*magenta, 100.0*yellow, 100.0*key)

def RGB_to_CMYKratio(rgb):
    cmyk = RGB_to_CMYK(rgb)
    return CMYK_to_CMYKratio(cmyk)

def RGB_to_HSV(rgb):
    red = rgb.red / 255.0
    green = rgb.green / 255.0
    blue = rgb.blue / 255.0

    m = min(red, green, blue)
    M = max(red, green, blue)

    value = M
    chroma = M - m

    if value != 0.0:
        saturation = chroma/M
    else:
        saturation = 0.0
        hue = 0.0
        return HsvColor(hue, saturation, value)

    if chroma == 0:
        hue = 0.0
    elif red == M:
        hue = ((green - blue) / chroma) % 6
    elif green == M:
        hue = ((blue - red) / chroma) + 2
    elif blue == M:
		hue = ((red - green) / chroma) + 4;

    hue *= 60.0
    #if hue < 0.0:
    #    hue += 360.0;

    return HsvColor(hue, saturation, value)

def RGBhex_to_RGB(rgbHex):

    initial = int(rgbHex.value)
    red = initial / 65536
    green = (initial-red*65536) / 256
    blue = (initial-red*65536-green*256)

    return RgbColor(red, green, blue)

def RGBhex_to_CMYK(rgbHex):
    rgb = RGBhex_to_RGB(rgbHex)
    return RGB_to_CMYK(rgb)

def RGBhex_to_CMYKratio(rgbHex):
    rgb = RGBhex_to_RGB(rgbHex)
    return RGB_to_CMYKratio(rgb)

def CMYK_to_RGB(cmyk):
    cyanDiv = cmyk.cyan / 100.0
    magentaDiv = cmyk.magenta / 100.0
    yellowDiv = cmyk.yellow / 100.0
    keyDiv = cmyk.key / 100.0

    redRatio = -1 * ((cyanDiv * (1.0 - keyDiv)) - (1.0 - keyDiv))
    greenRatio = -1 * ((magentaDiv * (1.0 - keyDiv)) - (1.0 - keyDiv))
    blueRatio = -1 * ((yellowDiv * (1.0 - keyDiv)) - (1.0 - keyDiv))

    red = int(redRatio * 255)
    green = int(greenRatio * 255)
    blue = int(blueRatio * 255)

    return RgbColor(red, green, blue)

def CMYK_to_RGBhex(cmyk):
    rgb = CMYK_to_RGB(cmyk)
    return RGB_to_RGBhex(rgb)

def CMYK_to_CMYKratio(cmyk):
    return CmykRatioColor(cmyk.cyan, cmyk.magenta, cmyk.yellow, cmyk.key)

def CMYKratio_to_RGB(cmykr):
    cmyk = CMYKratio_to_CMYK(cmykr)
    return CMYK_to_RGB(cmyk)

def CMYKratio_to_RGBhex(cmykr):
    cmyk = CMYKratio_to_CMYK(cmykr)
    return CMYK_to_RGBhex(cmyk)

def CMYKratio_to_CMYK(cmykr):
    cyan = cmykr.cyan * 100.0
    magenta = cmykr.magenta * 100.0
    yellow = cmykr.yellow * 100.0
    key = cmykr.key * 100.0

    return CmykColor(cyan, magenta, yellow, key)

def main():

    #print("argv: " + str(sys.argv))

    if len(sys.argv) == 2 and (sys.argv[1] == "-h" or sys.argv[1] == "-help"):
        printHelp()

    elif len(sys.argv) == 6 and sys.argv[1] == "-rgb":
        try:
            rgb = RgbColor(0,0,0)
            rgb.parseString(sys.argv[2], sys.argv[3], sys.argv[4])
        except InvalidColorError:
            exitWithError()

        if sys.argv[5] == "-rgbh":
            print(RGB_to_RGBhex(rgb))
        elif sys.argv[5] == "-cmyk":
            print(RGB_to_CMYK(rgb))
        elif sys.argv[5] == "-cmykr":
            print(RGB_to_CMYKratio(rgb))
        elif sys.argv[5] == "-hsv":
            print(RGB_to_HSV(rgb))
        else:
            exitWithError()

    elif len(sys.argv) == 4 and sys.argv[1] == "-rgbh":
        try:
            rgbHex = RgbHexColor(0)
            rgbHex.parseString(sys.argv[2])
        except InvalidColorError:
            exitWithError()

        if sys.argv[3] == "-rgb":
            print(RGBhex_to_RGB(rgbHex))
        elif sys.argv[3] == "-cmyk":
            print(RGBhex_to_CMYK(rgbHex))
        elif sys.argv[3] == "-cmykr":
            print(RGBhex_to_CMYKratio(rgbHex))
        else:
            exitWithError()

    elif len(sys.argv) == 7 and sys.argv[1] == "-cmyk":
        try:
            cmyk = CmykColor(0,0,0,0)
            cmyk.parseString(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
        except InvalidColorError:
            exitWithError()

        if sys.argv[6] == "-rgb":
            print(CMYK_to_RGB(cmyk))
        elif sys.argv[6] == "-rgbh":
            print(CMYK_to_RGBhex(cmyk))
        elif sys.argv[6] == "-cmykr":
            print(CMYK_to_CMYKratio(cmyk))
        else:
            exitWithError()

    elif len(sys.argv) == 7 and sys.argv[1] == "-cmykr":
        try:
            cmykr = CmykRatioColor(0,0,0,0)
            cmykr.parseString(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
        except InvalidColorError:
            exitWithError()

        if sys.argv[6] == "-rgb":
            print(CMYKratio_to_RGB(cmykr))
        elif sys.argv[6] == "-rgbh":
            print(CMYKratio_to_RGBhex(cmykr))
        elif sys.argv[6] == "-cmyk":
            print(CMYKratio_to_CMYK(cmykr))
        else:
            exitWithError()

    else:
        exitWithError()

if __name__ == "__main__":
    main()
<commit_msg>Added HSV conversion functions<commit_after>#!/usr/bin/python

#
# Script: metrochrome.py
# Author: Michael Stockman
#
# Purpose: A command line based tool for exploration and manipulation of color
# values.
#

import sys

class InvalidColorError(Exception):
    def __init__(self):
        self.value = "Invalid color error"
    def __str__(self):
        return repr(self.value)

class RgbColor:
    """A color represented in RGB color space by values of red green and blue"""
    def __init__(self, red, green, blue):
        self.red = red
        self.green = green
        self.blue = blue
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "%i %i %i" % (self.red, self.green, self.blue)

    def parseString(self, red, green, blue):
        try:
            self.red = int(red)
            self.green = int(green)
            self.blue = int(blue)
        except:
            raise InvalidColorError()
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        r = self.red
        g = self.green
        b = self.blue
        if r < 0 or r > 255 or g < 0 or g > 255 or b < 0 or b > 255:
            return True
        else:
            return False

class RgbHexColor:
    def __init__(self, invalue):
        self.value = invalue
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "#%0.6X" % self.value

    def parseString(self, string):
        if string.startswith("#"):
            string = string[1:]
        if len(string) != 6:
            raise InvalidColorError()
        try:
            val = int(string, 16)
        except:
            raise InvalidColorError()
        self.value = val
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        if self.value < 0 or self.value > 16777215:
            return True
        else:
            return False

class CmykColor:
    def __init__(self, cyan, magenta, yellow, key):
        self.cyan = cyan
        self.magenta = magenta
        self.yellow = yellow
        self.key = key
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "%.1f %.1f %.1f %.1f" % (self.cyan, self.magenta, self.yellow, self.key)

    def parseString(self, cyan, magenta, yellow, key):
        try:
            self.cyan = float(cyan)
            self.magenta = float(magenta)
            self.yellow = float(yellow)
            self.key = float(key)
        except:
            raise InvalidColorError()
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        c = self.cyan
        m = self.magenta
        y = self.yellow
        k = self.key
        if c < 0.0 or c > 100.0 or m < 0.0 or m > 100.0 or y < 0.0 or y > 100.0 or k < 0.0 or k > 100.0:
            return True
        else:
            return False

class CmykRatioColor:
    def __init__(self, cyan, magenta, yellow, key):
        self.cyan = cyan / 100.0
        self.magenta = magenta / 100.0
        self.yellow = yellow / 100.0
        self.key = key / 100.0
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "%.3g %.3g %.3g %.3g" % (self.cyan, self.magenta, self.yellow, self.key)

    def parseString(self, cyan, magenta, yellow, key):
        try:
            self.cyan = float(cyan)
            self.magenta = float(magenta)
            self.yellow = float(yellow)
            self.key = float(key)
        except:
            raise InvalidColorError()
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        c = self.cyan
        m = self.magenta
        y = self.yellow
        k = self.key
        if c < 0.0 or c > 1.0 or m < 0.0 or m > 1.0 or y < 0.0 or y > 1.0 or k < 0.0 or k > 1.0:
            return True
        else:
            return False

class HsvColor:
    def __init__(self, hue, saturation, value):
        self.hue = hue
        self.saturation = saturation
        self.value = value
        if self.invalid():
            raise InvalidColorError()

    def __str__(self):
        return "%.1f %.3f %.3f" % (self.hue, self.saturation, self.value)

    def parseString(self, hue, saturation, lightness):
        try:
            self.hue = float(hue)
            self.saturation = float(saturation)
            self.value = float(value)
        except:
            raise InvalidColorError()
        if self.invalid():
            raise InvalidColorError()

    def invalid(self):
        h = self.hue
        s = self.saturation
        v = self.value
        if h < 0.0 or h > 360.0 or s < 0.0 or s > 1.0 or v < 0.0 or v > 1.0:
            return True
        else:
            return False

def printHelp():
    print("""
metrochrome.py - a command line tool for exploring color

++ Convert color representations from one color space to another: RGB, RGB (hexadecimal), CMYK, etc.
++ Find variations on a color tint, hue, shade

    Usage Options
--------------------------------

* Display this help screen *
    metrochrome.py -help   OR   metrochrome.py -h

* Convert color space *
    metrochrome.py <in_color_space> <in_color> <out_color_space>

    color space flags:
    -rgb = RGB color space written as three numbers 0-255 separted by spaces
    -rgbh = RGB in hexadecimal format with a leading hash mark (e.g. #00FF30)
    -cmyk = CMYK color space 4 space separated percentages from 0 to 100 such as 0 100 95.5 82.7
    -cmykr = CMYK in percent format 4 space separated ratio values 0 to 1 such as 0 1 0.955 0.827

    examples:
    metrochrome.py -rgb 0 0 0 -cmyk        # converts RGB to CMYK and prints (0, 0, 0, 1)
    metrochrome.py -rgb 255 255 255 -rgbh  # converts RGB to hexadecimal and prints #FFFFFF
    metrochrome.py -cmyk (0, 1, 0.955, 0.827) -rgb  # converts CMYK to RGB and prints #FFFFFF
""")

def exitWithError():
    """If the inputs are unusable explain how to print the help screen"""
    print("Unusable parameters...\nRun 'python metrochrome.py -help' to display the help screen.")
    sys.exit(1)

def RGB_to_RGBhex(inputRgb):
    total = inputRgb.red*65536 + inputRgb.green*256 + inputRgb.blue
    return RgbHexColor(total)

def RGB_to_CMYK(inputRgb):
    redRatio = inputRgb.red / 255.0
    greenRatio = inputRgb.green / 255.0
    blueRatio = inputRgb.blue / 255.0

    key = 1.0 - max(redRatio, greenRatio, blueRatio)

    if key == 1.0:
        cyan = 0.0
        magenta = 0.0
        yellow = 0.0
    else:
        cyan = (1.0 - redRatio - key) / (1.0 - key)
        magenta = (1.0 - greenRatio - key) / (1.0 - key)
        yellow = (1.0 - blueRatio - key) / (1.0 - key)

    return CmykColor(100.0*cyan, 100.0*magenta, 100.0*yellow, 100.0*key)

def RGB_to_CMYKratio(rgb):
    cmyk = RGB_to_CMYK(rgb)
    return CMYK_to_CMYKratio(cmyk)

def RGB_to_HSV(rgb):
    red = rgb.red / 255.0
    green = rgb.green / 255.0
    blue = rgb.blue / 255.0

    m = min(red, green, blue)
    M = max(red, green, blue)

    value = M
    chroma = M - m

    if value != 0.0:
        saturation = chroma/M
    else:
        saturation = 0.0
        hue = 0.0
        return HsvColor(hue, saturation, value)

    if chroma == 0:
        hue = 0.0
    elif red == M:
        hue = ((green - blue) / chroma) % 6
    elif green == M:
        hue = ((blue - red) / chroma) + 2
    elif blue == M:
		hue = ((red - green) / chroma) + 4;

    hue *= 60.0

    return HsvColor(hue, saturation, value)

def RGBhex_to_RGB(rgbHex):

    initial = int(rgbHex.value)
    red = initial / 65536
    green = (initial-red*65536) / 256
    blue = (initial-red*65536-green*256)

    return RgbColor(red, green, blue)

def RGBhex_to_CMYK(rgbHex):
    rgb = RGBhex_to_RGB(rgbHex)
    return RGB_to_CMYK(rgb)

def RGBhex_to_CMYKratio(rgbHex):
    rgb = RGBhex_to_RGB(rgbHex)
    return RGB_to_CMYKratio(rgb)

def RGBhex_to_HSV(rgbHex):
    rgb = RGBhex_to_RGB(rgbHex)
    return RGB_to_HSV(rgb)

def CMYK_to_RGB(cmyk):
    cyanDiv = cmyk.cyan / 100.0
    magentaDiv = cmyk.magenta / 100.0
    yellowDiv = cmyk.yellow / 100.0
    keyDiv = cmyk.key / 100.0

    redRatio = -1 * ((cyanDiv * (1.0 - keyDiv)) - (1.0 - keyDiv))
    greenRatio = -1 * ((magentaDiv * (1.0 - keyDiv)) - (1.0 - keyDiv))
    blueRatio = -1 * ((yellowDiv * (1.0 - keyDiv)) - (1.0 - keyDiv))

    red = int(redRatio * 255)
    green = int(greenRatio * 255)
    blue = int(blueRatio * 255)

    return RgbColor(red, green, blue)

def CMYK_to_RGBhex(cmyk):
    rgb = CMYK_to_RGB(cmyk)
    return RGB_to_RGBhex(rgb)

def CMYK_to_CMYKratio(cmyk):
    return CmykRatioColor(cmyk.cyan, cmyk.magenta, cmyk.yellow, cmyk.key)

def CMYK_to_HSV(cmyk):
    rgb = CMYK_to_RGB(cmyk)
    return RGB_to_HSV(rgb)

def CMYKratio_to_RGB(cmykr):
    cmyk = CMYKratio_to_CMYK(cmykr)
    return CMYK_to_RGB(cmyk)

def CMYKratio_to_RGBhex(cmykr):
    cmyk = CMYKratio_to_CMYK(cmykr)
    return CMYK_to_RGBhex(cmyk)

def CMYKratio_to_CMYK(cmykr):
    cyan = cmykr.cyan * 100.0
    magenta = cmykr.magenta * 100.0
    yellow = cmykr.yellow * 100.0
    key = cmykr.key * 100.0

    return CmykColor(cyan, magenta, yellow, key)

def CMYKratio_to_HSV(cmykr):
    rgb = CMYKratio_to_RGB(cmykr)
    return RGB_to_HSV(rgb)

def main():

    #print("argv: " + str(sys.argv))

    if len(sys.argv) == 2 and (sys.argv[1] == "-h" or sys.argv[1] == "-help"):
        printHelp()

    elif len(sys.argv) == 6 and sys.argv[1] == "-rgb":
        try:
            rgb = RgbColor(0,0,0)
            rgb.parseString(sys.argv[2], sys.argv[3], sys.argv[4])
        except InvalidColorError:
            exitWithError()

        if sys.argv[5] == "-rgbh":
            print(RGB_to_RGBhex(rgb))
        elif sys.argv[5] == "-cmyk":
            print(RGB_to_CMYK(rgb))
        elif sys.argv[5] == "-cmykr":
            print(RGB_to_CMYKratio(rgb))
        elif sys.argv[5] == "-hsv":
            print(RGB_to_HSV(rgb))
        else:
            exitWithError()

    elif len(sys.argv) == 4 and sys.argv[1] == "-rgbh":
        try:
            rgbHex = RgbHexColor(0)
            rgbHex.parseString(sys.argv[2])
        except InvalidColorError:
            exitWithError()

        if sys.argv[3] == "-rgb":
            print(RGBhex_to_RGB(rgbHex))
        elif sys.argv[3] == "-cmyk":
            print(RGBhex_to_CMYK(rgbHex))
        elif sys.argv[3] == "-cmykr":
            print(RGBhex_to_CMYKratio(rgbHex))
        elif sys.argv[3] == "-hsv":
            print(RGBhex_to_HSV(rgbHex))
        else:
            exitWithError()

    elif len(sys.argv) == 7 and sys.argv[1] == "-cmyk":
        try:
            cmyk = CmykColor(0,0,0,0)
            cmyk.parseString(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
        except InvalidColorError:
            exitWithError()

        if sys.argv[6] == "-rgb":
            print(CMYK_to_RGB(cmyk))
        elif sys.argv[6] == "-rgbh":
            print(CMYK_to_RGBhex(cmyk))
        elif sys.argv[6] == "-cmykr":
            print(CMYK_to_CMYKratio(cmyk))
        elif sys.argv[6] == "-hsv":
            print(CMYK_to_HSV(cmyk))
        else:
            exitWithError()

    elif len(sys.argv) == 7 and sys.argv[1] == "-cmykr":
        try:
            cmykr = CmykRatioColor(0,0,0,0)
            cmykr.parseString(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
        except InvalidColorError:
            exitWithError()

        if sys.argv[6] == "-rgb":
            print(CMYKratio_to_RGB(cmykr))
        elif sys.argv[6] == "-rgbh":
            print(CMYKratio_to_RGBhex(cmykr))
        elif sys.argv[6] == "-cmyk":
            print(CMYKratio_to_CMYK(cmykr))
        elif sys.argv[6] == "-hsv":
            print(CMYKratio_to_HSV(cmykr))
        else:
            exitWithError()

    else:
        exitWithError()

if __name__ == "__main__":
    main()
<|endoftext|>