<gh_stars>1-10
##############################
# Main method for feedback generation given a file annotated with medical entities
#
# by <NAME> (based on code by <NAME>)
# October 2018
##############################

import os
import pandas as pd
import pickle
import re
import csv
from nltk.tokenize import RegexpTokenizer


# Define paths
FEEDBACK_DATA = "util/automatic_feedback/Feedback_creator/"
FEEDBACK_DATA = "../../util/automatic_feedback/Feedback_creator/"
FEEDBACK_FILE = FEEDBACK_DATA + "AdaptivesFeedback_neu.xls"
RESULTS_PATH = "feedbackResults/"


# sheet_id maps the sheet names to case IDs
sheet_id = {"<NAME>_b<NAME>": "1", "<NAME>": "2", "<NAME>_<NAME>": "3", "<NAME>_Hyperthyreosis fact.":"4",
            "<NAME>_Spinalkanalstenose": "5", "<NAME>_ostp. WK#":"6", "<NAME>_<NAME>.":"7", "<NAME>_EBV":"8"}

class Feedback_creator:
    """
    This Module realizes the generation of feedback for a given student essay
    """
    def __init__(self, entities:dict, name:str, caseId, essay):
        """
        creates an instance of the feedback creator class
        Reads in the Excel file, that maps the mentioning / not mentioning of terms to feedback that should be given
        """

        self.essay_name = name
        self.file_path = RESULTS_PATH + name
        self.caseId = caseId
        self.essay = essay
        self.entities = entities

        if not os.path.exists(self.file_path):
            os.makedirs(self.file_path)

        # save the given essay in a file
        with open(self.file_path + "/" + self.essay_name + ".txt", 'w') as essaytext:
            essaytext.write(self.essay)

        # case_feedback maps case IDs to lists of triples of the form (main_terms, synonyms, feedback)
        if os.path.isfile(FEEDBACK_DATA + "feedback_dict"):
            self.case_feedback = pickle.load(open(FEEDBACK_DATA + "feedback_dict", "rb"))
        else:
            self.case_feedback = dict()

            # read from Excel file
            xl = pd.ExcelFile(FEEDBACK_FILE)
            for sheet_name in sheet_id.keys():
                self.case_feedback[sheet_id[sheet_name]] = []
                xl_case = xl.parse(sheet_name)

                # check whether header of the file is correct
                header = xl_case.columns
                if not header[0] == "Katergorie" and header[1] == "Fachbegriff" and header[2] == "Synonyme" and header[3] == "Feedback":
                    print("Error - header of feedback changed!")
                    exit()

                # parse data into dictionary; ".values" converts to numpy matrix
                for row in xl_case.values:
                    # collect relevant terms but correct the inconsistency that terms are separated by either comma, semicolon or linebreaks
                    # An inconsistency we cannot correct is when terms are not separated at all, as in "NOT Anitibiothikaresistenz NOT Resistenz"
                    # different options are separated by "&&"
                    terms_main = [sorted([term.strip() for term in option.replace(",", ";").replace("\n", ";").split(";")
                                          if term.strip()]) for option in row[1].split("&&")] if not row[1].strip() == "andere" else [[]]

                    terms_synonyms = [sorted([term.strip() for term in option.replace(",", ";").replace("\n", ";").split(";")
                                          if term.strip()]) for option in row[2].split("&&")]  if not type(row[2]) == float else [[] for x in terms_main]

                    # there should be synonyms for each option
                    if len(terms_main) >1 and len(terms_main) != len(terms_synonyms):
                        print("not the same number of main and synonym options!")
                        print(sheet_name)
                        print(row)

                    self.case_feedback[sheet_id[sheet_name]].append(((terms_main, terms_synonyms, row[3], row[0])))
            pickle.dump(self.case_feedback, open(FEEDBACK_DATA + "feedback_dict", "wb"))

        # create CSV file to save feedback stats if it doesn't exist yet for this case
        if not os.path.isfile(RESULTS_PATH + "stats_case" + self.caseId + ".csv"):
            with open(RESULTS_PATH + "stats_case" + self.caseId + ".csv", "w") as f:
                writer = csv.writer(f, delimiter=",")
                header = ["studentId"]
                for i, x in enumerate(self.case_feedback[self.caseId]):
                    header.append(str(i) + " feedback given")
                    header.append(str(i) + " specified terms")
                    header.append(str(i) + " found terms")
                writer.writerow(header)



    def generate_feedback(self, methods:list, gazetteers:list):
        """
        Generates feedback according to the medical entities found in the essay with the specified methods and gazetteers
        Feedback can be found at FEEDBACK_PATH
        :param methods: list of methods used for annotation
        :param gazetteers: list of gazetteers that should be taken into account
        """
        # all Feedback transmitted to CASUS is saved here
        feedbackList = []
        feedbackCount = 0

        # will be printed in CSV feedback stats file with 1st column the essayID
        # then for each feedback there are 3 columns:
        # 0 / 1 (feedback given / not given)
        # specified terms (main term or synonym associated with found term)
        # found terms
        # specified and found terms of multiple conditions are seperated with "&",
        # various found terms or specified synonyms with ";"
        feedbackStats = [self.essay_name]
        # count of the feedback (= line in feedback table)
        statscount = 0

        # save feedback to a file
        with open(self.file_path + "/" + self.essay_name.split(".")[0] + "_feedback.txt", "w") as feedback_f:

            tokenizer = RegexpTokenizer(
                r'[\w-]+')  # finds all strings with alphanumerical characters or with '-' or '_' (the latter implicitly through \w)
            token_offsets = list(tokenizer.span_tokenize(self.essay))

            # check for each feedback whether it fulfills the constraints (terms existing or missing)
            for (terms_main, terms_synonyms, feedback,feedbackType) in self.case_feedback[self.caseId]:

                fulfilled, useful_terms_main_advanced, useful_terms_synonyms_advanced = \
                    self.check_conditions(terms_main, terms_synonyms, methods, gazetteers)

                #print("terms: " + str(useful_terms_main_advanced))
                #print("syn: " + str(useful_terms_synonyms_advanced))

                # if all conditions are fulfilled (e.g. MRT & NOT Roentgen are two conditions)
                if all(x == True for x in fulfilled):
                    #print("fulfilled")

                    # each fulfilled feedback will be transmitted to CASUS with the following information:
                    # (feedbackID, set of offsets of found terms, set of associated strings, feedback text)
                    # feedbackID is simply a count of all given feedbacks
                    feedback_entry = (feedbackCount,[],[],feedback,feedbackType)

                    # record that feedback was given in feedback stats
                    feedbackStats.append("1")
                    feedbackStats.append("")
                    feedbackStats.append("")

                    # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options)
                    # a term made of various words is still one option, e.g. Hepatitis Serologie
                    for i, option in enumerate(terms_main):
                        #print("Option " + str(i) + ": " + ''.join(option))

                        # the last feedback often has "andere" (others) as the main term and all possible terms as synonyms
                        # in this case, the terms_main contains only the empty set, so the only option is empty

                        # if the option is a negated term (so the term was not found in the essay),
                        #  record the negated term and all its synonyms that were not found in the essay in the feedback file
                        if len(option) != 0 and "NOT" in option[0]:
                            feedback_f.write("\nTRIGGER: None of the following terms detected\n")
                            feedback_f.writelines(sorted([t.strip()+",  " for t in (option + terms_synonyms[i])]))

                            # record reason for feedback in feedback stats as the main term that was not found
                            if feedbackStats[statscount * 3 + 2] == "":
                                feedbackStats[statscount * 3 + 2] = ''.join(option)
                            # if this is not the first option
                            else:
                                feedbackStats[statscount * 3 + 2] += " & " + ''.join(option)

                        # if the option is a positive term (and the term or a synonym was found)
                        else:
                            feedback_f.write("\nTRIGGER: Detection of the following terms\n")
                            feedbackString = ""

                            # if the main term is detected, report only this, else report detected synonyms
                            l = useful_terms_main_advanced[i] if len(useful_terms_main_advanced[i]) != 0 \
                                else useful_terms_synonyms_advanced[i]
                            syn = False if len(useful_terms_main_advanced[i]) != 0 else True

                            # for each option, various terms may have been found (especially regarding synonyms)
                            for found, targetTerm in l:
                                same = False
                                # various words in the text may be associated with a targetTerm, they are listed in found
                                # e.g. ((8, 'körperlicher'), (9, 'untersuchung')) or if only one word ((14, 'labors'),)
                                foundlist = list(found)
                                tokenstring = ""
                                indexlist = []
                                # get the complete found string and associated word indices (indices of tokenized text)
                                for index, token in foundlist:
                                    if tokenstring == "":
                                        tokenstring = token
                                    else:
                                        tokenstring = tokenstring + " " + token
                                    indexlist.append(index)

                                # get the start and end character offsets of the found words
                                startchars = token_offsets[indexlist[0]]
                                endchars = token_offsets[indexlist[-1]]
                                start = startchars[0]
                                end = endchars[1]

                                # add the offsets and the found term to the feedback to be transmitted to CASUS
                                # various synonyms may be associated with the same word in the text
                                # in this case don't add the term twice to the list of words that triggered the feedback
                                if not tokenstring in feedback_entry[2]:
                                    feedback_entry[1].append((start,end))
                                    feedback_entry[2].append(tokenstring)

                                # if the found term matches exactly the main term (or synonym)
                                # this is recorded with a different wording in the feedback file
                                if tokenstring.lower() == targetTerm.lower():
                                    text_feedback =  " wurde im Text gefunden an Position " + str(indexlist) +\
                                                     " (" + str(start) + "-" + str(end) + ")"
                                else:
                                    text_feedback = " " + str(indexlist) + " (" + str(start) + "-" + str(end) + \
                                                     ") im Text wurde assoziiert mit " + targetTerm

                                # various found terms in the essay (e.g. of various synonyms) are separated by ";"
                                pre = "" if feedbackString == "" else "; "
                                feedbackString = feedbackString + pre + tokenstring + text_feedback

                                # option == [] means that the main term is "andere" and the "synonyms" are simply a list of
                                # terms such that if found the feedback is triggered (but we do not wan to call them "synonyms" of andere)
                                if option != [] and syn:
                                    feedbackString = feedbackString + " und ist ein Synonym von " +  ''.join(option)

                                # record given term and found term for feedback stats
                                if feedbackStats[statscount * 3 + 2] == "":
                                    feedbackStats[statscount * 3 + 2] = targetTerm
                                    feedbackStats[statscount * 3 + 3] = tokenstring
                                elif pre == "":
                                    feedbackStats[statscount * 3 + 2] += "; " + targetTerm
                                    feedbackStats[statscount * 3 + 3] += "; " + tokenstring
                                else:
                                    feedbackStats[statscount * 3 + 2] += " & " + targetTerm
                                    feedbackStats[statscount * 3 + 3] += " & " + tokenstring

                            feedback_f.write(feedbackString)

                    feedback_f.write("\n\nFEEDBACK:\n")
                    feedback_f.write(feedback+"\n\n")
                    feedback_f.write("_____________________________________________\n")
                    feedbackList.append(feedback_entry)
                    feedbackCount+=1

                # if the feedback is not given since conditions not satisfied
                # this is only recorded in feedback stats - no feedback recorded in feedback file or given to CASUS
                else:
                    feedbackStats.append("0")
                    feedbackStats.append("")
                    feedbackStats.append("")

                    # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options)
                    # a term made of various words is still one option, e.g. Hepatitis Serologie
                    for i, option in enumerate(terms_main):

                        # if this is the "andere" row (thus none of the specified terms was found)
                        # record "andere" as main term (and nothing in the found column)
                        if len(option) == 0:
                            feedbackStats[statscount * 3 + 2] = "andere"

                        else:
                            neg = True if "NOT" in option[0] else False

                            # if no terms for this option found in the essay, i.e.
                            # a) term is negative and satisfied (so not found in essay)
                            # b) term is positive and not satisfied (so not found in essay)
                            # record the main term (and nothing in the found column)
                            # NOTE: satisfied option is possible if various options and one of the others is not satisfied
                            if (fulfilled[i] == True and neg == True) or (fulfilled[i] == False and neg == False):
                                if feedbackStats[statscount * 3 + 2] == "":
                                    feedbackStats[statscount * 3 + 2] = ''.join(option)
                                else:
                                    feedbackStats[statscount * 3 + 2] += " & " + ''.join(option)

                            # if terms for this option found in the essay
                            # record both the given term and the found version of it
                            else:
                                l = useful_terms_main_advanced[i] if len(useful_terms_main_advanced[i]) != 0 \
                                    else useful_terms_synonyms_advanced[i]
                                for x, (found, targetTerm) in enumerate(l):
                                    foundlist = list(found)
                                    tokenstring = ""
                                    for index, token in foundlist:
                                        if tokenstring == "":
                                            tokenstring = token
                                        else:
                                            tokenstring = tokenstring + " " + token

                                    if feedbackStats[statscount * 3 + 2] == "":
                                        feedbackStats[statscount * 3 + 2] = targetTerm
                                        feedbackStats[statscount * 3 + 3] = tokenstring
                                    # if this is not the first found term and the list isn't empty
                                    elif  x > 0:
                                        feedbackStats[statscount * 3 + 2] += "; " + targetTerm
                                        feedbackStats[statscount * 3 + 3] += "; " + tokenstring
                                    # if this is the first found term and the list isn't empty, i.e. another option has
                                    # been recorded before
                                    else:
                                        feedbackStats[statscount * 3 + 2] += " & " + targetTerm
                                        feedbackStats[statscount * 3 + 3] += " & " + tokenstring

                statscount += 1
            #print(feedbackStats)

            # write the feedback stats to the stats CSV file
            with open(RESULTS_PATH + "stats_case" + self.caseId + ".csv") as inf, open(RESULTS_PATH + "stats_caseNew" + self.caseId + ".csv", 'w') as outf:
                reader = csv.reader(inf, delimiter = ",")
                writer = csv.writer(outf, delimiter = ",")
                # check if the essay name already exists, i.e. if a user submitted a solution for this case before
                # if so, the old stats are replaced with the new
                found = False
                for line in reader:
                    if line[0] == self.essay_name:
                        writer.writerow(feedbackStats)
                        found = True
                    else:
                        writer.writerow(line)
                if not found:
                    writer.writerow(feedbackStats)

            os.remove(RESULTS_PATH + "stats_case" + self.caseId + ".csv")
            os.rename(RESULTS_PATH + "stats_caseNew" + self.caseId + ".csv", RESULTS_PATH + "stats_case" + self.caseId + ".csv")

        return feedbackList



    def check_conditions(self,terms_main: list, terms_synonyms: list, methods: list, gazetteers: list):
        """
        checks for a set of feedback conditions whether at least one is fulfilled, w.r.t. an essay
        conditions are the existence or non-existence of certain terms or a collection of terms

        :param terms_main: The main technical terms that trigger the feedback
        :param terms_synonyms: List of synonyms of the main technical terms
        :param methods: methods of which annotations should be combined
        :param gazetteers: gazetteers which should be taken into account. Corresponds to all gazetteers if list is empty.
        :return: Tuple of the form (fulfilled:list of bool, terms_found_main_advanced:list, terms_found_synonyms_advanced:list)
                 fulfilled indicates for each condition whether it is fulfilled
                 the two lists contain the terms that appeared in the essay and are associated with the main terms of synonyms
        """

        # merge the annotations of the specified methods and gazetteers
        # NOTE: right now we are only using one method and gazetteer! however the merge also changes the format, so we currently need it
        # can be used in future if we wanted to use various methods and/or gazetteers
        new_method_name = ("_".join(sorted(methods)) + "_" + "_".join(sorted(gazetteers))).split(".")[0]
        found_entities_extended = self.merge_annotations(methods, gazetteers, new_method_name)

        # get all tokens of the original essay
        tokens, matching_information = found_entities_extended[new_method_name]

        fulfilled = [False for x in terms_main]
        useful_terms_main_advanced = [set()for x in terms_main]
        useful_terms_synonyms_advanced = [set()for x in terms_main]

        # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options)
        # a term made of various words is still one option, e.g. Hepatitis Serologie
        for i, option in enumerate(terms_main):

            # it is important to distinguish between terms that should be missing or existing for feedback
            # because when the condition is missing terms, all of them must miss.
            # when a term must exist, at least one of them (=main + synonyms) must exist
            mode = "missing" if len(option) != 0 and "NOT" in option[0] else "existing"

            # keep track whether a matching term was found
            detected = False

            # process every main term and synonym
            for terms, term_type in zip([option, terms_synonyms[i]], ["main", "synonyms"]):
                for term in terms:

                    # try to find the term with the help of gazetteers and advanced matching methods
                    # other entries are "essay_n_gram, gaz_idx, gazetteer_filename"
                    for essay_n_gram, gazetteer_entry, ___, ____ in matching_information:
                        if gazetteer_entry.lower() == term.replace("NOT", "").strip().lower():
                            detected = True
                            # add the found words and their indices (an n-gram tuple) and the term or synonym (including NOT) it matches
                            # essay_n_gram is e.g.: ((8, 'körperlicher'), (9, 'untersuchung')) or if only one word ((14, 'labors'),)
                            useful_terms_main_advanced[i].add(
                                (essay_n_gram, term)) if term_type == "main" else useful_terms_synonyms_advanced[i].add((essay_n_gram, term))

            # check whether the condition is fulfilled, i.e. NOT TERM and none found or TERM and some found
            if mode == "missing" and not detected:
                fulfilled[i] = True
            if mode == "existing" and detected:
                fulfilled[i] = True

        return (fulfilled, list(useful_terms_main_advanced),
                    list(useful_terms_synonyms_advanced))



    def merge_annotations(self,merge_methods:list, merge_gazetteers:list, new_method_name:str):
        """
        creates an annotated version of an essay that corresponds to a merge of the specified methods and gazetteers
        :param merge_methods: list of methods of which to merge the annotations
        :param merge_gazetteers: gazetteers that should be taken into account
        :param new_method_name: name of this new method / setup.
        """
        merged_annotations = []
        first_iteration = True

        # read all versions of the specified essay, that were annotated with the given set of methods
        for method in merge_methods:
            with open(self.file_path  + "/" + "%s_annotated_%s.txt"%(self.essay_name, method), "r") as annotated_essay:
                # extract annotations of the current method
                for i, line in enumerate(annotated_essay.readlines()):
                    extracted_annotations = []
                    token = line.split("\t")[0].strip()
                    if "{" in line and any([gz in line for gz in merge_gazetteers]):
                        extracted_annotations = extract_annotations_from_line(line, merge_gazetteers)

                    # store the annotation information in the merged annotations list. Add some more information if this is the first iteration
                    if first_iteration:
                        merged_annotations.append((token, [(method, a[1], a[2]) for a in extracted_annotations if a[2] in merge_gazetteers]))
                    else:
                        for a in extracted_annotations:
                            if a[2] in merge_gazetteers:
                                merged_annotations[i][1].append((method, a[1], a[2]))

                # merge the metadata (matching informations) of every method
                found_entities_extended = self.merge_metadata(method, new_method_name, merge_gazetteers)

                first_iteration = False

        # choose an offset for the entity ids of each method to ensure distinct id ranges
        #method_id_range = dict()
        #cur_range = 0
        #for method in merge_methods:
        #    method_id_range[method] = cur_range
        #    cur_range += 100

        # create an essay version and store it
        #with open(self.file_path + "/" + "%s_annotatedNew_%s.txt"%(self.essay_name, new_method_name), "w") as new_essay:
        #    for token, annotations in merged_annotations:
        #        # construct and write each line
        #        essay_string = "%s"%token
        #        if annotations:
        #            essay_string += "\t{"
        #        for method, entity_id, gazetteer in annotations:
        #            essay_string += "(%s, %s, %s), "%(gazetteer, method, str(int(entity_id + method_id_range[method])))
        #        if annotations:
        #            essay_string = essay_string.strip(", ") + "}"
        #        essay_string += "\n"
        #        new_essay.write(essay_string)

        return found_entities_extended


    def merge_metadata(self,old_method:str, new_method_name:str, gazetteers:list):
        """
        merges the found_entities_dicts of the methods specified by old_method and new_method_name.
        new_method_name usually refers to a method that is already a merge of various basic methods
        :param old_method: name of the first method (usually basic method)
        :param new_method_name: name of the second method. Creates it if not existent (usually either a merge of several basic methods or not existent yet)
        :param gazetteers: the gazetteers that should be taken into account
        """
        # dont merge if both are the same methods
        if old_method == new_method_name:
            return

        # load old metadata
        found_entities_extended = self.entities[old_method]
        # if new_method_name does not exist yet, create a copy of the old method with a new name, taking into account only the specified gazetteer
        if not new_method_name in found_entities_extended.keys():
            # extract the matching information for this particular essay
            tokens_all, matching_information_old = found_entities_extended[old_method]

            # add each matching instance to the new set if it only involves one of the specified gazetteers
            matching_information_new = set()
            for essay_ng, entity_string, gaz_idx, gaz_name in matching_information_old:
                if gaz_name in gazetteers:
                    matching_information_new.add((essay_ng, entity_string, gaz_idx, gaz_name))
            found_entities_extended[new_method_name] = (tokens_all, matching_information_new)

        # else merge the old and new method
        else:
            tokens_all, matching_information_old = found_entities_extended[old_method]
            tokens_all, matching_information_new = found_entities_extended[new_method_name]

            # add each matching instance to the new method's set if it only involves one of the specified gazetteers
            for essay_ng, entity_string, gaz_idx, gaz_name in matching_information_old:
                if gaz_name in gazetteers:
                    matching_information_new.add((essay_ng, entity_string, gaz_idx, gaz_name))
            found_entities_extended[new_method_name] = (tokens_all, matching_information_new)

        return found_entities_extended



def extract_annotations_from_line(line: str, gazetteers):
    """
    gets a line from a raw annotated essay and extracts all annotations:
    Example of input line:
    "HIV {(wikipedia_symptome.txt, 5), (wiki_krankheitsbilder.txt, 6)}"

    Note: This method assumes that the line contains an annotation
    :param line: the line to extract annotations from
    :return: a list of tuples, that have the form (token, gazetteer_category, entity_id, gazetteers)
            One tuple for each annotation. the gazetteer string at the end is not clean and has a lot of different parts to it!

    """
    extracted_annotations = []
    word = line.split("\t")[0].strip()
    # extract the annotation string
    annotation_string_raw = re.findall("\{.*\}", line)[0]
    annotation_strings = annotation_string_raw.split("),")

    # assign all annotations to the word
    for annotation_string in annotation_strings:
        annotation_gazetteer, entity_id = annotation_string.split(",")
        for gz in gazetteers:
            if gz in annotation_gazetteer:
                extracted_annotations.append(
                (word, int(entity_id.strip(" )}")), annotation_gazetteer.strip(" ({")))
            else:
                print("WRONG GAZETTEER")
                continue

    return extracted_annotations

##############################
# Main method for feedback generation given a file annotated with medical entities
#
# by <NAME> (based on code by <NAME>)
# October 2018
##############################

import os
import pandas as pd
import pickle
import re
import csv
from nltk.tokenize import RegexpTokenizer


# Define paths
FEEDBACK_DATA = "util/automatic_feedback/Feedback_creator/"
FEEDBACK_DATA = "../../util/automatic_feedback/Feedback_creator/"
FEEDBACK_FILE = FEEDBACK_DATA + "AdaptivesFeedback_neu.xls"
RESULTS_PATH = "feedbackResults/"


# sheet_id maps the sheet names to case IDs
sheet_id = {"<NAME>_b<NAME>": "1", "<NAME>": "2", "<NAME>_<NAME>": "3", "<NAME>_Hyperthyreosis fact.":"4",
            "<NAME>_Spinalkanalstenose": "5", "<NAME>_ostp. WK#":"6", "<NAME>_<NAME>.":"7", "<NAME>_EBV":"8"}

class Feedback_creator:
    """
    This Module realizes the generation of feedback for a given student essay
    """
    def __init__(self, entities:dict, name:str, caseId, essay):
        """
        creates an instance of the feedback creator class
        Reads in the Excel file, that maps the mentioning / not mentioning of terms to feedback that should be given
        """

        self.essay_name = name
        self.file_path = RESULTS_PATH + name
        self.caseId = caseId
        self.essay = essay
        self.entities = entities

        if not os.path.exists(self.file_path):
            os.makedirs(self.file_path)

        # save the given essay in a file
        with open(self.file_path + "/" + self.essay_name + ".txt", 'w') as essaytext:
            essaytext.write(self.essay)

        # case_feedback maps case IDs to lists of triples of the form (main_terms, synonyms, feedback)
        if os.path.isfile(FEEDBACK_DATA + "feedback_dict"):
            self.case_feedback = pickle.load(open(FEEDBACK_DATA + "feedback_dict", "rb"))
        else:
            self.case_feedback = dict()

            # read from Excel file
            xl = pd.ExcelFile(FEEDBACK_FILE)
            for sheet_name in sheet_id.keys():
                self.case_feedback[sheet_id[sheet_name]] = []
                xl_case = xl.parse(sheet_name)

                # check whether header of the file is correct
                header = xl_case.columns
                if not header[0] == "Katergorie" and header[1] == "Fachbegriff" and header[2] == "Synonyme" and header[3] == "Feedback":
                    print("Error - header of feedback changed!")
                    exit()

                # parse data into dictionary; ".values" converts to numpy matrix
                for row in xl_case.values:
                    # collect relevant terms but correct the inconsistency that terms are separated by either comma, semicolon or linebreaks
                    # An inconsistency we cannot correct is when terms are not separated at all, as in "NOT Anitibiothikaresistenz NOT Resistenz"
                    # different options are separated by "&&"
                    terms_main = [sorted([term.strip() for term in option.replace(",", ";").replace("\n", ";").split(";")
                                          if term.strip()]) for option in row[1].split("&&")] if not row[1].strip() == "andere" else [[]]

                    terms_synonyms = [sorted([term.strip() for term in option.replace(",", ";").replace("\n", ";").split(";")
                                          if term.strip()]) for option in row[2].split("&&")]  if not type(row[2]) == float else [[] for x in terms_main]

                    # there should be synonyms for each option
                    if len(terms_main) >1 and len(terms_main) != len(terms_synonyms):
                        print("not the same number of main and synonym options!")
                        print(sheet_name)
                        print(row)

                    self.case_feedback[sheet_id[sheet_name]].append(((terms_main, terms_synonyms, row[3], row[0])))
            pickle.dump(self.case_feedback, open(FEEDBACK_DATA + "feedback_dict", "wb"))

        # create CSV file to save feedback stats if it doesn't exist yet for this case
        if not os.path.isfile(RESULTS_PATH + "stats_case" + self.caseId + ".csv"):
            with open(RESULTS_PATH + "stats_case" + self.caseId + ".csv", "w") as f:
                writer = csv.writer(f, delimiter=",")
                header = ["studentId"]
                for i, x in enumerate(self.case_feedback[self.caseId]):
                    header.append(str(i) + " feedback given")
                    header.append(str(i) + " specified terms")
                    header.append(str(i) + " found terms")
                writer.writerow(header)



    def generate_feedback(self, methods:list, gazetteers:list):
        """
        Generates feedback according to the medical entities found in the essay with the specified methods and gazetteers
        Feedback can be found at FEEDBACK_PATH
        :param methods: list of methods used for annotation
        :param gazetteers: list of gazetteers that should be taken into account
        """
        # all Feedback transmitted to CASUS is saved here
        feedbackList = []
        feedbackCount = 0

        # will be printed in CSV feedback stats file with 1st column the essayID
        # then for each feedback there are 3 columns:
        # 0 / 1 (feedback given / not given)
        # specified terms (main term or synonym associated with found term)
        # found terms
        # specified and found terms of multiple conditions are seperated with "&",
        # various found terms or specified synonyms with ";"
        feedbackStats = [self.essay_name]
        # count of the feedback (= line in feedback table)
        statscount = 0

        # save feedback to a file
        with open(self.file_path + "/" + self.essay_name.split(".")[0] + "_feedback.txt", "w") as feedback_f:

            tokenizer = RegexpTokenizer(
                r'[\w-]+')  # finds all strings with alphanumerical characters or with '-' or '_' (the latter implicitly through \w)
            token_offsets = list(tokenizer.span_tokenize(self.essay))

            # check for each feedback whether it fulfills the constraints (terms existing or missing)
            for (terms_main, terms_synonyms, feedback,feedbackType) in self.case_feedback[self.caseId]:

                fulfilled, useful_terms_main_advanced, useful_terms_synonyms_advanced = \
                    self.check_conditions(terms_main, terms_synonyms, methods, gazetteers)

                #print("terms: " + str(useful_terms_main_advanced))
                #print("syn: " + str(useful_terms_synonyms_advanced))

                # if all conditions are fulfilled (e.g. MRT & NOT Roentgen are two conditions)
                if all(x == True for x in fulfilled):
                    #print("fulfilled")

                    # each fulfilled feedback will be transmitted to CASUS with the following information:
                    # (feedbackID, set of offsets of found terms, set of associated strings, feedback text)
                    # feedbackID is simply a count of all given feedbacks
                    feedback_entry = (feedbackCount,[],[],feedback,feedbackType)

                    # record that feedback was given in feedback stats
                    feedbackStats.append("1")
                    feedbackStats.append("")
                    feedbackStats.append("")

                    # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options)
                    # a term made of various words is still one option, e.g. Hepatitis Serologie
                    for i, option in enumerate(terms_main):
                        #print("Option " + str(i) + ": " + ''.join(option))

                        # the last feedback often has "andere" (others) as the main term and all possible terms as synonyms
                        # in this case, the terms_main contains only the empty set, so the only option is empty

                        # if the option is a negated term (so the term was not found in the essay),
                        #  record the negated term and all its synonyms that were not found in the essay in the feedback file
                        if len(option) != 0 and "NOT" in option[0]:
                            feedback_f.write("\nTRIGGER: None of the following terms detected\n")
                            feedback_f.writelines(sorted([t.strip()+",  " for t in (option + terms_synonyms[i])]))

                            # record reason for feedback in feedback stats as the main term that was not found
                            if feedbackStats[statscount * 3 + 2] == "":
                                feedbackStats[statscount * 3 + 2] = ''.join(option)
                            # if this is not the first option
                            else:
                                feedbackStats[statscount * 3 + 2] += " & " + ''.join(option)

                        # if the option is a positive term (and the term or a synonym was found)
                        else:
                            feedback_f.write("\nTRIGGER: Detection of the following terms\n")
                            feedbackString = ""

                            # if the main term is detected, report only this, else report detected synonyms
                            l = useful_terms_main_advanced[i] if len(useful_terms_main_advanced[i]) != 0 \
                                else useful_terms_synonyms_advanced[i]
                            syn = False if len(useful_terms_main_advanced[i]) != 0 else True

                            # for each option, various terms may have been found (especially regarding synonyms)
                            for found, targetTerm in l:
                                same = False
                                # various words in the text may be associated with a targetTerm, they are listed in found
                                # e.g. ((8, 'körperlicher'), (9, 'untersuchung')) or if only one word ((14, 'labors'),)
                                foundlist = list(found)
                                tokenstring = ""
                                indexlist = []
                                # get the complete found string and associated word indices (indices of tokenized text)
                                for index, token in foundlist:
                                    if tokenstring == "":
                                        tokenstring = token
                                    else:
                                        tokenstring = tokenstring + " " + token
                                    indexlist.append(index)

                                # get the start and end character offsets of the found words
                                startchars = token_offsets[indexlist[0]]
                                endchars = token_offsets[indexlist[-1]]
                                start = startchars[0]
                                end = endchars[1]

                                # add the offsets and the found term to the feedback to be transmitted to CASUS
                                # various synonyms may be associated with the same word in the text
                                # in this case don't add the term twice to the list of words that triggered the feedback
                                if not tokenstring in feedback_entry[2]:
                                    feedback_entry[1].append((start,end))
                                    feedback_entry[2].append(tokenstring)

                                # if the found term matches exactly the main term (or synonym)
                                # this is recorded with a different wording in the feedback file
                                if tokenstring.lower() == targetTerm.lower():
                                    text_feedback =  " wurde im Text gefunden an Position " + str(indexlist) +\
                                                     " (" + str(start) + "-" + str(end) + ")"
                                else:
                                    text_feedback = " " + str(indexlist) + " (" + str(start) + "-" + str(end) + \
                                                     ") im Text wurde assoziiert mit " + targetTerm

                                # various found terms in the essay (e.g. of various synonyms) are separated by ";"
                                pre = "" if feedbackString == "" else "; "
                                feedbackString = feedbackString + pre + tokenstring + text_feedback

                                # option == [] means that the main term is "andere" and the "synonyms" are simply a list of
                                # terms such that if found the feedback is triggered (but we do not wan to call them "synonyms" of andere)
                                if option != [] and syn:
                                    feedbackString = feedbackString + " und ist ein Synonym von " +  ''.join(option)

                                # record given term and found term for feedback stats
                                if feedbackStats[statscount * 3 + 2] == "":
                                    feedbackStats[statscount * 3 + 2] = targetTerm
                                    feedbackStats[statscount * 3 + 3] = tokenstring
                                elif pre == "":
                                    feedbackStats[statscount * 3 + 2] += "; " + targetTerm
                                    feedbackStats[statscount * 3 + 3] += "; " + tokenstring
                                else:
                                    feedbackStats[statscount * 3 + 2] += " & " + targetTerm
                                    feedbackStats[statscount * 3 + 3] += " & " + tokenstring

                            feedback_f.write(feedbackString)

                    feedback_f.write("\n\nFEEDBACK:\n")
                    feedback_f.write(feedback+"\n\n")
                    feedback_f.write("_____________________________________________\n")
                    feedbackList.append(feedback_entry)
                    feedbackCount+=1

                # if the feedback is not given since conditions not satisfied
                # this is only recorded in feedback stats - no feedback recorded in feedback file or given to CASUS
                else:
                    feedbackStats.append("0")
                    feedbackStats.append("")
                    feedbackStats.append("")

                    # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options)
                    # a term made of various words is still one option, e.g. Hepatitis Serologie
                    for i, option in enumerate(terms_main):

                        # if this is the "andere" row (thus none of the specified terms was found)
                        # record "andere" as main term (and nothing in the found column)
                        if len(option) == 0:
                            feedbackStats[statscount * 3 + 2] = "andere"

                        else:
                            neg = True if "NOT" in option[0] else False

                            # if no terms for this option found in the essay, i.e.
                            # a) term is negative and satisfied (so not found in essay)
                            # b) term is positive and not satisfied (so not found in essay)
                            # record the main term (and nothing in the found column)
                            # NOTE: satisfied option is possible if various options and one of the others is not satisfied
                            if (fulfilled[i] == True and neg == True) or (fulfilled[i] == False and neg == False):
                                if feedbackStats[statscount * 3 + 2] == "":
                                    feedbackStats[statscount * 3 + 2] = ''.join(option)
                                else:
                                    feedbackStats[statscount * 3 + 2] += " & " + ''.join(option)

                            # if terms for this option found in the essay
                            # record both the given term and the found version of it
                            else:
                                l = useful_terms_main_advanced[i] if len(useful_terms_main_advanced[i]) != 0 \
                                    else useful_terms_synonyms_advanced[i]
                                for x, (found, targetTerm) in enumerate(l):
                                    foundlist = list(found)
                                    tokenstring = ""
                                    for index, token in foundlist:
                                        if tokenstring == "":
                                            tokenstring = token
                                        else:
                                            tokenstring = tokenstring + " " + token

                                    if feedbackStats[statscount * 3 + 2] == "":
                                        feedbackStats[statscount * 3 + 2] = targetTerm
                                        feedbackStats[statscount * 3 + 3] = tokenstring
                                    # if this is not the first found term and the list isn't empty
                                    elif  x > 0:
                                        feedbackStats[statscount * 3 + 2] += "; " + targetTerm
                                        feedbackStats[statscount * 3 + 3] += "; " + tokenstring
                                    # if this is the first found term and the list isn't empty, i.e. another option has
                                    # been recorded before
                                    else:
                                        feedbackStats[statscount * 3 + 2] += " & " + targetTerm
                                        feedbackStats[statscount * 3 + 3] += " & " + tokenstring

                statscount += 1
            #print(feedbackStats)

            # write the feedback stats to the stats CSV file
            with open(RESULTS_PATH + "stats_case" + self.caseId + ".csv") as inf, open(RESULTS_PATH + "stats_caseNew" + self.caseId + ".csv", 'w') as outf:
                reader = csv.reader(inf, delimiter = ",")
                writer = csv.writer(outf, delimiter = ",")
                # check if the essay name already exists, i.e. if a user submitted a solution for this case before
                # if so, the old stats are replaced with the new
                found = False
                for line in reader:
                    if line[0] == self.essay_name:
                        writer.writerow(feedbackStats)
                        found = True
                    else:
                        writer.writerow(line)
                if not found:
                    writer.writerow(feedbackStats)

            os.remove(RESULTS_PATH + "stats_case" + self.caseId + ".csv")
            os.rename(RESULTS_PATH + "stats_caseNew" + self.caseId + ".csv", RESULTS_PATH + "stats_case" + self.caseId + ".csv")

        return feedbackList



    def check_conditions(self,terms_main: list, terms_synonyms: list, methods: list, gazetteers: list):
        """
        checks for a set of feedback conditions whether at least one is fulfilled, w.r.t. an essay
        conditions are the existence or non-existence of certain terms or a collection of terms

        :param terms_main: The main technical terms that trigger the feedback
        :param terms_synonyms: List of synonyms of the main technical terms
        :param methods: methods of which annotations should be combined
        :param gazetteers: gazetteers which should be taken into account. Corresponds to all gazetteers if list is empty.
        :return: Tuple of the form (fulfilled:list of bool, terms_found_main_advanced:list, terms_found_synonyms_advanced:list)
                 fulfilled indicates for each condition whether it is fulfilled
                 the two lists contain the terms that appeared in the essay and are associated with the main terms of synonyms
        """

        # merge the annotations of the specified methods and gazetteers
        # NOTE: right now we are only using one method and gazetteer! however the merge also changes the format, so we currently need it
        # can be used in future if we wanted to use various methods and/or gazetteers
        new_method_name = ("_".join(sorted(methods)) + "_" + "_".join(sorted(gazetteers))).split(".")[0]
        found_entities_extended = self.merge_annotations(methods, gazetteers, new_method_name)

        # get all tokens of the original essay
        tokens, matching_information = found_entities_extended[new_method_name]

        fulfilled = [False for x in terms_main]
        useful_terms_main_advanced = [set()for x in terms_main]
        useful_terms_synonyms_advanced = [set()for x in terms_main]

        # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options)
        # a term made of various words is still one option, e.g. Hepatitis Serologie
        for i, option in enumerate(terms_main):

            # it is important to distinguish between terms that should be missing or existing for feedback
            # because when the condition is missing terms, all of them must miss.
            # when a term must exist, at least one of them (=main + synonyms) must exist
            mode = "missing" if len(option) != 0 and "NOT" in option[0] else "existing"

            # keep track whether a matching term was found
            detected = False

            # process every main term and synonym
            for terms, term_type in zip([option, terms_synonyms[i]], ["main", "synonyms"]):
                for term in terms:

                    # try to find the term with the help of gazetteers and advanced matching methods
                    # other entries are "essay_n_gram, gaz_idx, gazetteer_filename"
                    for essay_n_gram, gazetteer_entry, ___, ____ in matching_information:
                        if gazetteer_entry.lower() == term.replace("NOT", "").strip().lower():
                            detected = True
                            # add the found words and their indices (an n-gram tuple) and the term or synonym (including NOT) it matches
                            # essay_n_gram is e.g.: ((8, 'körperlicher'), (9, 'untersuchung')) or if only one word ((14, 'labors'),)
                            useful_terms_main_advanced[i].add(
                                (essay_n_gram, term)) if term_type == "main" else useful_terms_synonyms_advanced[i].add((essay_n_gram, term))

            # check whether the condition is fulfilled, i.e. NOT TERM and none found or TERM and some found
            if mode == "missing" and not detected:
                fulfilled[i] = True
            if mode == "existing" and detected:
                fulfilled[i] = True

        return (fulfilled, list(useful_terms_main_advanced),
                    list(useful_terms_synonyms_advanced))



    def merge_annotations(self,merge_methods:list, merge_gazetteers:list, new_method_name:str):
        """
        creates an annotated version of an essay that corresponds to a merge of the specified methods and gazetteers
        :param merge_methods: list of methods of which to merge the annotations
        :param merge_gazetteers: gazetteers that should be taken into account
        :param new_method_name: name of this new method / setup.
        """
        merged_annotations = []
        first_iteration = True

        # read all versions of the specified essay, that were annotated with the given set of methods
        for method in merge_methods:
            with open(self.file_path  + "/" + "%s_annotated_%s.txt"%(self.essay_name, method), "r") as annotated_essay:
                # extract annotations of the current method
                for i, line in enumerate(annotated_essay.readlines()):
                    extracted_annotations = []
                    token = line.split("\t")[0].strip()
                    if "{" in line and any([gz in line for gz in merge_gazetteers]):
                        extracted_annotations = extract_annotations_from_line(line, merge_gazetteers)

                    # store the annotation information in the merged annotations list. Add some more information if this is the first iteration
                    if first_iteration:
                        merged_annotations.append((token, [(method, a[1], a[2]) for a in extracted_annotations if a[2] in merge_gazetteers]))
                    else:
                        for a in extracted_annotations:
                            if a[2] in merge_gazetteers:
                                merged_annotations[i][1].append((method, a[1], a[2]))

                # merge the metadata (matching informations) of every method
                found_entities_extended = self.merge_metadata(method, new_method_name, merge_gazetteers)

                first_iteration = False

        # choose an offset for the entity ids of each method to ensure distinct id ranges
        #method_id_range = dict()
        #cur_range = 0
        #for method in merge_methods:
        #    method_id_range[method] = cur_range
        #    cur_range += 100

        # create an essay version and store it
        #with open(self.file_path + "/" + "%s_annotatedNew_%s.txt"%(self.essay_name, new_method_name), "w") as new_essay:
        #    for token, annotations in merged_annotations:
        #        # construct and write each line
        #        essay_string = "%s"%token
        #        if annotations:
        #            essay_string += "\t{"
        #        for method, entity_id, gazetteer in annotations:
        #            essay_string += "(%s, %s, %s), "%(gazetteer, method, str(int(entity_id + method_id_range[method])))
        #        if annotations:
        #            essay_string = essay_string.strip(", ") + "}"
        #        essay_string += "\n"
        #        new_essay.write(essay_string)

        return found_entities_extended


    def merge_metadata(self,old_method:str, new_method_name:str, gazetteers:list):
        """
        merges the found_entities_dicts of the methods specified by old_method and new_method_name.
        new_method_name usually refers to a method that is already a merge of various basic methods
        :param old_method: name of the first method (usually basic method)
        :param new_method_name: name of the second method. Creates it if not existent (usually either a merge of several basic methods or not existent yet)
        :param gazetteers: the gazetteers that should be taken into account
        """
        # dont merge if both are the same methods
        if old_method == new_method_name:
            return

        # load old metadata
        found_entities_extended = self.entities[old_method]
        # if new_method_name does not exist yet, create a copy of the old method with a new name, taking into account only the specified gazetteer
        if not new_method_name in found_entities_extended.keys():
            # extract the matching information for this particular essay
            tokens_all, matching_information_old = found_entities_extended[old_method]

            # add each matching instance to the new set if it only involves one of the specified gazetteers
            matching_information_new = set()
            for essay_ng, entity_string, gaz_idx, gaz_name in matching_information_old:
                if gaz_name in gazetteers:
                    matching_information_new.add((essay_ng, entity_string, gaz_idx, gaz_name))
            found_entities_extended[new_method_name] = (tokens_all, matching_information_new)

        # else merge the old and new method
        else:
            tokens_all, matching_information_old = found_entities_extended[old_method]
            tokens_all, matching_information_new = found_entities_extended[new_method_name]

            # add each matching instance to the new method's set if it only involves one of the specified gazetteers
            for essay_ng, entity_string, gaz_idx, gaz_name in matching_information_old:
                if gaz_name in gazetteers:
                    matching_information_new.add((essay_ng, entity_string, gaz_idx, gaz_name))
            found_entities_extended[new_method_name] = (tokens_all, matching_information_new)

        return found_entities_extended



def extract_annotations_from_line(line: str, gazetteers):
    """
    gets a line from a raw annotated essay and extracts all annotations:
    Example of input line:
    "HIV {(wikipedia_symptome.txt, 5), (wiki_krankheitsbilder.txt, 6)}"

    Note: This method assumes that the line contains an annotation
    :param line: the line to extract annotations from
    :return: a list of tuples, that have the form (token, gazetteer_category, entity_id, gazetteers)
            One tuple for each annotation. the gazetteer string at the end is not clean and has a lot of different parts to it!

    """
    extracted_annotations = []
    word = line.split("\t")[0].strip()
    # extract the annotation string
    annotation_string_raw = re.findall("\{.*\}", line)[0]
    annotation_strings = annotation_string_raw.split("),")

    # assign all annotations to the word
    for annotation_string in annotation_strings:
        annotation_gazetteer, entity_id = annotation_string.split(",")
        for gz in gazetteers:
            if gz in annotation_gazetteer:
                extracted_annotations.append(
                (word, int(entity_id.strip(" )}")), annotation_gazetteer.strip(" ({")))
            else:
                print("WRONG GAZETTEER")
                continue

    return extracted_annotations
############################## # Main method for feedback generation given a file annotated with medical entities # # by <NAME> (based on code by <NAME>) # October 2018 ############################## # Define paths # sheet_id maps the sheet names to case IDs #":"6", "<NAME>_<NAME>.":"7", "<NAME>_EBV":"8"} This Module realizes the generation of feedback for a given student essay creates an instance of the feedback creator class         Reads in the Excel file, that maps the mentioning / not mentioning of terms to feedback that should be given # save the given essay in a file # case_feedback maps case IDs to lists of triples of the form (main_terms, synonyms, feedback) # read from Excel file # check whether header of the file is correct # parse data into dictionary; ".values" converts to numpy matrix # collect relevant terms but correct the inconsistency that terms are separated by either comma, semicolon or linebreaks # An inconsistency we cannot correct is when terms are not separated at all, as in "NOT Anitibiothikaresistenz NOT Resistenz" # different options are separated by "&&" # there should be synonyms for each option # create CSV file to save feedback stats if it doesn't exist yet for this case Generates feedback according to the medical entities found in the essay with the specified methods and gazetteers         Feedback can be found at FEEDBACK_PATH         :param methods: list of methods used for annotation         :param gazetteers: list of gazetteers that should be taken into account # all Feedback transmitted to CASUS is saved here # will be printed in CSV feedback stats file with 1st column the essayID # then for each feedback there are 3 columns: # 0 / 1 (feedback given / not given) # specified terms (main term or synonym associated with found term) # found terms # specified and found terms of multiple conditions are seperated with "&", # various found terms or specified synonyms with ";" # count of the feedback (= line in feedback table) # save feedback to a file # finds all strings with alphanumerical characters or with '-' or '_' (the latter implicitly through \w) # check for each feedback whether it fulfills the constraints (terms existing or missing) #print("terms: " + str(useful_terms_main_advanced)) #print("syn: " + str(useful_terms_synonyms_advanced)) # if all conditions are fulfilled (e.g. MRT & NOT Roentgen are two conditions) #print("fulfilled") # each fulfilled feedback will be transmitted to CASUS with the following information: # (feedbackID, set of offsets of found terms, set of associated strings, feedback text) # feedbackID is simply a count of all given feedbacks # record that feedback was given in feedback stats # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options) # a term made of various words is still one option, e.g. Hepatitis Serologie #print("Option " + str(i) + ": " + ''.join(option)) # the last feedback often has "andere" (others) as the main term and all possible terms as synonyms # in this case, the terms_main contains only the empty set, so the only option is empty # if the option is a negated term (so the term was not found in the essay), #  record the negated term and all its synonyms that were not found in the essay in the feedback file # record reason for feedback in feedback stats as the main term that was not found # if this is not the first option # if the option is a positive term (and the term or a synonym was found) # if the main term is detected, report only this, else report detected synonyms # for each option, various terms may have been found (especially regarding synonyms) # various words in the text may be associated with a targetTerm, they are listed in found # e.g. ((8, 'körperlicher'), (9, 'untersuchung')) or if only one word ((14, 'labors'),) # get the complete found string and associated word indices (indices of tokenized text) # get the start and end character offsets of the found words # add the offsets and the found term to the feedback to be transmitted to CASUS # various synonyms may be associated with the same word in the text # in this case don't add the term twice to the list of words that triggered the feedback # if the found term matches exactly the main term (or synonym) # this is recorded with a different wording in the feedback file # various found terms in the essay (e.g. of various synonyms) are separated by ";" # option == [] means that the main term is "andere" and the "synonyms" are simply a list of # terms such that if found the feedback is triggered (but we do not wan to call them "synonyms" of andere) # record given term and found term for feedback stats # if the feedback is not given since conditions not satisfied # this is only recorded in feedback stats - no feedback recorded in feedback file or given to CASUS # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options) # a term made of various words is still one option, e.g. Hepatitis Serologie # if this is the "andere" row (thus none of the specified terms was found) # record "andere" as main term (and nothing in the found column) # if no terms for this option found in the essay, i.e. # a) term is negative and satisfied (so not found in essay) # b) term is positive and not satisfied (so not found in essay) # record the main term (and nothing in the found column) # NOTE: satisfied option is possible if various options and one of the others is not satisfied # if terms for this option found in the essay # record both the given term and the found version of it # if this is not the first found term and the list isn't empty # if this is the first found term and the list isn't empty, i.e. another option has # been recorded before #print(feedbackStats) # write the feedback stats to the stats CSV file # check if the essay name already exists, i.e. if a user submitted a solution for this case before # if so, the old stats are replaced with the new checks for a set of feedback conditions whether at least one is fulfilled, w.r.t. an essay         conditions are the existence or non-existence of certain terms or a collection of terms          :param terms_main: The main technical terms that trigger the feedback         :param terms_synonyms: List of synonyms of the main technical terms         :param methods: methods of which annotations should be combined         :param gazetteers: gazetteers which should be taken into account. Corresponds to all gazetteers if list is empty.         :return: Tuple of the form (fulfilled:list of bool, terms_found_main_advanced:list, terms_found_synonyms_advanced:list)                  fulfilled indicates for each condition whether it is fulfilled                  the two lists contain the terms that appeared in the essay and are associated with the main terms of synonyms # merge the annotations of the specified methods and gazetteers # NOTE: right now we are only using one method and gazetteer! however the merge also changes the format, so we currently need it # can be used in future if we wanted to use various methods and/or gazetteers # get all tokens of the original essay # some main terms are made of two different conditions, e.g. MRT & NOT Roentgen (these are two options) # a term made of various words is still one option, e.g. Hepatitis Serologie # it is important to distinguish between terms that should be missing or existing for feedback # because when the condition is missing terms, all of them must miss. # when a term must exist, at least one of them (=main + synonyms) must exist # keep track whether a matching term was found # process every main term and synonym # try to find the term with the help of gazetteers and advanced matching methods # other entries are "essay_n_gram, gaz_idx, gazetteer_filename" # add the found words and their indices (an n-gram tuple) and the term or synonym (including NOT) it matches # essay_n_gram is e.g.: ((8, 'körperlicher'), (9, 'untersuchung')) or if only one word ((14, 'labors'),) # check whether the condition is fulfilled, i.e. NOT TERM and none found or TERM and some found creates an annotated version of an essay that corresponds to a merge of the specified methods and gazetteers         :param merge_methods: list of methods of which to merge the annotations         :param merge_gazetteers: gazetteers that should be taken into account         :param new_method_name: name of this new method / setup. # read all versions of the specified essay, that were annotated with the given set of methods # extract annotations of the current method # store the annotation information in the merged annotations list. Add some more information if this is the first iteration # merge the metadata (matching informations) of every method # choose an offset for the entity ids of each method to ensure distinct id ranges #method_id_range = dict() #cur_range = 0 #for method in merge_methods: #    method_id_range[method] = cur_range #    cur_range += 100 # create an essay version and store it #with open(self.file_path + "/" + "%s_annotatedNew_%s.txt"%(self.essay_name, new_method_name), "w") as new_essay: #    for token, annotations in merged_annotations: #        # construct and write each line #        essay_string = "%s"%token #        if annotations: #            essay_string += "\t{" #        for method, entity_id, gazetteer in annotations: #            essay_string += "(%s, %s, %s), "%(gazetteer, method, str(int(entity_id + method_id_range[method]))) #        if annotations: #            essay_string = essay_string.strip(", ") + "}" #        essay_string += "\n" #        new_essay.write(essay_string) merges the found_entities_dicts of the methods specified by old_method and new_method_name.         new_method_name usually refers to a method that is already a merge of various basic methods         :param old_method: name of the first method (usually basic method)         :param new_method_name: name of the second method. Creates it if not existent (usually either a merge of several basic methods or not existent yet)         :param gazetteers: the gazetteers that should be taken into account # dont merge if both are the same methods # load old metadata # if new_method_name does not exist yet, create a copy of the old method with a new name, taking into account only the specified gazetteer # extract the matching information for this particular essay # add each matching instance to the new set if it only involves one of the specified gazetteers # else merge the old and new method # add each matching instance to the new method's set if it only involves one of the specified gazetteers gets a line from a raw annotated essay and extracts all annotations:     Example of input line:     "HIV {(wikipedia_symptome.txt, 5), (wiki_krankheitsbilder.txt, 6)}"      Note: This method assumes that the line contains an annotation     :param line: the line to extract annotations from     :return: a list of tuples, that have the form (token, gazetteer_category, entity_id, gazetteers)             One tuple for each annotation. the gazetteer string at the end is not clean and has a lot of different parts to it! # extract the annotation string # assign all annotations to the word