From a7f9cde5096c5089e6fbdc1099ebd9d8e325a254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Fri, 12 May 2023 11:56:26 +0000 Subject: [PATCH 01/11] draft --- docs/pipelines/misc/measurements.md | 73 +- edsnlp/pipelines/misc/measurements/factory.py | 15 +- .../misc/measurements/measurements.py | 277 +- .../pipelines/misc/measurements/patterns.py | 2405 ++++++++++++++--- 4 files changed, 2387 insertions(+), 383 deletions(-) diff --git a/docs/pipelines/misc/measurements.md b/docs/pipelines/misc/measurements.md index 4630f993c..a30041025 100644 --- a/docs/pipelines/misc/measurements.md +++ b/docs/pipelines/misc/measurements.md @@ -1,21 +1,14 @@ # Measurements The `eds.measurements` pipeline's role is to detect and normalise numerical measurements within a medical document. -We use simple regular expressions to extract and normalize measurements, and use `Measurement` classes to store them. - -!!! warning - - The ``measurements`` pipeline is still in active development and has not been rigorously validated. - If you come across a measurement expression that goes undetected, please file an issue ! +We use simple regular expressions to extract and normalize measurements, and use `SimpleMeasurement` classes to store them. ## Scope -The `eds.measurements` pipeline can extract simple (eg `3cm`) measurements. -It can detect elliptic enumerations (eg `32, 33 et 34kg`) of measurements of the same type and split the measurements accordingly. - -The normalized value can then be accessed via the `span._.value` attribute and converted on the fly to a desired unit. +By default, the `eds.measurements` pipeline lets you match all measurements, i.e measurements in most units as well as unitless measurements. If a unit is not in our register, +then you can add It manually. If not, the measurement will be matched without Its unit. -The current pipeline annotates the following measurements out of the box: +If you prefer to match specific measurements only, you can create your own measurement config. Nevertheless, some default measurements configs are already provided out of the box: | Measurement name | Example | | ---------------- | ---------------------- | @@ -24,8 +17,41 @@ The current pipeline annotates the following measurements out of the box: | `eds.bmi` | `BMI: 24`, `24 kg.m-2` | | `eds.volume` | `2 cac`, `8ml` | +The normalized value can then be accessed via the `span._.value` attribute and converted on the fly to a desired unit (eg `span._.value.g_per_cl` or `span._.value.kg_per_m3` for a density). + +The measurements that can be extracted can have one or many of the following characteristics: +- Unitless measurements +- Measurements with unit +- Measurements with range indication (escpecially < or >) +- Measurements with power + +The measurement can be written in many coplex forms. Among them, this pipe can detect: +- Measurements with range indication, numerical value, power and units in many different orders and separated by customizable stop words +- Composed units (eg `1m50`) +- Measurement with "unitless patterns", i.e some textual information next to a numerical value which allows us to retrieve a unit even if It is not written (eg in the text `Height: 80`, this pipe will a detect the numlerical value `80`and match It to the unit `kg`) +- Elliptic enumerations (eg `32, 33 et 34mol`) of measurements of the same type and split the measurements accordingly + ## Usage +The matched measurements are labelised with `eds.measurement` by default. However, if you are only creating your own measurement or using a predefined one, your measurements will be labeled with the name of this measurement (eg `eds.weight`). + +As said before, each matched measurement can be accessed via the `span._.value`. This gives you a `SimpleMeasurement` object with the following attributes : +- `value_range` ("<", "=" or ">") +- `value` +- `unit` +- `registry` (This attribute stores the entire unit config like the link between each unit, Its dimension like `length`, `quantity of matter`...) + +`SimpleMeasurement` objects are especially usefull when converting measurements to an other specified unit with the same dimension (eg densities stay densities). To do so, simply call your `SimpleMeasurement` followed by `.` + name of the usual unit abbreviation with `per` and `_` as separators (eg `object.kg_per_dm3`, `mol_per_l`, `g_per_cm2`). + +Moreover, for now, `SimpleMeasurement` objects can be manipulated with the following operations: +- compared with an other `SimpleMeasurement` object with the same dimension with automatic conversion (eg a density in kg_per_m3 and a density in g_per_l) +- summed with an other `SimpleMeasurement` object with the same dimension with automatic conversion +- substracted with an other `SimpleMeasurement` object with the same dimension with automatic conversion + +Note that for all operations listed above, different `value_range` attributes between two units do not matter: by default, the `value_range` of the first measurement is kept. + +Below is a complete example on a use case where we want to extract size, weigth and bmi measurements a simple text. + ```python import spacy @@ -77,7 +103,7 @@ str(measurements[4]._.value.kg_per_m2) ## Custom measurement -You can declare custom measurements by changing the patterns +You can declare custom measurements by changing the patterns. ```python import spacy @@ -114,21 +140,24 @@ nlp.add_pipe( ## Declared extensions The `eds.measurements` pipeline declares a single [spaCy extension](https://spacy.io/usage/processing-pipelines#custom-components-attributes) on the `Span` object, -the `value` attribute that is a `Measurement` instance. +the `value` attribute that is a `SimpleMeasurement` instance. ## Configuration The pipeline can be configured using the following parameters : -| Parameter | Explanation | Default | -| ----------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| `measurements` | A list or dict of the measurements to extract | `["eds.size", "eds.weight", "eds.angle"]` | -| `units_config` | A dict describing the units with lexical patterns, dimensions, scales, ... | ... | -| `number_terms` | A dict describing the textual forms of common numbers | ... | -| `stopwords` | A list of stopwords that do not matter when placed between a unitless trigger | ... | -| `unit_divisors` | A list of terms used to divide two units (like: m / s) | ... | -| `ignore_excluded` | Whether to ignore excluded tokens for matching | `False` | -| `attr` | spaCy attribute to match on, eg `NORM` or `TEXT` | `"NORM"` | +| Parameter | Explanation | Default | +| ------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `measurements` | A list or dict of the measurements to extract | `None` # Extract measurements from all units | +| `units_config` | A dict describing the units with lexical patterns, dimensions, scales, ... | ... # Config of mostly all commonly used units | +| `number_terms` | A dict describing the textual forms of common numbers | ... # Config of mostly all commonly used textual forms of common numbers | +| `value_range_terms` | A dict describing the textual forms of ranges ("<", "=" or ">") | ... # Config of mostly all commonly used range terms | +| `stopwords_unitless` | A list of stopwords that do not matter when placed between a unitless trigger | `["par", "sur", "de", "a", ":", ",", "et"]` | +| `stopwords_measure_unit` | A list of stopwords that do not matter when placed between a measure and a unit | `["|", "¦", "…", "."]` | +| `measure_before_unit` | A bool to tell if the numerical value is usually placed before the unit | `["par", "sur", "de", "a", ":", ",", "et"]` | +| `unit_divisors` | A list of terms used to divide two units (like: m / s) | `["/", "par"]` | +| `ignore_excluded` | Whether to ignore excluded tokens for matching | `False` | +| `attr` | spaCy attribute to match on, eg `NORM` or `TEXT` | `"NORM"` | ## Authors and citation diff --git a/edsnlp/pipelines/misc/measurements/factory.py b/edsnlp/pipelines/misc/measurements/factory.py index 2e9aefc07..d6e5deff6 100644 --- a/edsnlp/pipelines/misc/measurements/factory.py +++ b/edsnlp/pipelines/misc/measurements/factory.py @@ -15,9 +15,12 @@ ignore_excluded=True, units_config=patterns.units_config, number_terms=patterns.number_terms, + value_range_terms=patterns.value_range_terms, unit_divisors=patterns.unit_divisors, measurements=None, - stopwords=patterns.stopwords, + stopwords_unitless=patterns.stopwords_unitless, + stopwords_measure_unit=patterns.stopwords_measure_unit, + measure_before_unit=False, ) @@ -29,7 +32,10 @@ def create_component( measurements: Optional[Union[Dict[str, MeasureConfig], List[str]]], units_config: Dict[str, UnitConfig], number_terms: Dict[str, List[str]], - stopwords: List[str], + value_range_terms: Dict[str, List[str]], + stopwords_unitless: List[str], + stopwords_measure_unit: List[str], + measure_before_unit: bool, unit_divisors: List[str], ignore_excluded: bool, attr: str, @@ -38,9 +44,12 @@ def create_component( nlp, units_config=units_config, number_terms=number_terms, + value_range_terms=value_range_terms, unit_divisors=unit_divisors, measurements=measurements, - stopwords=stopwords, + stopwords_unitless=stopwords_unitless, + stopwords_measure_unit=stopwords_measure_unit, + measure_before_unit=measure_before_unit, attr=attr, ignore_excluded=ignore_excluded, ) diff --git a/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/pipelines/misc/measurements/measurements.py index a80b88169..8bfa917e7 100644 --- a/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/pipelines/misc/measurements/measurements.py @@ -19,16 +19,15 @@ __all__ = ["MeasurementsMatcher"] -AFTER_SNIPPET_LIMIT = 6 +AFTER_SNIPPET_LIMIT = 8 BEFORE_SNIPPET_LIMIT = 10 class UnitConfig(TypedDict): - dim: str - degree: int scale: float terms: List[str] followed_by: Optional[str] = None + ui_decomposition: Dict[str, int] class UnitlessRange(TypedDict): @@ -81,13 +80,23 @@ def __getitem__(self, item) -> "SimpleMeasurement": class UnitRegistry: def __init__(self, config: Dict[str, UnitConfig]): + def generate_inverse_terms(unit_terms): + for unit_term in unit_terms: + yield "/" + unit_term + yield unit_term + "⁻¹" + yield unit_term + "-1" + self.config = {unicodedata.normalize("NFKC", k): v for k, v in config.items()} for unit, unit_config in list(self.config.items()): - if not unit.startswith("per_") and "per_" + unit not in unit_config: + if not unit.startswith("per_") and "per_" + unit not in self.config: self.config["per_" + unit] = { - "dim": unit_config["dim"], - "degree": -unit_config["degree"], "scale": 1 / unit_config["scale"], + "terms": list(generate_inverse_terms(unit_config["terms"])), + "followed_by": None, + "ui_decomposition": { + dim: -degree + for dim, degree in unit_config["ui_decomposition"].items() + }, } @lru_cache(maxsize=-1) @@ -96,13 +105,16 @@ def parse_unit(self, unit: str) -> Tuple[str, float]: scale = 1 for part in regex.split("(?) to their lexical variants + stopwords_unitless: List[str] A list of stopwords that do not matter when placed between a unitless - trigger - and a number + trigger and a number + stopwords_measure_unit: List[str] + A list of stopwords that do not matter when placed between a unit and a number + These stopwords do not matter only in one of the following pattern : + unit - stopwords - measure or measure - stopwords - unit, according to + measure_before_unit parameter. + measure_before_unit: bool + Set It True if the measure is generally before the unit, False in the other case. + This parameter will indicate if the stopwords in stopwords_measure_unit should + not matter in the unit - stopwords - measure patterns only (False) or in + the measure - stopwords - unit patterns only (True) unit_divisors: List[str] A list of terms used to divide two units (like: m / s) attr : str @@ -230,6 +287,7 @@ def __init__( ignore_excluded : bool Whether to exclude pollution patterns when matching in the text """ + self.all_measurements = True if measurements is None else False if measurements is None: measurements = common_measurements @@ -242,10 +300,31 @@ def __init__( self.regex_matcher = RegexMatcher(attr=attr, ignore_excluded=True) self.term_matcher = EDSPhraseMatcher(nlp.vocab, attr=attr, ignore_excluded=True) self.unitless_patterns: Dict[str, UnitlessPatternConfigWithName] = {} + self.value_range_label_hashes: Set[int] = set() self.unit_part_label_hashes: Set[int] = set() self.unitless_label_hashes: Set[int] = set() self.unit_followers: Dict[str, str] = {} self.measure_names: Dict[str, str] = {} + self.measure_before_unit = measure_before_unit + + # INTERVALS + self.regex_matcher.add( + "interval", + [r"-?\s*\d+(?:[.,]\d+)?\s*-\s*-?\s*\d+(?:[.,]\d+)?"], + ) + + # POWERS OF 10 + self.regex_matcher.add( + "pow10", + [ + r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)", + ], + ) + + # MEASUREMENT VALUE RANGES + for value_range, terms in value_range_terms.items(): + self.term_matcher.build_patterns(nlp, {value_range: terms}) + self.value_range_label_hashes.add(nlp.vocab.strings[value_range]) # NUMBER PATTERNS self.regex_matcher.add( @@ -253,6 +332,7 @@ def __init__( [ r"(? Iterable[Span]: unit_label_hashes.add(units[-1].label) current = [] last = None - if len(current) > 0 or unit_part.label_ != "per": - current.append(unit_part) + current.append(unit_part) last = unit_part end = next( @@ -352,7 +445,6 @@ def extract_units(self, term_matches: Iterable[Span]) -> Iterable[Span]: unit = "_".join(part.label_ for part in current[: end + 1]) units.append(Span(doc, current[0].start, current[end].end, unit)) unit_label_hashes.add(units[-1].label) - return units @classmethod @@ -490,6 +582,16 @@ def get_matches_before(i): return yield i - j, ent + # Return a float based on the measure (float) and the power of 10 extracted with regex (string) + def combine_measure_pow10(measure, pow10_text): + pow10 = int( + re.fullmatch( + r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)", + pow10_text, + ).group(1) + ) + return measure * 10**pow10 + # Make a pseudo sentence to query higher order patterns in the main loop # `offsets` is a mapping from matches indices (ie match n°i) to # char indices in the pseudo sentence @@ -497,10 +599,15 @@ def get_matches_before(i): doc, matches, { - self.nlp.vocab.strings["stopword"]: ",", + self.nlp.vocab.strings["stopword"]: "o", + self.nlp.vocab.strings["interval"]: ",", + self.nlp.vocab.strings["stopword_unitless"]: ",", + self.nlp.vocab.strings["stopword_measure_unit"]: "s", self.nlp.vocab.strings["number"]: "n", + self.nlp.vocab.strings["pow10"]: "p", **{name: "u" for name in unit_label_hashes}, **{name: "n" for name in self.number_label_hashes}, + **{name: "r" for name in self.value_range_label_hashes}, }, ) @@ -523,6 +630,29 @@ def get_matches_before(i): except ValueError: continue + # Link It to Its adjacent power if available + try: + pow10_idx, pow10_ent = next( + (j, ent) + for j, ent in get_matches_after(number_idx) + if ent.label == self.nlp.vocab.strings["pow10"] + ) + pseudo_sent = pseudo[offsets[number_idx] + 1 : offsets[pow10_idx]] + if re.fullmatch(r"[,o]*", pseudo_sent): + pow10_text = pow10_ent.text + value = combine_measure_pow10(value, pow10_text) + except: + pass + + # Check if the measurement is an =, < or > measurement + try: + if pseudo[offsets[number_idx] - 1] == "r": + value_range = matches[number_idx - 1][0].label_ + else: + value_range = "=" + except: + value_range = "=" + unit_idx = unit_text = unit_norm = None # Find the closest unit after the number @@ -537,10 +667,11 @@ def get_matches_before(i): pass # Try to pair the number with this next unit if the two are only separated - # by numbers and separators alternatively (as in [1][,] [2] [and] [3] cm) + # by numbers (with or without powers of 10) and separators + # (as in [1][,] [2] [and] [3] cm) try: pseudo_sent = pseudo[offsets[number_idx] + 1 : offsets[unit_idx]] - if not re.fullmatch(r"(,n)*", pseudo_sent): + if not re.fullmatch(r"[,o]*p?([,o]n?p?)*", pseudo_sent): unit_text, unit_norm = None, None except TypeError: pass @@ -550,7 +681,10 @@ def get_matches_before(i): if unit_norm is None and number_idx - 1 in matched_unit_indices: try: unit_before = matches[number_idx - 1][0] - if unit_before.end == number.start: + if ( + unit_before.end == number.start + and pseudo[offsets[number_idx] - 2] == "n" + ): unit_norm = self.unit_followers[unit_before.label_] except (KeyError, AttributeError, IndexError): pass @@ -566,22 +700,80 @@ def get_matches_before(i): ) unit_norm = None if re.fullmatch( - r"[,n]*", + r"[,onr]*", pseudo[offsets[unitless_idx] + 1 : offsets[number_idx]], ): unitless_pattern = self.unitless_patterns[unitless_text.label_] unit_norm = next( scope["unit"] for scope in unitless_pattern["ranges"] - if ("min" not in scope or value >= scope["min"]) - and ("max" not in scope or value < scope["max"]) + if ( + "min" not in scope + or value >= scope["min"] + or value_range == ">" + ) + and ( + "max" not in scope + or value < scope["max"] + or value_range == "<" + ) ) except StopIteration: pass - # Otherwise, skip this number + # If no unit was matched, take the nearest unit only if + # It is separated by a stopword from stopwords_measure_unit and / or a value_range_term + # Take It before or after the measure according to if not unit_norm: - continue + try: + if not self.measure_before_unit: + (unit_before_idx, unit_before_text) = next( + (j, e) + for j, e in get_matches_before(number_idx) + if e.label in unit_label_hashes + ) + if re.fullmatch( + r"[sor]*", + pseudo[offsets[unit_before_idx] + 1 : offsets[number_idx]], + ): + unit_norm = unit_before_text.label_ + # Check if there is a power of 10 before the unit + if ( + offsets[unit_before_idx] >= 1 + and pseudo[offsets[unit_before_idx] - 1] == "p" + ): + pow10_text = matches[unit_before_idx - 1][0].text + value = combine_measure_pow10(value, pow10_text) + else: + (unit_after_idx, unit_after_text) = next( + (j, e) + for j, e in get_matches_after(number_idx) + if e.label in unit_label_hashes + ) + if re.fullmatch( + r"[sop]*", + pseudo[offsets[number_idx] + 1 : offsets[unit_after_idx]], + ): + unit_norm = unit_after_text.label_ + # Check if there is a power of 10 between the measure and the unit without considering + # the one that we have already considered at the beginning of thos program + try: + (pow10_idx, pow10_ent) = next( + (j, e) + for j, e in get_matches_before(unit_after_idx) + if e.label == self.nlp.vocab.strings["pow10"] + ) + if pow10_idx > pseudo[offsets[number_idx] + 1]: + pow10_text = pow10_ent.text + value = combine_measure_pow10(value, pow10_text) + except: + pass + except: + pass + + # Otherwise, set the unit as no_unit + if not unit_norm: + unit_norm = "nounit" # Compute the final entity if unit_text and unit_text.end == number.start: @@ -597,13 +789,20 @@ def get_matches_before(i): except KeyError: continue - # If the measure was not requested, dismiss it - # Otherwise, relabel the entity and create the value attribute - if dims not in self.measure_names: - continue - - ent._.value = SimpleMeasurement(value, unit_norm, self.unit_registry) - ent.label_ = self.measure_names[dims] + if self.all_measurements: + ent._.value = SimpleMeasurement( + value_range, value, unit_norm, self.unit_registry + ) + ent.label_ = "eds.measurement" + else: + # If the measure was not requested, dismiss it + # Otherwise, relabel the entity and create the value attribute + if dims not in self.measure_names: + continue + ent._.value = SimpleMeasurement( + value_range, value, unit_norm, self.unit_registry + ) + ent.label_ = self.measure_names[dims] measurements.append(ent) diff --git a/edsnlp/pipelines/misc/measurements/patterns.py b/edsnlp/pipelines/misc/measurements/patterns.py index b2f0830a8..209882514 100644 --- a/edsnlp/pipelines/misc/measurements/patterns.py +++ b/edsnlp/pipelines/misc/measurements/patterns.py @@ -31,12 +31,88 @@ "1000": ["mille", "milles"], } + +value_range_terms = { + "<": ["<", "<=", "inferieure a", "inferieur a", "inf a", "inf"], + ">": [">", ">=", "superieure a", "sup a", "sup a", "sup"], +} + + +common_measurements = { + "eds.weight": { + "unit": "kg", + "unitless_patterns": [ + { + "terms": ["poids", "poid", "pese", "pesant", "pesait", "pesent"], + "ranges": [ + {"min": 0, "max": 200, "unit": "kg"}, + {"min": 200, "unit": "g"}, + ], + } + ], + }, + "eds.size": { + "unit": "m", + "unitless_patterns": [ + { + "terms": [ + "mesure", + "taille", + "mesurant", + "mesurent", + "mesurait", + "mesuree", + "hauteur", + "largeur", + "longueur", + ], + "ranges": [ + {"min": 0, "max": 3, "unit": "m"}, + {"min": 3, "unit": "cm"}, + ], + } + ], + }, + "eds.bmi": { + "unit": "kg_per_m2", + "unitless_patterns": [ + {"terms": ["imc", "bmi"], "ranges": [{"unit": "kg_per_m2"}]} + ], + }, + "eds.volume": {"unit": "m3", "unitless_patterns": []}, +} + + +unit_divisors = ["/", "par"] + + +stopwords_unitless = ["par", "sur", "de", "a", ":", ",", "et"] + + +stopwords_measure_unit = ["|", "¦", "…", "."] + + units_config = { - # Lengths - "µm": { - "dim": "length", - "degree": 1, - "scale": 1e-4, + "fm": { + "scale": 1e-15, + "terms": ["femtometre", "femtometres", "femto-metre", "femto-metres", "fm"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "pm": { + "scale": 1e-12, + "terms": ["picometre", "picometres", "pico-metre", "pico-metres", "pm"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "nm": { + "scale": 1e-09, + "terms": ["nanometre", "nanometres", "nano-metre", "nano-metres", "nm"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "μm": { + "scale": 1e-06, "terms": [ "micrometre", "micrometres", @@ -46,40 +122,106 @@ "um", ], "followed_by": None, + "ui_decomposition": {"length": 1}, }, "mm": { - "dim": "length", - "degree": 1, - "scale": 1e-1, + "scale": 0.001, "terms": ["millimetre", "millimetres", "milimetre", "milimetres", "mm"], "followed_by": None, + "ui_decomposition": {"length": 1}, }, "cm": { - "dim": "length", - "degree": 1, - "scale": 1e0, + "scale": 0.01, "terms": ["centimetre", "centimetres", "cm"], "followed_by": None, + "ui_decomposition": {"length": 1}, }, "dm": { - "dim": "length", - "degree": 1, - "scale": 1e1, + "scale": 0.1, "terms": ["decimetre", "decimetres", "dm"], "followed_by": None, + "ui_decomposition": {"length": 1}, }, "m": { - "dim": "length", - "degree": 1, - "scale": 1e2, + "scale": 1.0, "terms": ["metre", "metres", "m"], "followed_by": "cm", + "ui_decomposition": {"length": 1}, + }, + "dam": { + "scale": 10.0, + "terms": ["decametre", "decametres", "dam"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "hm": { + "scale": 100.0, + "terms": ["hectometre", "hectometres", "hm"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "km": { + "scale": 1000.0, + "terms": ["kilometre", "kilometres", "km"], + "followed_by": "m", + "ui_decomposition": {"length": 1}, + }, + "fg": { + "scale": 1e-18, + "terms": [ + "femtogramme", + "femtogrammes", + "femto-gramme", + "femto-grammes", + "fgr", + "fg", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "pg": { + "scale": 1e-15, + "terms": [ + "picogramme", + "picogrammes", + "pico-gramme", + "pico-grammes", + "pgr", + "pg", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "ng": { + "scale": 1e-12, + "terms": [ + "nanogramme", + "nanogrammes", + "nano-gramme", + "nano-grammes", + "ngr", + "ng", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "µg": { + "scale": 1e-9, + "terms": [ + "microgramme", + "microgrammes", + "micro-gramme", + "micro-grammes", + "µgr", + "µg", + "ugr", + "ug", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1}, }, - # Weights "mg": { - "dim": "mass", - "degree": 1, - "scale": 1e0, + "scale": 1e-6, "terms": [ "milligramme", "miligramme", @@ -89,489 +231,2114 @@ "mg", ], "followed_by": None, + "ui_decomposition": {"mass": 1}, }, "cg": { - "dim": "mass", - "degree": 1, - "scale": 1e1, + "scale": 1e-5, "terms": ["centigramme", "centigrammes", "cg", "cgr"], "followed_by": None, + "ui_decomposition": {"mass": 1}, }, "dg": { - "dim": "mass", - "degree": 1, - "scale": 1e2, + "scale": 1e-4, "terms": ["decigramme", "decigrammes", "dgr", "dg"], "followed_by": None, + "ui_decomposition": {"mass": 1}, }, "g": { - "dim": "mass", - "degree": 1, - "scale": 1e3, + "scale": 0.001, "terms": ["gramme", "grammes", "gr", "g"], "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "dag": { + "scale": 0.01, + "terms": ["decagramme", "decagrammes", "dagr", "dag"], + "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "hg": { + "scale": 0.1, + "terms": ["hectogramme", "hectogrammes", "hgr", "hg"], + "followed_by": None, + "ui_decomposition": {"mass": 1}, }, "kg": { - "dim": "mass", - "degree": 1, - "scale": 1e6, + "scale": 1.0, "terms": ["kilo", "kilogramme", "kilogrammes", "kgr", "kg"], "followed_by": "g", + "ui_decomposition": {"mass": 1}, + }, + "fs": { + "scale": 1e-15, + "terms": [ + "femtoseconde", + "femtosecondes", + "femto-seconde", + "femto-secondes", + "fs", + ], + "followed_by": None, + "ui_decomposition": {"time": 1}, + }, + "ps": { + "scale": 1e-12, + "terms": ["picoseconde", "picosecondes", "pico-seconde", "pico-secondes", "ps"], + "followed_by": None, + "ui_decomposition": {"time": 1}, }, - # Durations - "second": { - "dim": "time", - "degree": 1, + "ns": { + "scale": 1e-09, + "terms": ["nanoseconde", "nanosecondes", "nano-seconde", "nano-secondes", "ns"], + "followed_by": None, + "ui_decomposition": {"time": 1}, + }, + "µs": { + "scale": 1e-06, + "terms": [ + "microseconde", + "microsecondes", + "micro-seconde", + "micro-secondes", + "µs", + "us", + ], + "followed_by": None, + "ui_decomposition": {"time": 1}, + }, + "ms": { + "scale": 0.001, + "terms": ["milliseconde", "millisecondes", "miliseconde", "milisecondes", "ms"], + "followed_by": None, + "ui_decomposition": {"time": 1}, + }, + "s": { "scale": 1, "terms": ["seconde", "secondes", "s"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, - "minute": { - "dim": "time", - "degree": 1, + "min": { "scale": 60, "terms": ["mn", "min", "minute", "minutes"], "followed_by": "second", + "ui_decomposition": {"time": 1}, }, - "hour": { - "dim": "time", - "degree": 1, + "h": { "scale": 3600, - "terms": ["heure", "h"], + "terms": ["heure", "heures", "h"], "followed_by": "minute", + "ui_decomposition": {"time": 1}, }, "day": { - "dim": "time", - "degree": 1, - "scale": 3600 * 1, + "scale": 3600, "terms": ["jour", "jours", "j"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, "month": { - "dim": "time", - "degree": 1, - "scale": 3600 * 30.4167, + "scale": 109500.12, "terms": ["mois"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, "week": { - "dim": "time", - "degree": 1, - "scale": 3600 * 7, + "scale": 25200, "terms": ["semaine", "semaines"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, "year": { - "dim": "time", - "degree": 1, - "scale": 3600 * 365.25, + "scale": 1314900.0, "terms": ["an", "année", "ans", "années"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, - # Angle "arc-second": { - "dim": "time", - "degree": 1, - "scale": 2 / 60.0, + "scale": 0.03333333333333333, "terms": ['"', "''"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, "arc-minute": { - "dim": "time", - "degree": 1, "scale": 2, "terms": ["'"], "followed_by": "arc-second", + "ui_decomposition": {"time": 1}, }, "degree": { - "dim": "time", - "degree": 1, "scale": 120, "terms": ["degre", "°", "deg"], "followed_by": "arc-minute", + "ui_decomposition": {"time": 1}, }, - # Temperature "celcius": { - "dim": "temperature", - "degree": 1, "scale": 1, "terms": ["°C", "° celcius", "celcius"], "followed_by": None, + "ui_decomposition": {"temperature": 1}, + }, + "fl": { + "scale": 1e-18, + "terms": ["femtolitre", "femtolitres", "femto-litre", "femto-litres", "fl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "pl": { + "scale": 1e-15, + "terms": ["picolitre", "picolitres", "pico-litre", "pico-litres", "pl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "nl": { + "scale": 1e-12, + "terms": ["nanolitre", "nanolitres", "nano-litre", "nano-litres", "nl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "µl": { + "scale": 1e-09, + "terms": [ + "microlitre", + "microlitres", + "micro-litre", + "micro-litres", + "µl", + "ul", + ], + "followed_by": None, + "ui_decomposition": {"length": 3}, }, - # Volumes "ml": { - "dim": "length", - "degree": 3, - "scale": 1e0, + "scale": 1e-06, "terms": ["mililitre", "millilitre", "mililitres", "millilitres", "ml"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "cl": { - "dim": "length", - "degree": 3, - "scale": 1e1, + "scale": 1e-05, "terms": ["centilitre", "centilitres", "cl"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "dl": { - "dim": "length", - "degree": 3, - "scale": 1e2, + "scale": 0.0001, "terms": ["decilitre", "decilitres", "dl"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "l": { - "dim": "length", - "degree": 3, - "scale": 1e3, - "terms": ["litre", "litres", "l", "dm3"], + "scale": 0.001, + "terms": ["litre", "litres", "l"], "followed_by": "ml", + "ui_decomposition": {"length": 3}, + }, + "dal": { + "scale": 0.01, + "terms": ["decalitre", "decalitres", "dal"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "hl": { + "scale": 0.1, + "terms": ["hectolitre", "hectolitres", "hl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "kl": { + "scale": 1.0, + "terms": ["kilolitre", "kilolitres", "kl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, }, "cac": { - "dim": "length", - "degree": 3, - "scale": 5e-3, + "scale": 5e-09, "terms": ["cac", "c.a.c", "cuillere à café", "cuillères à café"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "goutte": { - "dim": "length", - "degree": 3, - "scale": 5e-5, + "scale": 5e-11, "terms": ["gt", "goutte"], "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "fm3": { + "scale": 1e-45, + "terms": ["fm3", "fm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "pm3": { + "scale": 1e-36, + "terms": ["pm3", "pm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "nm3": { + "scale": 1e-27, + "terms": ["nm3", "nm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "µm3": { + "scale": 1e-18, + "terms": ["um3", "um³", "µm3", "µm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, }, "mm3": { - "dim": "length", - "degree": 3, - "scale": 1e-3, + "scale": 1e-09, "terms": ["mm3", "mm³"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "cm3": { - "dim": "length", - "degree": 3, - "scale": 1e0, + "scale": 1e-06, "terms": ["cm3", "cm³", "cc"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "dm3": { - "dim": "length", - "degree": 3, - "scale": 1e3, + "scale": 0.001, "terms": ["dm3", "dm³"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "m3": { - "dim": "length", - "degree": 3, - "scale": 1e6, + "scale": 1, "terms": ["m3", "m³"], "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "dam3": { + "scale": 1000.0, + "terms": ["dam3", "dam³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "hm3": { + "scale": 1000000.0, + "terms": ["hm3", "hm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "km3": { + "scale": 1000000000.0, + "terms": ["km3", "km³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, }, - # Surfaces - "µm2": { - "dim": "length", - "degree": 2, - "scale": 1e-8, - "terms": ["µm2", "µm²"], + "fm2": { + "scale": 1e-30, + "terms": ["fm2", "fm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, + }, + "pm2": { + "scale": 1e-24, + "terms": ["fm2", "fm²"], + "followed_by": None, + "ui_decomposition": {"length": 2}, + }, + "nm2": { + "scale": 1e-18, + "terms": ["nm2", "nm²"], + "followed_by": None, + "ui_decomposition": {"length": 2}, + }, + "μm2": { + "scale": 1e-12, + "terms": ["µm2", "µm²", "um2", "um²"], + "followed_by": None, + "ui_decomposition": {"length": 2}, }, "mm2": { - "dim": "length", - "degree": 2, - "scale": 1e-2, + "scale": 1e-06, "terms": ["mm2", "mm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, "cm2": { - "dim": "length", - "degree": 2, - "scale": 1e0, + "scale": 0.0001, "terms": ["cm2", "cm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, "dm2": { - "dim": "length", - "degree": 2, - "scale": 1e2, + "scale": 0.01, "terms": ["dm2", "dm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, "m2": { - "dim": "length", - "degree": 2, - "scale": 1e4, + "scale": 1.0, "terms": ["m2", "m²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, - # International units - "mui": { - "dim": "ui", - "degree": 1, - "scale": 1e0, - "terms": ["mui", "m ui"], + "dam2": { + "scale": 100.0, + "terms": ["dam2", "dam²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, - "dui": { - "dim": "ui", - "degree": 1, - "scale": 1e1, - "terms": ["dui", "d ui"], + "hm2": { + "scale": 10000.0, + "terms": ["hm2", "hm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, - "cui": { - "dim": "ui", - "degree": 1, - "scale": 1e2, - "terms": ["cui", "c ui"], + "km2": { + "scale": 1000000.0, + "terms": ["km2", "km²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, - "ui": { - "dim": "ui", - "degree": 1, - "scale": 1e3, - "terms": ["ui"], + "fui": { + "scale": 1e-15, + "terms": ["fui", "f ui", "fu", "f u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - # Inverse - "per_µm": { - "dim": "length", - "degree": -1, - "scale": 1e4, - "terms": ["µm-1"], + "pui": { + "scale": 1e-12, + "terms": ["pui", "p ui", "pu", "p u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_mm": { - "dim": "length", - "degree": -1, - "scale": 1e1, - "terms": ["mm-1"], + "nui": { + "scale": 1e-09, + "terms": ["nui", "n ui", "nu", "n u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_cm": { - "dim": "length", - "degree": -1, - "scale": 1e0, - "terms": ["cm-1"], + "µui": { + "scale": 1e-06, + "terms": ["µui", "µ ui", "uui", "u ui", "µu", "µ u", "uu", "u u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_dm": { - "dim": "length", - "degree": -1, - "scale": 1e-1, - "terms": ["dm-1"], + "mui": { + "scale": 0.001, + "terms": ["mui", "m ui", "mu", "m u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_m": { - "dim": "length", - "degree": -1, - "scale": 1e-3, - "terms": ["m-1"], + "cui": { + "scale": 0.01, + "terms": ["cui", "c ui", "cu", "c u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_mg": { - "dim": "mass", - "degree": -1, - "scale": 1e-0, - "terms": ["mgr-1", "mg-1", "mgr⁻¹", "mg⁻¹"], + "dui": { + "scale": 0.1, + "terms": ["dui", "d ui", "du", "d u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_cg": { - "dim": "mass", - "degree": -1, - "scale": 1e-1, - "terms": ["cg-1", "cgr-1", "cg⁻¹", "cgr⁻¹"], + "ui": { + "scale": 1.0, + "terms": ["ui", "u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_dg": { - "dim": "mass", - "degree": -1, - "scale": 1e-2, - "terms": ["dgr-1", "dg-1", "dgr⁻¹", "dg⁻¹"], + "daui": { + "scale": 10.0, + "terms": ["daui", "dau"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_g": { - "dim": "mass", - "degree": -1, - "scale": 1e-3, - "terms": ["gr-1", "g-1", "gr⁻¹", "g⁻¹"], + "hui": { + "scale": 100.0, + "terms": ["hui", "hu"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_kg": { - "dim": "mass", - "degree": -1, - "scale": 1e-6, - "terms": ["kgr-1", "kg-1", "kgr⁻¹", "kg⁻¹"], + "kui": { + "scale": 1000.0, + "terms": ["kui", "ku"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_ml": { - "dim": "length", - "degree": -3, - "scale": 1e-0, - "terms": ["ml-1", "ml⁻¹"], + "fmol": { + "scale": 1e-15, + "terms": ["fmol", "f mol", "fmole", "f mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_cl": { - "dim": "length", - "degree": -3, - "scale": 1e-1, - "terms": ["cl-1", "cl⁻¹"], + "pmol": { + "scale": 1e-12, + "terms": ["pmol", "p mol", "pmole", "p mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_dl": { - "dim": "length", - "degree": -3, - "scale": 1e-2, - "terms": ["dl-1", "dl⁻¹"], + "nmol": { + "scale": 1e-09, + "terms": ["nmol", "n mol", "nmole", "n mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_l": { - "dim": "length", - "degree": -3, - "scale": 1e-3, - "terms": ["l-1", "l⁻¹"], + "µmol": { + "scale": 1e-06, + "terms": ["µmol", "µ mol", "umole", "u mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_mm3": { - "dim": "length", - "degree": -3, - "scale": 1e3, - "terms": ["mm-3", "mm⁻³"], + "mmol": { + "scale": 0.001, + "terms": ["mmol", "m mol", "mmole", "m mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_cm3": { - "dim": "length", - "degree": -3, - "scale": 1e-0, - "terms": ["cm-3", "cm⁻³", "cc-1", "cc⁻¹"], + "cmol": { + "scale": 0.01, + "terms": ["cmol", "c mol", "cmole", "c mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_dm3": { - "dim": "length", - "degree": -3, - "scale": 1e-3, - "terms": ["dm-3", "dm⁻³"], + "dmol": { + "scale": 0.1, + "terms": ["dmol", "d mol", "dmole", "d mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_m3": { - "dim": "length", - "degree": -3, - "scale": 1e-6, - "terms": ["m-3", "m⁻³"], + "mol": { + "scale": 1.0, + "terms": ["mol", "mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_mui": { - "dim": "ui", - "degree": -1, - "scale": 1e-0, - "terms": ["mui-1", "mui⁻¹"], + "damol": { + "scale": 10.0, + "terms": ["damol", "da mol", "damole", "da mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_dui": { - "dim": "ui", - "degree": -1, - "scale": 1e-1, - "terms": ["dui-1", "dui⁻¹"], + "hmol": { + "scale": 100.0, + "terms": ["hmol", "h mol", "hmole", "h mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_cui": { - "dim": "ui", - "degree": -1, - "scale": 1e-2, - "terms": ["cui-1", "cui⁻¹"], + "kmol": { + "scale": 1000.0, + "terms": ["kmol", "k mol", "kmole", "k mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_ui": { - "dim": "ui", - "degree": -1, - "scale": 1e-3, - "terms": ["ui-1", "ui⁻¹"], + "per_fm": { + "scale": 1.0e15, + "terms": [ + "/femtometre", + "femtometre⁻¹", + "femtometre-1", + "/femtometres", + "femtometres⁻¹", + "femtometres-1", + "/femto-metre", + "femto-metre⁻¹", + "femto-metre-1", + "/femto-metres", + "femto-metres⁻¹", + "femto-metres-1", + "/fm", + "fm⁻¹", + "fm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - # Surfaces - "per_µm2": { - "dim": "length", - "degree": -2, - "scale": 1e8, - "terms": ["µm-2", "µm⁻²"], + "per_pm": { + "scale": 1.0e12, + "terms": [ + "/picometre", + "picometre⁻¹", + "picometre-1", + "/picometres", + "picometres⁻¹", + "picometres-1", + "/pico-metre", + "pico-metre⁻¹", + "pico-metre-1", + "/pico-metres", + "pico-metres⁻¹", + "pico-metres-1", + "/pm", + "pm⁻¹", + "pm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "per_mm2": { - "dim": "length", - "degree": -2, - "scale": 1e2, - "terms": ["mm-2", "mm⁻²"], + "per_nm": { + "scale": 1.0e9, + "terms": [ + "/nanometre", + "nanometre⁻¹", + "nanometre-1", + "/nanometres", + "nanometres⁻¹", + "nanometres-1", + "/nano-metre", + "nano-metre⁻¹", + "nano-metre-1", + "/nano-metres", + "nano-metres⁻¹", + "nano-metres-1", + "/nm", + "nm⁻¹", + "nm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "per_cm2": { - "dim": "length", - "degree": -2, - "scale": 1e-0, - "terms": ["cm-2", "cm⁻²"], + "per_μm": { + "scale": 1.0e6, + "terms": [ + "/micrometre", + "micrometre⁻¹", + "micrometre-1", + "/micrometres", + "micrometres⁻¹", + "micrometres-1", + "/micro-metre", + "micro-metre⁻¹", + "micro-metre-1", + "/micrometres", + "micrometres⁻¹", + "micrometres-1", + "/µm", + "µm⁻¹", + "µm-1", + "/um", + "um⁻¹", + "um-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "per_dm2": { - "dim": "length", - "degree": -2, - "scale": 1e-2, - "terms": ["dm-2", "dm⁻²"], + "per_mm": { + "scale": 1000.0, + "terms": [ + "/millimetre", + "millimetre⁻¹", + "millimetre-1", + "/millimetres", + "millimetres⁻¹", + "millimetres-1", + "/milimetre", + "milimetre⁻¹", + "milimetre-1", + "/milimetres", + "milimetres⁻¹", + "milimetres-1", + "/mm", + "mm⁻¹", + "mm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "per_m2": { - "dim": "length", - "degree": -2, - "scale": 1e-4, - "terms": ["m-2", "m⁻²"], + "per_cm": { + "scale": 100.0, + "terms": [ + "/centimetre", + "centimetre⁻¹", + "centimetre-1", + "/centimetres", + "centimetres⁻¹", + "centimetres-1", + "/cm", + "cm⁻¹", + "cm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, -} - - -common_measurements = { - "eds.weight": { - "unit": "kg", - "unitless_patterns": [ - { - "terms": ["poids", "poid", "pese", "pesant", "pesait", "pesent"], - "ranges": [ - {"min": 0, "max": 200, "unit": "kg"}, - {"min": 200, "unit": "g"}, - ], - } + "per_dm": { + "scale": 10.0, + "terms": [ + "/decimetre", + "decimetre⁻¹", + "decimetre-1", + "/decimetres", + "decimetres⁻¹", + "decimetres-1", + "/dm", + "dm⁻¹", + "dm-1", ], + "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "eds.size": { - "unit": "m", - "unitless_patterns": [ - { - "terms": [ - "mesure", - "taille", - "mesurant", - "mesurent", - "mesurait", - "mesuree", - "hauteur", - "largeur", - "longueur", - ], - "ranges": [ - {"min": 0, "max": 3, "unit": "m"}, - {"min": 3, "unit": "cm"}, - ], - } + "per_m": { + "scale": 1.0, + "terms": [ + "/metre", + "metre⁻¹", + "metre-1", + "/metres", + "metres⁻¹", + "metres-1", + "/m", + "m⁻¹", + "m-1", ], + "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "eds.bmi": { - "unit": "kg_per_m2", - "unitless_patterns": [ - {"terms": ["imc", "bmi"], "ranges": [{"unit": "kg_per_m2"}]} + "per_dam": { + "scale": 0.1, + "terms": [ + "/decametre", + "decametre⁻¹", + "decametre-1", + "/decametres", + "decametres⁻¹", + "decametres-1", + "/dam", + "dam⁻¹", + "dam-1", ], + "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "eds.volume": {"unit": "m3", "unitless_patterns": []}, -} - -unit_divisors = ["/", "par"] - -stopwords = ["par", "sur", "de", "a", ":", ",", "et"] + "per_hm": { + "scale": 0.01, + "terms": [ + "/hectometre", + "hectometre⁻¹", + "hectometre-1", + "/hectometres", + "hectometres⁻¹", + "hectometres-1", + "/hm", + "hm⁻¹", + "hm-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -1}, + }, + "per_km": { + "scale": 0.001, + "terms": [ + "/kilometre", + "kilometre⁻¹", + "kilometre-1", + "/kilometres", + "kilometres⁻¹", + "kilometres-1", + "/km", + "km⁻¹", + "km-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -1}, + }, + "per_fg": { + "scale": 1.0e18, + "terms": [ + "/femtogramme", + "femtogramme⁻¹", + "femtogramme-1", + "/femtogrammes", + "femtogrammes⁻¹", + "femtogrammes-1", + "/femto-gramme", + "femto-gramme⁻¹", + "femto-gramme-1", + "/femto-grammes", + "femto-grammes⁻¹", + "femto-grammes-1", + "/fgr", + "fgr⁻¹", + "fgr-1", + "/fg", + "fg⁻¹", + "fg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_pg": { + "scale": 1.0e15, + "terms": [ + "/picogramme", + "picogramme⁻¹", + "picogramme-1", + "/picogrammes", + "picogrammes⁻¹", + "picogrammes-1", + "/pico-gramme", + "pico-gramme⁻¹", + "pico-gramme-1", + "/pico-grammes", + "pico-grammes⁻¹", + "pico-grammes-1", + "/pgr", + "pgr⁻¹", + "pgr-1", + "/pg", + "pg⁻¹", + "pg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_ng": { + "scale": 1.0e12, + "terms": [ + "/nanogramme", + "nanogramme⁻¹", + "nanogramme-1", + "/nanogrammes", + "nanogrammes⁻¹", + "nanogrammes-1", + "/nano-gramme", + "nano-gramme⁻¹", + "nano-gramme-1", + "/nano-grammes", + "nano-grammes⁻¹", + "nano-grammes-1", + "/ngr", + "ngr⁻¹", + "ngr-1", + "/ng", + "ng⁻¹", + "ng-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_µg": { + "scale": 1.0e9, + "terms": [ + "/microgramme", + "microgramme⁻¹", + "microgramme-1", + "/microgrammes", + "microgrammes⁻¹", + "microgrammes-1", + "/micro-gramme", + "micro-gramme⁻¹", + "micro-gramme-1", + "/micro-grammes", + "micro-grammes⁻¹", + "micro-grammes-1", + "/µgr", + "µgr⁻¹", + "µgr-1", + "/µg", + "µg⁻¹", + "µg-1", + "/ugr", + "ugr⁻¹", + "ugr-1", + "/ug", + "ug⁻¹", + "ug-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_mg": { + "scale": 1.0e6, + "terms": [ + "/milligramme", + "milligramme⁻¹", + "milligramme-1", + "/miligramme", + "miligramme⁻¹", + "miligramme-1", + "/milligrammes", + "milligrammes⁻¹", + "milligrammes-1", + "/miligrammes", + "miligrammes⁻¹", + "miligrammes-1", + "/mgr", + "mgr⁻¹", + "mgr-1", + "/mg", + "mg⁻¹", + "mg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_cg": { + "scale": 1.0e5, + "terms": [ + "/centigramme", + "centigramme⁻¹", + "centigramme-1", + "/centigrammes", + "centigrammes⁻¹", + "centigrammes-1", + "/cg", + "cg⁻¹", + "cg-1", + "/cgr", + "cgr⁻¹", + "cgr-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_dg": { + "scale": 1.0e4, + "terms": [ + "/decigramme", + "decigramme⁻¹", + "decigramme-1", + "/decigrammes", + "decigrammes⁻¹", + "decigrammes-1", + "/dgr", + "dgr⁻¹", + "dgr-1", + "/dg", + "dg⁻¹", + "dg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_g": { + "scale": 1000.0, + "terms": [ + "/gramme", + "gramme⁻¹", + "gramme-1", + "/grammes", + "grammes⁻¹", + "grammes-1", + "/gr", + "gr⁻¹", + "gr-1", + "/g", + "g⁻¹", + "g-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_dag": { + "scale": 100.0, + "terms": [ + "/decagramme", + "decagramme⁻¹", + "decagramme-1", + "/decagrammes", + "decagrammes⁻¹", + "decagrammes-1", + "/dagr", + "dagr⁻¹", + "dagr-1", + "/dag", + "dag⁻¹", + "dag-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_hg": { + "scale": 10.0, + "terms": [ + "/hectogramme", + "hectogramme⁻¹", + "hectogramme-1", + "/hectogrammes", + "hectogrammes⁻¹", + "hectogrammes-1", + "/hgr", + "hgr⁻¹", + "hgr-1", + "/hg", + "hg⁻¹", + "hg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_kg": { + "scale": 1.0, + "terms": [ + "/kilo", + "kilo⁻¹", + "kilo-1", + "/kilogramme", + "kilogramme⁻¹", + "kilogramme-1", + "/kilogrammes", + "kilogrammes⁻¹", + "kilogrammes-1", + "/kgr", + "kgr⁻¹", + "kgr-1", + "/kg", + "kg⁻¹", + "kg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_fs": { + "scale": 1.0e15, + "terms": [ + "/femtoseconde", + "femtoseconde⁻¹", + "femtoseconde-1", + "/femtosecondes", + "femtosecondes⁻¹", + "femtosecondes-1", + "/femto-seconde", + "femto-seconde⁻¹", + "femto-seconde-1", + "/femto-secondes", + "femto-secondes⁻¹", + "femto-secondes-1", + "/fs", + "fs⁻¹", + "fs-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_ps": { + "scale": 1.0e12, + "terms": [ + "/picoseconde", + "picoseconde⁻¹", + "picoseconde-1", + "/picosecondes", + "picosecondes⁻¹", + "picosecondes-1", + "/pico-seconde", + "pico-seconde⁻¹", + "pico-seconde-1", + "/pico-secondes", + "pico-secondes⁻¹", + "pico-secondes-1", + "/ps", + "ps⁻¹", + "ps-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_ns": { + "scale": 1.0e9, + "terms": [ + "/nanoseconde", + "nanoseconde⁻¹", + "nanoseconde-1", + "/nanosecondes", + "nanosecondes⁻¹", + "nanosecondes-1", + "/nano-seconde", + "nano-seconde⁻¹", + "nano-seconde-1", + "/nano-secondes", + "nano-secondes⁻¹", + "nano-secondes-1", + "/ns", + "ns⁻¹", + "ns-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_µs": { + "scale": 1.0e6, + "terms": [ + "/microseconde", + "microseconde⁻¹", + "microseconde-1", + "/microsecondes", + "microsecondes⁻¹", + "microsecondes-1", + "/micro-seconde", + "micro-seconde⁻¹", + "micro-seconde-1", + "/micro-secondes", + "micro-secondes⁻¹", + "micro-secondes-1", + "/µs", + "µs⁻¹", + "µs-1", + "/us", + "us⁻¹", + "us-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_ms": { + "scale": 1000.0, + "terms": [ + "/milliseconde", + "milliseconde⁻¹", + "milliseconde-1", + "/millisecondes", + "millisecondes⁻¹", + "millisecondes-1", + "/miliseconde", + "miliseconde⁻¹", + "miliseconde-1", + "/milisecondes", + "milisecondes⁻¹", + "milisecondes-1", + "/ms", + "ms⁻¹", + "ms-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_s": { + "scale": 1.0, + "terms": [ + "/seconde", + "seconde⁻¹", + "seconde-1", + "/secondes", + "secondes⁻¹", + "secondes-1", + "/s", + "s⁻¹", + "s-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_min": { + "scale": 0.016666666666666666, + "terms": [ + "/mn", + "mn⁻¹", + "mn-1", + "/min", + "min⁻¹", + "min-1", + "/minute", + "minute⁻¹", + "minute-1", + "/minutes", + "minutes⁻¹", + "minutes-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_h": { + "scale": 0.0002777777777777778, + "terms": [ + "/heure", + "heure⁻¹", + "heure-1", + "/heures", + "heures⁻¹", + "heures-1", + "/h", + "h⁻¹", + "h-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_day": { + "scale": 0.0002777777777777778, + "terms": [ + "/jour", + "jour⁻¹", + "jour-1", + "/jours", + "jours⁻¹", + "jours-1", + "/j", + "j⁻¹", + "j-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_month": { + "scale": 9.132410083203562e-06, + "terms": ["/mois", "mois⁻¹", "mois-1"], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_week": { + "scale": 3.968253968253968e-05, + "terms": [ + "/semaine", + "semaine⁻¹", + "semaine-1", + "/semaines", + "semaines⁻¹", + "semaines-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_year": { + "scale": 7.605141075366948e-07, + "terms": [ + "/an", + "an⁻¹", + "an-1", + "/année", + "année⁻¹", + "année-1", + "/ans", + "ans⁻¹", + "ans-1", + "/années", + "années⁻¹", + "années-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_arc-second": { + "scale": 30.0, + "terms": ['/"', '"⁻¹', '"-1', "/''", "''⁻¹", "''-1"], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_arc-minute": { + "scale": 0.5, + "terms": ["/'", "'⁻¹", "'-1"], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_degree": { + "scale": 0.008333333333333333, + "terms": [ + "/degre", + "degre⁻¹", + "degre-1", + "/°", + "°⁻¹", + "°-1", + "/deg", + "deg⁻¹", + "deg-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_celcius": { + "scale": 1.0, + "terms": [ + "/°C", + "°C⁻¹", + "°C-1", + "/° celcius", + "° celcius⁻¹", + "° celcius-1", + "/celcius", + "celcius⁻¹", + "celcius-1", + ], + "followed_by": None, + "ui_decomposition": {"temperature": -1}, + }, + "per_fl": { + "scale": 1.0e18, + "terms": [ + "/femtolitre", + "femtolitre⁻¹", + "femtolitre-1", + "/femtolitres", + "femtolitres⁻¹", + "femtolitres-1", + "/femto-litre", + "femto-litre⁻¹", + "femto-litre-1", + "/femto-litres", + "femto-litres⁻¹", + "femto-litres-1", + "/fl", + "fl⁻¹", + "fl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_pl": { + "scale": 1.0e15, + "terms": [ + "/picolitre", + "picolitre⁻¹", + "picolitre-1", + "/picolitres", + "picolitres⁻¹", + "picolitres-1", + "/pico-litre", + "pico-litre⁻¹", + "pico-litre-1", + "/pico-litres", + "pico-litres⁻¹", + "pico-litres-1", + "/pl", + "pl⁻¹", + "pl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_nl": { + "scale": 1.0e12, + "terms": [ + "/nanolitre", + "nanolitre⁻¹", + "nanolitre-1", + "/nanolitres", + "nanolitres⁻¹", + "nanolitres-1", + "/nano-litre", + "nano-litre⁻¹", + "nano-litre-1", + "/nano-litres", + "nano-litres⁻¹", + "nano-litres-1", + "/nl", + "nl⁻¹", + "nl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_µl": { + "scale": 1.0e9, + "terms": [ + "/microlitre", + "microlitre⁻¹", + "microlitre-1", + "/microlitres", + "microlitres⁻¹", + "microlitres-1", + "/micro-litre", + "micro-litre⁻¹", + "micro-litre-1", + "/micro-litres", + "micro-litres⁻¹", + "micro-litres-1", + "/µl", + "µl⁻¹", + "µl-1", + "/ul", + "ul⁻¹", + "ul-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_ml": { + "scale": 1.0e6, + "terms": [ + "/mililitre", + "mililitre⁻¹", + "mililitre-1", + "/millilitre", + "millilitre⁻¹", + "millilitre-1", + "/mililitres", + "mililitres⁻¹", + "mililitres-1", + "/millilitres", + "millilitres⁻¹", + "millilitres-1", + "/ml", + "ml⁻¹", + "ml-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_cl": { + "scale": 1.0e5, + "terms": [ + "/centilitre", + "centilitre⁻¹", + "centilitre-1", + "/centilitres", + "centilitres⁻¹", + "centilitres-1", + "/cl", + "cl⁻¹", + "cl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_dl": { + "scale": 1.0e4, + "terms": [ + "/decilitre", + "decilitre⁻¹", + "decilitre-1", + "/decilitres", + "decilitres⁻¹", + "decilitres-1", + "/dl", + "dl⁻¹", + "dl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_l": { + "scale": 1000.0, + "terms": [ + "/litre", + "litre⁻¹", + "litre-1", + "/litres", + "litres⁻¹", + "litres-1", + "/l", + "l⁻¹", + "l-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_dal": { + "scale": 100.0, + "terms": [ + "/decalitre", + "decalitre⁻¹", + "decalitre-1", + "/decalitres", + "decalitres⁻¹", + "decalitres-1", + "/dal", + "dal⁻¹", + "dal-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_hl": { + "scale": 10.0, + "terms": [ + "/hectolitre", + "hectolitre⁻¹", + "hectolitre-1", + "/hectolitres", + "hectolitres⁻¹", + "hectolitres-1", + "/hl", + "hl⁻¹", + "hl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_kl": { + "scale": 1.0, + "terms": [ + "/kilolitre", + "kilolitre⁻¹", + "kilolitre-1", + "/kilolitres", + "kilolitres⁻¹", + "kilolitres-1", + "/kl", + "kl⁻¹", + "kl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_cac": { + "scale": 200000000.0, + "terms": [ + "/cac", + "cac⁻¹", + "cac-1", + "/c.a.c", + "c.a.c⁻¹", + "c.a.c-1", + "/cuillere à café", + "cuillere à café⁻¹", + "cuillere à café-1", + "/cuillères à café", + "cuillères à café⁻¹", + "cuillères à café-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_goutte": { + "scale": 20000000000.0, + "terms": ["/gt", "gt⁻¹", "gt-1", "/goutte", "goutte⁻¹", "goutte-1"], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_fm3": { + "scale": 1.0e45, + "terms": [ + "/fm3", + "fm3⁻¹", + "fm3-1", + "/fm³", + "fm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_pm3": { + "scale": 1.0e36, + "terms": [ + "/pm3", + "pm3⁻¹", + "pm3-1", + "/pm³", + "pm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_nm3": { + "scale": 1.0e27, + "terms": [ + "/nm3", + "nm3⁻¹", + "nm3-1", + "/nm³", + "nm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_µm3": { + "scale": 1.0e18, + "terms": [ + "/um3", + "um3⁻¹", + "um3-1", + "/um³", + "um⁻³", + "/µm3", + "µm3⁻¹", + "µm3-1", + "/µm³", + "µm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_mm3": { + "scale": 1.0e9, + "terms": [ + "/mm3", + "mm3⁻¹", + "mm3-1", + "/mm³", + "mm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_cm3": { + "scale": 1.0e6, + "terms": ["/cm3", "cm3⁻¹", "cm3-1", "/cm³", "cm⁻³", "/cc", "cc⁻¹", "cc-1"], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_dm3": { + "scale": 1000.0, + "terms": [ + "/dm3", + "dm3⁻¹", + "dm3-1", + "/dm³", + "dm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_m3": { + "scale": 1.0, + "terms": [ + "/m3", + "m3⁻¹", + "m3-1", + "/m³", + "m⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_dam3": { + "scale": 0.001, + "terms": [ + "/dam3", + "dam3⁻¹", + "dam3-1", + "/dam³", + "dam⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_hm3": { + "scale": 1e-06, + "terms": [ + "/hm3", + "hm3⁻¹", + "hm3-1", + "/hm³", + "hm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_km3": { + "scale": 1e-09, + "terms": [ + "/km3", + "km3⁻¹", + "km3-1", + "/km³", + "km⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_fm2": { + "scale": 1.0 + 30, + "terms": [ + "/fm2", + "fm2⁻¹", + "fm2-1", + "/fm²", + "fm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_pm2": { + "scale": 1.0e24, + "terms": [ + "/fm2", + "fm2⁻¹", + "fm2-1", + "/fm²", + "fm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_nm2": { + "scale": 1.0e18, + "terms": [ + "/nm2", + "nm2⁻¹", + "nm2-1", + "/nm²", + "nm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_μm2": { + "scale": 1.0e12, + "terms": [ + "/µm2", + "µm2⁻¹", + "µm2-1", + "/µm²", + "µm⁻²", + "/um2", + "um2⁻¹", + "um2-1", + "/um²", + "um⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_mm2": { + "scale": 1.0e6, + "terms": [ + "/mm2", + "mm2⁻¹", + "mm2-1", + "/mm²", + "mm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_cm2": { + "scale": 1.0e4, + "terms": [ + "/cm2", + "cm2⁻¹", + "cm2-1", + "/cm²", + "cm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_dm2": { + "scale": 100.0, + "terms": [ + "/dm2", + "dm2⁻¹", + "dm2-1", + "/dm²", + "dm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_m2": { + "scale": 1.0, + "terms": [ + "/m2", + "m2⁻¹", + "m2-1", + "/m²", + "m⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_dam2": { + "scale": 0.01, + "terms": [ + "/dam2", + "dam2⁻¹", + "dam2-1", + "/dam²", + "dam⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_hm2": { + "scale": 1.0e-4, + "terms": [ + "/hm2", + "hm2⁻¹", + "hm2-1", + "/hm²", + "hm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_km2": { + "scale": 1e-06, + "terms": [ + "/km2", + "km2⁻¹", + "km2-1", + "/km²", + "km⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_fui": { + "scale": 1.0e15, + "terms": [ + "/fui", + "fui⁻¹", + "fui-1", + "/f ui", + "f ui⁻¹", + "f ui-1", + "/fu", + "fu⁻¹", + "fu-1", + "/f u", + "f u⁻¹", + "f u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_pui": { + "scale": 1.0e12, + "terms": [ + "/pui", + "pui⁻¹", + "pui-1", + "/p ui", + "p ui⁻¹", + "p ui-1", + "/pu", + "pu⁻¹", + "pu-1", + "/p u", + "p u⁻¹", + "p u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_nui": { + "scale": 1.0e9, + "terms": [ + "/nui", + "nui⁻¹", + "nui-1", + "/n ui", + "n ui⁻¹", + "n ui-1", + "/nu", + "nu⁻¹", + "nu-1", + "/n u", + "n u⁻¹", + "n u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_µui": { + "scale": 1.0e6, + "terms": [ + "/µui", + "µui⁻¹", + "µui-1", + "/µ ui", + "µ ui⁻¹", + "µ ui-1", + "/uui", + "uui⁻¹", + "uui-1", + "/u ui", + "u ui⁻¹", + "u ui-1", + "/µu", + "µu⁻¹", + "µu-1", + "/µ u", + "µ u⁻¹", + "µ u-1", + "/uu", + "uu⁻¹", + "uu-1", + "/u u", + "u u⁻¹", + "u u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_mui": { + "scale": 1000.0, + "terms": [ + "/mui", + "mui⁻¹", + "mui-1", + "/m ui", + "m ui⁻¹", + "m ui-1", + "/mu", + "mu⁻¹", + "mu-1", + "/m u", + "m u⁻¹", + "m u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_cui": { + "scale": 100.0, + "terms": [ + "/cui", + "cui⁻¹", + "cui-1", + "/c ui", + "c ui⁻¹", + "c ui-1", + "/cu", + "cu⁻¹", + "cu-1", + "/c u", + "c u⁻¹", + "c u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_dui": { + "scale": 10.0, + "terms": [ + "/dui", + "dui⁻¹", + "dui-1", + "/d ui", + "d ui⁻¹", + "d ui-1", + "/du", + "du⁻¹", + "du-1", + "/d u", + "d u⁻¹", + "d u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_ui": { + "scale": 1.0, + "terms": ["/ui", "ui⁻¹", "ui-1", "/u", "u⁻¹", "u-1"], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_daui": { + "scale": 0.1, + "terms": ["/daui", "daui⁻¹", "daui-1", "/dau", "dau⁻¹", "dau-1"], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_hui": { + "scale": 0.01, + "terms": ["/hui", "hui⁻¹", "hui-1", "/hu", "hu⁻¹", "hu-1"], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_kui": { + "scale": 0.001, + "terms": ["/kui", "kui⁻¹", "kui-1", "/ku", "ku⁻¹", "ku-1"], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_fmol": { + "scale": 1.0e15, + "terms": [ + "/fmol", + "fmol⁻¹", + "fmol-1", + "/f mol", + "f mol⁻¹", + "f mol-1", + "/fmole", + "fmole⁻¹", + "fmole-1", + "/f mole", + "f mole⁻¹", + "f mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_pmol": { + "scale": 1.0e12, + "terms": [ + "/pmol", + "pmol⁻¹", + "pmol-1", + "/p mol", + "p mol⁻¹", + "p mol-1", + "/pmole", + "pmole⁻¹", + "pmole-1", + "/p mole", + "p mole⁻¹", + "p mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_nmol": { + "scale": 1.0e9, + "terms": [ + "/nmol", + "nmol⁻¹", + "nmol-1", + "/n mol", + "n mol⁻¹", + "n mol-1", + "/nmole", + "nmole⁻¹", + "nmole-1", + "/n mole", + "n mole⁻¹", + "n mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_µmol": { + "scale": 1.0e6, + "terms": [ + "/µmol", + "µmol⁻¹", + "µmol-1", + "/µ mol", + "µ mol⁻¹", + "µ mol-1", + "/umole", + "umole⁻¹", + "umole-1", + "/u mole", + "u mole⁻¹", + "u mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_mmol": { + "scale": 1000.0, + "terms": [ + "/mmol", + "mmol⁻¹", + "mmol-1", + "/m mol", + "m mol⁻¹", + "m mol-1", + "/mmole", + "mmole⁻¹", + "mmole-1", + "/m mole", + "m mole⁻¹", + "m mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_cmol": { + "scale": 100.0, + "terms": [ + "/cmol", + "cmol⁻¹", + "cmol-1", + "/c mol", + "c mol⁻¹", + "c mol-1", + "/cmole", + "cmole⁻¹", + "cmole-1", + "/c mole", + "c mole⁻¹", + "c mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_dmol": { + "scale": 10.0, + "terms": [ + "/dmol", + "dmol⁻¹", + "dmol-1", + "/d mol", + "d mol⁻¹", + "d mol-1", + "/dmole", + "dmole⁻¹", + "dmole-1", + "/d mole", + "d mole⁻¹", + "d mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_mol": { + "scale": 1.0, + "terms": ["/mol", "mol⁻¹", "mol-1", "/mole", "mole⁻¹", "mole-1"], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_damol": { + "scale": 0.1, + "terms": [ + "/damol", + "damol⁻¹", + "damol-1", + "/da mol", + "da mol⁻¹", + "da mol-1", + "/damole", + "damole⁻¹", + "damole-1", + "/da mole", + "da mole⁻¹", + "da mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_hmol": { + "scale": 0.01, + "terms": [ + "/hmol", + "hmol⁻¹", + "hmol-1", + "/h mol", + "h mol⁻¹", + "h mol-1", + "/hmole", + "hmole⁻¹", + "hmole-1", + "/h mole", + "h mole⁻¹", + "h mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_kmol": { + "scale": 0.001, + "terms": [ + "/kmol", + "kmol⁻¹", + "kmol-1", + "/k mol", + "k mol⁻¹", + "k mol-1", + "/kmole", + "kmole⁻¹", + "kmole-1", + "/k mole", + "k mole⁻¹", + "k mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "nounit": { + "scale": 1, + "terms": [], + "followed_by": None, + "ui_decomposition": {"nounit": 1}, + }, + "percent": { + "scale": 0.01, + "terms": [ + "%", + "pourcent", + "pourcents", + ], + "followed_by": None, + "ui_decomposition": {"nounit": 1}, + }, + "permille": { + "scale": 0.001, + "terms": [ + "‰", + "pourmille", + "pour mille", + "pourmilles", + "pour milles", + ], + "followed_by": None, + "ui_decomposition": {"nounit": 1}, + }, +} From 80fb3e3b44084c22d66c24622b89476894649342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Mon, 15 May 2023 14:19:23 +0000 Subject: [PATCH 02/11] Modified table pipeline to add extraction of cells as spans + measurement pipeline bug fixes --- edsnlp/pipelines/factories.py | 1 + .../misc/measurements/measurements.py | 64 ++++-- .../pipelines/misc/measurements/patterns.py | 11 +- edsnlp/pipelines/misc/tables/__init__.py | 2 + edsnlp/pipelines/misc/tables/factory.py | 38 ++++ edsnlp/pipelines/misc/tables/patterns.py | 2 + edsnlp/pipelines/misc/tables/tables.py | 212 ++++++++++++++++++ edsnlp/resources/verbs.csv.gz | Bin 200566 -> 200865 bytes 8 files changed, 305 insertions(+), 25 deletions(-) create mode 100644 edsnlp/pipelines/misc/tables/__init__.py create mode 100644 edsnlp/pipelines/misc/tables/factory.py create mode 100644 edsnlp/pipelines/misc/tables/patterns.py create mode 100644 edsnlp/pipelines/misc/tables/tables.py diff --git a/edsnlp/pipelines/factories.py b/edsnlp/pipelines/factories.py index c3c226112..113716331 100644 --- a/edsnlp/pipelines/factories.py +++ b/edsnlp/pipelines/factories.py @@ -15,6 +15,7 @@ from .misc.measurements.factory import create_component as measurements from .misc.reason.factory import create_component as reason from .misc.sections.factory import create_component as sections +from .misc.tables.factory import create_component as tables from .ner.adicap.factory import create_component as adicap from .ner.cim10.factory import create_component as cim10 from .ner.covid.factory import create_component as covid diff --git a/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/pipelines/misc/measurements/measurements.py index 8bfa917e7..78a6522ca 100644 --- a/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/pipelines/misc/measurements/measurements.py @@ -138,10 +138,10 @@ def __getitem__(self, item: int): return [self][item] def __str__(self): - return f"{self.value_range} {self.value} {self.unit}" + return f"{self.value} {self.unit}" def __repr__(self): - return f"Measurement({self.value_range}, {self.value}, {repr(self.unit)})" + return f"Measurement({self.value}, {repr(self.unit)})" def __eq__(self, other: Any): if isinstance(other, SimpleMeasurement): @@ -202,7 +202,7 @@ def __getattr__(self, other_unit): return super().__geattr__(other_unit) try: return self.convert_to(other_unit) - except KeyError as e: + except KeyError: raise AttributeError() @classmethod @@ -244,7 +244,7 @@ def __init__( ---------- nlp : Language The SpaCy object. - measurements : Dict[str, MeasureConfig] + measurements : Union[List[str], Tuple[str], Dict[str, MeasureConfig]] A mapping from measure names to MeasureConfig Each measure's configuration has the following shape: { @@ -271,15 +271,18 @@ def __init__( A list of stopwords that do not matter when placed between a unitless trigger and a number stopwords_measure_unit: List[str] - A list of stopwords that do not matter when placed between a unit and a number - These stopwords do not matter only in one of the following pattern : - unit - stopwords - measure or measure - stopwords - unit, according to - measure_before_unit parameter. + A list of stopwords that do not matter when placed between a unit and + a number + These stopwords do not matter only in one of the following pattern: + unit - stopwords - measure or measure - stopwords - unit, + according to measure_before_unit parameter. measure_before_unit: bool - Set It True if the measure is generally before the unit, False in the other case. - This parameter will indicate if the stopwords in stopwords_measure_unit should - not matter in the unit - stopwords - measure patterns only (False) or in - the measure - stopwords - unit patterns only (True) + Set It True if the measure is generally before the unit, False + in the other case. + This parameter will indicate if the stopwords in + stopwords_measure_unit should not matter in the unit-stopwords-measure + patterns only (False) or in the measure-stopwords- unit patterns + only (True) unit_divisors: List[str] A list of terms used to divide two units (like: m / s) attr : str @@ -317,7 +320,10 @@ def __init__( self.regex_matcher.add( "pow10", [ - r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)", + ( + r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|" + r"(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)" + ), ], ) @@ -582,11 +588,15 @@ def get_matches_before(i): return yield i - j, ent - # Return a float based on the measure (float) and the power of 10 extracted with regex (string) + # Return a float based on the measure (float) and the power of + # 10 extracted with regex (string) def combine_measure_pow10(measure, pow10_text): pow10 = int( re.fullmatch( - r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)", + ( + r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|" + r"(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)" + ), pow10_text, ).group(1) ) @@ -641,7 +651,7 @@ def combine_measure_pow10(measure, pow10_text): if re.fullmatch(r"[,o]*", pseudo_sent): pow10_text = pow10_ent.text value = combine_measure_pow10(value, pow10_text) - except: + except (AttributeError, StopIteration): pass # Check if the measurement is an =, < or > measurement @@ -650,7 +660,7 @@ def combine_measure_pow10(measure, pow10_text): value_range = matches[number_idx - 1][0].label_ else: value_range = "=" - except: + except (KeyError, AttributeError, IndexError): value_range = "=" unit_idx = unit_text = unit_norm = None @@ -722,7 +732,8 @@ def combine_measure_pow10(measure, pow10_text): pass # If no unit was matched, take the nearest unit only if - # It is separated by a stopword from stopwords_measure_unit and / or a value_range_term + # It is separated by a stopword from stopwords_measure_unit and + # / or a value_range_term # Take It before or after the measure according to if not unit_norm: try: @@ -755,8 +766,9 @@ def combine_measure_pow10(measure, pow10_text): pseudo[offsets[number_idx] + 1 : offsets[unit_after_idx]], ): unit_norm = unit_after_text.label_ - # Check if there is a power of 10 between the measure and the unit without considering - # the one that we have already considered at the beginning of thos program + # Check if there is a power of 10 between the measure and + # the unit without considering the one that we have already + # considered at the beginning of thos program try: (pow10_idx, pow10_ent) = next( (j, e) @@ -766,14 +778,18 @@ def combine_measure_pow10(measure, pow10_text): if pow10_idx > pseudo[offsets[number_idx] + 1]: pow10_text = pow10_ent.text value = combine_measure_pow10(value, pow10_text) - except: + except (AttributeError, StopIteration): pass - except: + except (AttributeError, StopIteration): pass - # Otherwise, set the unit as no_unit + # Otherwise, set the unit as no_unit if the value + # is not written with letters if not unit_norm: - unit_norm = "nounit" + if number.label_ == "number": + unit_norm = "nounit" + else: + continue # Compute the final entity if unit_text and unit_text.end == number.start: diff --git a/edsnlp/pipelines/misc/measurements/patterns.py b/edsnlp/pipelines/misc/measurements/patterns.py index 209882514..52a888fbc 100644 --- a/edsnlp/pipelines/misc/measurements/patterns.py +++ b/edsnlp/pipelines/misc/measurements/patterns.py @@ -34,7 +34,7 @@ value_range_terms = { "<": ["<", "<=", "inferieure a", "inferieur a", "inf a", "inf"], - ">": [">", ">=", "superieure a", "sup a", "sup a", "sup"], + ">": [">", ">=", "superieure a", "superieur a", "sup a", "sup"], } @@ -2341,4 +2341,13 @@ "followed_by": None, "ui_decomposition": {"nounit": 1}, }, + "mmhg": { + "scale": 133.3224, + "terms": [ + "mmhg", + "torr", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1, "length": -1, "time": -2}, + }, } diff --git a/edsnlp/pipelines/misc/tables/__init__.py b/edsnlp/pipelines/misc/tables/__init__.py new file mode 100644 index 000000000..861ef7628 --- /dev/null +++ b/edsnlp/pipelines/misc/tables/__init__.py @@ -0,0 +1,2 @@ +from .patterns import regex, sep +from .tables import TablesMatcher diff --git a/edsnlp/pipelines/misc/tables/factory.py b/edsnlp/pipelines/misc/tables/factory.py new file mode 100644 index 000000000..801dea164 --- /dev/null +++ b/edsnlp/pipelines/misc/tables/factory.py @@ -0,0 +1,38 @@ +from typing import Dict, List, Optional, Union + +from spacy.language import Language + +from edsnlp.pipelines.misc.tables import TablesMatcher +from edsnlp.utils.deprecation import deprecated_factory + +DEFAULT_CONFIG = dict( + tables_pattern=None, + sep_pattern=None, + attr="TEXT", + ignore_excluded=True, + col_names=False, + row_names=False, +) + + +@deprecated_factory("tables", "eds.tables", default_config=DEFAULT_CONFIG) +@Language.factory("eds.tables", default_config=DEFAULT_CONFIG) +def create_component( + nlp: Language, + name: str, + tables_pattern: Optional[Dict[str, Union[List[str], str]]], + sep_pattern: Optional[str], + attr: str, + ignore_excluded: bool, + col_names: Optional[bool] = False, + row_names: Optional[bool] = False, +): + return TablesMatcher( + nlp, + tables_pattern=tables_pattern, + sep_pattern=sep_pattern, + attr=attr, + ignore_excluded=ignore_excluded, + col_names=col_names, + row_names=row_names, + ) diff --git a/edsnlp/pipelines/misc/tables/patterns.py b/edsnlp/pipelines/misc/tables/patterns.py new file mode 100644 index 000000000..7f29c7b4c --- /dev/null +++ b/edsnlp/pipelines/misc/tables/patterns.py @@ -0,0 +1,2 @@ +sep = r"¦" +regex = rf"(?:{sep}?(?:[^{sep}\n]*{sep})+[^{sep}\n]*{sep}?\n)+" diff --git a/edsnlp/pipelines/misc/tables/tables.py b/edsnlp/pipelines/misc/tables/tables.py new file mode 100644 index 000000000..792cdc7b8 --- /dev/null +++ b/edsnlp/pipelines/misc/tables/tables.py @@ -0,0 +1,212 @@ +from typing import Dict, Optional, Union + +import pandas as pd +from spacy.language import Language +from spacy.tokens import Doc, Span + +from edsnlp.matchers.phrase import EDSPhraseMatcher +from edsnlp.matchers.regex import RegexMatcher +from edsnlp.pipelines.misc.tables import patterns + + +class TablesMatcher: + """ + Pipeline to identify the Tables. + + It adds the key `tables` to doc.spans. + + Parameters + ---------- + nlp : Language + spaCy nlp pipeline to use for matching. + tables_pattern : Optional[str] + The regex pattern to identify tables. + sep_pattern : Optional[str] + The regex pattern to identify separators + in the detected tables + col_names : Optional[bool] + Whether the tables_pattern matches column names + row_names : Optional[bool] + Whether the table_pattern matches row names + attr : str + spaCy's attribute to use: + a string with the value "TEXT" or "NORM", or a dict with + the key 'term_attr'. We can also add a key for each regex. + ignore_excluded : bool + Whether to skip excluded tokens. + """ + + def __init__( + self, + nlp: Language, + tables_pattern: Optional[str], + sep_pattern: Optional[str], + attr: Union[Dict[str, str], str], + ignore_excluded: bool, + col_names: Optional[bool] = False, + row_names: Optional[bool] = False, + ): + + if tables_pattern is None: + tables_pattern = patterns.regex + + if sep_pattern is None: + sep_pattern = patterns.sep + + self.regex_matcher = RegexMatcher(attr=attr, ignore_excluded=True) + self.regex_matcher.add("row", [tables_pattern]) + + self.term_matcher = EDSPhraseMatcher(nlp.vocab, attr=attr, ignore_excluded=True) + self.term_matcher.build_patterns( + nlp, + { + "eol_pattern": "\n", + "sep_pattern": sep_pattern, + }, + ) + + self.col_names = col_names + self.row_names = row_names + + if not Span.has_extension("to_pd_table"): + Span.set_extension("to_pd_table", method=self.to_pd_table) + + self.set_extensions() + + @classmethod + def set_extensions(cls) -> None: + """ + Set extensions for the tables pipeline. + """ + + if not Span.has_extension("table"): + Span.set_extension("table", default=None) + + def get_tables(self, matches): + """ + Convert spans of tables to dictionnaries + + Parameters + ---------- + matches : List[Span] + + Returns + ------- + List[Span] + """ + + # Dictionnaries linked to each table + # Has the following format : + # List[Dict[Union[str, int], List[Span]]] + # - List of dictionnaries containing the tables. Keys are + # column names (str) if col_names is set to True, else row + # names (str) if row_names is set to True, else index of + # column (int) + tables_list = [] + + # Returned list + tables = [] + + # Iter through matches to consider each table individually + for table in matches: + # We store each row in a list and store each of hese lists + # in processed_table for post processing + # considering the self.col_names and self.row_names var + processed_table = [] + delimiters = [ + delimiter + for delimiter in self.term_matcher(table, as_spans=True) + if delimiter.start >= table.start and delimiter.end <= table.end + ] + + last = table.start + row = [] + # Parse the table to match each cell thanks to delimiters + for delimiter in delimiters: + row.append(table[last - table.start : delimiter.start - table.start]) + last = delimiter.end + + # End the actual row if there is an end of line + if delimiter.label_ == "eol_pattern": + processed_table.append(row) + row = [] + + # Remove first or last column in case the separator pattern is + # also used in the raw table to draw the outlines + if all(row[0].start == row[0].end for row in processed_table): + processed_table = [row[1:] for row in processed_table] + if all(row[-1].start == row[-1].end for row in processed_table): + processed_table = [row[:-1] for row in processed_table] + + tables_list.append(processed_table) + + # Convert to dictionnaries according to self.col_names + # and self.row_names + if self.col_names: + for table_index in range(len(tables_list)): + tables_list[table_index] = { + tables_list[table_index][0][column_index].text: [ + tables_list[table_index][row_index][column_index] + for row_index in range(1, len(tables_list[table_index])) + ] + for column_index in range(len(tables_list[table_index][0])) + } + elif self.row_names: + for table_index in range(len(tables_list)): + tables_list[table_index] = { + tables_list[table_index][row_index][0].text: [ + tables_list[table_index][row_index][column_index] + for column_index in range(1, len(tables_list[table_index][0])) + ] + for row_index in range(len(tables_list[table_index])) + } + else: + for table_index in range(len(tables_list)): + tables_list[table_index] = { + column_index: [ + tables_list[table_index][row_index][column_index] + for row_index in range(len(tables_list[table_index])) + ] + for column_index in range(len(tables_list[table_index][0])) + } + + for i in range(len(matches)): + ent = matches[i] + ent._.table = tables_list[i] + tables.append(ent) + + return tables + + def __call__(self, doc: Doc) -> Doc: + """ + Find spans that contain tables + + Parameters + ---------- + doc : Doc + + Returns + ------- + Doc + """ + matches = list(self.regex_matcher(doc, as_spans=True)) + tables = self.get_tables(matches) + doc.spans["tables"] = tables + + return doc + + def to_pd_table(self, span, as_spans=True) -> pd.DataFrame: + """ + Return pandas DataFrame + """ + if as_spans: + table = span._.table + else: + table = { + key: [str(cell) for cell in data] + for key, data in list(span._.table.items()) + } + if self.row_names: + return pd.DataFrame.from_dict(table, orient="index") + else: + return pd.DataFrame.from_dict(table) diff --git a/edsnlp/resources/verbs.csv.gz b/edsnlp/resources/verbs.csv.gz index b05fb4eeffde8d7c4d8403757b12d2445c925f9c..370d9d3c1a5eb3fbcc6088761d60ac0fadacddfc 100644 GIT binary patch literal 200865 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≧szBZ^-2E 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

ipqZvs9^^R%D)#}WukKNVPP9G(|0A4_ShA)46 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 From 53da619c08409fd0d1d3961970957980ac5d5404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Fri, 30 Jun 2023 12:35:53 +0000 Subject: [PATCH 08/11] Bug fixes on measurements pipe --- .../misc/measurements/measurements.py | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/pipelines/misc/measurements/measurements.py index cb8465823..9014a3faa 100644 --- a/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/pipelines/misc/measurements/measurements.py @@ -1044,7 +1044,12 @@ def get_distance_between_columns(column1_key, column2_key): try: unit_column_key = sorted( [ - unit_column_key + ( + unit_column_key, + get_distance_between_columns( + unit_column_key, value_column_key + ), + ) for unit_column_key in unit_column_keys if unit_column_key not in [ @@ -1053,13 +1058,18 @@ def get_distance_between_columns(column1_key, column2_key): if v != value_column_key ] ], - key=lambda unit_column_key: get_distance_between_columns( - unit_column_key, value_column_key - ), - )[0 : min(2, len(unit_column_keys))][ - 0 * (not measure_before_unit_in_table) - - 1 * measure_before_unit_in_table - ] + key=lambda unit_column_key_tuple: unit_column_key_tuple[1], + )[0 : min(2, len(unit_column_keys))] + if ( + len(unit_column_key) == 1 + or unit_column_key[0][1] != unit_column_key[1][1] + ): + unit_column_key = unit_column_key[0][0] + else: + unit_column_key = unit_column_key[ + 0 * (not measure_before_unit_in_table) + - 1 * measure_before_unit_in_table + ][0] except IndexError: unit_column_key = value_column_key else: @@ -1080,7 +1090,12 @@ def get_distance_between_columns(column1_key, column2_key): try: pow10_column_key = sorted( [ - pow10_column_key + ( + pow10_column_key, + get_distance_between_columns( + pow10_column_key, value_column_key + ), + ) for pow10_column_key in pow10_column_keys if pow10_column_key not in [ @@ -1088,14 +1103,24 @@ def get_distance_between_columns(column1_key, column2_key): for v in value_column_keys if v != value_column_key ] + + [u for u in unit_column_keys if u != unit_column_key] + # The pow10_column_key cannot be a unit_column_key + # other than the one selected ], - key=lambda pow10_column_key: get_distance_between_columns( - pow10_column_key, value_column_key - ), - )[0 : min(2, len(pow10_column_keys))][ - 0 * (not measure_before_power_in_table) - - 1 * measure_before_power_in_table - ] + key=lambda pow10_column_key_tuple: pow10_column_key_tuple[ + 1 + ], + )[0 : min(2, len(pow10_column_keys))] + if ( + len(pow10_column_key) == 1 + or pow10_column_key[0][1] != pow10_column_key[1][1] + ): + pow10_column_key = pow10_column_key[0][0] + else: + pow10_column_key = pow10_column_key[ + 0 * (not measure_before_power_in_table) + - 1 * measure_before_power_in_table + ][0] except IndexError: pow10_column_key = value_column_key else: From a90c07491cc946e93b34949d22388543f7117774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Wed, 26 Jul 2023 09:34:01 +0000 Subject: [PATCH 09/11] Fixed bug for /24h --- edsnlp/pipelines/misc/measurements/measurements.py | 4 ++-- edsnlp/pipelines/misc/measurements/patterns.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/pipelines/misc/measurements/measurements.py index 9014a3faa..3686120fd 100644 --- a/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/pipelines/misc/measurements/measurements.py @@ -368,7 +368,7 @@ def __init__( "pow10", [ ( - r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|" + r"(?:(?:\s*x?\s*10\s*(?:\*{1,2}|\^)\s*)|" r"(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)" ), ], @@ -586,7 +586,7 @@ def combine_measure_pow10( pow10 = int( re.fullmatch( ( - r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|" + r"(?:(?:\s*x?\s*10\s*(?:\*{1,2}|\^)\s*)|" r"(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)" ), pow10_text, diff --git a/edsnlp/pipelines/misc/measurements/patterns.py b/edsnlp/pipelines/misc/measurements/patterns.py index cf34e2311..f989df2fe 100644 --- a/edsnlp/pipelines/misc/measurements/patterns.py +++ b/edsnlp/pipelines/misc/measurements/patterns.py @@ -1381,6 +1381,9 @@ "/j", "j⁻¹", "j-1", + "/24h", + "24h⁻¹", + "24h-1", ], "followed_by": None, "ui_decomposition": {"time": -1}, From 34d36844376ece8e2564ac758ae527e8f26a905c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Fri, 1 Dec 2023 15:28:51 +0000 Subject: [PATCH 10/11] Initiate the repo --- .gitignore | 6 + .pre-commit-config.yaml | 57 - NER_model/configs/config.cfg | 185 + NER_model/configs/config_v1.cfg | 185 + .../configs/expe_lang_model/config_DrBert.cfg | 185 + .../expe_lang_model/config_camembert_base.cfg | 185 + .../expe_lang_model/config_camembert_bio.cfg | 185 + .../expe_lang_model/config_eds_finetune.cfg | 185 + .../expe_lang_model/config_eds_scratch.cfg | 185 + NER_model/configs/rebus/__.cfg | 188 + NER_model/configs/rebus/config.cfg | 186 + .../configs/rebus/config_0407_Pierre.cfg | 186 + NER_model/configs/rebus/config_1907.cfg | 188 + NER_model/configs/rebus/config_backup2.cfg | 188 + NER_model/configs/rebus/config_j.cfg | 186 + .../data/NLP_diabeto/clean/annotation.conf | 39 + .../data/NLP_diabeto/clean/kb_shortcuts.conf | 6 + NER_model/data/NLP_diabeto/clean/visual.conf | 42 + .../data/NLP_diabeto/raw/annotation.conf | 39 + .../data/NLP_diabeto/raw/kb_shortcuts.conf | 6 + NER_model/data/NLP_diabeto/raw/visual.conf | 42 + NER_model/dvc.lock | 75 + NER_model/dvc.yaml | 28 + NER_model/evaluate.md | 572 ++ NER_model/poetry.lock | 5043 +++++++++++++++++ NER_model/project.lock | 108 + NER_model/project.yml | 138 + NER_model/pyproject.toml | 121 + NER_model/scripts/convert.py | 153 + NER_model/scripts/evaluate.py | 214 + NER_model/scripts/infer.py | 63 + NER_model/scripts/new_evaluate.py | 233 + NER_model/scripts/package.py | 324 ++ NER_model/scripts/save_to_brat.py | 129 + NER_model/scripts/visualize_model.py | 14 + Normalisation/drugs/exception.py | 14 + Normalisation/drugs/normalisation.py | 246 + Normalisation/extract_measurement/config.py | 16 + .../extract_measurements_from_brat.py | 348 ++ .../extract_pandas_from_brat.py | 69 + Normalisation/extract_measurement/main.py | 31 + .../measurements_patterns.py | 332 ++ Normalisation/inference/config.py | 84 + .../inference/extract_pandas_from_brat.py | 70 + .../inference/get_normalization_with_coder.py | 125 + Normalisation/inference/main.py | 111 + Normalisation/inference/text_preprocessor.py | 40 + .../training}/__init__.py | 0 Normalisation/training/data_util.py | 174 + Normalisation/training/extract_bert.py | 8 + .../training/extract_pandas_from_brat.py | 70 + .../training/generate_term_embeddings.py | 84 + .../training/generate_term_embeddings.sh | 22 + .../generate_term_embeddings_coder_eds.py | 85 + .../generate_term_embeddings_coder_eds.sh | 22 + ...enerate_term_embeddings_coder_eds_cased.py | 87 + ...enerate_term_embeddings_coder_eds_cased.sh | 22 + .../training/generate_umls_embeddings.py | 94 + .../training/generate_umls_embeddings.sh | 22 + .../generate_umls_normalized_embeddings.py | 171 + .../generate_umls_normalized_embeddings.sh | 25 + ...te_umls_normalized_embeddings_coder_eds.py | 172 + ...te_umls_normalized_embeddings_coder_eds.sh | 25 + ...s_normalized_embeddings_coder_eds_cased.py | 174 + ...s_normalized_embeddings_coder_eds_cased.sh | 25 + Normalisation/training/get_matches.py | 34 + Normalisation/training/get_matches.sh | 22 + Normalisation/training/load_umls.py | 177 + .../training/load_umls_normalized.py | 199 + ...ls_embeddings_coder_eds_cased.sh-11575.err | 2 + ...ls_embeddings_coder_eds_cased.sh-11575.out | 2 + Normalisation/training/loss.py | 108 + Normalisation/training/model.py | 178 + Normalisation/training/sampler_util.py | 53 + Normalisation/training/train.py | 324 ++ Normalisation/training/train_coder_slurm.cfg | 7 + Normalisation/training/trans.py | 14 + bash_scripts/NER_model/expe_data_size.sh | 55 + bash_scripts/NER_model/expe_hyperparams.sh | 46 + bash_scripts/NER_model/expe_model_lang.sh | 60 + bash_scripts/NER_model/infer.sh | 21 + bash_scripts/NER_model/save.sh | 26 + bash_scripts/NER_model/test.sh | 26 + bash_scripts/NER_model/train.sh | 35 + bash_scripts/NER_model/train_v1.sh | 57 + .../Normalisation/extract_measurement.sh | 39 + bash_scripts/Normalisation/infer_coder.sh | 43 + .../Normalisation/infer_coder_quaero.sh | 32 + bash_scripts/Normalisation/train_coder.sh | 7 + demo/requirements.txt | 2 - CITATION.cff => edsnlp/CITATION.cff | 0 LICENSE => edsnlp/LICENSE | 0 Makefile => edsnlp/Makefile | 0 README.md => edsnlp/README.md | 0 edsnlp/__init__.py | 13 - changelog.md => edsnlp/changelog.md | 0 contributing.md => edsnlp/contributing.md | 0 {demo => edsnlp/demo}/app.py | 0 .../docs}/advanced-tutorials/fastapi.md | 0 .../docs}/advanced-tutorials/index.md | 0 .../docs}/advanced-tutorials/word-vectors.md | 0 .../docs}/assets/logo/aphp-blue.svg | 0 .../docs}/assets/logo/aphp-white.svg | 0 {docs => edsnlp/docs}/assets/logo/edsnlp.svg | 0 .../docs}/assets/stylesheets/extra.css | 0 .../templates/python/material/docstring.html | 0 .../python/material/docstring/parameters.html | 0 .../templates/python/material/function.html | 0 .../docs}/assets/termynal/termynal.css | 0 .../docs}/assets/termynal/termynal.js | 0 edsnlp/docs/changelog.md | 304 + edsnlp/docs/contributing.md | 127 + {docs => edsnlp/docs}/index.md | 0 .../docs}/pipelines/architecture.md | 0 .../pipelines/core/contextual-matcher.md | 0 .../docs}/pipelines/core/endlines.md | 0 {docs => edsnlp/docs}/pipelines/core/index.md | 0 .../docs}/pipelines/core/matcher.md | 0 .../docs}/pipelines/core/normalisation.md | 0 .../pipelines/core/resources/alignment.svg | 0 .../core/resources/span-alignment.svg | 0 .../docs}/pipelines/core/sentences.md | 0 .../docs}/pipelines/core/terminology.md | 0 {docs => edsnlp/docs}/pipelines/index.md | 0 .../pipelines/misc/consultation-dates.md | 0 {docs => edsnlp/docs}/pipelines/misc/dates.md | 0 {docs => edsnlp/docs}/pipelines/misc/index.md | 0 .../docs}/pipelines/misc/measurements.md | 0 .../docs}/pipelines/misc/reason.md | 0 .../docs}/pipelines/misc/sections.md | 0 .../docs}/pipelines/misc/tables.md | 0 {docs => edsnlp/docs}/pipelines/ner/adicap.md | 0 {docs => edsnlp/docs}/pipelines/ner/cim10.md | 0 {docs => edsnlp/docs}/pipelines/ner/covid.md | 0 {docs => edsnlp/docs}/pipelines/ner/drugs.md | 0 {docs => edsnlp/docs}/pipelines/ner/index.md | 0 {docs => edsnlp/docs}/pipelines/ner/score.md | 0 {docs => edsnlp/docs}/pipelines/ner/umls.md | 0 .../docs}/pipelines/qualifiers/family.md | 0 .../docs}/pipelines/qualifiers/history.md | 0 .../docs}/pipelines/qualifiers/hypothesis.md | 0 .../docs}/pipelines/qualifiers/index.md | 0 .../docs}/pipelines/qualifiers/negation.md | 0 .../pipelines/qualifiers/reported-speech.md | 0 .../docs}/pipelines/trainable/edsnlp-ner.svg | 0 .../docs}/pipelines/trainable/index.md | 0 .../docs}/pipelines/trainable/ner.md | 0 edsnlp/docs/reference/components.md | 3 + edsnlp/docs/reference/conjugator.md | 3 + edsnlp/docs/reference/connectors/brat.md | 3 + edsnlp/docs/reference/connectors/index.md | 3 + edsnlp/docs/reference/connectors/labeltool.md | 3 + edsnlp/docs/reference/connectors/omop.md | 3 + edsnlp/docs/reference/extensions.md | 3 + edsnlp/docs/reference/index.md | 3 + edsnlp/docs/reference/language.md | 3 + edsnlp/docs/reference/matchers/index.md | 3 + edsnlp/docs/reference/matchers/regex.md | 3 + edsnlp/docs/reference/matchers/simstring.md | 3 + edsnlp/docs/reference/matchers/utils/index.md | 3 + .../docs/reference/matchers/utils/offset.md | 3 + edsnlp/docs/reference/matchers/utils/text.md | 3 + edsnlp/docs/reference/models/index.md | 3 + .../docs/reference/models/pytorch_wrapper.md | 3 + edsnlp/docs/reference/models/stack_crf_ner.md | 3 + edsnlp/docs/reference/models/torch/crf.md | 3 + edsnlp/docs/reference/models/torch/index.md | 3 + .../reference/patch_spacy_dot_components.md | 3 + edsnlp/docs/reference/pipelines/base.md | 3 + .../pipelines/core/context/context.md | 3 + .../pipelines/core/context/factory.md | 3 + .../reference/pipelines/core/context/index.md | 3 + .../contextual_matcher/contextual_matcher.md | 3 + .../core/contextual_matcher/factory.md | 3 + .../core/contextual_matcher/index.md | 3 + .../core/contextual_matcher/models.md | 3 + .../pipelines/core/endlines/endlines.md | 3 + .../pipelines/core/endlines/endlinesmodel.md | 3 + .../pipelines/core/endlines/factory.md | 3 + .../pipelines/core/endlines/functional.md | 3 + .../pipelines/core/endlines/index.md | 3 + edsnlp/docs/reference/pipelines/core/index.md | 3 + .../pipelines/core/matcher/factory.md | 3 + .../reference/pipelines/core/matcher/index.md | 3 + .../pipelines/core/matcher/matcher.md | 3 + .../core/normalizer/accents/accents.md | 3 + .../core/normalizer/accents/factory.md | 3 + .../core/normalizer/accents/index.md | 3 + .../core/normalizer/accents/patterns.md | 3 + .../pipelines/core/normalizer/factory.md | 3 + .../pipelines/core/normalizer/index.md | 3 + .../core/normalizer/lowercase/factory.md | 3 + .../core/normalizer/lowercase/index.md | 3 + .../pipelines/core/normalizer/normalizer.md | 3 + .../core/normalizer/pollution/factory.md | 3 + .../core/normalizer/pollution/index.md | 3 + .../core/normalizer/pollution/patterns.md | 3 + .../core/normalizer/pollution/pollution.md | 3 + .../core/normalizer/quotes/factory.md | 3 + .../pipelines/core/normalizer/quotes/index.md | 3 + .../core/normalizer/quotes/patterns.md | 3 + .../core/normalizer/quotes/quotes.md | 3 + .../core/normalizer/spaces/factory.md | 3 + .../pipelines/core/normalizer/spaces/index.md | 3 + .../core/normalizer/spaces/spaces.md | 3 + .../pipelines/core/sentences/factory.md | 3 + .../pipelines/core/sentences/index.md | 3 + .../pipelines/core/sentences/terms.md | 3 + .../pipelines/core/terminology/factory.md | 3 + .../pipelines/core/terminology/index.md | 3 + .../pipelines/core/terminology/terminology.md | 3 + edsnlp/docs/reference/pipelines/factories.md | 3 + edsnlp/docs/reference/pipelines/index.md | 3 + .../consultation_dates/consultation_dates.md | 3 + .../misc/consultation_dates/factory.md | 3 + .../misc/consultation_dates/index.md | 3 + .../misc/consultation_dates/patterns.md | 3 + .../reference/pipelines/misc/dates/dates.md | 3 + .../reference/pipelines/misc/dates/factory.md | 3 + .../reference/pipelines/misc/dates/index.md | 3 + .../reference/pipelines/misc/dates/models.md | 3 + .../pipelines/misc/dates/patterns/absolute.md | 3 + .../misc/dates/patterns/atomic/days.md | 3 + .../misc/dates/patterns/atomic/delimiters.md | 3 + .../misc/dates/patterns/atomic/directions.md | 3 + .../misc/dates/patterns/atomic/index.md | 3 + .../misc/dates/patterns/atomic/modes.md | 3 + .../misc/dates/patterns/atomic/months.md | 3 + .../misc/dates/patterns/atomic/numbers.md | 3 + .../misc/dates/patterns/atomic/time.md | 3 + .../misc/dates/patterns/atomic/units.md | 3 + .../misc/dates/patterns/atomic/years.md | 3 + .../pipelines/misc/dates/patterns/current.md | 3 + .../pipelines/misc/dates/patterns/duration.md | 3 + .../misc/dates/patterns/false_positive.md | 3 + .../pipelines/misc/dates/patterns/index.md | 3 + .../pipelines/misc/dates/patterns/relative.md | 3 + edsnlp/docs/reference/pipelines/misc/index.md | 3 + .../pipelines/misc/measurements/factory.md | 3 + .../pipelines/misc/measurements/index.md | 3 + .../misc/measurements/measurements.md | 3 + .../pipelines/misc/measurements/patterns.md | 3 + .../pipelines/misc/reason/factory.md | 3 + .../reference/pipelines/misc/reason/index.md | 3 + .../pipelines/misc/reason/patterns.md | 3 + .../reference/pipelines/misc/reason/reason.md | 3 + .../pipelines/misc/sections/factory.md | 3 + .../pipelines/misc/sections/index.md | 3 + .../pipelines/misc/sections/patterns.md | 3 + .../pipelines/misc/sections/sections.md | 3 + .../reference/pipelines/ner/adicap/adicap.md | 3 + .../reference/pipelines/ner/adicap/factory.md | 3 + .../reference/pipelines/ner/adicap/index.md | 3 + .../reference/pipelines/ner/adicap/models.md | 3 + .../pipelines/ner/adicap/patterns.md | 3 + .../reference/pipelines/ner/cim10/factory.md | 3 + .../reference/pipelines/ner/cim10/index.md | 3 + .../reference/pipelines/ner/cim10/patterns.md | 3 + .../reference/pipelines/ner/covid/factory.md | 3 + .../reference/pipelines/ner/covid/index.md | 3 + .../reference/pipelines/ner/covid/patterns.md | 3 + .../reference/pipelines/ner/drugs/factory.md | 3 + .../reference/pipelines/ner/drugs/index.md | 3 + .../reference/pipelines/ner/drugs/patterns.md | 3 + edsnlp/docs/reference/pipelines/ner/index.md | 3 + .../pipelines/ner/scores/base_score.md | 3 + .../pipelines/ner/scores/charlson/factory.md | 3 + .../pipelines/ner/scores/charlson/index.md | 3 + .../pipelines/ner/scores/charlson/patterns.md | 3 + .../ner/scores/elstonellis/factory.md | 3 + .../pipelines/ner/scores/elstonellis/index.md | 3 + .../ner/scores/elstonellis/patterns.md | 3 + .../ner/scores/emergency/ccmu/factory.md | 3 + .../ner/scores/emergency/ccmu/index.md | 3 + .../ner/scores/emergency/ccmu/patterns.md | 3 + .../ner/scores/emergency/gemsa/factory.md | 3 + .../ner/scores/emergency/gemsa/index.md | 3 + .../ner/scores/emergency/gemsa/patterns.md | 3 + .../pipelines/ner/scores/emergency/index.md | 3 + .../ner/scores/emergency/priority/factory.md | 3 + .../ner/scores/emergency/priority/index.md | 3 + .../ner/scores/emergency/priority/patterns.md | 3 + .../reference/pipelines/ner/scores/factory.md | 3 + .../reference/pipelines/ner/scores/index.md | 3 + .../pipelines/ner/scores/sofa/factory.md | 3 + .../pipelines/ner/scores/sofa/index.md | 3 + .../pipelines/ner/scores/sofa/patterns.md | 3 + .../pipelines/ner/scores/sofa/sofa.md | 3 + .../pipelines/ner/scores/tnm/factory.md | 3 + .../pipelines/ner/scores/tnm/index.md | 3 + .../pipelines/ner/scores/tnm/models.md | 3 + .../pipelines/ner/scores/tnm/patterns.md | 3 + .../reference/pipelines/ner/scores/tnm/tnm.md | 3 + .../reference/pipelines/ner/umls/factory.md | 3 + .../reference/pipelines/ner/umls/index.md | 3 + .../reference/pipelines/ner/umls/patterns.md | 3 + .../reference/pipelines/qualifiers/base.md | 3 + .../pipelines/qualifiers/factories.md | 3 + .../pipelines/qualifiers/family/factory.md | 3 + .../pipelines/qualifiers/family/family.md | 3 + .../pipelines/qualifiers/family/index.md | 3 + .../pipelines/qualifiers/family/patterns.md | 3 + .../pipelines/qualifiers/history/factory.md | 3 + .../pipelines/qualifiers/history/history.md | 3 + .../pipelines/qualifiers/history/index.md | 3 + .../pipelines/qualifiers/history/patterns.md | 3 + .../qualifiers/hypothesis/factory.md | 3 + .../qualifiers/hypothesis/hypothesis.md | 3 + .../pipelines/qualifiers/hypothesis/index.md | 3 + .../qualifiers/hypothesis/patterns.md | 3 + .../reference/pipelines/qualifiers/index.md | 3 + .../pipelines/qualifiers/negation/factory.md | 3 + .../pipelines/qualifiers/negation/index.md | 3 + .../pipelines/qualifiers/negation/negation.md | 3 + .../pipelines/qualifiers/negation/patterns.md | 3 + .../qualifiers/reported_speech/factory.md | 3 + .../qualifiers/reported_speech/index.md | 3 + .../qualifiers/reported_speech/patterns.md | 3 + .../reported_speech/reported_speech.md | 3 + .../docs/reference/pipelines/terminations.md | 3 + .../reference/pipelines/trainable/index.md | 3 + .../pipelines/trainable/nested_ner.md | 3 + .../docs/reference/processing/distributed.md | 3 + edsnlp/docs/reference/processing/helpers.md | 3 + edsnlp/docs/reference/processing/index.md | 3 + edsnlp/docs/reference/processing/parallel.md | 3 + edsnlp/docs/reference/processing/simple.md | 3 + edsnlp/docs/reference/processing/utils.md | 3 + edsnlp/docs/reference/processing/wrapper.md | 3 + edsnlp/docs/reference/utils/blocs.md | 3 + edsnlp/docs/reference/utils/colors.md | 3 + edsnlp/docs/reference/utils/deprecation.md | 3 + edsnlp/docs/reference/utils/examples.md | 3 + edsnlp/docs/reference/utils/extensions.md | 3 + edsnlp/docs/reference/utils/filter.md | 3 + edsnlp/docs/reference/utils/inclusion.md | 3 + edsnlp/docs/reference/utils/index.md | 3 + edsnlp/docs/reference/utils/lists.md | 3 + edsnlp/docs/reference/utils/merge_configs.md | 3 + edsnlp/docs/reference/utils/regex.md | 3 + edsnlp/docs/reference/utils/resources.md | 3 + edsnlp/docs/reference/utils/training.md | 3 + edsnlp/docs/reference/viz/index.md | 3 + edsnlp/docs/reference/viz/quick_examples.md | 3 + {docs => edsnlp/docs}/references.bib | 0 {docs => edsnlp/docs}/resources/sections.svg | 0 {docs => edsnlp/docs}/scripts/plugin.py | 0 {docs => edsnlp/docs}/tokenizers.md | 0 .../docs}/tutorials/detecting-dates.md | 0 {docs => edsnlp/docs}/tutorials/endlines.md | 0 {docs => edsnlp/docs}/tutorials/index.md | 0 .../docs}/tutorials/matching-a-terminology.md | 0 .../docs}/tutorials/multiple-texts.md | 0 .../docs}/tutorials/qualifying-entities.md | 0 .../docs}/tutorials/quick-examples.md | 0 {docs => edsnlp/docs}/tutorials/reason.md | 0 {docs => edsnlp/docs}/tutorials/spacy101.md | 0 .../docs}/utilities/connectors/brat.md | 0 .../docs}/utilities/connectors/index.md | 0 .../docs}/utilities/connectors/labeltool.md | 0 .../docs}/utilities/connectors/omop.md | 0 {docs => edsnlp/docs}/utilities/evaluation.md | 0 {docs => edsnlp/docs}/utilities/index.md | 0 {docs => edsnlp/docs}/utilities/matchers.md | 0 .../docs}/utilities/processing/index.md | 0 .../docs}/utilities/processing/multi.md | 0 .../docs}/utilities/processing/single.md | 0 .../docs}/utilities/processing/spark.md | 0 {docs => edsnlp/docs}/utilities/regex.md | 0 .../docs}/utilities/tests/blocs.md | 0 .../docs}/utilities/tests/examples.md | 0 .../docs}/utilities/tests/index.md | 0 edsnlp/edsnlp/__init__.py | 23 + edsnlp/{ => edsnlp}/components.py | 0 edsnlp/{ => edsnlp}/conjugator.py | 0 edsnlp/{ => edsnlp}/connectors/__init__.py | 0 edsnlp/{ => edsnlp}/connectors/brat.py | 46 +- edsnlp/{ => edsnlp}/connectors/labeltool.py | 0 edsnlp/{ => edsnlp}/connectors/omop.py | 0 edsnlp/edsnlp/corpus_reader.py | 175 + edsnlp/edsnlp/evaluate.py | 431 ++ edsnlp/{ => edsnlp}/extensions.py | 0 edsnlp/{ => edsnlp}/language.py | 0 .../{models => edsnlp/matchers}/__init__.py | 0 edsnlp/{ => edsnlp}/matchers/phrase.pxd | 0 edsnlp/{ => edsnlp}/matchers/phrase.pyx | 0 edsnlp/{ => edsnlp}/matchers/regex.py | 0 edsnlp/{ => edsnlp}/matchers/simstring.py | 0 .../{ => edsnlp}/matchers/utils/__init__.py | 0 edsnlp/{ => edsnlp}/matchers/utils/offset.py | 0 edsnlp/{ => edsnlp}/matchers/utils/text.py | 0 .../patch_spacy_dot_components.py | 0 .../torch => edsnlp/pipelines}/__init__.py | 0 edsnlp/edsnlp/pipelines/base.py | 318 ++ edsnlp/edsnlp/pipelines/clean_entities.py | 47 + .../pipelines/core}/__init__.py | 0 .../pipelines/core/context/__init__.py | 0 .../pipelines/core/context/context.py | 0 .../pipelines/core/context/factory.py | 0 .../core/contextual_matcher/__init__.py | 0 .../contextual_matcher/contextual_matcher.py | 0 .../core/contextual_matcher/factory.py | 0 .../core/contextual_matcher/models.py | 0 .../pipelines/core/endlines/__init__.py | 0 .../pipelines/core/endlines/endlines.py | 0 .../pipelines/core/endlines/endlinesmodel.py | 0 .../pipelines/core/endlines/factory.py | 0 .../pipelines/core/endlines/functional.py | 0 .../pipelines/core/matcher/__init__.py | 0 .../pipelines/core/matcher/factory.py | 0 .../pipelines/core/matcher/matcher.py | 0 .../pipelines/core/normalizer/__init__.py | 0 .../core/normalizer/accents/__init__.py | 0 .../core/normalizer/accents/accents.py | 0 .../core/normalizer/accents/factory.py | 0 .../core/normalizer/accents/patterns.py | 0 .../pipelines/core/normalizer/factory.py | 0 .../core/normalizer/lowercase/__init__.py | 0 .../core/normalizer/lowercase/factory.py | 0 .../pipelines/core/normalizer/normalizer.py | 0 .../core/normalizer/pollution/__init__.py | 0 .../core/normalizer/pollution/factory.py | 0 .../core/normalizer/pollution/patterns.py | 0 .../core/normalizer/pollution/pollution.py | 0 .../core/normalizer/quotes/__init__.py | 0 .../core/normalizer/quotes/factory.py | 0 .../core/normalizer/quotes/patterns.py | 0 .../core/normalizer/quotes/quotes.py | 0 .../core/normalizer/spaces/__init__.py | 0 .../core/normalizer/spaces/factory.py | 0 .../core/normalizer/spaces/spaces.py | 0 .../pipelines/core/sentences/__init__.py | 0 .../pipelines/core/sentences/factory.py | 0 .../pipelines/core/sentences/sentences.pxd | 0 .../pipelines/core/sentences/sentences.pyx | 0 .../pipelines/core/sentences/terms.py | 0 .../pipelines/core/terminology/__init__.py | 0 .../pipelines/core/terminology/factory.py | 0 .../pipelines/core/terminology/terminology.py | 0 edsnlp/{ => edsnlp}/pipelines/factories.py | 3 +- .../pipelines/misc}/__init__.py | 0 .../misc/consultation_dates/__init__.py | 0 .../consultation_dates/consultation_dates.py | 0 .../misc/consultation_dates/factory.py | 0 .../misc/consultation_dates/patterns.py | 0 .../pipelines/misc/dates/__init__.py | 0 .../pipelines/misc/dates/dates.py | 0 .../pipelines/misc/dates/factory.py | 0 .../pipelines/misc/dates/models.py | 0 .../pipelines/misc/dates/patterns/__init__.py | 0 .../pipelines/misc/dates/patterns/absolute.py | 0 .../misc/dates/patterns/atomic}/__init__.py | 0 .../misc/dates/patterns/atomic/days.py | 0 .../misc/dates/patterns/atomic/delimiters.py | 0 .../misc/dates/patterns/atomic/directions.py | 0 .../misc/dates/patterns/atomic/modes.py | 0 .../misc/dates/patterns/atomic/months.py | 0 .../misc/dates/patterns/atomic/numbers.py | 0 .../misc/dates/patterns/atomic/time.py | 0 .../misc/dates/patterns/atomic/units.py | 0 .../misc/dates/patterns/atomic/years.py | 0 .../pipelines/misc/dates/patterns/current.py | 0 .../pipelines/misc/dates/patterns/duration.py | 0 .../misc/dates/patterns/false_positive.py | 0 .../pipelines/misc/dates/patterns/relative.py | 0 .../pipelines/misc/measurements/__init__.py | 0 .../pipelines/misc/measurements/factory.py | 27 +- .../misc/measurements/measurements.py | 228 +- .../pipelines/misc/measurements/patterns.py | 10 + .../pipelines/misc/reason/__init__.py | 0 .../pipelines/misc/reason/factory.py | 0 .../pipelines/misc/reason/patterns.py | 0 .../pipelines/misc/reason/reason.py | 0 .../pipelines/misc/sections/__init__.py | 0 .../pipelines/misc/sections/factory.py | 0 .../pipelines/misc/sections/patterns.py | 0 .../pipelines/misc/sections/sections.py | 0 .../pipelines/misc/tables/__init__.py | 0 .../pipelines/misc/tables/factory.py | 0 .../pipelines/misc/tables/patterns.py | 0 .../pipelines/misc/tables/tables.py | 32 +- .../pipelines/ner}/__init__.py | 0 .../pipelines/ner/adicap}/__init__.py | 0 .../pipelines/ner/adicap/adicap.py | 0 .../pipelines/ner/adicap/factory.py | 0 .../pipelines/ner/adicap/models.py | 0 .../pipelines/ner/adicap/patterns.py | 0 .../pipelines/ner/cim10}/__init__.py | 0 .../pipelines/ner/cim10/factory.py | 0 .../pipelines/ner/cim10/patterns.py | 0 .../pipelines/ner/covid}/__init__.py | 0 .../pipelines/ner/covid/factory.py | 0 .../pipelines/ner/covid/patterns.py | 0 .../pipelines/ner/drugs}/__init__.py | 0 .../pipelines/ner/drugs/factory.py | 0 .../pipelines/ner/drugs/patterns.py | 0 .../pipelines/ner/scores/__init__.py | 0 .../pipelines/ner/scores/base_score.py | 0 .../ner/scores/charlson}/__init__.py | 0 .../pipelines/ner/scores/charlson/factory.py | 0 .../pipelines/ner/scores/charlson/patterns.py | 0 .../ner/scores/elstonellis}/__init__.py | 0 .../ner/scores/elstonellis/factory.py | 0 .../ner/scores/elstonellis/patterns.py | 0 .../ner/scores/emergency}/__init__.py | 0 .../ner/scores/emergency/ccmu}/__init__.py | 0 .../ner/scores/emergency/ccmu/factory.py | 0 .../ner/scores/emergency/ccmu/patterns.py | 0 .../ner/scores/emergency/gemsa}/__init__.py | 0 .../ner/scores/emergency/gemsa/factory.py | 0 .../ner/scores/emergency/gemsa/patterns.py | 0 .../scores/emergency/priority}/__init__.py | 0 .../ner/scores/emergency/priority/factory.py | 0 .../ner/scores/emergency/priority/patterns.py | 0 .../pipelines/ner/scores/factory.py | 0 .../pipelines/ner/scores/sofa/__init__.py | 0 .../pipelines/ner/scores/sofa/factory.py | 0 .../pipelines/ner/scores/sofa/patterns.py | 0 .../pipelines/ner/scores/sofa/sofa.py | 0 .../pipelines/ner/scores/tnm/__init__.py | 0 .../pipelines/ner/scores/tnm/factory.py | 0 .../pipelines/ner/scores/tnm/models.py | 0 .../pipelines/ner/scores/tnm/patterns.py | 0 .../pipelines/ner/scores/tnm/tnm.py | 0 .../pipelines/ner/umls}/__init__.py | 0 .../pipelines/ner/umls/factory.py | 0 .../pipelines/ner/umls/patterns.py | 0 .../pipelines/qualifiers}/__init__.py | 0 .../{ => edsnlp}/pipelines/qualifiers/base.py | 0 .../pipelines/qualifiers/factories.py | 0 .../pipelines/qualifiers/family/__init__.py | 0 .../pipelines/qualifiers/family/factory.py | 0 .../pipelines/qualifiers/family/family.py | 0 .../pipelines/qualifiers/family/patterns.py | 0 .../pipelines/qualifiers/history/__init__.py | 0 .../pipelines/qualifiers/history/factory.py | 0 .../pipelines/qualifiers/history/history.py | 0 .../pipelines/qualifiers/history/patterns.py | 0 .../qualifiers/hypothesis/__init__.py | 0 .../qualifiers/hypothesis/factory.py | 0 .../qualifiers/hypothesis/hypothesis.py | 0 .../qualifiers/hypothesis/patterns.py | 0 .../pipelines/qualifiers/negation/__init__.py | 0 .../pipelines/qualifiers/negation/factory.py | 0 .../pipelines/qualifiers/negation/negation.py | 0 .../pipelines/qualifiers/negation/patterns.py | 0 .../qualifiers/reported_speech/__init__.py | 0 .../qualifiers/reported_speech/factory.py | 0 .../qualifiers/reported_speech/patterns.py | 0 .../reported_speech/reported_speech.py | 0 edsnlp/{ => edsnlp}/pipelines/terminations.py | 0 .../pipelines/trainable}/__init__.py | 0 .../pipelines/trainable/layers}/__init__.py | 0 .../pipelines/trainable/layers}/crf.py | 4 +- .../trainable/nested_ner/__init__.py | 1 + .../pipelines/trainable/nested_ner/factory.py | 83 + .../trainable/nested_ner}/nested_ner.py | 138 +- .../trainable/nested_ner}/stack_crf_ner.py | 27 +- .../pipelines/trainable}/pytorch_wrapper.py | 13 +- .../trainable/span_qualifier/__init__.py | 1 + .../trainable/span_qualifier/factory.py | 124 + .../span_qualifier/span_multi_classifier.py | 240 + .../span_qualifier/span_qualifier.py | 529 ++ .../trainable/span_qualifier/utils.py | 184 + edsnlp/{ => edsnlp}/processing/__init__.py | 0 edsnlp/{ => edsnlp}/processing/distributed.py | 0 edsnlp/{ => edsnlp}/processing/helpers.py | 0 edsnlp/{ => edsnlp}/processing/parallel.py | 0 edsnlp/{ => edsnlp}/processing/simple.py | 0 edsnlp/{ => edsnlp}/processing/utils.py | 0 edsnlp/{ => edsnlp}/processing/wrapper.py | 0 edsnlp/{ => edsnlp}/resources/adicap.json.gz | Bin edsnlp/{ => edsnlp}/resources/cim10.csv.gz | Bin edsnlp/{ => edsnlp}/resources/drugs.json | 0 edsnlp/{ => edsnlp}/resources/verbs.csv.gz | Bin edsnlp/{ => edsnlp}/utils/__init__.py | 0 edsnlp/{ => edsnlp}/utils/blocs.py | 0 edsnlp/{ => edsnlp}/utils/colors.py | 0 edsnlp/{ => edsnlp}/utils/deprecation.py | 0 edsnlp/{ => edsnlp}/utils/examples.py | 0 edsnlp/{ => edsnlp}/utils/extensions.py | 0 edsnlp/{ => edsnlp}/utils/filter.py | 100 +- edsnlp/{ => edsnlp}/utils/inclusion.py | 0 edsnlp/{ => edsnlp}/utils/lists.py | 0 edsnlp/{ => edsnlp}/utils/merge_configs.py | 0 edsnlp/{ => edsnlp}/utils/regex.py | 0 edsnlp/{ => edsnlp}/utils/resources.py | 0 edsnlp/edsnlp/utils/span_getters.py | 96 + edsnlp/{ => edsnlp}/utils/training.py | 0 edsnlp/{ => edsnlp}/viz/__init__.py | 0 edsnlp/{ => edsnlp}/viz/quick_examples.py | 0 mkdocs.yml => edsnlp/mkdocs.yml | 0 {notebooks => edsnlp/notebooks}/README.md | 0 .../notebooks}/connectors/context.py | 0 edsnlp/notebooks/connectors/omop.md | 99 + edsnlp/notebooks/context.py | 5 + edsnlp/notebooks/dates/context.py | 5 + edsnlp/notebooks/dates/prototype.md | 89 + edsnlp/notebooks/dates/user-guide.md | 93 + edsnlp/notebooks/endlines/endlines-example.md | 182 + edsnlp/notebooks/normalizer/context.py | 5 + edsnlp/notebooks/normalizer/profiling.md | 235 + edsnlp/notebooks/normalizer/prototype.md | 109 + edsnlp/notebooks/pipeline.md | 193 + edsnlp/notebooks/premier-pipeline.md | 260 + .../notebooks/sections}/context.py | 0 edsnlp/notebooks/sections/section-dataset.md | 158 + edsnlp/notebooks/sections/testing.md | 168 + edsnlp/notebooks/sentences/context.py | 5 + .../notebooks}/sentences/sentences.md | 0 edsnlp/notebooks/tnm/prototype.md | 63 + edsnlp/notebooks/tokenizer/context.py | 5 + edsnlp/notebooks/tokenizer/tokenizer.md | 141 + edsnlp/notebooks/utilities/brat.md | 99 + edsnlp/notebooks/utilities/context.py | 5 + edsnlp/pipelines/base.py | 64 - pyproject.toml => edsnlp/pyproject.toml | 14 +- {scripts => edsnlp/scripts}/adicap.py | 0 {scripts => edsnlp/scripts}/cim10.py | 0 .../scripts}/conjugate_verbs.py | 0 {scripts => edsnlp/scripts}/context.py | 0 {scripts => edsnlp/scripts}/serve.py | 0 setup.py => edsnlp/setup.py | 0 {tests => edsnlp/tests}/conftest.py | 0 .../tests}/connectors/test_brat.py | 0 .../tests}/connectors/test_labeltool.py | 0 .../tests}/connectors/test_omop.py | 0 .../tests}/matchers/test_phrase.py | 0 .../tests}/matchers/test_regex.py | 0 .../tests}/matchers/test_simstring.py | 0 .../pipelines/core/test_contextual_matcher.py | 0 .../tests}/pipelines/core/test_endlines.py | 0 .../tests}/pipelines/core/test_matcher.py | 0 .../pipelines/core/test_normalisation.py | 0 .../tests}/pipelines/core/test_sentences.py | 0 .../tests}/pipelines/core/test_terminology.py | 0 .../pipelines/misc/test_consultation_date.py | 0 .../tests}/pipelines/misc/test_dates.py | 0 .../pipelines/misc/test_measurements.py | 0 .../tests}/pipelines/misc/test_reason.py | 0 .../tests}/pipelines/misc/test_sections.py | 0 .../tests}/pipelines/ner/test_adicap.py | 0 .../pipelines/ner/test_adicap_decoder.py | 0 .../tests}/pipelines/ner/test_cim10.py | 0 .../tests}/pipelines/ner/test_covid.py | 0 .../tests}/pipelines/ner/test_drugs.py | 0 .../tests}/pipelines/ner/test_score.py | 0 .../tests}/pipelines/ner/test_tnm.py | 0 .../tests}/pipelines/ner/test_umls.py | 0 .../tests}/pipelines/qualifiers/conftest.py | 0 .../pipelines/qualifiers/test_family.py | 0 .../pipelines/qualifiers/test_history.py | 0 .../pipelines/qualifiers/test_hypothesis.py | 0 .../pipelines/qualifiers/test_negation.py | 0 .../qualifiers/test_reported_speech.py | 0 .../tests}/pipelines/test_pipelines.py | 0 .../pipelines/trainable/test_nested_ner.py | 0 .../tests}/processing/test_processing.py | 0 {tests => edsnlp/tests}/readme.md | 0 {tests => edsnlp/tests}/test_conjugator.py | 0 {tests => edsnlp/tests}/test_docs.py | 0 {tests => edsnlp/tests}/test_language.py | 0 .../tests}/utils/test_examples.py | 0 {tests => edsnlp/tests}/utils/test_filter.py | 0 .../tests}/utils/test_quick_examples.py | 0 notebooks/export_pandas_to_brat.py | 55 + notebooks/get_stats_by_section_on_cim10.md | 1828 ++++++ notebooks/knowledge.py | 650 +++ tests/resources/brat_data/subfolder/doc-1.ann | 24 - tests/resources/brat_data/subfolder/doc-1.txt | 10 - 670 files changed, 22368 insertions(+), 317 deletions(-) delete mode 100644 .pre-commit-config.yaml create mode 100644 NER_model/configs/config.cfg create mode 100644 NER_model/configs/config_v1.cfg create mode 100644 NER_model/configs/expe_lang_model/config_DrBert.cfg create mode 100644 NER_model/configs/expe_lang_model/config_camembert_base.cfg create mode 100644 NER_model/configs/expe_lang_model/config_camembert_bio.cfg create mode 100644 NER_model/configs/expe_lang_model/config_eds_finetune.cfg create mode 100644 NER_model/configs/expe_lang_model/config_eds_scratch.cfg create mode 100644 NER_model/configs/rebus/__.cfg create mode 100644 NER_model/configs/rebus/config.cfg create mode 100644 NER_model/configs/rebus/config_0407_Pierre.cfg create mode 100644 NER_model/configs/rebus/config_1907.cfg create mode 100644 NER_model/configs/rebus/config_backup2.cfg create mode 100644 NER_model/configs/rebus/config_j.cfg create mode 100644 NER_model/data/NLP_diabeto/clean/annotation.conf create mode 100644 NER_model/data/NLP_diabeto/clean/kb_shortcuts.conf create mode 100644 NER_model/data/NLP_diabeto/clean/visual.conf create mode 100644 NER_model/data/NLP_diabeto/raw/annotation.conf create mode 100644 NER_model/data/NLP_diabeto/raw/kb_shortcuts.conf create mode 100644 NER_model/data/NLP_diabeto/raw/visual.conf create mode 100644 NER_model/dvc.lock create mode 100644 NER_model/dvc.yaml create mode 100644 NER_model/evaluate.md create mode 100644 NER_model/poetry.lock create mode 100644 NER_model/project.lock create mode 100644 NER_model/project.yml create mode 100644 NER_model/pyproject.toml create mode 100644 NER_model/scripts/convert.py create mode 100644 NER_model/scripts/evaluate.py create mode 100644 NER_model/scripts/infer.py create mode 100644 NER_model/scripts/new_evaluate.py create mode 100644 NER_model/scripts/package.py create mode 100644 NER_model/scripts/save_to_brat.py create mode 100644 NER_model/scripts/visualize_model.py create mode 100644 Normalisation/drugs/exception.py create mode 100644 Normalisation/drugs/normalisation.py create mode 100644 Normalisation/extract_measurement/config.py create mode 100644 Normalisation/extract_measurement/extract_measurements_from_brat.py create mode 100644 Normalisation/extract_measurement/extract_pandas_from_brat.py create mode 100644 Normalisation/extract_measurement/main.py create mode 100644 Normalisation/extract_measurement/measurements_patterns.py create mode 100644 Normalisation/inference/config.py create mode 100644 Normalisation/inference/extract_pandas_from_brat.py create mode 100644 Normalisation/inference/get_normalization_with_coder.py create mode 100644 Normalisation/inference/main.py create mode 100644 Normalisation/inference/text_preprocessor.py rename {edsnlp/matchers => Normalisation/training}/__init__.py (100%) create mode 100644 Normalisation/training/data_util.py create mode 100644 Normalisation/training/extract_bert.py create mode 100644 Normalisation/training/extract_pandas_from_brat.py create mode 100644 Normalisation/training/generate_term_embeddings.py create mode 100644 Normalisation/training/generate_term_embeddings.sh create mode 100644 Normalisation/training/generate_term_embeddings_coder_eds.py create mode 100644 Normalisation/training/generate_term_embeddings_coder_eds.sh create mode 100644 Normalisation/training/generate_term_embeddings_coder_eds_cased.py create mode 100644 Normalisation/training/generate_term_embeddings_coder_eds_cased.sh create mode 100644 Normalisation/training/generate_umls_embeddings.py create mode 100644 Normalisation/training/generate_umls_embeddings.sh create mode 100644 Normalisation/training/generate_umls_normalized_embeddings.py create mode 100644 Normalisation/training/generate_umls_normalized_embeddings.sh create mode 100644 Normalisation/training/generate_umls_normalized_embeddings_coder_eds.py create mode 100644 Normalisation/training/generate_umls_normalized_embeddings_coder_eds.sh create mode 100644 Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.py create mode 100644 Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.sh create mode 100644 Normalisation/training/get_matches.py create mode 100644 Normalisation/training/get_matches.sh create mode 100644 Normalisation/training/load_umls.py create mode 100644 Normalisation/training/load_umls_normalized.py create mode 100644 Normalisation/training/log/generate_umls_embeddings_coder_eds_cased.sh-11575.err create mode 100644 Normalisation/training/log/generate_umls_embeddings_coder_eds_cased.sh-11575.out create mode 100644 Normalisation/training/loss.py create mode 100644 Normalisation/training/model.py create mode 100644 Normalisation/training/sampler_util.py create mode 100644 Normalisation/training/train.py create mode 100644 Normalisation/training/train_coder_slurm.cfg create mode 100644 Normalisation/training/trans.py create mode 100644 bash_scripts/NER_model/expe_data_size.sh create mode 100644 bash_scripts/NER_model/expe_hyperparams.sh create mode 100644 bash_scripts/NER_model/expe_model_lang.sh create mode 100644 bash_scripts/NER_model/infer.sh create mode 100644 bash_scripts/NER_model/save.sh create mode 100644 bash_scripts/NER_model/test.sh create mode 100644 bash_scripts/NER_model/train.sh create mode 100644 bash_scripts/NER_model/train_v1.sh create mode 100644 bash_scripts/Normalisation/extract_measurement.sh create mode 100644 bash_scripts/Normalisation/infer_coder.sh create mode 100644 bash_scripts/Normalisation/infer_coder_quaero.sh create mode 100755 bash_scripts/Normalisation/train_coder.sh delete mode 100644 demo/requirements.txt rename CITATION.cff => edsnlp/CITATION.cff (100%) rename LICENSE => edsnlp/LICENSE (100%) rename Makefile => edsnlp/Makefile (100%) rename README.md => edsnlp/README.md (100%) delete mode 100644 edsnlp/__init__.py rename changelog.md => edsnlp/changelog.md (100%) rename contributing.md => edsnlp/contributing.md (100%) rename {demo => edsnlp/demo}/app.py (100%) rename {docs => edsnlp/docs}/advanced-tutorials/fastapi.md (100%) rename {docs => edsnlp/docs}/advanced-tutorials/index.md (100%) rename {docs => edsnlp/docs}/advanced-tutorials/word-vectors.md (100%) rename {docs => edsnlp/docs}/assets/logo/aphp-blue.svg (100%) rename {docs => edsnlp/docs}/assets/logo/aphp-white.svg (100%) rename {docs => edsnlp/docs}/assets/logo/edsnlp.svg (100%) rename {docs => edsnlp/docs}/assets/stylesheets/extra.css (100%) rename {docs => edsnlp/docs}/assets/templates/python/material/docstring.html (100%) rename {docs => edsnlp/docs}/assets/templates/python/material/docstring/parameters.html (100%) rename {docs => edsnlp/docs}/assets/templates/python/material/function.html (100%) rename {docs => edsnlp/docs}/assets/termynal/termynal.css (100%) rename {docs => edsnlp/docs}/assets/termynal/termynal.js (100%) create mode 100644 edsnlp/docs/changelog.md create mode 100644 edsnlp/docs/contributing.md rename {docs => edsnlp/docs}/index.md (100%) rename {docs => edsnlp/docs}/pipelines/architecture.md (100%) rename {docs => edsnlp/docs}/pipelines/core/contextual-matcher.md (100%) rename {docs => edsnlp/docs}/pipelines/core/endlines.md (100%) rename {docs => edsnlp/docs}/pipelines/core/index.md (100%) rename {docs => edsnlp/docs}/pipelines/core/matcher.md (100%) rename {docs => edsnlp/docs}/pipelines/core/normalisation.md (100%) rename {docs => edsnlp/docs}/pipelines/core/resources/alignment.svg (100%) rename {docs => edsnlp/docs}/pipelines/core/resources/span-alignment.svg (100%) rename {docs => edsnlp/docs}/pipelines/core/sentences.md (100%) rename {docs => edsnlp/docs}/pipelines/core/terminology.md (100%) rename {docs => edsnlp/docs}/pipelines/index.md (100%) rename {docs => edsnlp/docs}/pipelines/misc/consultation-dates.md (100%) rename {docs => edsnlp/docs}/pipelines/misc/dates.md (100%) rename {docs => edsnlp/docs}/pipelines/misc/index.md (100%) rename {docs => edsnlp/docs}/pipelines/misc/measurements.md (100%) rename {docs => edsnlp/docs}/pipelines/misc/reason.md (100%) rename {docs => edsnlp/docs}/pipelines/misc/sections.md (100%) rename {docs => edsnlp/docs}/pipelines/misc/tables.md (100%) rename {docs => edsnlp/docs}/pipelines/ner/adicap.md (100%) rename {docs => edsnlp/docs}/pipelines/ner/cim10.md (100%) rename {docs => edsnlp/docs}/pipelines/ner/covid.md (100%) rename {docs => edsnlp/docs}/pipelines/ner/drugs.md (100%) rename {docs => edsnlp/docs}/pipelines/ner/index.md (100%) rename {docs => edsnlp/docs}/pipelines/ner/score.md (100%) rename {docs => edsnlp/docs}/pipelines/ner/umls.md (100%) rename {docs => edsnlp/docs}/pipelines/qualifiers/family.md (100%) rename {docs => edsnlp/docs}/pipelines/qualifiers/history.md (100%) rename {docs => edsnlp/docs}/pipelines/qualifiers/hypothesis.md (100%) rename {docs => edsnlp/docs}/pipelines/qualifiers/index.md (100%) rename {docs => edsnlp/docs}/pipelines/qualifiers/negation.md (100%) rename {docs => edsnlp/docs}/pipelines/qualifiers/reported-speech.md (100%) rename {docs => edsnlp/docs}/pipelines/trainable/edsnlp-ner.svg (100%) rename {docs => edsnlp/docs}/pipelines/trainable/index.md (100%) rename {docs => edsnlp/docs}/pipelines/trainable/ner.md (100%) create mode 100644 edsnlp/docs/reference/components.md create mode 100644 edsnlp/docs/reference/conjugator.md create mode 100644 edsnlp/docs/reference/connectors/brat.md create mode 100644 edsnlp/docs/reference/connectors/index.md create mode 100644 edsnlp/docs/reference/connectors/labeltool.md create mode 100644 edsnlp/docs/reference/connectors/omop.md create mode 100644 edsnlp/docs/reference/extensions.md create mode 100644 edsnlp/docs/reference/index.md create mode 100644 edsnlp/docs/reference/language.md create mode 100644 edsnlp/docs/reference/matchers/index.md create mode 100644 edsnlp/docs/reference/matchers/regex.md create mode 100644 edsnlp/docs/reference/matchers/simstring.md create mode 100644 edsnlp/docs/reference/matchers/utils/index.md create mode 100644 edsnlp/docs/reference/matchers/utils/offset.md create mode 100644 edsnlp/docs/reference/matchers/utils/text.md create mode 100644 edsnlp/docs/reference/models/index.md create mode 100644 edsnlp/docs/reference/models/pytorch_wrapper.md create mode 100644 edsnlp/docs/reference/models/stack_crf_ner.md create mode 100644 edsnlp/docs/reference/models/torch/crf.md create mode 100644 edsnlp/docs/reference/models/torch/index.md create mode 100644 edsnlp/docs/reference/patch_spacy_dot_components.md create mode 100644 edsnlp/docs/reference/pipelines/base.md create mode 100644 edsnlp/docs/reference/pipelines/core/context/context.md create mode 100644 edsnlp/docs/reference/pipelines/core/context/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/context/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/contextual_matcher/contextual_matcher.md create mode 100644 edsnlp/docs/reference/pipelines/core/contextual_matcher/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/contextual_matcher/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/contextual_matcher/models.md create mode 100644 edsnlp/docs/reference/pipelines/core/endlines/endlines.md create mode 100644 edsnlp/docs/reference/pipelines/core/endlines/endlinesmodel.md create mode 100644 edsnlp/docs/reference/pipelines/core/endlines/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/endlines/functional.md create mode 100644 edsnlp/docs/reference/pipelines/core/endlines/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/matcher/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/matcher/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/matcher/matcher.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/accents/accents.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/accents/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/accents/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/accents/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/lowercase/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/lowercase/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/normalizer.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/pollution/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/pollution/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/pollution/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/pollution/pollution.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/quotes/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/quotes/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/quotes/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/quotes/quotes.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/spaces/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/spaces/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/normalizer/spaces/spaces.md create mode 100644 edsnlp/docs/reference/pipelines/core/sentences/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/sentences/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/sentences/terms.md create mode 100644 edsnlp/docs/reference/pipelines/core/terminology/factory.md create mode 100644 edsnlp/docs/reference/pipelines/core/terminology/index.md create mode 100644 edsnlp/docs/reference/pipelines/core/terminology/terminology.md create mode 100644 edsnlp/docs/reference/pipelines/factories.md create mode 100644 edsnlp/docs/reference/pipelines/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/consultation_dates/consultation_dates.md create mode 100644 edsnlp/docs/reference/pipelines/misc/consultation_dates/factory.md create mode 100644 edsnlp/docs/reference/pipelines/misc/consultation_dates/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/consultation_dates/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/dates.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/factory.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/models.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/absolute.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/days.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/delimiters.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/directions.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/modes.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/months.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/numbers.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/time.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/units.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/years.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/current.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/duration.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/false_positive.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/dates/patterns/relative.md create mode 100644 edsnlp/docs/reference/pipelines/misc/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/measurements/factory.md create mode 100644 edsnlp/docs/reference/pipelines/misc/measurements/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/measurements/measurements.md create mode 100644 edsnlp/docs/reference/pipelines/misc/measurements/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/misc/reason/factory.md create mode 100644 edsnlp/docs/reference/pipelines/misc/reason/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/reason/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/misc/reason/reason.md create mode 100644 edsnlp/docs/reference/pipelines/misc/sections/factory.md create mode 100644 edsnlp/docs/reference/pipelines/misc/sections/index.md create mode 100644 edsnlp/docs/reference/pipelines/misc/sections/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/misc/sections/sections.md create mode 100644 edsnlp/docs/reference/pipelines/ner/adicap/adicap.md create mode 100644 edsnlp/docs/reference/pipelines/ner/adicap/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/adicap/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/adicap/models.md create mode 100644 edsnlp/docs/reference/pipelines/ner/adicap/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/cim10/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/cim10/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/cim10/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/covid/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/covid/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/covid/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/drugs/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/drugs/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/drugs/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/base_score.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/charlson/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/charlson/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/charlson/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/elstonellis/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/elstonellis/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/elstonellis/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/sofa/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/sofa/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/sofa/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/sofa/sofa.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/tnm/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/tnm/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/tnm/models.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/tnm/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/ner/scores/tnm/tnm.md create mode 100644 edsnlp/docs/reference/pipelines/ner/umls/factory.md create mode 100644 edsnlp/docs/reference/pipelines/ner/umls/index.md create mode 100644 edsnlp/docs/reference/pipelines/ner/umls/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/base.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/factories.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/family/factory.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/family/family.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/family/index.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/family/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/history/factory.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/history/history.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/history/index.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/history/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/hypothesis/factory.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/hypothesis/hypothesis.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/hypothesis/index.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/hypothesis/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/index.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/negation/factory.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/negation/index.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/negation/negation.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/negation/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/reported_speech/factory.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/reported_speech/index.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/reported_speech/patterns.md create mode 100644 edsnlp/docs/reference/pipelines/qualifiers/reported_speech/reported_speech.md create mode 100644 edsnlp/docs/reference/pipelines/terminations.md create mode 100644 edsnlp/docs/reference/pipelines/trainable/index.md create mode 100644 edsnlp/docs/reference/pipelines/trainable/nested_ner.md create mode 100644 edsnlp/docs/reference/processing/distributed.md create mode 100644 edsnlp/docs/reference/processing/helpers.md create mode 100644 edsnlp/docs/reference/processing/index.md create mode 100644 edsnlp/docs/reference/processing/parallel.md create mode 100644 edsnlp/docs/reference/processing/simple.md create mode 100644 edsnlp/docs/reference/processing/utils.md create mode 100644 edsnlp/docs/reference/processing/wrapper.md create mode 100644 edsnlp/docs/reference/utils/blocs.md create mode 100644 edsnlp/docs/reference/utils/colors.md create mode 100644 edsnlp/docs/reference/utils/deprecation.md create mode 100644 edsnlp/docs/reference/utils/examples.md create mode 100644 edsnlp/docs/reference/utils/extensions.md create mode 100644 edsnlp/docs/reference/utils/filter.md create mode 100644 edsnlp/docs/reference/utils/inclusion.md create mode 100644 edsnlp/docs/reference/utils/index.md create mode 100644 edsnlp/docs/reference/utils/lists.md create mode 100644 edsnlp/docs/reference/utils/merge_configs.md create mode 100644 edsnlp/docs/reference/utils/regex.md create mode 100644 edsnlp/docs/reference/utils/resources.md create mode 100644 edsnlp/docs/reference/utils/training.md create mode 100644 edsnlp/docs/reference/viz/index.md create mode 100644 edsnlp/docs/reference/viz/quick_examples.md rename {docs => edsnlp/docs}/references.bib (100%) rename {docs => edsnlp/docs}/resources/sections.svg (100%) rename {docs => edsnlp/docs}/scripts/plugin.py (100%) rename {docs => edsnlp/docs}/tokenizers.md (100%) rename {docs => edsnlp/docs}/tutorials/detecting-dates.md (100%) rename {docs => edsnlp/docs}/tutorials/endlines.md (100%) rename {docs => edsnlp/docs}/tutorials/index.md (100%) rename {docs => edsnlp/docs}/tutorials/matching-a-terminology.md (100%) rename {docs => edsnlp/docs}/tutorials/multiple-texts.md (100%) rename {docs => edsnlp/docs}/tutorials/qualifying-entities.md (100%) rename {docs => edsnlp/docs}/tutorials/quick-examples.md (100%) rename {docs => edsnlp/docs}/tutorials/reason.md (100%) rename {docs => edsnlp/docs}/tutorials/spacy101.md (100%) rename {docs => edsnlp/docs}/utilities/connectors/brat.md (100%) rename {docs => edsnlp/docs}/utilities/connectors/index.md (100%) rename {docs => edsnlp/docs}/utilities/connectors/labeltool.md (100%) rename {docs => edsnlp/docs}/utilities/connectors/omop.md (100%) rename {docs => edsnlp/docs}/utilities/evaluation.md (100%) rename {docs => edsnlp/docs}/utilities/index.md (100%) rename {docs => edsnlp/docs}/utilities/matchers.md (100%) rename {docs => edsnlp/docs}/utilities/processing/index.md (100%) rename {docs => edsnlp/docs}/utilities/processing/multi.md (100%) rename {docs => edsnlp/docs}/utilities/processing/single.md (100%) rename {docs => edsnlp/docs}/utilities/processing/spark.md (100%) rename {docs => edsnlp/docs}/utilities/regex.md (100%) rename {docs => edsnlp/docs}/utilities/tests/blocs.md (100%) rename {docs => edsnlp/docs}/utilities/tests/examples.md (100%) rename {docs => edsnlp/docs}/utilities/tests/index.md (100%) create mode 100644 edsnlp/edsnlp/__init__.py rename edsnlp/{ => edsnlp}/components.py (100%) rename edsnlp/{ => edsnlp}/conjugator.py (100%) rename edsnlp/{ => edsnlp}/connectors/__init__.py (100%) rename edsnlp/{ => edsnlp}/connectors/brat.py (95%) rename edsnlp/{ => edsnlp}/connectors/labeltool.py (100%) rename edsnlp/{ => edsnlp}/connectors/omop.py (100%) create mode 100644 edsnlp/edsnlp/corpus_reader.py create mode 100644 edsnlp/edsnlp/evaluate.py rename edsnlp/{ => edsnlp}/extensions.py (100%) rename edsnlp/{ => edsnlp}/language.py (100%) rename edsnlp/{models => edsnlp/matchers}/__init__.py (100%) rename edsnlp/{ => edsnlp}/matchers/phrase.pxd (100%) rename edsnlp/{ => edsnlp}/matchers/phrase.pyx (100%) rename edsnlp/{ => edsnlp}/matchers/regex.py (100%) rename edsnlp/{ => edsnlp}/matchers/simstring.py (100%) rename edsnlp/{ => edsnlp}/matchers/utils/__init__.py (100%) rename edsnlp/{ => edsnlp}/matchers/utils/offset.py (100%) rename edsnlp/{ => edsnlp}/matchers/utils/text.py (100%) rename edsnlp/{ => edsnlp}/patch_spacy_dot_components.py (100%) rename edsnlp/{models/torch => edsnlp/pipelines}/__init__.py (100%) create mode 100644 edsnlp/edsnlp/pipelines/base.py create mode 100644 edsnlp/edsnlp/pipelines/clean_entities.py rename edsnlp/{pipelines => edsnlp/pipelines/core}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/context/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/context/context.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/context/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/contextual_matcher/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/contextual_matcher/contextual_matcher.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/contextual_matcher/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/contextual_matcher/models.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/endlines/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/endlines/endlines.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/endlines/endlinesmodel.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/endlines/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/endlines/functional.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/matcher/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/matcher/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/matcher/matcher.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/accents/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/accents/accents.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/accents/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/accents/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/lowercase/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/lowercase/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/normalizer.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/pollution/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/pollution/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/pollution/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/pollution/pollution.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/quotes/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/quotes/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/quotes/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/quotes/quotes.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/spaces/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/spaces/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/normalizer/spaces/spaces.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/sentences/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/sentences/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/sentences/sentences.pxd (100%) rename edsnlp/{ => edsnlp}/pipelines/core/sentences/sentences.pyx (100%) rename edsnlp/{ => edsnlp}/pipelines/core/sentences/terms.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/terminology/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/terminology/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/core/terminology/terminology.py (100%) rename edsnlp/{ => edsnlp}/pipelines/factories.py (93%) rename edsnlp/{pipelines/core => edsnlp/pipelines/misc}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/consultation_dates/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/consultation_dates/consultation_dates.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/consultation_dates/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/consultation_dates/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/dates.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/models.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/absolute.py (100%) rename edsnlp/{pipelines/misc => edsnlp/pipelines/misc/dates/patterns/atomic}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/days.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/delimiters.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/directions.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/modes.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/months.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/numbers.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/time.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/units.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/atomic/years.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/current.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/duration.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/false_positive.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/dates/patterns/relative.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/measurements/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/measurements/factory.py (71%) rename edsnlp/{ => edsnlp}/pipelines/misc/measurements/measurements.py (86%) rename edsnlp/{ => edsnlp}/pipelines/misc/measurements/patterns.py (99%) rename edsnlp/{ => edsnlp}/pipelines/misc/reason/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/reason/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/reason/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/reason/reason.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/sections/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/sections/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/sections/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/sections/sections.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/tables/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/tables/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/tables/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/misc/tables/tables.py (90%) rename edsnlp/{pipelines/misc/dates/patterns/atomic => edsnlp/pipelines/ner}/__init__.py (100%) rename edsnlp/{pipelines/ner => edsnlp/pipelines/ner/adicap}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/adicap/adicap.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/adicap/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/adicap/models.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/adicap/patterns.py (100%) rename edsnlp/{pipelines/ner/adicap => edsnlp/pipelines/ner/cim10}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/cim10/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/cim10/patterns.py (100%) rename edsnlp/{pipelines/ner/cim10 => edsnlp/pipelines/ner/covid}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/covid/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/covid/patterns.py (100%) rename edsnlp/{pipelines/ner/covid => edsnlp/pipelines/ner/drugs}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/drugs/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/drugs/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/base_score.py (100%) rename edsnlp/{pipelines/ner/drugs => edsnlp/pipelines/ner/scores/charlson}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/charlson/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/charlson/patterns.py (100%) rename edsnlp/{pipelines/ner/scores/charlson => edsnlp/pipelines/ner/scores/elstonellis}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/elstonellis/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/elstonellis/patterns.py (100%) rename edsnlp/{pipelines/ner/scores/elstonellis => edsnlp/pipelines/ner/scores/emergency}/__init__.py (100%) rename edsnlp/{pipelines/ner/scores/emergency => edsnlp/pipelines/ner/scores/emergency/ccmu}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/emergency/ccmu/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/emergency/ccmu/patterns.py (100%) rename edsnlp/{pipelines/ner/scores/emergency/ccmu => edsnlp/pipelines/ner/scores/emergency/gemsa}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/emergency/gemsa/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/emergency/gemsa/patterns.py (100%) rename edsnlp/{pipelines/ner/scores/emergency/gemsa => edsnlp/pipelines/ner/scores/emergency/priority}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/emergency/priority/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/emergency/priority/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/sofa/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/sofa/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/sofa/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/sofa/sofa.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/tnm/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/tnm/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/tnm/models.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/tnm/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/scores/tnm/tnm.py (100%) rename edsnlp/{pipelines/ner/scores/emergency/priority => edsnlp/pipelines/ner/umls}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/umls/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/ner/umls/patterns.py (100%) rename edsnlp/{pipelines/ner/umls => edsnlp/pipelines/qualifiers}/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/base.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/factories.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/family/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/family/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/family/family.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/family/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/history/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/history/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/history/history.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/history/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/hypothesis/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/hypothesis/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/hypothesis/hypothesis.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/hypothesis/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/negation/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/negation/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/negation/negation.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/negation/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/reported_speech/__init__.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/reported_speech/factory.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/reported_speech/patterns.py (100%) rename edsnlp/{ => edsnlp}/pipelines/qualifiers/reported_speech/reported_speech.py (100%) rename edsnlp/{ => edsnlp}/pipelines/terminations.py (100%) rename edsnlp/{pipelines/qualifiers => edsnlp/pipelines/trainable}/__init__.py (100%) rename edsnlp/{pipelines/trainable => edsnlp/pipelines/trainable/layers}/__init__.py (100%) rename edsnlp/{models/torch => edsnlp/pipelines/trainable/layers}/crf.py (99%) create mode 100644 edsnlp/edsnlp/pipelines/trainable/nested_ner/__init__.py create mode 100644 edsnlp/edsnlp/pipelines/trainable/nested_ner/factory.py rename edsnlp/{pipelines/trainable => edsnlp/pipelines/trainable/nested_ner}/nested_ner.py (82%) rename edsnlp/{models => edsnlp/pipelines/trainable/nested_ner}/stack_crf_ner.py (87%) rename edsnlp/{models => edsnlp/pipelines/trainable}/pytorch_wrapper.py (95%) create mode 100644 edsnlp/edsnlp/pipelines/trainable/span_qualifier/__init__.py create mode 100644 edsnlp/edsnlp/pipelines/trainable/span_qualifier/factory.py create mode 100644 edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_multi_classifier.py create mode 100644 edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_qualifier.py create mode 100644 edsnlp/edsnlp/pipelines/trainable/span_qualifier/utils.py rename edsnlp/{ => edsnlp}/processing/__init__.py (100%) rename edsnlp/{ => edsnlp}/processing/distributed.py (100%) rename edsnlp/{ => edsnlp}/processing/helpers.py (100%) rename edsnlp/{ => edsnlp}/processing/parallel.py (100%) rename edsnlp/{ => edsnlp}/processing/simple.py (100%) rename edsnlp/{ => edsnlp}/processing/utils.py (100%) rename edsnlp/{ => edsnlp}/processing/wrapper.py (100%) rename edsnlp/{ => edsnlp}/resources/adicap.json.gz (100%) rename edsnlp/{ => edsnlp}/resources/cim10.csv.gz (100%) rename edsnlp/{ => edsnlp}/resources/drugs.json (100%) rename edsnlp/{ => edsnlp}/resources/verbs.csv.gz (100%) rename edsnlp/{ => edsnlp}/utils/__init__.py (100%) rename edsnlp/{ => edsnlp}/utils/blocs.py (100%) rename edsnlp/{ => edsnlp}/utils/colors.py (100%) rename edsnlp/{ => edsnlp}/utils/deprecation.py (100%) rename edsnlp/{ => edsnlp}/utils/examples.py (100%) rename edsnlp/{ => edsnlp}/utils/extensions.py (100%) rename edsnlp/{ => edsnlp}/utils/filter.py (69%) rename edsnlp/{ => edsnlp}/utils/inclusion.py (100%) rename edsnlp/{ => edsnlp}/utils/lists.py (100%) rename edsnlp/{ => edsnlp}/utils/merge_configs.py (100%) rename edsnlp/{ => edsnlp}/utils/regex.py (100%) rename edsnlp/{ => edsnlp}/utils/resources.py (100%) create mode 100644 edsnlp/edsnlp/utils/span_getters.py rename edsnlp/{ => edsnlp}/utils/training.py (100%) rename edsnlp/{ => edsnlp}/viz/__init__.py (100%) rename edsnlp/{ => edsnlp}/viz/quick_examples.py (100%) rename mkdocs.yml => edsnlp/mkdocs.yml (100%) rename {notebooks => edsnlp/notebooks}/README.md (100%) rename {notebooks => edsnlp/notebooks}/connectors/context.py (100%) create mode 100644 edsnlp/notebooks/connectors/omop.md create mode 100644 edsnlp/notebooks/context.py create mode 100644 edsnlp/notebooks/dates/context.py create mode 100644 edsnlp/notebooks/dates/prototype.md create mode 100644 edsnlp/notebooks/dates/user-guide.md create mode 100644 edsnlp/notebooks/endlines/endlines-example.md create mode 100644 edsnlp/notebooks/normalizer/context.py create mode 100644 edsnlp/notebooks/normalizer/profiling.md create mode 100644 edsnlp/notebooks/normalizer/prototype.md create mode 100644 edsnlp/notebooks/pipeline.md create mode 100644 edsnlp/notebooks/premier-pipeline.md rename {notebooks/sentences => edsnlp/notebooks/sections}/context.py (100%) create mode 100644 edsnlp/notebooks/sections/section-dataset.md create mode 100644 edsnlp/notebooks/sections/testing.md create mode 100644 edsnlp/notebooks/sentences/context.py rename {notebooks => edsnlp/notebooks}/sentences/sentences.md (100%) create mode 100644 edsnlp/notebooks/tnm/prototype.md create mode 100644 edsnlp/notebooks/tokenizer/context.py create mode 100644 edsnlp/notebooks/tokenizer/tokenizer.md create mode 100644 edsnlp/notebooks/utilities/brat.md create mode 100644 edsnlp/notebooks/utilities/context.py delete mode 100644 edsnlp/pipelines/base.py rename pyproject.toml => edsnlp/pyproject.toml (87%) rename {scripts => edsnlp/scripts}/adicap.py (100%) rename {scripts => edsnlp/scripts}/cim10.py (100%) rename {scripts => edsnlp/scripts}/conjugate_verbs.py (100%) rename {scripts => edsnlp/scripts}/context.py (100%) rename {scripts => edsnlp/scripts}/serve.py (100%) rename setup.py => edsnlp/setup.py (100%) rename {tests => edsnlp/tests}/conftest.py (100%) rename {tests => edsnlp/tests}/connectors/test_brat.py (100%) rename {tests => edsnlp/tests}/connectors/test_labeltool.py (100%) rename {tests => edsnlp/tests}/connectors/test_omop.py (100%) rename {tests => edsnlp/tests}/matchers/test_phrase.py (100%) rename {tests => edsnlp/tests}/matchers/test_regex.py (100%) rename {tests => edsnlp/tests}/matchers/test_simstring.py (100%) rename {tests => edsnlp/tests}/pipelines/core/test_contextual_matcher.py (100%) rename {tests => edsnlp/tests}/pipelines/core/test_endlines.py (100%) rename {tests => edsnlp/tests}/pipelines/core/test_matcher.py (100%) rename {tests => edsnlp/tests}/pipelines/core/test_normalisation.py (100%) rename {tests => edsnlp/tests}/pipelines/core/test_sentences.py (100%) rename {tests => edsnlp/tests}/pipelines/core/test_terminology.py (100%) rename {tests => edsnlp/tests}/pipelines/misc/test_consultation_date.py (100%) rename {tests => edsnlp/tests}/pipelines/misc/test_dates.py (100%) rename {tests => edsnlp/tests}/pipelines/misc/test_measurements.py (100%) rename {tests => edsnlp/tests}/pipelines/misc/test_reason.py (100%) rename {tests => edsnlp/tests}/pipelines/misc/test_sections.py (100%) rename {tests => edsnlp/tests}/pipelines/ner/test_adicap.py (100%) rename {tests => edsnlp/tests}/pipelines/ner/test_adicap_decoder.py (100%) rename {tests => edsnlp/tests}/pipelines/ner/test_cim10.py (100%) rename {tests => edsnlp/tests}/pipelines/ner/test_covid.py (100%) rename {tests => edsnlp/tests}/pipelines/ner/test_drugs.py (100%) rename {tests => edsnlp/tests}/pipelines/ner/test_score.py (100%) rename {tests => edsnlp/tests}/pipelines/ner/test_tnm.py (100%) rename {tests => edsnlp/tests}/pipelines/ner/test_umls.py (100%) rename {tests => edsnlp/tests}/pipelines/qualifiers/conftest.py (100%) rename {tests => edsnlp/tests}/pipelines/qualifiers/test_family.py (100%) rename {tests => edsnlp/tests}/pipelines/qualifiers/test_history.py (100%) rename {tests => edsnlp/tests}/pipelines/qualifiers/test_hypothesis.py (100%) rename {tests => edsnlp/tests}/pipelines/qualifiers/test_negation.py (100%) rename {tests => edsnlp/tests}/pipelines/qualifiers/test_reported_speech.py (100%) rename {tests => edsnlp/tests}/pipelines/test_pipelines.py (100%) rename {tests => edsnlp/tests}/pipelines/trainable/test_nested_ner.py (100%) rename {tests => edsnlp/tests}/processing/test_processing.py (100%) rename {tests => edsnlp/tests}/readme.md (100%) rename {tests => edsnlp/tests}/test_conjugator.py (100%) rename {tests => edsnlp/tests}/test_docs.py (100%) rename {tests => edsnlp/tests}/test_language.py (100%) rename {tests => edsnlp/tests}/utils/test_examples.py (100%) rename {tests => edsnlp/tests}/utils/test_filter.py (100%) rename {tests => edsnlp/tests}/utils/test_quick_examples.py (100%) create mode 100644 notebooks/export_pandas_to_brat.py create mode 100644 notebooks/get_stats_by_section_on_cim10.md create mode 100644 notebooks/knowledge.py delete mode 100644 tests/resources/brat_data/subfolder/doc-1.ann delete mode 100644 tests/resources/brat_data/subfolder/doc-1.txt diff --git a/.gitignore b/.gitignore index bf0d160a0..02b093061 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,9 @@ _build/ *.tar.gz *.tsv *.ann +*.spacy +*.RRF +*.pkl # Editors .idea @@ -71,3 +74,6 @@ _build/ docs/reference docs/changelog.md docs/contributing.md + +# Logs +*.log diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 08f632d5b..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 - hooks: - - id: trailing-whitespace - exclude: | - (?x)^( - tests/resources/.*| - edsnlp/resources/.* - )$ - - id: no-commit-to-branch - - id: end-of-file-fixer - - id: check-yaml - args: ["--unsafe"] - - id: check-toml - - id: check-json - - id: check-symlinks - - id: check-docstring-first - - id: check-added-large-files - - id: detect-private-key - - repo: https://github.com/pycqa/isort - rev: 5.11.5 - hooks: - - id: isort - name: isort (python) - args: ["--profile", "black"] - - id: isort - name: isort (cython) - types: [cython] - args: ["--profile", "black"] - - id: isort - name: isort (pyi) - types: [pyi] - args: ["--profile", "black"] - - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - - repo: https://github.com/asottile/blacken-docs - rev: v1.10.0 - hooks: - - id: blacken-docs - additional_dependencies: [black==20.8b1] - exclude: notebooks/ - - repo: https://github.com/pycqa/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - - repo: https://github.com/econchick/interrogate - rev: 1.5.0 - hooks: - - id: interrogate - args: ["--config=pyproject.toml"] - pass_filenames: false diff --git a/NER_model/configs/config.cfg b/NER_model/configs/config.cfg new file mode 100644 index 000000000..008785353 --- /dev/null +++ b/NER_model/configs/config.cfg @@ -0,0 +1,185 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["DISO","Constantes","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","SECTION_motif","SECTION_histoire","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_conclusion"] + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Allergie", "_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Allergie":["Chemical_and_drugs"],"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = -1 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 3072 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.8 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.2 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/config_v1.cfg b/NER_model/configs/config_v1.cfg new file mode 100644 index 000000000..f13894a1f --- /dev/null +++ b/NER_model/configs/config_v1.cfg @@ -0,0 +1,185 @@ +[paths] +train = "corpus/train_test.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["DISO","Constantes","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","SECTION_motif","SECTION_histoire","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_conclusion"] + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Allergie", "_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Allergie":["Chemical_and_drugs"],"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = -1 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 3072 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.8 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.2 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/expe_lang_model/config_DrBert.cfg b/NER_model/configs/expe_lang_model/config_DrBert.cfg new file mode 100644 index 000000000..4c8083882 --- /dev/null +++ b/NER_model/configs/expe_lang_model/config_DrBert.cfg @@ -0,0 +1,185 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../word-embedding/DrBERT-7GB" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["DISO","Constantes","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","SECTION_motif","SECTION_histoire","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_conclusion"] + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Allergie", "_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Allergie":["Chemical_and_drugs"],"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = -1 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 3072 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.8 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.2 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/expe_lang_model/config_camembert_base.cfg b/NER_model/configs/expe_lang_model/config_camembert_base.cfg new file mode 100644 index 000000000..15a92b06c --- /dev/null +++ b/NER_model/configs/expe_lang_model/config_camembert_base.cfg @@ -0,0 +1,185 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../word-embedding/camembert-base" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["DISO","Constantes","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","SECTION_motif","SECTION_histoire","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_conclusion"] + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Allergie", "_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Allergie":["Chemical_and_drugs"],"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = -1 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 3072 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.8 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.2 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/expe_lang_model/config_camembert_bio.cfg b/NER_model/configs/expe_lang_model/config_camembert_bio.cfg new file mode 100644 index 000000000..09eb8484f --- /dev/null +++ b/NER_model/configs/expe_lang_model/config_camembert_bio.cfg @@ -0,0 +1,185 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../word-embedding/camembert-bio-base" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["DISO","Constantes","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","SECTION_motif","SECTION_histoire","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_conclusion"] + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Allergie", "_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Allergie":["Chemical_and_drugs"],"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = -1 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 3072 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.8 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.2 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/expe_lang_model/config_eds_finetune.cfg b/NER_model/configs/expe_lang_model/config_eds_finetune.cfg new file mode 100644 index 000000000..008785353 --- /dev/null +++ b/NER_model/configs/expe_lang_model/config_eds_finetune.cfg @@ -0,0 +1,185 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["DISO","Constantes","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","SECTION_motif","SECTION_histoire","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_conclusion"] + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Allergie", "_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Allergie":["Chemical_and_drugs"],"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = -1 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 3072 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.8 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.2 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/expe_lang_model/config_eds_scratch.cfg b/NER_model/configs/expe_lang_model/config_eds_scratch.cfg new file mode 100644 index 000000000..6a4d93f9b --- /dev/null +++ b/NER_model/configs/expe_lang_model/config_eds_scratch.cfg @@ -0,0 +1,185 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../word-embedding/training-from-scratch-2021-08-13" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["DISO","Constantes","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","SECTION_motif","SECTION_histoire","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_conclusion"] + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Allergie", "_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Allergie":["Chemical_and_drugs"],"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = -1 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 3072 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.8 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.2 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/rebus/__.cfg b/NER_model/configs/rebus/__.cfg new file mode 100644 index 000000000..849b3a6ec --- /dev/null +++ b/NER_model/configs/rebus/__.cfg @@ -0,0 +1,188 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["Disorders","SOSY","Chemical_and_drugs","Medical_Procedure", "Concept"] + + +[components.qualifier] +factory = "eds.span_qualifier" + + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = true +qualifiers = ["_.SOSY_type", "_.negation", "_.hypothetique","_.family"] +label_constraints = {"_.negation": [ "Disorders", "SOSY","Chemical_and_drugs","Medical_Procedure"], "_.hypothetique":[ "Disorders", "SOSY","Chemical_and_drugs","Medical_Procedure"],"_.family":[ "Disorders", "SOSY","Chemical_and_drugs","Medical_Procedure"],"SOSY_type":["SOSY"]} + + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 1 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds-medic.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds-medic.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 2 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = 100000000000 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 1000 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.5 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.5 +speed = 0.0 +ents_per_type = null +qual_per_type = null + + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/rebus/config.cfg b/NER_model/configs/rebus/config.cfg new file mode 100644 index 000000000..9e30cc62a --- /dev/null +++ b/NER_model/configs/rebus/config.cfg @@ -0,0 +1,186 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels =["DISO","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","route","SECTION_motif","SECTION_histoire","BIO_milieu","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_traitement","SECTION_evolution","SECTION_autre","SECTION_conclusion"] + + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds-medic.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds-medic.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = 100000000000 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 500 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.5 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.5 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/rebus/config_0407_Pierre.cfg b/NER_model/configs/rebus/config_0407_Pierre.cfg new file mode 100644 index 000000000..9e30cc62a --- /dev/null +++ b/NER_model/configs/rebus/config_0407_Pierre.cfg @@ -0,0 +1,186 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels =["DISO","BIO_comp","Chemical_and_drugs","dosage","BIO","strength","form","SECTION_antecedent","route","SECTION_motif","SECTION_histoire","BIO_milieu","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_traitement","SECTION_evolution","SECTION_autre","SECTION_conclusion"] + + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds-medic.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds-medic.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = 100000000000 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 500 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.5 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.5 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/rebus/config_1907.cfg b/NER_model/configs/rebus/config_1907.cfg new file mode 100644 index 000000000..1f063303b --- /dev/null +++ b/NER_model/configs/rebus/config_1907.cfg @@ -0,0 +1,188 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["sosydiso"] + + +[components.qualifier] +factory = "eds.span_qualifier" + + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = true +qualifiers = ["_.negation", "_.hypothetique","_.family"] +label_constraints = {"_.negation": ["sosydiso"], "_.hypothetique":["sosydiso"],"_.family":["sosydiso"]} + + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 1 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds-medic.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds-medic.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training]max_len +accumulate_gradient = 2 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = 100000000000 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 1000 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.5 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.5 +speed = 0.0 +ents_per_type = null +qual_per_type = null + + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/rebus/config_backup2.cfg b/NER_model/configs/rebus/config_backup2.cfg new file mode 100644 index 000000000..275ab1b7c --- /dev/null +++ b/NER_model/configs/rebus/config_backup2.cfg @@ -0,0 +1,188 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner","qualifier"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels = ["Disorders","SOSY","Chemical_and_drugs","Concept","gender","Medical_Procedure","SECTION_examen_complementaire","SECTION_conclusion","SECTION_motif","SECTION_antecedent","SECTION_head","SECTION_traitement_entree","SECTION_traitement_sortie","SECTION_evolution","SECTION_antecedent_familiaux","SECTION_histoire","SECTION_traitement","SECTION_examen_clinique","SECTION_autre","SECTION_mode_de_vie","metadata"] + + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Disorders_type","_.SOSY_type","_.Chemical_and_drugs_type","_.Concept_type","_.negation","_.hypothetique","_.family","_.Medical_Procedure_type","_.gender_type"] + +label_constraints = {"_.Disorders_type":["Disorders"],"_.SOSY_type": ["SOSY"],"_.Chemical_and_drugs_type":["Chemical_and_drugs"],"_.Concept_type":["Concept"],"_.negation":["Disorders","SOSY","Chemical_and_drugs", "Medical_Procedure"],"_.hypothetique":["Disorders","SOSY","Chemical_and_drugs", "Medical_Procedure"],"_.family":["Disorders","SOSY","Chemical_and_drugs", "Medical_Procedure"],"_.Medical_Procedure_type":["Medical_Procedure"], "_.gender_type":["gender"]} + + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 1 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds-medic.Corpus.v1" +path = ${paths.train} +max_length = 384 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds-medic.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 5 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = 100000000000 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 1000 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.5 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.5 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/configs/rebus/config_j.cfg b/NER_model/configs/rebus/config_j.cfg new file mode 100644 index 000000000..5c0779f58 --- /dev/null +++ b/NER_model/configs/rebus/config_j.cfg @@ -0,0 +1,186 @@ +[paths] +train = "corpus/train.spacy" +dev = "corpus/dev.spacy" +vectors = null +init_tok2vec = null +bert = "../../../word-embedding/finetuning-camembert-2021-07-29" + +[system] +gpu_allocator = "pytorch" +seed = 0 + +[nlp] +lang = "eds" +pipeline = ["tok2vec","ner"] +batch_size = 1 +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null + +[components] + +[components.ner] +factory = "nested_ner" +scorer = {"@scorers": "eds.nested_ner_scorer.v1"} +ent_labels =["DISO","BIO_comp","CHEM","dosage","BIO","strength","form","SECTION_antecedent","route","SECTION_motif","SECTION_histoire","BIO_milieu","SECTION_examen_clinique","SECTION_examen_complementaire","SECTION_mode_de_vie","SECTION_traitement_entree","SECTION_antecedent_familiaux","SECTION_traitement_sortie","SECTION_traitement","SECTION_evolution","SECTION_autre","SECTION_conclusion"] + + +[components.qualifier] +factory = "eds.span_qualifier" + +[components.qualifier.candidate_getter] +@misc = "eds.candidate_span_qualifier_getter" +on_ents = true +on_span_groups = false + +qualifiers = ["_.Action","_.Certainty","_.Temporality","_.Negation","_.Family"] +label_constraints = {"_.Action":["Chemical_and_drugs"],"_.Certainty":["Chemical_and_drugs"],"_.Temporality":["Chemical_and_drugs"],"_.Negation":["Chemical_and_drugs"],"_.Family":["Chemical_and_drugs"]} + +[components.qualifier.model] +@architectures = "eds.span_multi_classifier.v1" +projection_mode = "dot" +pooler_mode = "max" + +[components.qualifier.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.qualifier.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.qualifier.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.clean-entities] +factory = "clean-entities" + +[components.ner.model] +@architectures = "eds.stack_crf_ner_model.v1" +mode = "joint" + +[components.ner.model.tok2vec] +@architectures = "spacy.Tok2Vec.v2" + +[components.ner.model.tok2vec.embed] +@architectures = "spacy-transformers.TransformerListener.v1" +pooling = {"@layers":"reduce_mean.v1"} + +[components.ner.model.tok2vec.encode] +@architectures = "spacy.MishWindowEncoder.v2" +width = 768 +window_size = 1 +depth = 2 + +[components.tok2vec] +factory = "transformer" +max_batch_items = 4096 +set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"} + +[components.tok2vec.model] +@architectures = "spacy-transformers.TransformerModel.v3" +name = ${paths.bert} +# name = "camembert-base" +mixed_precision = true + +[components.tok2vec.model.get_spans] +@span_getters = "spacy-transformers.strided_spans.v1" +window = 128 +stride = 64 + +[components.tok2vec.model.grad_scaler_config] + +[components.tok2vec.model.tokenizer_config] +use_fast = true + +[components.tok2vec.model.transformer_config] + +[corpora] + +[corpora.train] +@readers = "eds-medic.Corpus.v1" +path = ${paths.train} +max_length = 200 +gold_preproc = false +limit = 0 +augmenter = null +seed = ${system.seed} +shuffle = true +filter_expr = null +#filter_expr = "any(ent._.event_type is not None for ent in doc.ents)" + +[corpora.dev] +@readers = "eds-medic.Corpus.v1" +path = ${paths.dev} +max_length = 0 +gold_preproc = false +limit = 0 +augmenter = null + +[training] +accumulate_gradient = 1 +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +patience = 1500 +max_epochs = 100000000000 +max_steps = 20000 +eval_frequency = 100 +frozen_components = [] +annotating_components = [] +before_to_disk = null + +[training.batcher] +@batchers = "spacy.batch_by_padded.v1" +size = 500 +discard_oversize = true +buffer = 256 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = true +#@loggers = "DVCLive.v1" + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 0.0 +use_averages = false +eps = 0.000001 + +[training.optimizer.learn_rate] +@schedules = "warmup_linear.v1" +warmup_steps = 250 +total_steps = ${training.max_steps} +initial_rate = 0.0001 + +[training.score_weights] +ents_f = 0.5 +ents_p = 0.0 +ents_r = 0.0 +qual_f = 0.5 +speed = 0.0 +ents_per_type = null +qual_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] diff --git a/NER_model/data/NLP_diabeto/clean/annotation.conf b/NER_model/data/NLP_diabeto/clean/annotation.conf new file mode 100644 index 000000000..010a43788 --- /dev/null +++ b/NER_model/data/NLP_diabeto/clean/annotation.conf @@ -0,0 +1,39 @@ +[entities] +Chemical_and_drugs +BIO +BIO_comp +DISO +BIO_milieu +Constantes +!SECTION_ + SECTION_examen_complementaire + SECTION_conclusion + SECTION_motif + SECTION_antecedent + SECTION_head + SECTION_traitement_entree + SECTION_traitement_sortie + SECTION_evolution + SECTION_antecedent_familiaux + SECTION_histoire + SECTION_traitement + SECTION_examen_clinique + SECTION_autre + SECTION_mode_de_vie + + +[attributes] +Action Arg:Chemical_and_drugs, Value:Start|Stop|Increase|Decrease|OtherChange|UniqueDose|Unknown +Certainty Arg:Chemical_and_drugs, Value:Certain|Hypothetical|Conditional|Unknown +Temporality Arg:Chemical_and_drugs, Value:Past|Present|Future|Unknown +Negation Arg:Chemical_and_drugs +Allergie Arg:Chemical_and_drugs +Family Arg:Chemical_and_drugs +Tech Arg:Chemical_and_drugs, Value:dosage|form|route|strength + + +[relations] + Arg1:, Arg2:, : + + +[events] \ No newline at end of file diff --git a/NER_model/data/NLP_diabeto/clean/kb_shortcuts.conf b/NER_model/data/NLP_diabeto/clean/kb_shortcuts.conf new file mode 100644 index 000000000..4b83757fb --- /dev/null +++ b/NER_model/data/NLP_diabeto/clean/kb_shortcuts.conf @@ -0,0 +1,6 @@ +B BIO +C Chemical_and_drugs +X BIO_comp +M BIO_milieu +D DISO +T Constantes \ No newline at end of file diff --git a/NER_model/data/NLP_diabeto/clean/visual.conf b/NER_model/data/NLP_diabeto/clean/visual.conf new file mode 100644 index 000000000..a0e119d6c --- /dev/null +++ b/NER_model/data/NLP_diabeto/clean/visual.conf @@ -0,0 +1,42 @@ +[labels] +BIO | BIO +BIO_comp | BIO_comp +Chemical_and_drugs | Chemical_and_drugs +SECTION_examen_complementaire | SECTION_examen_complementaire +SECTION_conclusion | SECTION_conclusion +SECTION_motif | SECTION_motif +SECTION_antecedent | SECTION_antecedent +SECTION_head | SECTION_head +SECTION_traitement_entree | SECTION_traitement_entree +SECTION_traitement_sortie | SECTION_traitement_sortie +SECTION_evolution | SECTION_evolution +SECTION_antecedent_familiaux | SECTION_antecedent_familiaux +SECTION_histoire | SECTION_histoire +SECTION_traitement | SECTION_traitement +SECTION_examen_clinique | SECTION_examen_clinique +SECTION_autre | SECTION_autre +SECTION_mode_de_vie | SECTION_mode_de_vie +DISO | DISO +CONST | Constantes + +[drawing] +Chemical_and_drugs bgColor:#d62728 +DISO bgColor:#ff7f0e +SECTION_examen_complementaire bgColor:#17becf +SECTION_conclusion bgColor:#17becf +SECTION_motif bgColor:#17becf +SECTION_antecedent bgColor:#17becf +SECTION_head bgColor:#17becf +SECTION_traitement_entree bgColor:#17becf +SECTION_traitement_sortie bgColor:#17becf +SECTION_evolution bgColor:#17becf +SECTION_antecedent_familiaux bgColor:#17becf +SECTION_histoire bgColor:#17becf +SECTION_traitement bgColor:#17becf +SECTION_examen_clinique bgColor:#17becf +SECTION_autre bgColor:#17becf +SECTION_mode_de_vie bgColor:#17becf +BIO bgColor:#8c564b +BIO_comp bgColor:#779999 +BIO_milieu bgColor:#2ca02c +Constantes bgColor:#9467bd diff --git a/NER_model/data/NLP_diabeto/raw/annotation.conf b/NER_model/data/NLP_diabeto/raw/annotation.conf new file mode 100644 index 000000000..010a43788 --- /dev/null +++ b/NER_model/data/NLP_diabeto/raw/annotation.conf @@ -0,0 +1,39 @@ +[entities] +Chemical_and_drugs +BIO +BIO_comp +DISO +BIO_milieu +Constantes +!SECTION_ + SECTION_examen_complementaire + SECTION_conclusion + SECTION_motif + SECTION_antecedent + SECTION_head + SECTION_traitement_entree + SECTION_traitement_sortie + SECTION_evolution + SECTION_antecedent_familiaux + SECTION_histoire + SECTION_traitement + SECTION_examen_clinique + SECTION_autre + SECTION_mode_de_vie + + +[attributes] +Action Arg:Chemical_and_drugs, Value:Start|Stop|Increase|Decrease|OtherChange|UniqueDose|Unknown +Certainty Arg:Chemical_and_drugs, Value:Certain|Hypothetical|Conditional|Unknown +Temporality Arg:Chemical_and_drugs, Value:Past|Present|Future|Unknown +Negation Arg:Chemical_and_drugs +Allergie Arg:Chemical_and_drugs +Family Arg:Chemical_and_drugs +Tech Arg:Chemical_and_drugs, Value:dosage|form|route|strength + + +[relations] + Arg1:, Arg2:, : + + +[events] \ No newline at end of file diff --git a/NER_model/data/NLP_diabeto/raw/kb_shortcuts.conf b/NER_model/data/NLP_diabeto/raw/kb_shortcuts.conf new file mode 100644 index 000000000..4b83757fb --- /dev/null +++ b/NER_model/data/NLP_diabeto/raw/kb_shortcuts.conf @@ -0,0 +1,6 @@ +B BIO +C Chemical_and_drugs +X BIO_comp +M BIO_milieu +D DISO +T Constantes \ No newline at end of file diff --git a/NER_model/data/NLP_diabeto/raw/visual.conf b/NER_model/data/NLP_diabeto/raw/visual.conf new file mode 100644 index 000000000..a0e119d6c --- /dev/null +++ b/NER_model/data/NLP_diabeto/raw/visual.conf @@ -0,0 +1,42 @@ +[labels] +BIO | BIO +BIO_comp | BIO_comp +Chemical_and_drugs | Chemical_and_drugs +SECTION_examen_complementaire | SECTION_examen_complementaire +SECTION_conclusion | SECTION_conclusion +SECTION_motif | SECTION_motif +SECTION_antecedent | SECTION_antecedent +SECTION_head | SECTION_head +SECTION_traitement_entree | SECTION_traitement_entree +SECTION_traitement_sortie | SECTION_traitement_sortie +SECTION_evolution | SECTION_evolution +SECTION_antecedent_familiaux | SECTION_antecedent_familiaux +SECTION_histoire | SECTION_histoire +SECTION_traitement | SECTION_traitement +SECTION_examen_clinique | SECTION_examen_clinique +SECTION_autre | SECTION_autre +SECTION_mode_de_vie | SECTION_mode_de_vie +DISO | DISO +CONST | Constantes + +[drawing] +Chemical_and_drugs bgColor:#d62728 +DISO bgColor:#ff7f0e +SECTION_examen_complementaire bgColor:#17becf +SECTION_conclusion bgColor:#17becf +SECTION_motif bgColor:#17becf +SECTION_antecedent bgColor:#17becf +SECTION_head bgColor:#17becf +SECTION_traitement_entree bgColor:#17becf +SECTION_traitement_sortie bgColor:#17becf +SECTION_evolution bgColor:#17becf +SECTION_antecedent_familiaux bgColor:#17becf +SECTION_histoire bgColor:#17becf +SECTION_traitement bgColor:#17becf +SECTION_examen_clinique bgColor:#17becf +SECTION_autre bgColor:#17becf +SECTION_mode_de_vie bgColor:#17becf +BIO bgColor:#8c564b +BIO_comp bgColor:#779999 +BIO_milieu bgColor:#2ca02c +Constantes bgColor:#9467bd diff --git a/NER_model/dvc.lock b/NER_model/dvc.lock new file mode 100644 index 000000000..727e13edd --- /dev/null +++ b/NER_model/dvc.lock @@ -0,0 +1,75 @@ +schema: '2.0' +stages: + convert: + cmd: python -m spacy project run convert + deps: + - path: data/NLP_diabeto/test + md5: 3c83eeee1f827072761e56c3b135d56b.dir + size: 214537 + nfiles: 20 + - path: data/NLP_diabeto/train + md5: 02f70168761f49b871c0fae54f4cbd2a.dir + size: 1903044 + nfiles: 166 + - path: data/NLP_diabeto/val + md5: cba6c8166b223f5f007c9f51caefa124.dir + size: 213280 + nfiles: 20 + - path: scripts/convert.py + md5: 4cc6b87c58ddf5320123fde3fc04ac03 + size: 3268 + outs: + - path: corpus/dev.spacy + md5: 8ed7e6e2ebbbdde02f52e115c81a3c3b + size: 200671 + - path: corpus/test.spacy + md5: 64a9905aae87723375427b0fe0885472 + size: 212753 + - path: corpus/train.spacy + md5: 46d25cdcdcb64a19d02fb130a67f1090 + size: 1751413 + train: + cmd: python -m spacy project run train + deps: + - path: configs/config.cfg + md5: 0714cebf0e60346234c2a244d1924198 + size: 4450 + - path: corpus/dev.spacy + md5: 97ad6c031ae1b3522131d2675a1464a0 + size: 215568 + - path: corpus/train.spacy + md5: bd6c7c646cf288ee49f855177841c399 + size: 1741469 + outs: + - path: training/model-best + md5: 7342ebecce870db3e5435d9c5ad27967.dir + size: 474900558 + nfiles: 14 + package: + cmd: python -m spacy project run package + deps: + - path: training/model-best + md5: 7342ebecce870db3e5435d9c5ad27967.dir + size: 474900558 + nfiles: 14 + outs: + - path: packages/eds_medic-0.1.0/dist/eds_medic-0.1.0-py3-none-any.whl + md5: 1b491e3d4ce4f7822f381adc6525f0ba + size: 438563366 + evaluate: + cmd: python -m spacy project run evaluate + deps: + - path: corpus/test.spacy + md5: 0812cf1d19ab475be07eb65162e0ce2c + size: 180628 + - path: training/model-best + md5: 7342ebecce870db3e5435d9c5ad27967.dir + size: 474900558 + nfiles: 14 + outs: + - path: corpus/output.spacy + md5: 43bb5d78ab495c8be01f30b106cd2c3c + size: 176719 + - path: training/test_metrics.json + md5: 81d641b7c91e4b88587693f30364a092 + size: 1357 diff --git a/NER_model/dvc.yaml b/NER_model/dvc.yaml new file mode 100644 index 000000000..8102b2cde --- /dev/null +++ b/NER_model/dvc.yaml @@ -0,0 +1,28 @@ +# 60288f251a06a6832464289af6745578 +# This file is auto-generated by spaCy based on your project.yml. If you've +# edited your project.yml, you can regenerate this file by running: +# python -m spacy project dvc +stages: + train: + cmd: python -m spacy project run train + deps: + - configs/config.cfg + - corpus/dev.spacy + - corpus/train.spacy + outs: + - training/model-best + evaluate: + cmd: python -m spacy project run evaluate + deps: + - corpus/test.spacy + - training/model-best + outs: + - corpus/output.spacy + - training/test_metrics.json + package: + cmd: python -m spacy project run package + deps: + - training/model-best + outs: + - packages/eds_medic-0.1.0/dist/eds_medic-0.1.0-py3-none-any.whl: + cache: false diff --git a/NER_model/evaluate.md b/NER_model/evaluate.md new file mode 100644 index 000000000..87ad8a543 --- /dev/null +++ b/NER_model/evaluate.md @@ -0,0 +1,572 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.15.0 + kernelspec: + display_name: BioMedics_client + language: python + name: biomedics_client +--- + +```python +%reload_ext autoreload +%autoreload 2 +%reload_ext jupyter_black +``` + +```python +import spacy +import pandas as pd +from edsnlp.connectors.brat import BratConnector +from edsnlp.evaluate import evaluate_test, evaluate +``` + +# Expe Data Size + +```python +GOLD_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/test" + +loader = spacy.blank("eds") +brat = BratConnector(GOLD_PATH) +gold_docs = brat.brat2docs(loader) + +scores = [] +for i in [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 62]: + PRED_PATH = f"/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/expe_data_size/pred_{i}" + loader = spacy.blank("eds") + brat = BratConnector(PRED_PATH) + pred_docs = brat.brat2docs(loader) + score = pd.DataFrame( + evaluate_test( + gold_docs, + pred_docs, + boostrap_level="doc", + exact=True, + n_draw=5000, + alpha=0.05, + digits=5, + ) + ).T.sort_index() + score[["n_docs"]] = i + scores.append(score) +``` + +```python +import altair as alt +from functools import reduce + +alt.data_transformers.disable_max_rows() +result = ( + pd.concat(scores)[["n_docs", "Precision", "Recall", "F1"]] + .dropna() + .reset_index() + .rename(columns={"index": "label"}) + .melt( + id_vars=["n_docs", "label"], + value_vars=["Precision", "Recall", "F1"], + var_name="metric", + value_name="summary", + ) +) +result["mean"] = result["summary"].str.split().str.get(0) +result["lower"] = ( + result["summary"].str.split().str.get(1).str.split("-").str.get(0).str.slice(1) +) +result["upper"] = ( + result["summary"].str.split().str.get(1).str.split("-").str.get(1).str.slice(0, -1) +) +result = result[ + result.label.isin( + [ + "Overall", + "DISO", + "Constantes", + "BIO_comp", + "Chemical_and_drugs", + "dosage", + "BIO", + "strength", + "form", + "SECTION_antecedent", + "SECTION_motif", + "SECTION_histoire", + "SECTION_examen_clinique", + "SECTION_examen_complementaire", + "SECTION_mode_de_vie", + "SECTION_traitement_entree", + "SECTION_antecedent_familiaux", + "SECTION_traitement_sortie", + "SECTION_conclusion", + ] + ) +] +label_dropdown = alt.binding_select(options=list(result.label.unique()), name="Label ") +label_selection = alt.selection_point( + fields=["label"], bind=label_dropdown, value="Overall" +) + +metric_dropdown = alt.binding_select( + options=list(result.metric.unique()), name="Metric " +) +metric_selection = alt.selection_point( + fields=["metric"], bind=metric_dropdown, value="F1" +) + +line = ( + alt.Chart(result) + .mark_line(point=True) + .encode( + x="n_docs:O", + y=alt.Y(f"mean:Q").scale(zero=False, domain=[60, 100]), + ) +) + +band = ( + alt.Chart(result) + .mark_area(opacity=0.5) + .encode( + x="n_docs:O", + y=alt.Y(f"upper:Q").title(""), + y2=alt.Y2(f"lower:Q").title(""), + ) +) + +chart = line + band +chart = ( + chart.add_params(metric_selection) + .transform_filter(metric_selection) + .add_params(label_selection) + .transform_filter(label_selection) + .properties(width=600) +) + +display(chart) +display(result) +chart.save("metrics_by_n_docs.html") +``` + +# Expe Section + +```python +from os.path import isfile, isdir, join, basename +import edsnlp +import spacy +from edsnlp.evaluate import compute_scores + +nlp = spacy.blank("eds") +nlp.add_pipe("eds.normalizer") +nlp.add_pipe("eds.sections") + +GOLD_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/test" + +loader = spacy.blank("eds") +brat = BratConnector(GOLD_PATH) +gold_docs = brat.brat2docs(loader) + +ML_PRED_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/expe_lang_model/pred_model_eds_finetune" + +brat = BratConnector(ML_PRED_PATH) +ents_ml_pred = brat.brat2docs(loader) + +mapping = { + "antécédents": "SECTION_antecedent", + "motif": "SECTION_motif", + "histoire de la maladie": "SECTION_histoire", + "examens": "SECTION_examen_clinique", + "examens complémentaires": "SECTION_examen_complementaire", + "habitus": "SECTION_mode_de_vie", + "traitements entrée": "SECTION_traitement_entree", + "antécédents familiaux": "SECTION_antecedent_familiaux", + "traitements sortie": "SECTION_traitement_sortie", + "conclusion": "SECTION_conclusion", +} +rule_pred_docs = [] +for doc in gold_docs: + rule_pred_doc = nlp(doc.text) + rule_pred_doc._.note_id = doc._.note_id + del rule_pred_doc.spans["sections"] + rule_pred_docs.append(rule_pred_doc) +ents_rule_pred = [] +for doc in rule_pred_docs: + annotation = [doc._.note_id] + for label, ents in doc.spans.items(): + for ent in ents: + if ent.label_ in mapping.keys(): + annotation.append( + [ent.text, mapping[ent.label_], ent.start_char, ent.end_char] + ) + ents_rule_pred.append(annotation) + + +def get_annotation(docs): + full_annot = [] + for doc in docs: + annotation = [doc._.note_id] + for label, ents in doc.spans.items(): + for ent in ents: + if label in mapping.values(): + annotation.append([ent.text, label, ent.start_char, ent.end_char]) + full_annot.append(annotation) + return full_annot + + +ents_gold, ents_ml_pred = ( + get_annotation(gold_docs), + get_annotation(ents_ml_pred), +) +ents_gold.sort(key=lambda l: l[0]) +ents_ml_pred.sort(key=lambda l: l[0]) +ents_rule_pred.sort(key=lambda l: l[0]) + +scores_rule = ( + pd.DataFrame( + compute_scores( + ents_gold=ents_gold, + ents_pred=ents_rule_pred, + boostrap_level="doc", + exact=True, + n_draw=5000, + alpha=0.05, + digits=2, + ) + ) + .T.sort_index()[["N_entity", "Precision", "Recall", "F1"]] + .drop( + index=[ + "ents_per_type", + ] + ) +) +scores_rule.columns = pd.MultiIndex.from_product( + [["Rule-Based"], ["N_entity", "Precision", "Recall", "F1"]] +) + +scores_ml = ( + pd.DataFrame( + compute_scores( + ents_gold=ents_gold, + ents_pred=ents_ml_pred, + boostrap_level="doc", + exact=True, + n_draw=5000, + alpha=0.05, + digits=2, + ) + ) + .T.sort_index()[["Precision", "Recall", "F1"]] + .drop( + index=[ + "ents_per_type", + ] + ) +) +scores_ml.columns = pd.MultiIndex.from_product( + [["ML (NER)"], ["Precision", "Recall", "F1"]] +) +result = scores_rule.merge(scores_ml, left_index=True, right_index=True) +result +``` + +```python +import numpy as np + + +def highlight_max(row): + Precision_max = ( + row[:, "Precision"].str.split(" ").str.get(0).astype(float) + == row[:, "Precision"].str.split(" ").str.get(0).astype(float).max() + ) + Recall_max = ( + row[:, "Recall"].str.split(" ").str.get(0).astype(float) + == row[:, "Recall"].str.split(" ").str.get(0).astype(float).max() + ) + F1_max = ( + row[:, "F1"].str.split(" ").str.get(0).astype(float) + == row[:, "F1"].str.split(" ").str.get(0).astype(float).max() + ) + s_max = [False] + for i in range(len(F1_max)): + s_max.append(Precision_max[i]) + s_max.append(Recall_max[i]) + s_max.append(F1_max[i]) + return ["font-weight: bold" if cell else "" for cell in s_max] + + +def remove_confidence(row): + return row[:, :].str.split(" ").str.get(0) + + +result.apply(remove_confidence, axis=1).style.apply(highlight_max, axis=1) +``` + +# Expe lang models + +```python +import spacy + +GOLD_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/test" + +loader = spacy.blank("eds") +brat = BratConnector(GOLD_PATH) +gold_docs = brat.brat2docs(loader) + +CAM_BASE_PRED_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/expe_lang_model/pred_model_camembert_base" + +brat = BratConnector(CAM_BASE_PRED_PATH) +cam_base_pred_docs = brat.brat2docs(loader) + +CAM_BIO_PRED_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/expe_lang_model/pred_model_camembert_bio" + +brat = BratConnector(CAM_BIO_PRED_PATH) +cam_bio_pred_docs = brat.brat2docs(loader) + +DR_BERT_PRED_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/expe_lang_model/pred_model_DrBert" + +brat = BratConnector(DR_BERT_PRED_PATH) +DrBert_pred_docs = brat.brat2docs(loader) + +EDS_FINE_PRED_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/expe_lang_model/pred_model_eds_finetune" + +brat = BratConnector(EDS_FINE_PRED_PATH) +eds_finetune_pred_docs = brat.brat2docs(loader) + +EDS_SCRATCH_PRED_PATH = "/export/home/cse200093/scratch/BioMedics/NER_model/data/NLP_diabeto/expe_lang_model/pred_model_eds_scratch" + +brat = BratConnector(EDS_SCRATCH_PRED_PATH) +eds_scratch_pred_docs = brat.brat2docs(loader) +``` + +```python +scores_cam_base = ( + pd.DataFrame( + evaluate_test( + gold_docs, + cam_base_pred_docs, + boostrap_level="doc", + exact=True, + n_draw=5000, + alpha=0.05, + digits=2, + ) + ) + .T.rename( + index={ + "DISO": "Diso", + "Chemical_and_drugs": "Drugs", + "dosage": "Drugs_Dosage", + "form": "Drugs_Form", + "strength": "Drugs_Strength", + "Overall": "overall", + } + ) + .sort_index()[["N_entity", "Precision", "Recall", "F1"]] + .drop( + index=[ + "ents_per_type", + "route", + "SECTION_traitement", + "SECTION_evolution", + "BIO_milieu", + ] + ) +) +scores_cam_base.columns = pd.MultiIndex.from_product( + [["CamemBert-Base"], ["N_entity", "Precision", "Recall", "F1"]] +) + +scores_cam_bio = ( + pd.DataFrame( + evaluate_test( + gold_docs, + cam_bio_pred_docs, + boostrap_level="doc", + exact=True, + n_draw=5000, + alpha=0.05, + digits=2, + ) + ) + .T.rename( + index={ + "DISO": "Diso", + "Chemical_and_drugs": "Drugs", + "dosage": "Drugs_Dosage", + "form": "Drugs_Form", + "strength": "Drugs_Strength", + "Overall": "overall", + } + ) + .sort_index()[["Precision", "Recall", "F1"]] + .drop( + index=[ + "ents_per_type", + "route", + "SECTION_traitement", + "SECTION_evolution", + "BIO_milieu", + ] + ) +) +scores_cam_bio.columns = pd.MultiIndex.from_product( + [["CamemBert-Bio"], ["Precision", "Recall", "F1"]] +) + +scores_DrBert = ( + pd.DataFrame( + evaluate_test( + gold_docs, + DrBert_pred_docs, + boostrap_level="doc", + exact=True, + n_draw=5000, + alpha=0.05, + digits=2, + ) + ) + .T.rename( + index={ + "DISO": "Diso", + "Chemical_and_drugs": "Drugs", + "dosage": "Drugs_Dosage", + "form": "Drugs_Form", + "strength": "Drugs_Strength", + "Overall": "overall", + } + ) + .sort_index()[["Precision", "Recall", "F1"]] + .drop( + index=[ + "ents_per_type", + "route", + "SECTION_traitement", + "SECTION_evolution", + "BIO_milieu", + ] + ) +) +scores_DrBert.columns = pd.MultiIndex.from_product( + [["DrBert"], ["Precision", "Recall", "F1"]] +) + +scores_eds_finetune = ( + pd.DataFrame( + evaluate_test( + gold_docs, + eds_finetune_pred_docs, + boostrap_level="doc", + exact=True, + n_draw=5000, + alpha=0.05, + digits=2, + ) + ) + .T.rename( + index={ + "DISO": "Diso", + "Chemical_and_drugs": "Drugs", + "dosage": "Drugs_Dosage", + "form": "Drugs_Form", + "strength": "Drugs_Strength", + "Overall": "overall", + } + ) + .sort_index()[["Precision", "Recall", "F1"]] + .drop( + index=[ + "ents_per_type", + "route", + "SECTION_traitement", + "SECTION_evolution", + "BIO_milieu", + ] + ) +) +scores_eds_finetune.columns = pd.MultiIndex.from_product( + [["CamemBert-EDS Finetuned"], ["Precision", "Recall", "F1"]] +) + +scores_eds_scratch = ( + pd.DataFrame( + evaluate_test( + gold_docs, + eds_scratch_pred_docs, + boostrap_level="doc", + exact=True, + n_draw=5000, + alpha=0.05, + digits=2, + ) + ) + .T.rename( + index={ + "DISO": "Diso", + "Chemical_and_drugs": "Drugs", + "dosage": "Drugs_Dosage", + "form": "Drugs_Form", + "strength": "Drugs_Strength", + "Overall": "overall", + } + ) + .sort_index()[["Precision", "Recall", "F1"]] + .drop( + index=[ + "ents_per_type", + "route", + "SECTION_traitement", + "SECTION_evolution", + "BIO_milieu", + ] + ) +) +scores_eds_scratch.columns = pd.MultiIndex.from_product( + [["CamemBert-EDS Scratch"], ["Precision", "Recall", "F1"]] +) + +result = ( + scores_cam_base.merge(scores_cam_bio, left_index=True, right_index=True) + .merge(scores_DrBert, left_index=True, right_index=True) + .merge(scores_eds_finetune, left_index=True, right_index=True) + .merge(scores_eds_scratch, left_index=True, right_index=True) +) +``` + +```python +import numpy as np + + +def highlight_max(row): + Precision_max = ( + row[:, "Precision"].str.split(" ").str.get(0).astype(float) + == row[:, "Precision"].str.split(" ").str.get(0).astype(float).max() + ) + Recall_max = ( + row[:, "Recall"].str.split(" ").str.get(0).astype(float) + == row[:, "Recall"].str.split(" ").str.get(0).astype(float).max() + ) + F1_max = ( + row[:, "F1"].str.split(" ").str.get(0).astype(float) + == row[:, "F1"].str.split(" ").str.get(0).astype(float).max() + ) + s_max = [False] + for i in range(len(F1_max)): + s_max.append(Precision_max[i]) + s_max.append(Recall_max[i]) + s_max.append(F1_max[i]) + return ["font-weight: bold" if cell else "" for cell in s_max] + + +def remove_confidence(row): + return row[:, :].str.split(" ").str.get(0) + + +result.apply(remove_confidence, axis=1).style.apply(highlight_max, axis=1) +``` + +```python + +``` diff --git a/NER_model/poetry.lock b/NER_model/poetry.lock new file mode 100644 index 000000000..171173247 --- /dev/null +++ b/NER_model/poetry.lock @@ -0,0 +1,5043 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.8.6" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41d55fc043954cddbbd82503d9cc3f4814a40bcef30b3569bc7b5e34130718c1"}, + {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d84166673694841d8953f0a8d0c90e1087739d24632fe86b1a08819168b4566"}, + {file = "aiohttp-3.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:253bf92b744b3170eb4c4ca2fa58f9c4b87aeb1df42f71d4e78815e6e8b73c9e"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd194939b1f764d6bb05490987bfe104287bbf51b8d862261ccf66f48fb4096"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c5f938d199a6fdbdc10bbb9447496561c3a9a565b43be564648d81e1102ac22"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2817b2f66ca82ee699acd90e05c95e79bbf1dc986abb62b61ec8aaf851e81c93"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fa375b3d34e71ccccf172cab401cd94a72de7a8cc01847a7b3386204093bb47"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de50a199b7710fa2904be5a4a9b51af587ab24c8e540a7243ab737b45844543"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1d8cb0b56b3587c5c01de3bf2f600f186da7e7b5f7353d1bf26a8ddca57f965"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8e31e9db1bee8b4f407b77fd2507337a0a80665ad7b6c749d08df595d88f1cf5"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7bc88fc494b1f0311d67f29fee6fd636606f4697e8cc793a2d912ac5b19aa38d"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ec00c3305788e04bf6d29d42e504560e159ccaf0be30c09203b468a6c1ccd3b2"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad1407db8f2f49329729564f71685557157bfa42b48f4b93e53721a16eb813ed"}, + {file = "aiohttp-3.8.6-cp310-cp310-win32.whl", hash = "sha256:ccc360e87341ad47c777f5723f68adbb52b37ab450c8bc3ca9ca1f3e849e5fe2"}, + {file = "aiohttp-3.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:93c15c8e48e5e7b89d5cb4613479d144fda8344e2d886cf694fd36db4cc86865"}, + {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e2f9cc8e5328f829f6e1fb74a0a3a939b14e67e80832975e01929e320386b34"}, + {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e6a00ffcc173e765e200ceefb06399ba09c06db97f401f920513a10c803604ca"}, + {file = "aiohttp-3.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:41bdc2ba359032e36c0e9de5a3bd00d6fb7ea558a6ce6b70acedf0da86458321"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14cd52ccf40006c7a6cd34a0f8663734e5363fd981807173faf3a017e202fec9"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d5b785c792802e7b275c420d84f3397668e9d49ab1cb52bd916b3b3ffcf09ad"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1bed815f3dc3d915c5c1e556c397c8667826fbc1b935d95b0ad680787896a358"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96603a562b546632441926cd1293cfcb5b69f0b4159e6077f7c7dbdfb686af4d"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d76e8b13161a202d14c9584590c4df4d068c9567c99506497bdd67eaedf36403"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e3f1e3f1a1751bb62b4a1b7f4e435afcdade6c17a4fd9b9d43607cebd242924a"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76b36b3124f0223903609944a3c8bf28a599b2cc0ce0be60b45211c8e9be97f8"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a2ece4af1f3c967a4390c284797ab595a9f1bc1130ef8b01828915a05a6ae684"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:16d330b3b9db87c3883e565340d292638a878236418b23cc8b9b11a054aaa887"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42c89579f82e49db436b69c938ab3e1559e5a4409eb8639eb4143989bc390f2f"}, + {file = "aiohttp-3.8.6-cp311-cp311-win32.whl", hash = "sha256:efd2fcf7e7b9d7ab16e6b7d54205beded0a9c8566cb30f09c1abe42b4e22bdcb"}, + {file = "aiohttp-3.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:3b2ab182fc28e7a81f6c70bfbd829045d9480063f5ab06f6e601a3eddbbd49a0"}, + {file = "aiohttp-3.8.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fdee8405931b0615220e5ddf8cd7edd8592c606a8e4ca2a00704883c396e4479"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d25036d161c4fe2225d1abff2bd52c34ed0b1099f02c208cd34d8c05729882f0"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d791245a894be071d5ab04bbb4850534261a7d4fd363b094a7b9963e8cdbd31"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cccd1de239afa866e4ce5c789b3032442f19c261c7d8a01183fd956b1935349"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f13f60d78224f0dace220d8ab4ef1dbc37115eeeab8c06804fec11bec2bbd07"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a9b5a0606faca4f6cc0d338359d6fa137104c337f489cd135bb7fbdbccb1e39"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:13da35c9ceb847732bf5c6c5781dcf4780e14392e5d3b3c689f6d22f8e15ae31"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4d4cbe4ffa9d05f46a28252efc5941e0462792930caa370a6efaf491f412bc66"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:229852e147f44da0241954fc6cb910ba074e597f06789c867cb7fb0621e0ba7a"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:713103a8bdde61d13490adf47171a1039fd880113981e55401a0f7b42c37d071"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:45ad816b2c8e3b60b510f30dbd37fe74fd4a772248a52bb021f6fd65dff809b6"}, + {file = "aiohttp-3.8.6-cp36-cp36m-win32.whl", hash = "sha256:2b8d4e166e600dcfbff51919c7a3789ff6ca8b3ecce16e1d9c96d95dd569eb4c"}, + {file = "aiohttp-3.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0912ed87fee967940aacc5306d3aa8ba3a459fcd12add0b407081fbefc931e53"}, + {file = "aiohttp-3.8.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e2a988a0c673c2e12084f5e6ba3392d76c75ddb8ebc6c7e9ead68248101cd446"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf3fd9f141700b510d4b190094db0ce37ac6361a6806c153c161dc6c041ccda"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3161ce82ab85acd267c8f4b14aa226047a6bee1e4e6adb74b798bd42c6ae1f80"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95fc1bf33a9a81469aa760617b5971331cdd74370d1214f0b3109272c0e1e3c"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c43ecfef7deaf0617cee936836518e7424ee12cb709883f2c9a1adda63cc460"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca80e1b90a05a4f476547f904992ae81eda5c2c85c66ee4195bb8f9c5fb47f28"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:90c72ebb7cb3a08a7f40061079817133f502a160561d0675b0a6adf231382c92"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bb54c54510e47a8c7c8e63454a6acc817519337b2b78606c4e840871a3e15349"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:de6a1c9f6803b90e20869e6b99c2c18cef5cc691363954c93cb9adeb26d9f3ae"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a3628b6c7b880b181a3ae0a0683698513874df63783fd89de99b7b7539e3e8a8"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fc37e9aef10a696a5a4474802930079ccfc14d9f9c10b4662169671ff034b7df"}, + {file = "aiohttp-3.8.6-cp37-cp37m-win32.whl", hash = "sha256:f8ef51e459eb2ad8e7a66c1d6440c808485840ad55ecc3cafefadea47d1b1ba2"}, + {file = "aiohttp-3.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:b2fe42e523be344124c6c8ef32a011444e869dc5f883c591ed87f84339de5976"}, + {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e2ee0ac5a1f5c7dd3197de309adfb99ac4617ff02b0603fd1e65b07dc772e4b"}, + {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01770d8c04bd8db568abb636c1fdd4f7140b284b8b3e0b4584f070180c1e5c62"}, + {file = "aiohttp-3.8.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3c68330a59506254b556b99a91857428cab98b2f84061260a67865f7f52899f5"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89341b2c19fb5eac30c341133ae2cc3544d40d9b1892749cdd25892bbc6ac951"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71783b0b6455ac8f34b5ec99d83e686892c50498d5d00b8e56d47f41b38fbe04"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f628dbf3c91e12f4d6c8b3f092069567d8eb17814aebba3d7d60c149391aee3a"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04691bc6601ef47c88f0255043df6f570ada1a9ebef99c34bd0b72866c217ae"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee912f7e78287516df155f69da575a0ba33b02dd7c1d6614dbc9463f43066e3"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9c19b26acdd08dd239e0d3669a3dddafd600902e37881f13fbd8a53943079dbc"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:99c5ac4ad492b4a19fc132306cd57075c28446ec2ed970973bbf036bcda1bcc6"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f0f03211fd14a6a0aed2997d4b1c013d49fb7b50eeb9ffdf5e51f23cfe2c77fa"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:8d399dade330c53b4106160f75f55407e9ae7505263ea86f2ccca6bfcbdb4921"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ec4fd86658c6a8964d75426517dc01cbf840bbf32d055ce64a9e63a40fd7b771"}, + {file = "aiohttp-3.8.6-cp38-cp38-win32.whl", hash = "sha256:33164093be11fcef3ce2571a0dccd9041c9a93fa3bde86569d7b03120d276c6f"}, + {file = "aiohttp-3.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:bdf70bfe5a1414ba9afb9d49f0c912dc524cf60141102f3a11143ba3d291870f"}, + {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d52d5dc7c6682b720280f9d9db41d36ebe4791622c842e258c9206232251ab2b"}, + {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ac39027011414dbd3d87f7edb31680e1f430834c8cef029f11c66dad0670aa5"}, + {file = "aiohttp-3.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f5c7ce535a1d2429a634310e308fb7d718905487257060e5d4598e29dc17f0b"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b30e963f9e0d52c28f284d554a9469af073030030cef8693106d918b2ca92f54"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:918810ef188f84152af6b938254911055a72e0f935b5fbc4c1a4ed0b0584aed1"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:002f23e6ea8d3dd8d149e569fd580c999232b5fbc601c48d55398fbc2e582e8c"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcf3eabd3fd1a5e6092d1242295fa37d0354b2eb2077e6eb670accad78e40e1"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:255ba9d6d5ff1a382bb9a578cd563605aa69bec845680e21c44afc2670607a95"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d67f8baed00870aa390ea2590798766256f31dc5ed3ecc737debb6e97e2ede78"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:86f20cee0f0a317c76573b627b954c412ea766d6ada1a9fcf1b805763ae7feeb"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:39a312d0e991690ccc1a61f1e9e42daa519dcc34ad03eb6f826d94c1190190dd"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e827d48cf802de06d9c935088c2924e3c7e7533377d66b6f31ed175c1620e05e"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd111d7fc5591ddf377a408ed9067045259ff2770f37e2d94e6478d0f3fc0c17"}, + {file = "aiohttp-3.8.6-cp39-cp39-win32.whl", hash = "sha256:caf486ac1e689dda3502567eb89ffe02876546599bbf915ec94b1fa424eeffd4"}, + {file = "aiohttp-3.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3f0e27e5b733803333bb2371249f41cf42bae8884863e8e8965ec69bebe53132"}, + {file = "aiohttp-3.8.6.tar.gz", hash = "sha256:b0cf2a4501bff9330a8a5248b4ce951851e415bdcce9dc158e76cfd55e15085c"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<4.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiohttp-retry" +version = "2.8.3" +description = "Simple retry client for aiohttp" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiohttp_retry-2.8.3-py3-none-any.whl", hash = "sha256:3aeeead8f6afe48272db93ced9440cf4eda8b6fd7ee2abb25357b7eb28525b45"}, + {file = "aiohttp_retry-2.8.3.tar.gz", hash = "sha256:9a8e637e31682ad36e1ff9f8bcba912fcfc7d7041722bc901a4b948da4d71ea9"}, +] + +[package.dependencies] +aiohttp = "*" + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "amqp" +version = "5.2.0" +description = "Low-level AMQP client for Python (fork of amqplib)." +optional = false +python-versions = ">=3.6" +files = [ + {file = "amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637"}, + {file = "amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"}, +] + +[package.dependencies] +vine = ">=5.0.0,<6.0.0" + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +description = "ANTLR 4.9.3 runtime for Python 3.7" +optional = false +python-versions = "*" +files = [ + {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" +optional = false +python-versions = "*" +files = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] + +[package.dependencies] +six = ">=1.6.1,<2.0" +wheel = ">=0.23.0,<1.0" + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "asyncssh" +version = "2.14.1" +description = "AsyncSSH: Asynchronous SSHv2 client and server library" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "asyncssh-2.14.1-py3-none-any.whl", hash = "sha256:9611368f5db62d9adb0deaa3ff37080277f142acd101693a3a9d2b47a84d0e8b"}, + {file = "asyncssh-2.14.1.tar.gz", hash = "sha256:1ac31c333a0d83c88831523245500caa814503423741b0e465339ef6da5b5e29"}, +] + +[package.dependencies] +cryptography = ">=39.0" +typing-extensions = ">=3.6" + +[package.extras] +bcrypt = ["bcrypt (>=3.1.3)"] +fido2 = ["fido2 (>=0.9.2)"] +gssapi = ["gssapi (>=1.2.0)"] +libnacl = ["libnacl (>=1.4.2)"] +pkcs11 = ["python-pkcs11 (>=0.7.0)"] +pyopenssl = ["pyOpenSSL (>=23.0.0)"] +pywin32 = ["pywin32 (>=227)"] + +[[package]] +name = "atpublic" +version = "4.0" +description = "Keep all y'all's __all__'s in sync" +optional = false +python-versions = ">=3.8" +files = [ + {file = "atpublic-4.0-py3-none-any.whl", hash = "sha256:80057c55641253b86dcb68b524f82328172371b6547d4c7462a9127fbfbbabfc"}, + {file = "atpublic-4.0.tar.gz", hash = "sha256:0f40433219e124edf115c6c363808ca6f0e1cfa7d160d86b2fb94793086d1294"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = false +python-versions = ">=3.6" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[package.dependencies] +tzdata = {version = "*", optional = true, markers = "extra == \"tzdata\""} + +[package.extras] +tzdata = ["tzdata"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "billiard" +version = "4.2.0" +description = "Python multiprocessing fork with improvements and bugfixes" +optional = false +python-versions = ">=3.7" +files = [ + {file = "billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d"}, + {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, +] + +[[package]] +name = "blis" +version = "0.7.11" +description = "The Blis BLAS-like linear algebra library, as a self-contained C-extension." +optional = false +python-versions = "*" +files = [ + {file = "blis-0.7.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd5fba34c5775e4c440d80e4dea8acb40e2d3855b546e07c4e21fad8f972404c"}, + {file = "blis-0.7.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:31273d9086cab9c56986d478e3ed6da6752fa4cdd0f7b5e8e5db30827912d90d"}, + {file = "blis-0.7.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06883f83d4c8de8264154f7c4a420b4af323050ed07398c1ff201c34c25c0d2"}, + {file = "blis-0.7.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee493683e3043650d4413d531e79e580d28a3c7bdd184f1b9cfa565497bda1e7"}, + {file = "blis-0.7.11-cp310-cp310-win_amd64.whl", hash = "sha256:a73945a9d635eea528bccfdfcaa59dd35bd5f82a4a40d5ca31f08f507f3a6f81"}, + {file = "blis-0.7.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b68df4d01d62f9adaef3dad6f96418787265a6878891fc4e0fabafd6d02afba"}, + {file = "blis-0.7.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:162e60d941a8151418d558a94ee5547cb1bbeed9f26b3b6f89ec9243f111a201"}, + {file = "blis-0.7.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:686a7d0111d5ba727cd62f374748952fd6eb74701b18177f525b16209a253c01"}, + {file = "blis-0.7.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0421d6e44cda202b113a34761f9a062b53f8c2ae8e4ec8325a76e709fca93b6e"}, + {file = "blis-0.7.11-cp311-cp311-win_amd64.whl", hash = "sha256:0dc9dcb3843045b6b8b00432409fd5ee96b8344a324e031bfec7303838c41a1a"}, + {file = "blis-0.7.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dadf8713ea51d91444d14ad4104a5493fa7ecc401bbb5f4a203ff6448fadb113"}, + {file = "blis-0.7.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcdaf370f03adaf4171d6405a89fa66cb3c09399d75fc02e1230a78cd2759e4"}, + {file = "blis-0.7.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7de19264b1d49a178bf8035406d0ae77831f3bfaa3ce02942964a81a202abb03"}, + {file = "blis-0.7.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea55c6a4a60fcbf6a0fdce40df6e254451ce636988323a34b9c94b583fc11e5"}, + {file = "blis-0.7.11-cp312-cp312-win_amd64.whl", hash = "sha256:5a305dbfc96d202a20d0edd6edf74a406b7e1404f4fa4397d24c68454e60b1b4"}, + {file = "blis-0.7.11-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:68544a1cbc3564db7ba54d2bf8988356b8c7acd025966e8e9313561b19f0fe2e"}, + {file = "blis-0.7.11-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075431b13b9dd7b411894d4afbd4212acf4d0f56c5a20628f4b34902e90225f1"}, + {file = "blis-0.7.11-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:324fdf62af9075831aa62b51481960e8465674b7723f977684e32af708bb7448"}, + {file = "blis-0.7.11-cp36-cp36m-win_amd64.whl", hash = "sha256:afebdb02d2dcf9059f23ce1244585d3ce7e95c02a77fd45a500e4a55b7b23583"}, + {file = "blis-0.7.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2e62cd14b20e960f21547fee01f3a0b2ac201034d819842865a667c969c355d1"}, + {file = "blis-0.7.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b01c05a5754edc0b9a3b69be52cbee03f645b2ec69651d12216ea83b8122f0"}, + {file = "blis-0.7.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfee5ec52ba1e9002311d9191f7129d7b0ecdff211e88536fb24c865d102b50d"}, + {file = "blis-0.7.11-cp37-cp37m-win_amd64.whl", hash = "sha256:844b6377e3e7f3a2e92e7333cc644095386548ad5a027fdc150122703c009956"}, + {file = "blis-0.7.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6df00c24128e323174cde5d80ebe3657df39615322098ce06613845433057614"}, + {file = "blis-0.7.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:809d1da1331108935bf06e22f3cf07ef73a41a572ecd81575bdedb67defe3465"}, + {file = "blis-0.7.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfabd5272bbbe504702b8dfe30093653d278057656126716ff500d9c184b35a6"}, + {file = "blis-0.7.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca684f5c2f05269f17aefe7812360286e9a1cee3afb96d416485efd825dbcf19"}, + {file = "blis-0.7.11-cp38-cp38-win_amd64.whl", hash = "sha256:688a8b21d2521c2124ee8dfcbaf2c385981ccc27e313e052113d5db113e27d3b"}, + {file = "blis-0.7.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2ff7abd784033836b284ff9f4d0d7cb0737b7684daebb01a4c9fe145ffa5a31e"}, + {file = "blis-0.7.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9caffcd14795bfe52add95a0dd8426d44e737b55fcb69e2b797816f4da0b1d2"}, + {file = "blis-0.7.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fb36989ed61233cfd48915896802ee6d3d87882190000f8cfe0cf4a3819f9a8"}, + {file = "blis-0.7.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ea09f961871f880d5dc622dce6c370e4859559f0ead897ae9b20ddafd6b07a2"}, + {file = "blis-0.7.11-cp39-cp39-win_amd64.whl", hash = "sha256:5bb38adabbb22f69f22c74bad025a010ae3b14de711bf5c715353980869d491d"}, + {file = "blis-0.7.11.tar.gz", hash = "sha256:cec6d48f75f7ac328ae1b6fbb372dde8c8a57c89559172277f66e01ff08d4d42"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.15.0", markers = "python_version < \"3.9\""}, + {version = ">=1.19.0", markers = "python_version >= \"3.9\""}, +] + +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +optional = false +python-versions = "*" +files = [ + {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, + {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, +] + +[[package]] +name = "catalogue" +version = "2.0.10" +description = "Super lightweight function registries for your library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f"}, + {file = "catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = {version = ">=0.5", markers = "python_version < \"3.8\""} + +[[package]] +name = "celery" +version = "5.3.5" +description = "Distributed Task Queue." +optional = false +python-versions = ">=3.8" +files = [ + {file = "celery-5.3.5-py3-none-any.whl", hash = "sha256:30b75ac60fb081c2d9f8881382c148ed7c9052031a75a1e8743ff4b4b071f184"}, + {file = "celery-5.3.5.tar.gz", hash = "sha256:6b65d8dd5db499dd6190c45aa6398e171b99592f2af62c312f7391587feb5458"}, +] + +[package.dependencies] +"backports.zoneinfo" = {version = ">=0.2.1", markers = "python_version < \"3.9\""} +billiard = ">=4.2.0,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = ">=5.3.3,<6.0" +python-dateutil = ">=2.8.2" +tzdata = ">=2022.7" +vine = ">=5.1.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==41.0.5)"] +azureblockblob = ["azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb (==1.14.2)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elastic-transport (<=8.10.0)", "elasticsearch (<=8.10.1)"] +eventlet = ["eventlet (>=0.32.0)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] +mongodb = ["pymongo[srv] (>=4.0.2)"] +msgpack = ["msgpack (==1.0.7)"] +pymemcache = ["python-memcached (==1.59)"] +pyro = ["pyro4 (==4.82)"] +pytest = ["pytest-celery (==0.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem (==4.1.5)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.0)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.22.0)"] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "click-didyoumean" +version = "0.3.0" +description = "Enables git-like *did-you-mean* feature in click" +optional = false +python-versions = ">=3.6.2,<4.0.0" +files = [ + {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, + {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, +] + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +optional = false +python-versions = "*" +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] + +[[package]] +name = "click-repl" +version = "0.3.0" +description = "REPL plugin for Click" +optional = false +python-versions = ">=3.6" +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[package.dependencies] +click = ">=7.0" +prompt-toolkit = ">=3.0.36" + +[package.extras] +testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] + +[[package]] +name = "cloudpathlib" +version = "0.16.0" +description = "pathlib-style classes for cloud storage services." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cloudpathlib-0.16.0-py3-none-any.whl", hash = "sha256:f46267556bf91f03db52b5df7a152548596a15aabca1c8731ef32b0b25a1a6a3"}, + {file = "cloudpathlib-0.16.0.tar.gz", hash = "sha256:cdfcd35d46d529587d744154a0bdf962aca953b725c8784cd2ec478354ea63a3"}, +] + +[package.dependencies] +importlib_metadata = {version = "*", markers = "python_version < \"3.8\""} +typing_extensions = {version = ">4", markers = "python_version < \"3.11\""} + +[package.extras] +all = ["cloudpathlib[azure]", "cloudpathlib[gs]", "cloudpathlib[s3]"] +azure = ["azure-storage-blob (>=12)"] +gs = ["google-cloud-storage"] +s3 = ["boto3"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "confection" +version = "0.1.3" +description = "The sweetest config system for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "confection-0.1.3-py3-none-any.whl", hash = "sha256:58b125c9bc6786f32e37fe4d98bc3a03e5f509a4b9de02541b99c559f2026092"}, + {file = "confection-0.1.3.tar.gz", hash = "sha256:5a876d368a7698eec58791126757a75a3df16e26cc49653b52426e9ffd39f12f"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +srsly = ">=2.4.0,<3.0.0" +typing-extensions = {version = ">=3.7.4.1,<4.5.0", markers = "python_version < \"3.8\""} + +[[package]] +name = "configobj" +version = "5.0.8" +description = "Config file reading, writing and validation." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "configobj-5.0.8-py2.py3-none-any.whl", hash = "sha256:a7a8c6ab7daade85c3f329931a807c8aee750a2494363934f8ea84d8a54c87ea"}, + {file = "configobj-5.0.8.tar.gz", hash = "sha256:6f704434a07dc4f4dc7c9a745172c1cad449feb548febd9f7fe362629c627a97"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "41.0.5" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, + {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, + {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, + {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, + {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, + {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, + {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "cupy" +version = "11.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy-11.6.0.tar.gz", hash = "sha256:53dbb840072bb32d4bfbaa6bfa072365a30c98b1fcd1f43e48969071ad98f1a7"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.20,<1.27" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.12)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"] + +[[package]] +name = "cupy-cuda100" +version = "9.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.6.0" +files = [ + {file = "cupy_cuda100-9.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bd2252b13a845d664a13acf565c584c39c5cbf0511a55a2a7a22012ce32c5ae1"}, + {file = "cupy_cuda100-9.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cf06eca516d171c80316a9b214d5d6a6ad080b6da63241c0847fcbfdc9f260bb"}, + {file = "cupy_cuda100-9.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b9dd5062054d171a15907c29d3c4383b38f77935a7a461571f4c35accb9f30c4"}, + {file = "cupy_cuda100-9.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8079be975652b93f4307f15d6ccf56dcb128c3394db49ed55fd646561733daf1"}, + {file = "cupy_cuda100-9.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:01902a9d06b266b6a2e7cf866257f5a9fa8b7a0d5f4ea64b7b99a01dcedf3606"}, + {file = "cupy_cuda100-9.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:9b7d4fc7bfb09378e9a601cf37e5ac48cafd12434a360ce0b832f20a1d18aaad"}, + {file = "cupy_cuda100-9.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:aa1e7ea4e90f115099a15c64eaad138ecc8531ff0707ae4c78870126dd589211"}, + {file = "cupy_cuda100-9.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4578bdc28b31dc5f28e88e98cb0591570be2f7061940fa81631ffe40da1860c8"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.17,<1.24" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.10)"] +jenkins = ["codecov", "coverage (<5)", "coveralls", "pytest (>=6.2)", "pytest-cov", "pytest-timeout"] +setup = ["Cython (>=0.29.22,<3)", "fastrlock (>=0.5)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)"] +test = ["pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda101" +version = "9.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.6.0" +files = [ + {file = "cupy_cuda101-9.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9b41d9b2b638a759a23555c849a4f12d4fa47a8631f0f981852941984d745ab6"}, + {file = "cupy_cuda101-9.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:37d990500b1bd89e601fa408ba19a3ecd6a29159e9633dbfdc2d5e89041827e8"}, + {file = "cupy_cuda101-9.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:764c689b35a637e1b5887aebd2281f77c46f4ba97de4a427adf88c1f33caac49"}, + {file = "cupy_cuda101-9.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8bc286b1aa73436e7904bd6a47a2a4639cbe9fc8007fc8ea0e34a8bb34d34d52"}, + {file = "cupy_cuda101-9.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0a6ad4b304c751548b63ca18868e9fdc8b408a684fb46feec3edbe1bc576c3b9"}, + {file = "cupy_cuda101-9.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:ed37bd8cfff9201d64bb0293c577a37ead63057768938655cc72ed81092a84c0"}, + {file = "cupy_cuda101-9.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:97fe4754763bf514cd59e0df0feea34d884fcd96a8abb6ef0b00c6bfd202e170"}, + {file = "cupy_cuda101-9.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:27a32db3b318c06356ff56ec695f5fa52df3e0024c26bc30e09e6964c9168422"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.17,<1.24" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.10)"] +jenkins = ["codecov", "coverage (<5)", "coveralls", "pytest (>=6.2)", "pytest-cov", "pytest-timeout"] +setup = ["Cython (>=0.29.22,<3)", "fastrlock (>=0.5)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)"] +test = ["pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda102" +version = "11.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda102-11.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bf7db6d87983f2c932082a3f8357d051d6c96b08130163cc6d1a7e1d32f3740a"}, + {file = "cupy_cuda102-11.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:24c81c3fbdcd624b23a60a55141a0d474026b62a310693a7e38052d01852cb73"}, + {file = "cupy_cuda102-11.6.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:9adb9b13d93bd437769e9aa314f62199a22110b0eddc4e6a33a7aea6ff0b943d"}, + {file = "cupy_cuda102-11.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:8929520a5b04849fd6fdadb2174e80ea76aee548534d791b73cc27c79277dd34"}, + {file = "cupy_cuda102-11.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fcb8e54f76b7a7a58a0f0e2d8310166d948778254e60b5d5cbfb012cecc847e7"}, + {file = "cupy_cuda102-11.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2e8b20d1d2207eb3a373d53887d9c4142135046365185ea0a59fda0baccfd4e3"}, + {file = "cupy_cuda102-11.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b276fffded58073cfa18396bc6e915de1abf3c54bd2e69aab96158bf36aca313"}, + {file = "cupy_cuda102-11.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:2555ba27ea6d249c76d3062e479f20ffd0cf54f6c89de8f7c06373550d60f0d4"}, + {file = "cupy_cuda102-11.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e6feae926a8ff3fda54074f595015b4544b8549b821d8270f171a0ea0d041196"}, + {file = "cupy_cuda102-11.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:2971ac5b294aefe962751756ff08fe0b5342a9c2ffe558e0528a7b7ce4ddc7d8"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.20,<1.27" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.12)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"] + +[[package]] +name = "cupy-cuda110" +version = "11.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda110-11.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:c7661cd401870259276ca7233d297755eea3c67a8cfcbc0f03b933bb5caad6f4"}, + {file = "cupy_cuda110-11.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:de1e5a08c6e10b63a0514a9721df500ca1553ad58e97671b2f2058a4caf73909"}, + {file = "cupy_cuda110-11.6.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:bb49c44951969d7c7a8c6d4bb22a5a264a0d6d1e08b51f2433ac7e0e843d3a79"}, + {file = "cupy_cuda110-11.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:27232a9db619aaaa63e06c8885438b56ed8419b79832906975cc23539718c897"}, + {file = "cupy_cuda110-11.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7ff3439636fbdb7b17189703e85e8bf2f87558f5e2d83a195c1d87146c19c851"}, + {file = "cupy_cuda110-11.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:db0d5523117fe996632c61ddf9e1e5dd7c50db862feec5a11481db64679edc8c"}, + {file = "cupy_cuda110-11.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fe49e000ed81fdafd4c6cb89d411f68fa9e18bad45907d8e0f9a82a35957a320"}, + {file = "cupy_cuda110-11.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f73ac134c1ab2343423aaf7ddb16c4c2540d501a5a9c446b30802e8b7a1e86e"}, + {file = "cupy_cuda110-11.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:32952b79110183b24fab1e01f937f9e899122fb12622412efde5f9650ba09799"}, + {file = "cupy_cuda110-11.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:0bb7384dbfb2352c2d26a28995f0954297729398c6eb02820d0ab20979576885"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.20,<1.27" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.12)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"] + +[[package]] +name = "cupy-cuda111" +version = "10.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda111-10.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:f89c7c476fc11533a25873489aed9e93ff8eac87203b8da7b5f85a20de54d58a"}, + {file = "cupy_cuda111-10.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:2e2a3a8ec54a563d97658ae4d61fbcb1c92be17d9939bc51608fdd96f380f519"}, + {file = "cupy_cuda111-10.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8998a5e29fe7169c8b25331ddab8863234091c797bde55c000e73e2eb167f3fa"}, + {file = "cupy_cuda111-10.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a9816799733f10479b34be03a38451cd3be6d56b8ba6a43e9506a42089c424a9"}, + {file = "cupy_cuda111-10.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3a0ac219970058efe530c1e05c8ca8974524e2cc3f3de0a73fafd8883e30244b"}, + {file = "cupy_cuda111-10.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:90b7e3865145a6e7eb074f85d2b98a4b12350d758ad948cb240e0be09b24a577"}, + {file = "cupy_cuda111-10.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f2b357f9754190eea42600c3b55e34b6568acca17904ea856a24cefdbbaa1035"}, + {file = "cupy_cuda111-10.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:0d8a44629e0b885743e59f0ac184565d9e0a0944e7cf23374c4513e62159e823"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.18,<1.25" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.11)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2)", "pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda112" +version = "10.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda112-10.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:c811a26534f410e913af1f7501dc0161ab5eabc8a10bdd29fc72d967d45e6a57"}, + {file = "cupy_cuda112-10.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d5adc75ea17b3ae09734f4bb4668501c66df4d4379f60aa32f1eff48a2021b3"}, + {file = "cupy_cuda112-10.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bc2ce267e54ae58a8942f6d652a5d7716b24aa13c82e7a333757f534721b48c4"}, + {file = "cupy_cuda112-10.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fda0dff30224ae42023c5f19b0c2b914f85952489a19658d08974ab18a020a48"}, + {file = "cupy_cuda112-10.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a884af6cb594e89b0669964b47e1ae6fb13ae6d8c580db3426d08ee2ca84ecd4"}, + {file = "cupy_cuda112-10.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:3fd73efc6585c894c413bc08991ad2a4a0977cd72b149dae6d43d9ceba6226b7"}, + {file = "cupy_cuda112-10.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f63a97eabbf01a24269fa77991f64456705abc7a245fa44e4c8be1b777ee5dc1"}, + {file = "cupy_cuda112-10.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:caef546e3cf8399636657a15f9db086bbc47ea1e9bbf593f9ad356d4aadf115c"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.18,<1.25" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.11)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2)", "pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda113" +version = "10.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda113-10.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:27e5efe2c3afa80ff48654cb27f9e0eddb36f8b26ef0d32d3ba0a233e1359b51"}, + {file = "cupy_cuda113-10.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b96076d1ddd33fdb2c908ed0f8109caf69d37d36f839a8a8cdae1312508336f"}, + {file = "cupy_cuda113-10.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22363c2863727cae5154aa4bab9e8a648d7fe66c9e2195d81dd4e8693c2e61ce"}, + {file = "cupy_cuda113-10.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8cc69b9d5735372477a7af3822c8f8e996ffe6de05cfc917500af9dc0117ca3e"}, + {file = "cupy_cuda113-10.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:10dc6899577e445426d81f0960ba9059d9aaa750426997c61fad882d6345264c"}, + {file = "cupy_cuda113-10.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:c6893ac9040a11610e63973063dfd715dbda8bd07ef99951bab7a09c7f335e1e"}, + {file = "cupy_cuda113-10.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4bf4bc06d991c06b95f6fe558d117cafd93bd4eeaf80606f18dd31d20d2eff25"}, + {file = "cupy_cuda113-10.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:3745fc42dca86ba8a1109ddc7964aed8e1efc0ce8085cb2f140dcd6429f26354"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.18,<1.25" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.11)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2)", "pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda114" +version = "10.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda114-10.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:039e53c7269a1fb188140f945af8fb82c1dfd126de41f65af1d4f4604fbbcf19"}, + {file = "cupy_cuda114-10.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:5251c2f561f785c7693aeb062b206779fc7c959930d167f0671a24bb09a9a6ff"}, + {file = "cupy_cuda114-10.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:288b04bf7b7e5aa5480c7ecb3905c36e7ed61c7dab89ee82c099c3c03429335a"}, + {file = "cupy_cuda114-10.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9f17132d9c7d8cd5470343309da23f6d51dec1484a3eef123febc5ae1602b0d4"}, + {file = "cupy_cuda114-10.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2db41e7788ecc6c4eaa78f10808a5e1cd17ab5ac82fd56b69f37150f50b2ac1a"}, + {file = "cupy_cuda114-10.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:61015d9ea498df21ca7387e9180c171cf9a6ba9071bdc31c8088d78f97aa4f47"}, + {file = "cupy_cuda114-10.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:44ade90ffc5cd46a27507da82f9044dc59580e8d4798755c73b02f5d6440873b"}, + {file = "cupy_cuda114-10.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:2fb25ae4177995bb30c7844092702d2126574353898196d579748f0b61715365"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.18,<1.25" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.11)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2)", "pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda115" +version = "10.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda115-10.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:49d6826e87fe4a349d984a5dd8319e375df9cdc40ea9c9c22fa07fca69221fea"}, + {file = "cupy_cuda115-10.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:09ca4b383260568484a9cea37c61dd15336bd551ddd3e85630920bfb92c72df1"}, + {file = "cupy_cuda115-10.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2247d5951f1d94aa007a42755add93678a4eb74db8bd1758e27b144a68983055"}, + {file = "cupy_cuda115-10.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8a580ab452495d4dc006aff7fae8e94540925b3ee57576ed4bc461fe4564fbbd"}, + {file = "cupy_cuda115-10.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cf22c55f3eba48a1dd3d5f6863bf08ade32d377bc906a22e32178c3f9ff0ae80"}, + {file = "cupy_cuda115-10.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:61cc5124dd4325959cd7ddb0664e8b572b323db07c579962227833d6e3da3608"}, + {file = "cupy_cuda115-10.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2d984552ed184f90b3f648924871d1bd3a9f5b63a78f12d7d145a24407443e87"}, + {file = "cupy_cuda115-10.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f24a53ac1794c1d41b8c2283011c1366288a488920d6aab6a906f44cc02e145e"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.18,<1.25" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.11)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2)", "pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda116" +version = "10.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda116-10.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:08e1ce4e96bab3d2d7028aec6a13da9e3fced8fb6523ae8f45734a4d2271fd9a"}, + {file = "cupy_cuda116-10.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:2286ed8990b4e562fc68b10c037941f14bd9b463ba4b05309f1f2b040d45c756"}, + {file = "cupy_cuda116-10.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dc86583dc3e722996a95c00bdfd53b7b6e79a72d9ebdfecd38795a4915e3f6d1"}, + {file = "cupy_cuda116-10.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fb1116168c31c4af0adbff88c6242d5ec379ba699824b041f6fd6196f5a1e8fb"}, + {file = "cupy_cuda116-10.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5d100854febe2d41d8b347289417cfff28b4dcd241e28883ad8371dd98ff16e3"}, + {file = "cupy_cuda116-10.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:2fb8a5e6cc0c8690e1fed2a3a5b6f94c6a9463b6c2c4039e6d73dca1927deef5"}, + {file = "cupy_cuda116-10.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1e459f2c912c71f336698f378916fb82fc35e4ae10fd2221e9c42975ef41956f"}, + {file = "cupy_cuda116-10.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:111b5f9ae199ea4deb9e1f20cb2c318149a7c0c53f1005c5d30a4a61aa204b17"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.18,<1.25" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.11)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2)", "pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda117" +version = "10.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cupy_cuda117-10.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:23cffa50a8d756dd41b842d450252bf18295d8fe185803295c8e34fabb317ee3"}, + {file = "cupy_cuda117-10.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:1ead21cc1d0e3938f67531d8be94c6e1142ed246f7e67e6eded2155afd6a84c4"}, + {file = "cupy_cuda117-10.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7b1a59a41b6771e49404ff1770b20c1c0878454992c201f6e84216c0167a68fd"}, + {file = "cupy_cuda117-10.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:29e1545c7ebbe02f5ea1cc407444f1b0778836e2afc6392247a9c048953646fd"}, + {file = "cupy_cuda117-10.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4736794f258fe810c51b649c8a57faac5b8fdecbd47f1f81113a4b54fbc4af84"}, + {file = "cupy_cuda117-10.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:37b5784cf5cf1008d69ea8d4d443383f786715dbf305ef359ec7689b34a6689b"}, + {file = "cupy_cuda117-10.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a3ddaab1de49ade9ccd6fd749364fd945a026b582732f5c083f7dbbabffc1578"}, + {file = "cupy_cuda117-10.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:072aab906d0772172696ac8a8338c8d02f322d61e55b77ee02d403032ad2bc5f"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.18,<1.25" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.4,<1.11)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==0.950)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2)", "pytest (>=6.2)"] + +[[package]] +name = "cupy-cuda80" +version = "7.8.0" +description = "CuPy: NumPy-like API accelerated with CUDA" +optional = true +python-versions = ">=3.5.0" +files = [ + {file = "cupy_cuda80-7.8.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:4efdbd04db2339126ac305cae9756f91444c047a6cf5d5ccd265bd49bf5922ba"}, + {file = "cupy_cuda80-7.8.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:71536358e8218481110db47f375d87adc1636754fb12af879282d09473e76186"}, + {file = "cupy_cuda80-7.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:41fa1c27bf700f2aa3d8387716a03ce1ebbf889e3984080211c671a3acfd8e1e"}, + {file = "cupy_cuda80-7.8.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d2602d687a9e4ad32efe0f797efd544a8a57a7f68d5330a0f0bc6130b662adea"}, + {file = "cupy_cuda80-7.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:248d7bd759e6b1daa0948ca0a615a5347f6763cfc7f5e6f353b99214fd451610"}, + {file = "cupy_cuda80-7.8.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:86f36907f259c082b4b230ab6328b9a1b2e9dd0662b957f7090f5d3e308b7617"}, + {file = "cupy_cuda80-7.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:dad9c4ea275727df60af3f269326a8d495476b3251472379961a512155266596"}, +] + +[package.dependencies] +fastrlock = ">=0.3" +numpy = ">=1.9.0" +six = ">=1.9.0" + +[package.extras] +appveyor = ["attrs (<19.2.0)", "mock", "pytest (<4.2.0)"] +docs = ["sphinx (==3.0.4)", "sphinx-rtd-theme"] +doctest = ["matplotlib"] +jenkins = ["attrs (<19.2.0)", "codecov", "coveralls", "mock", "pytest (<4.2.0)", "pytest-cov", "pytest-timeout"] +setup = ["fastrlock (>=0.3)"] +stylecheck = ["autopep8 (==1.4.4)", "flake8 (==3.7.9)", "pbr (==4.0.4)", "pycodestyle (==2.5.0)"] +test = ["attrs (<19.2.0)", "mock", "pytest (<4.2.0)"] +travis = ["autopep8 (==1.4.4)", "flake8 (==3.7.9)", "pbr (==4.0.4)", "pycodestyle (==2.5.0)", "sphinx (==3.0.4)", "sphinx-rtd-theme"] + +[[package]] +name = "cupy-cuda90" +version = "8.6.0" +description = "CuPy: A NumPy-compatible array library accelerated by CUDA" +optional = true +python-versions = ">=3.5.0" +files = [ + {file = "cupy_cuda90-8.6.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa2c9ee21d9a23029cfeabcdbfeae836c563d65e78cb08a76e46986300e9e907"}, + {file = "cupy_cuda90-8.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ac6e2601b40b0237403c04075284e9574337113a1a74736d5ed3ca4c52bdb96a"}, + {file = "cupy_cuda90-8.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:99772ed8d9abee8a6781521e17610b67e24bf0db7e2e8cb8d1238f0d9f444412"}, + {file = "cupy_cuda90-8.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:00e5eb6a7de4917b5089b552e5d05113c1bcb3e8ce6bf074e711fb4c0097e843"}, + {file = "cupy_cuda90-8.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09106bc2311a502a6aad05d768b000ffac80ccec9aa13db145b77b1b70207a1b"}, + {file = "cupy_cuda90-8.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a97bd932e89517fcfc2888f8b626eef83d6c8a9d520b589f610d146eaee236f8"}, + {file = "cupy_cuda90-8.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f3d86cf1f485e02ace26827c6c552630854284ca7d96219c7ea2e18f6bdbd6c6"}, + {file = "cupy_cuda90-8.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48988bb51560a2498a1256fa1744561e9cb9d1384c2b88ab56bc499dc786afc0"}, + {file = "cupy_cuda90-8.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:d207b53b6e9e4d396795abc0e5cf0ffe46610174bb9640d4a9cfb9dd130e9ee3"}, +] + +[package.dependencies] +fastrlock = ">=0.3" +numpy = ">=1.15" + +[package.extras] +jenkins = ["attrs (<19.2.0)", "codecov", "coverage (<5)", "coveralls", "pytest (<4.2.0)", "pytest-cov (<2.10)", "pytest-timeout"] +setup = ["Cython (>=0.29.22)", "fastrlock (>=0.3)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)"] +test = ["attrs (<19.2.0)", "pytest (<4.2.0)"] + +[[package]] +name = "cupy-cuda91" +version = "7.8.0" +description = "CuPy: NumPy-like API accelerated with CUDA" +optional = true +python-versions = ">=3.5.0" +files = [ + {file = "cupy_cuda91-7.8.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:dfb60b10a7d47090327e815934dc1f94b495fca1737cfc31787f69a8100de3ea"}, + {file = "cupy_cuda91-7.8.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b6c75c6009ed509b1b6d51fe80e92f95fd170587f478622a62eca4a5e14051cb"}, + {file = "cupy_cuda91-7.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:db297b15085d715759251a85e8640fa44b397118c23f63d20009d1f73a4ebd08"}, + {file = "cupy_cuda91-7.8.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95360ece88a6f448022962b59a44d36e242c125eaf8d23534476f5a79f9f91b4"}, + {file = "cupy_cuda91-7.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c976609d78ab90f0f0f0fe2b043f93a00f690f3617bf571d0826465836f15920"}, + {file = "cupy_cuda91-7.8.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:16ebdde7d11795bcf0b862496b87a5a11ef7d86894078a8a065bb6ae72ce3a15"}, + {file = "cupy_cuda91-7.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:7f24901760b489397cec0eb9beb09edde470c2157622896ed84c1342f0eef29c"}, +] + +[package.dependencies] +fastrlock = ">=0.3" +numpy = ">=1.9.0" +six = ">=1.9.0" + +[package.extras] +appveyor = ["attrs (<19.2.0)", "mock", "pytest (<4.2.0)"] +docs = ["sphinx (==3.0.4)", "sphinx-rtd-theme"] +doctest = ["matplotlib"] +jenkins = ["attrs (<19.2.0)", "codecov", "coveralls", "mock", "pytest (<4.2.0)", "pytest-cov", "pytest-timeout"] +setup = ["fastrlock (>=0.3)"] +stylecheck = ["autopep8 (==1.4.4)", "flake8 (==3.7.9)", "pbr (==4.0.4)", "pycodestyle (==2.5.0)"] +test = ["attrs (<19.2.0)", "mock", "pytest (<4.2.0)"] +travis = ["autopep8 (==1.4.4)", "flake8 (==3.7.9)", "pbr (==4.0.4)", "pycodestyle (==2.5.0)", "sphinx (==3.0.4)", "sphinx-rtd-theme"] + +[[package]] +name = "cupy-cuda92" +version = "8.6.0" +description = "CuPy: A NumPy-compatible array library accelerated by CUDA" +optional = true +python-versions = ">=3.5.0" +files = [ + {file = "cupy_cuda92-8.6.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:93de0d7a5a7e82326e3a6d54fcc031464e5c514b3683db3e3f47cd1e0995dec9"}, + {file = "cupy_cuda92-8.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6acfc0b7f1b29b1cc5cb2d97c893d57c0f16208692174ec060d4162938fa9973"}, + {file = "cupy_cuda92-8.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c1c6a210f8978ee54b30f30fb4eecf9dd60d84788ab706d9120aae4caa8578a4"}, + {file = "cupy_cuda92-8.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:52c214d94cfc33b9513615f62877d1e235718c52c32178c645dcfbf72be4cd7b"}, + {file = "cupy_cuda92-8.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4df33964d2064da47fa5c3cdc27607cd6559a82baab098da5e11d61d5e1085a2"}, + {file = "cupy_cuda92-8.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf3c5a5357ad96c2f669f27c952629550fdb162ee72f066214c9cdfd1a8e096b"}, + {file = "cupy_cuda92-8.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:60a91cac6276a8229be642b85a2f48ce7ab2412713f25417d6c2c6bb940ed028"}, + {file = "cupy_cuda92-8.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:def44e69633bf7fbd279eb914f721852d0715701e62fced993fd1e2ecc0937e0"}, + {file = "cupy_cuda92-8.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:99f0ec8807115ad4473294d5f3adcc9f9cc6c906b212212ced802a07517dff09"}, +] + +[package.dependencies] +fastrlock = ">=0.3" +numpy = ">=1.15" + +[package.extras] +jenkins = ["attrs (<19.2.0)", "codecov", "coverage (<5)", "coveralls", "pytest (<4.2.0)", "pytest-cov (<2.10)", "pytest-timeout"] +setup = ["Cython (>=0.29.22)", "fastrlock (>=0.3)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)"] +test = ["attrs (<19.2.0)", "pytest (<4.2.0)"] + +[[package]] +name = "cymem" +version = "2.0.8" +description = "Manage calls to calloc/free through Cython" +optional = false +python-versions = "*" +files = [ + {file = "cymem-2.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77b5d3a73c41a394efd5913ab7e48512054cd2dabb9582d489535456641c7666"}, + {file = "cymem-2.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd33da892fb560ba85ea14b1528c381ff474048e861accc3366c8b491035a378"}, + {file = "cymem-2.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29a551eda23eebd6d076b855f77a5ed14a1d1cae5946f7b3cb5de502e21b39b0"}, + {file = "cymem-2.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8260445652ae5ab19fff6851f32969a7b774f309162e83367dd0f69aac5dbf7"}, + {file = "cymem-2.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:a63a2bef4c7e0aec7c9908bca0a503bf91ac7ec18d41dd50dc7dff5d994e4387"}, + {file = "cymem-2.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b84b780d52cb2db53d4494fe0083c4c5ee1f7b5380ceaea5b824569009ee5bd"}, + {file = "cymem-2.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d5f83dc3cb5a39f0e32653cceb7c8ce0183d82f1162ca418356f4a8ed9e203e"}, + {file = "cymem-2.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ac218cf8a43a761dc6b2f14ae8d183aca2bbb85b60fe316fd6613693b2a7914"}, + {file = "cymem-2.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c993589d1811ec665d37437d5677b8757f53afadd927bf8516ac8ce2d3a50c"}, + {file = "cymem-2.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:ab3cf20e0eabee9b6025ceb0245dadd534a96710d43fb7a91a35e0b9e672ee44"}, + {file = "cymem-2.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cb51fddf1b920abb1f2742d1d385469bc7b4b8083e1cfa60255e19bc0900ccb5"}, + {file = "cymem-2.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9235957f8c6bc2574a6a506a1687164ad629d0b4451ded89d49ebfc61b52660c"}, + {file = "cymem-2.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2cc38930ff5409f8d61f69a01e39ecb185c175785a1c9bec13bcd3ac8a614ba"}, + {file = "cymem-2.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bf49e3ea2c441f7b7848d5c61b50803e8cbd49541a70bb41ad22fce76d87603"}, + {file = "cymem-2.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:ecd12e3bacf3eed5486e4cd8ede3c12da66ee0e0a9d0ae046962bc2bb503acef"}, + {file = "cymem-2.0.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:167d8019db3b40308aabf8183fd3fbbc256323b645e0cbf2035301058c439cd0"}, + {file = "cymem-2.0.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17cd2c2791c8f6b52f269a756ba7463f75bf7265785388a2592623b84bb02bf8"}, + {file = "cymem-2.0.8-cp36-cp36m-win_amd64.whl", hash = "sha256:6204f0a3307bf45d109bf698ba37997ce765f21e359284328e4306c7500fcde8"}, + {file = "cymem-2.0.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9c05db55ea338648f8e5f51dd596568c7f62c5ae32bf3fa5b1460117910ebae"}, + {file = "cymem-2.0.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ce641f7ba0489bd1b42a4335a36f38c8507daffc29a512681afaba94a0257d2"}, + {file = "cymem-2.0.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b83a5972a64f62796118da79dfeed71f4e1e770b2b7455e889c909504c2358"}, + {file = "cymem-2.0.8-cp37-cp37m-win_amd64.whl", hash = "sha256:ada6eb022e4a0f4f11e6356a5d804ceaa917174e6cf33c0b3e371dbea4dd2601"}, + {file = "cymem-2.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e593cd57e2e19eb50c7ddaf7e230b73c890227834425b9dadcd4a86834ef2ab"}, + {file = "cymem-2.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d513f0d5c6d76facdc605e42aa42c8d50bb7dedca3144ec2b47526381764deb0"}, + {file = "cymem-2.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e370dd54359101b125bfb191aca0542718077b4edb90ccccba1a28116640fed"}, + {file = "cymem-2.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84f8c58cde71b8fc7024883031a4eec66c0a9a4d36b7850c3065493652695156"}, + {file = "cymem-2.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:6a6edddb30dd000a27987fcbc6f3c23b7fe1d74f539656952cb086288c0e4e29"}, + {file = "cymem-2.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b896c83c08dadafe8102a521f83b7369a9c5cc3e7768eca35875764f56703f4c"}, + {file = "cymem-2.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f8f2bfee34f6f38b206997727d29976666c89843c071a968add7d61a1e8024"}, + {file = "cymem-2.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7372e2820fa66fd47d3b135f3eb574ab015f90780c3a21cfd4809b54f23a4723"}, + {file = "cymem-2.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4e57bee56d35b90fc2cba93e75b2ce76feaca05251936e28a96cf812a1f5dda"}, + {file = "cymem-2.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ceeab3ce2a92c7f3b2d90854efb32cb203e78cb24c836a5a9a2cac221930303b"}, + {file = "cymem-2.0.8.tar.gz", hash = "sha256:8fb09d222e21dcf1c7e907dc85cf74501d4cea6c4ed4ac6c9e016f98fb59cbbf"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "dictdiffer" +version = "0.9.0" +description = "Dictdiffer is a library that helps you to diff and patch dictionaries." +optional = false +python-versions = "*" +files = [ + {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"}, + {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"}, +] + +[package.extras] +all = ["Sphinx (>=3)", "check-manifest (>=0.42)", "mock (>=1.3.0)", "numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "sphinx-rtd-theme (>=0.2)", "tox (>=3.7.0)"] +docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"] +numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"] +tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "tox (>=3.7.0)"] + +[[package]] +name = "diskcache" +version = "5.6.3" +description = "Disk Cache -- Disk and file backed persistent cache." +optional = false +python-versions = ">=3" +files = [ + {file = "diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19"}, + {file = "diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc"}, +] + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "distro" +version = "1.8.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, + {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, +] + +[[package]] +name = "dpath" +version = "2.1.6" +description = "Filesystem-like pathing and searching for dictionaries" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dpath-2.1.6-py3-none-any.whl", hash = "sha256:31407395b177ab63ef72e2f6ae268c15e938f2990a8ecf6510f5686c02b6db73"}, + {file = "dpath-2.1.6.tar.gz", hash = "sha256:f1e07c72e8605c6a9e80b64bc8f42714de08a789c7de417e49c3f87a19692e47"}, +] + +[[package]] +name = "dulwich" +version = "0.21.6" +description = "Python Git Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dulwich-0.21.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7f89bee4c97372e8aaf8ffaf5899f1bcd5184b5306d7eaf68738c1101ceba10e"}, + {file = "dulwich-0.21.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:847bb52562a211b596453a602e75739350c86d7edb846b5b1c46896a5c86b9bb"}, + {file = "dulwich-0.21.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e09d0b4e985b371aa6728773781b19298d361a00772e20f98522868cf7edc6f"}, + {file = "dulwich-0.21.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dfb50b3915e223a97f50fbac0dbc298d5fffeaac004eeeb3d552c57fe38416f"}, + {file = "dulwich-0.21.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a64eca1601e79c16df78afe08da9ac9497b934cbc5765990ca7d89a4b87453d9"}, + {file = "dulwich-0.21.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1fedd924763a5d640348db43a267a394aa80d551228ad45708e0b0cc2130bb62"}, + {file = "dulwich-0.21.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:edc21c3784dd9d9b85abd9fe53f81a884e2cdcc4e5e09ada17287420d64cfd46"}, + {file = "dulwich-0.21.6-cp310-cp310-win32.whl", hash = "sha256:daa3584beabfcf0da76df57535a23c80ff6d8ccde6ddbd23bdc79d317a0e20a7"}, + {file = "dulwich-0.21.6-cp310-cp310-win_amd64.whl", hash = "sha256:40623cc39a3f1634663d22d87f86e2e406cc8ff17ae7a3edc7fcf963c288992f"}, + {file = "dulwich-0.21.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e8ed878553f0b76facbb620b455fafa0943162fe8e386920717781e490444efa"}, + {file = "dulwich-0.21.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a89b19f4960e759915dbc23a4dd0abc067b55d8d65e9df50961b73091b87b81a"}, + {file = "dulwich-0.21.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28acbd08d6b38720d99cc01da9dd307a2e0585e00436c95bcac6357b9a9a6f76"}, + {file = "dulwich-0.21.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2f2683e0598f7c7071ef08a0822f062d8744549a0d45f2c156741033b7e3d7d"}, + {file = "dulwich-0.21.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54342cf96fe8a44648505c65f23d18889595762003a168d67d7263df66143bd2"}, + {file = "dulwich-0.21.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a3fc071e5b14f164191286f7ffc02f60fe8b439d01fad0832697cc08c2237dd"}, + {file = "dulwich-0.21.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32d7acfe3fe2ce4502446d8f7a5ab34cfd24c9ff8961e60337638410906a8fbb"}, + {file = "dulwich-0.21.6-cp311-cp311-win32.whl", hash = "sha256:5e58171a5d70f7910f73d25ff82a058edff09a4c1c3bd1de0dc6b1fbc9a42c3e"}, + {file = "dulwich-0.21.6-cp311-cp311-win_amd64.whl", hash = "sha256:ceabe8f96edfb9183034a860f5dc77586700b517457032867b64a03c44e5cf96"}, + {file = "dulwich-0.21.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4fdc2f081bc3e9e120079c2cea4be213e3f127335aca7c0ab0c19fe791270caa"}, + {file = "dulwich-0.21.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fe957564108f74325d0d042d85e0c67ef470921ca92b6e7d330c7c49a3b9c1d"}, + {file = "dulwich-0.21.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2912c8a845c8ccbc79d068a89db7172e355adeb84eb31f062cd3a406d528b30"}, + {file = "dulwich-0.21.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:81e237a6b1b20c79ef62ca19a8fb231f5519bab874b9a1c2acf9c05edcabd600"}, + {file = "dulwich-0.21.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:513d045e74307eeb31592255c38f37042c9aa68ce845a167943018ab5138b0e3"}, + {file = "dulwich-0.21.6-cp37-cp37m-win32.whl", hash = "sha256:e1ac882afa890ef993b8502647e6c6d2b3977ce56e3fe80058ce64607cbc7107"}, + {file = "dulwich-0.21.6-cp37-cp37m-win_amd64.whl", hash = "sha256:5d2ccf3d355850674f75655154a6519bf1f1664176c670109fa7041019b286f9"}, + {file = "dulwich-0.21.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:28c9724a167c84a83fc6238e0781f4702b5fe8c53ede31604525fb1a9d1833f4"}, + {file = "dulwich-0.21.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c816be529680659b6a19798287b4ec6de49040f58160d40b1b2934fd6c28e93f"}, + {file = "dulwich-0.21.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b0545f0fa9444a0eb84977d08e302e3f55fd7c34a0466ec28bedc3c839b2fc1f"}, + {file = "dulwich-0.21.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b1682e8e826471ea3c22b8521435e93799e3db8ad05dd3c8f9b1aaacfa78147"}, + {file = "dulwich-0.21.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ad45928a65f39ea0f451f9989b7aaedba9893d48c3189b544a70c6a1043f71"}, + {file = "dulwich-0.21.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1c9e55233f19cd19c484f607cd90ab578ac50ebfef607f77e3b35c2b6049470"}, + {file = "dulwich-0.21.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:18697b58e0fc5972de68b529b08ac9ddda3f39af27bcf3f6999635ed3da7ef68"}, + {file = "dulwich-0.21.6-cp38-cp38-win32.whl", hash = "sha256:22798e9ba59e32b8faff5d9067e2b5a308f6b0fba9b1e1e928571ad278e7b36c"}, + {file = "dulwich-0.21.6-cp38-cp38-win_amd64.whl", hash = "sha256:6c91e1ed20d3d9a6aaaed9e75adae37272b3fcbcc72bab1eb09574806da88563"}, + {file = "dulwich-0.21.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8b84450766a3b151c3676fec3e3ed76304e52a84d5d69ade0f34fff2782c1b41"}, + {file = "dulwich-0.21.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3da632648ee27b64bb5b285a3a94fddf297a596891cca12ac0df43c4f59448f"}, + {file = "dulwich-0.21.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cef50c0a19f322b7150248b8fa0862ce1652dec657e340c4020573721e85f215"}, + {file = "dulwich-0.21.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ac20dfcfd6057efb8499158d23f2c059f933aefa381e192100e6d8bc25d562"}, + {file = "dulwich-0.21.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81d10aa50c0a9a6dd495990c639358e3a3bbff39e17ff302179be6e93b573da7"}, + {file = "dulwich-0.21.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a9b52a08d49731375662936d05a12c4a64a6fe0ce257111f62638e475fb5d26d"}, + {file = "dulwich-0.21.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed2f1f638b9adfba862719693b371ffe5d58e94d552ace9a23dea0fb0db6f468"}, + {file = "dulwich-0.21.6-cp39-cp39-win32.whl", hash = "sha256:bf90f2f9328a82778cf85ab696e4a7926918c3f315c75fc432ba31346bfa89b7"}, + {file = "dulwich-0.21.6-cp39-cp39-win_amd64.whl", hash = "sha256:e0dee3840c3c72e1d60c8f87a7a715d8eac023b9e1b80199d97790f7a1c60d9c"}, + {file = "dulwich-0.21.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:32d3a35caad6879d04711b358b861142440a543f5f4e02df67b13cbcd57f84a6"}, + {file = "dulwich-0.21.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c04df87098053b7767b46fc04b7943d75443f91c73560ca50157cdc22e27a5d3"}, + {file = "dulwich-0.21.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e07f145c7b0d82a9f77d157f493a61900e913d1c1f8b1f40d07d919ffb0929a4"}, + {file = "dulwich-0.21.6-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:008ff08629ab16d3638a9f36cfc6f5bd74b4d594657f2dc1583d8d3201794571"}, + {file = "dulwich-0.21.6-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf469cd5076623c2aad69d01ce9d5392fcb38a5faef91abe1501be733453e37d"}, + {file = "dulwich-0.21.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6592ef2d16ac61a27022647cf64a048f5be6e0a6ab2ebc7322bfbe24fb2b971b"}, + {file = "dulwich-0.21.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99577b2b37f64bc87280079245fb2963494c345d7db355173ecec7ab3d64b949"}, + {file = "dulwich-0.21.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d7cd9fb896c65e4c28cb9332f2be192817805978dd8dc299681c4fe83c631158"}, + {file = "dulwich-0.21.6-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9002094198e57e88fe77412d3aa64dd05978046ae725a16123ba621a7704628"}, + {file = "dulwich-0.21.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9b6f8a16f32190aa88c37ef013858b3e01964774bc983900bd0d74ecb6576e6"}, + {file = "dulwich-0.21.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee8aba4dec4d0a52737a8a141f3456229c87dcfd7961f8115786a27b6ebefed"}, + {file = "dulwich-0.21.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a780e2a0ff208c4f218e72eff8d13f9aff485ff9a6f3066c22abe4ec8cec7dcd"}, + {file = "dulwich-0.21.6.tar.gz", hash = "sha256:30fbe87e8b51f3813c131e2841c86d007434d160bd16db586b40d47f31dd05b0"}, +] + +[package.dependencies] +urllib3 = ">=1.25" + +[package.extras] +fastimport = ["fastimport"] +https = ["urllib3 (>=1.24.1)"] +paramiko = ["paramiko"] +pgp = ["gpg"] + +[[package]] +name = "dvc" +version = "2.58.2" +description = "Git for data scientists - manage your code and data together" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dvc-2.58.2-py3-none-any.whl", hash = "sha256:3a935615812fd57c341e8b58a7f4de57d9d4a067500376041c3f1c805ee24345"}, + {file = "dvc-2.58.2.tar.gz", hash = "sha256:d40fff99b76719d1d524f103ad9dc64141bb363492abb9b8a61c6e70efe5a4dc"}, +] + +[package.dependencies] +colorama = ">=0.3.9" +configobj = ">=5.0.6" +distro = ">=1.3" +dpath = ">=2.1.0,<3" +dvc-data = ">=0.51.0,<0.52" +dvc-http = ">=2.29.0" +dvc-render = ">=0.3.1,<1" +dvc-studio-client = ">=0.9.2,<1" +dvc-task = ">=0.2.1,<1" +flatten-dict = ">=0.4.1,<1" +"flufl.lock" = ">=5" +funcy = ">=1.14" +grandalf = ">=0.7,<1" +hydra-core = ">=1.1" +iterative-telemetry = ">=0.0.7" +networkx = ">=2.5" +packaging = ">=19" +pathspec = ">=0.10.3" +platformdirs = ">=3.1.1,<4" +psutil = ">=5.8" +pydot = ">=1.2.4" +pygtrie = ">=2.3.2" +pyparsing = ">=2.4.7" +requests = ">=2.22" +rich = ">=12" +"ruamel.yaml" = ">=0.17.11" +scmrepo = ">=1.0.0,<2" +shortuuid = ">=0.5" +shtab = ">=1.3.4,<2" +tabulate = ">=0.8.7" +tomlkit = ">=0.11.1" +tqdm = ">=4.63.1,<5" +voluptuous = ">=0.11.7" +"zc.lockfile" = ">=1.2.1" + +[package.extras] +all = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs]"] +azure = ["dvc-azure (>=2.21.2)"] +dev = ["dvc[azure,gdrive,gs,hdfs,lint,oss,s3,ssh,terraform,tests,webdav,webhdfs]"] +gdrive = ["dvc-gdrive (==2.19.2)"] +gs = ["dvc-gs (==2.22.0)"] +hdfs = ["dvc-hdfs (==2.19)"] +lint = ["mypy (==1.3.0)", "pylint (==2.17.4)", "types-colorama", "types-psutil", "types-requests", "types-tabulate", "types-toml", "types-tqdm"] +oss = ["dvc-oss (==2.19)"] +s3 = ["dvc-s3 (==2.22.0)"] +ssh = ["dvc-ssh (>=2.22.1,<3)"] +ssh-gssapi = ["dvc-ssh[gssapi] (>=2.22.1,<3)"] +terraform = ["tpi[ssh] (>=2.1)"] +testing = ["pytest-benchmark[histogram]", "pytest-test-utils", "pytest-virtualenv"] +tests = ["beautifulsoup4 (>=4.4)", "dvc-ssh", "dvc[testing]", "filelock", "flaky", "pytest (>=7,<8)", "pytest-cov", "pytest-docker (>=1,<2)", "pytest-lazy-fixture", "pytest-mock", "pytest-test-utils", "pytest-timeout (>=2)", "pytest-xdist (>=3.2)", "pywin32 (>=225)"] +webdav = ["dvc-webdav (==2.19.1)"] +webhdfs = ["dvc-webhdfs (==2.19)"] +webhdfs-kerberos = ["dvc-webhdfs[kerberos] (==2.19)"] + +[[package]] +name = "dvc-data" +version = "0.51.0" +description = "dvc data" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dvc-data-0.51.0.tar.gz", hash = "sha256:32544bb7d3ae509f91c2d07a9dc3739dd87980be5b41582781efb4f246b58497"}, + {file = "dvc_data-0.51.0-py3-none-any.whl", hash = "sha256:f3244f7d848f10fdb21d5ff485410f6955a1de828a149577633e3ed0f451ebd2"}, +] + +[package.dependencies] +attrs = ">=21.3.0" +dictdiffer = ">=0.8.1" +diskcache = ">=5.2.1" +dvc-objects = ">=0.22.0,<1" +funcy = ">=1.14" +nanotime = ">=0.5.2" +pygtrie = ">=2.3.2" +shortuuid = ">=0.5.0" +sqltrie = ">=0.3.1,<1" + +[package.extras] +all = ["rich (>=10.11.0,<14.0.0)", "typer[all] (>=0.6)"] +cli = ["rich (>=10.11.0,<14.0.0)", "typer[all] (>=0.6)"] +dev = ["blake3 (>=0.3.1)", "mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-benchmark (==4.0.0)", "pytest-cov (==4.0.0)", "pytest-mock (==3.10.0)", "pytest-servers[s3] (==0.1.3)", "pytest-sugar (==0.9.6)", "rich (>=10.11.0,<14.0.0)", "typer[all] (>=0.6)"] +tests = ["mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-benchmark (==4.0.0)", "pytest-cov (==4.0.0)", "pytest-mock (==3.10.0)", "pytest-servers[s3] (==0.1.3)", "pytest-sugar (==0.9.6)"] + +[[package]] +name = "dvc-http" +version = "2.30.2" +description = "http plugin for dvc" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dvc-http-2.30.2.tar.gz", hash = "sha256:d7cf66e8f8359cc9f5ca137de24d259beebdec444516fc7d085ad26fa7d3b34b"}, + {file = "dvc_http-2.30.2-py3-none-any.whl", hash = "sha256:e5e8c915af84e6e464a67053e22b75fef77c2eabb3b7f4355c2b968ca7dcf52b"}, +] + +[package.dependencies] +aiohttp-retry = ">=2.5.0" +fsspec = {version = "*", extras = ["http"]} + +[package.extras] +tests = ["dvc[testing]", "flaky (==3.7.0)", "mypy (==0.910)", "pylint (==2.15.9)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "pytest-mock (==3.6.1)", "pytest-xdist (==2.4.0)", "rangehttpserver (==1.2.0)", "types-requests (==2.25.11)"] + +[[package]] +name = "dvc-objects" +version = "0.25.0" +description = "dvc objects" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dvc-objects-0.25.0.tar.gz", hash = "sha256:6e13add661ab7766cc26493102c7981b5164351f0ca4ee33d080d1651d4b5899"}, + {file = "dvc_objects-0.25.0-py3-none-any.whl", hash = "sha256:09f318cbb376750f4d2ef0afcde4ae41ca3f3071d6192bfee676812acd1f6d1f"}, +] + +[package.dependencies] +fsspec = ">=2022.10.0" +funcy = ">=1.14" +packaging = ">=19" +shortuuid = ">=0.5.0" +tqdm = ">=4.63.1,<5" +typing-extensions = ">=3.7.4" + +[package.extras] +dev = ["mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-mock (==3.8.2)", "pytest-servers[s3] (==0.1.3)", "pytest-sugar (==0.9.6)"] +tests = ["mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-mock (==3.8.2)", "pytest-servers[s3] (==0.1.3)", "pytest-sugar (==0.9.6)"] + +[[package]] +name = "dvc-render" +version = "0.6.0" +description = "DVC render" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dvc-render-0.6.0.tar.gz", hash = "sha256:69b7dfdadf890beb6d7fa5b3d4bd33323d78fc4c3ce33ed1bf777026192f9b4d"}, + {file = "dvc_render-0.6.0-py3-none-any.whl", hash = "sha256:2dc6c73d02538e9396475e146048e20242233d418967f82e0627e5caa3360303"}, +] + +[package.extras] +dev = ["flatten-dict (>=0.4.1,<1)", "funcy (>=1.17)", "matplotlib", "mkdocs (==1.5.2)", "mkdocs-gen-files (==0.5.0)", "mkdocs-material (==9.3.1)", "mkdocs-section-index (==0.3.6)", "mkdocstrings-python (==1.6.3)", "mypy (==0.981)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-cov (==3.0.0)", "pytest-mock (==3.8.2)", "pytest-sugar (>=0.9.6,<1.0)", "pytest-test-utils (>=0.0.6)", "tabulate (>=0.8.7)"] +docs = ["mkdocs (==1.5.2)", "mkdocs-gen-files (==0.5.0)", "mkdocs-material (==9.3.1)", "mkdocs-section-index (==0.3.6)", "mkdocstrings-python (==1.6.3)"] +markdown = ["flatten-dict (>=0.4.1,<1)", "matplotlib", "tabulate (>=0.8.7)"] +table = ["flatten-dict (>=0.4.1,<1)", "tabulate (>=0.8.7)"] +tests = ["flatten-dict (>=0.4.1,<1)", "funcy (>=1.17)", "matplotlib", "mypy (==0.981)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-cov (==3.0.0)", "pytest-mock (==3.8.2)", "pytest-sugar (>=0.9.6,<1.0)", "pytest-test-utils (>=0.0.6)", "tabulate (>=0.8.7)"] + +[[package]] +name = "dvc-studio-client" +version = "0.15.0" +description = "Small library to post data from DVC/DVCLive to Iterative Studio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dvc-studio-client-0.15.0.tar.gz", hash = "sha256:46dd508a0fb2c1c9986efd4111aa16ad3e40718c5e86a2be9f6e5ee509ff44a1"}, + {file = "dvc_studio_client-0.15.0-py3-none-any.whl", hash = "sha256:f51f36f9a86ea2bfcaed95b2ad6f532ed59a4d527c1febe079a938d79ff86796"}, +] + +[package.dependencies] +dulwich = "*" +requests = "*" +voluptuous = "*" + +[package.extras] +dev = ["mkdocs (==1.5.2)", "mkdocs-gen-files (==0.5.0)", "mkdocs-material (==9.2.2)", "mkdocs-section-index (==0.3.5)", "mkdocstrings-python (==1.5.0)", "pytest (==7.4.0)", "pytest-cov (==4.1.0)", "pytest-mock (==3.11.1)", "pytest-sugar (==0.9.7)"] +docs = ["mkdocs (==1.5.2)", "mkdocs-gen-files (==0.5.0)", "mkdocs-material (==9.2.2)", "mkdocs-section-index (==0.3.5)", "mkdocstrings-python (==1.5.0)"] +tests = ["pytest (==7.4.0)", "pytest-cov (==4.1.0)", "pytest-mock (==3.11.1)", "pytest-sugar (==0.9.7)"] + +[[package]] +name = "dvc-task" +version = "0.3.0" +description = "Extensible task queue used in DVC." +optional = false +python-versions = ">=3.8" +files = [ + {file = "dvc-task-0.3.0.tar.gz", hash = "sha256:6ab288bfbbc4a2df8ef145c543bb979d6cb8fb49037fec821a59ad6e1dfdddce"}, + {file = "dvc_task-0.3.0-py3-none-any.whl", hash = "sha256:637908e3a54670cb09924dd96161e025399c426fc3cb2e3b9b8a030d7cfcfbcd"}, +] + +[package.dependencies] +celery = ">=5.3.0,<6" +funcy = ">=1.17" +kombu = ">=5.3.0,<6" +pywin32 = {version = ">=225", markers = "sys_platform == \"win32\""} +shortuuid = ">=1.0.8" + +[package.extras] +dev = ["celery-types (==0.15.0)", "flaky (==3.7.0)", "mkdocs (==1.3.1)", "mkdocs-gen-files (==0.3.5)", "mkdocs-material (==8.4.1)", "mkdocs-section-index (==0.3.4)", "mkdocstrings-python (==0.7.1)", "mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-celery", "pytest-cov (==3.0.0)", "pytest-mock (==3.8.2)", "pytest-sugar (==0.9.6)", "pytest-test-utils (>=0.0.6)"] +docs = ["mkdocs (==1.3.1)", "mkdocs-gen-files (==0.3.5)", "mkdocs-material (==8.4.1)", "mkdocs-section-index (==0.3.4)", "mkdocstrings-python (==0.7.1)"] +tests = ["celery-types (==0.15.0)", "flaky (==3.7.0)", "mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-celery", "pytest-cov (==3.0.0)", "pytest-mock (==3.8.2)", "pytest-sugar (==0.9.6)", "pytest-test-utils (>=0.0.6)"] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastrlock" +version = "0.8.2" +description = "Fast, re-entrant optimistic lock implemented in Cython" +optional = true +python-versions = "*" +files = [ + {file = "fastrlock-0.8.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:94e348c72a1fd1f8191f25ea056448e4f5a87b8fbf005b39d290dcb0581a48cd"}, + {file = "fastrlock-0.8.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5595903444c854b99c42122b87edfe8a37cd698a4eae32f4fd1d2a7b6c115d"}, + {file = "fastrlock-0.8.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4bbde174a0aff5f6eeba75cf8c4c5d2a316316bc21f03a0bddca0fc3659a6f3"}, + {file = "fastrlock-0.8.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7a2ccaf88ac0db153e84305d1ef0aa138cea82c6a88309066f6eaa3bc98636cd"}, + {file = "fastrlock-0.8.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:31a27a2edf482df72b91fe6c6438314d2c65290aa7becc55589d156c9b91f0da"}, + {file = "fastrlock-0.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:e9904b5b37c3e5bb4a245c56bc4b7e497da57ffb8528f4fc39af9dcb168ee2e1"}, + {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:43a241655e83e4603a152192cf022d5ca348c2f4e56dfb02e5c9c4c1a32f9cdb"}, + {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9121a894d74e65557e47e777060a495ab85f4b903e80dd73a3c940ba042920d7"}, + {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:11bbbbc526363955aeddb9eec4cee2a0012322b7b2f15b54f44454fcf4fd398a"}, + {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:27786c62a400e282756ae1b090bcd7cfa35f28270cff65a9e7b27a5327a32561"}, + {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:08315bde19d0c2e6b06593d5a418be3dc8f9b1ee721afa96867b9853fceb45cf"}, + {file = "fastrlock-0.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8b49b5743ede51e0bcf6805741f39f5e0e0fd6a172ba460cb39e3097ba803bb"}, + {file = "fastrlock-0.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b443e73a4dfc7b6e0800ea4c13567b9694358e86f53bb2612a51c9e727cac67b"}, + {file = "fastrlock-0.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:b3853ed4ce522598dc886160a7bab432a093051af85891fa2f5577c1dcac8ed6"}, + {file = "fastrlock-0.8.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:790fc19bccbd39426060047e53629f171a44745613bf360a045e9f9c8c4a2cea"}, + {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:dbdce852e6bb66e1b8c36679d482971d69d93acf1785657522e51b7de30c3356"}, + {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d47713ffe6d4a627fbf078be9836a95ac106b4a0543e3841572c91e292a5d885"}, + {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:ea96503b918fceaf40443182742b8964d47b65c5ebdea532893cb9479620000c"}, + {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c6bffa978793bea5e1b00e677062e53a62255439339591b70e209fa1552d5ee0"}, + {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75c07726c8b1a52147fd7987d6baaa318c5dced1416c3f25593e40f56e10755b"}, + {file = "fastrlock-0.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88f079335e9da631efa64486c8207564a7bcd0c00526bb9e842e9d5b7e50a6cc"}, + {file = "fastrlock-0.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4fb2e77ff04bc4beb71d63c8e064f052ce5a6ea1e001d528d4d7f4b37d736f2e"}, + {file = "fastrlock-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4c9083ea89ab236b06e9ef2263971db3b4b507195fc7d5eecab95828dcae325"}, + {file = "fastrlock-0.8.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:98195866d3a9949915935d40a88e4f1c166e82e378f622c88025f2938624a90a"}, + {file = "fastrlock-0.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ea9bf5f9fad2b0077e944a7813f91593a4f61adf8faf734a70aed3f2b3a40"}, + {file = "fastrlock-0.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc1bf0ac8a194313cf6e645e300a8a379674ceed8e0b1e910a2de3e3c28989e"}, + {file = "fastrlock-0.8.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a3dcc876050b8f5cbc0ee84ef1e7f0c1dfe7c148f10098828bc4403683c33f10"}, + {file = "fastrlock-0.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:685e656048b59d8dfde8c601f188ad53a4d719eb97080cafc8696cda6d75865e"}, + {file = "fastrlock-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:fb5363cf0fddd9b50525ddbf64a1e1b28ec4c6dfb28670a940cb1cf988a6786b"}, + {file = "fastrlock-0.8.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:a74f5a92fa6e51c4f3c69b29c4662088b97be12f40652a21109605a175c81824"}, + {file = "fastrlock-0.8.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccf39ad5702e33e4d335b48ef9d56e21619b529b7f7471b5211419f380329b62"}, + {file = "fastrlock-0.8.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:66f2662c640bb71a1016a031eea6eef9d25c2bcdf7ffd1d1ddc5a58f9a1ced04"}, + {file = "fastrlock-0.8.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:17734e2e5af4c07ddb0fb10bd484e062c22de3be6b67940b9cc6ec2f18fa61ba"}, + {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:ab91b0c36e95d42e1041a4907e3eefd06c482d53af3c7a77be7e214cc7cd4a63"}, + {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b32fdf874868326351a75b1e4c02f97e802147119ae44c52d3d9da193ec34f5b"}, + {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2074548a335fcf7d19ebb18d9208da9e33b06f745754466a7e001d2b1c58dd19"}, + {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fb04442b6d1e2b36c774919c6bcbe3339c61b337261d4bd57e27932589095af"}, + {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1fed2f4797ad68e9982038423018cf08bec5f4ce9fed63a94a790773ed6a795c"}, + {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e380ec4e6d8b26e389713995a43cb7fe56baea2d25fe073d4998c4821a026211"}, + {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:25945f962c7bd808415cfde3da624d4399d4ea71ed8918538375f16bceb79e1c"}, + {file = "fastrlock-0.8.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c1719ddc8218b01e82fb2e82e8451bd65076cb96d7bef4477194bbb4305a968"}, + {file = "fastrlock-0.8.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5460c5ee6ced6d61ec8cd2324ebbe793a4960c4ffa2131ffff480e3b61c99ec5"}, + {file = "fastrlock-0.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:33145acbad8317584cd64588131c7e1e286beef6280c0009b4544c91fce171d2"}, + {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:59344c1d46b7dec97d3f22f1cc930fafe8980b3c5bc9c9765c56738a5f1559e4"}, + {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2a1c354f13f22b737621d914f3b4a8434ae69d3027a775e94b3e671756112f9"}, + {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:cf81e0278b645004388873e0a1f9e3bc4c9ab8c18e377b14ed1a544be4b18c9a"}, + {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b15430b93d7eb3d56f6ff690d2ebecb79ed0e58248427717eba150a508d1cd7"}, + {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:067edb0a0805bf61e17a251d5046af59f6e9d2b8ad01222e0ef7a0b7937d5548"}, + {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb31fe390f03f7ae886dcc374f1099ec88526631a4cb891d399b68181f154ff0"}, + {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:643e1e65b4f5b284427e61a894d876d10459820e93aa1e724dfb415117be24e0"}, + {file = "fastrlock-0.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5dfb78dd600a12f23fc0c3ec58f81336229fdc74501ecf378d1ce5b3f2f313ea"}, + {file = "fastrlock-0.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8ca0fe21458457077e4cb2d81e1ebdb146a00b3e9e2db6180a773f7ea905032"}, + {file = "fastrlock-0.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d918dfe473291e8bfd8e13223ea5cb9b317bd9f50c280923776c377f7c64b428"}, + {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c393af77c659a38bffbca215c0bcc8629ba4299568308dd7e4ff65d62cabed39"}, + {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73426f5eb2ecc10626c67cf86bd0af9e00d53e80e5c67d5ce8e18376d6abfa09"}, + {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:320fd55bafee3eb069cfb5d6491f811a912758387ef2193840e2663e80e16f48"}, + {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c1c91a68926421f5ccbc82c85f83bd3ba593b121a46a1b9a554b3f0dd67a4bf"}, + {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ad1bc61c7f6b0e58106aaab034916b6cb041757f708b07fbcdd9d6e1ac629225"}, + {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87f4e01b042c84e6090dbc4fbe3415ddd69f6bc0130382323f9d3f1b8dd71b46"}, + {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d34546ad2e4a480b94b6797bcc5a322b3c705c4c74c3e4e545c4a3841c1b2d59"}, + {file = "fastrlock-0.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ebb32d776b61acd49f859a1d16b9e3d84e7b46d0d92aebd58acd54dc38e96664"}, + {file = "fastrlock-0.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30bdbe4662992348132d03996700e1cf910d141d629179b967b146a22942264e"}, + {file = "fastrlock-0.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:07ed3c7b3867c05a3d6be4ced200c7767000f3431b9be6da66972822dd86e8be"}, + {file = "fastrlock-0.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:ddf5d247f686aec853ddcc9a1234bfcc6f57b0a0670d2ad82fc25d8ae7e6a15f"}, + {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7269bb3fc15587b0c191eecd95831d771a7d80f0c48929e560806b038ff3066c"}, + {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adcb9e77aa132cc6c9de2ffe7cf880a20aa8cdba21d367d1da1a412f57bddd5d"}, + {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:a3b8b5d2935403f1b4b25ae324560e94b59593a38c0d2e7b6c9872126a9622ed"}, + {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2587cedbb36c7988e707d83f0f1175c1f882f362b5ebbee25d70218ea33d220d"}, + {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9af691a9861027181d4de07ed74f0aee12a9650ac60d0a07f4320bff84b5d95f"}, + {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99dd6652bd6f730beadf74ef769d38c6bbd8ee6d1c15c8d138ea680b0594387f"}, + {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4d63b6596368dab9e0cc66bf047e7182a56f33b34db141816a4f21f5bf958228"}, + {file = "fastrlock-0.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ff75c90663d6e8996610d435e71487daa853871ad1770dd83dc0f2fc4997241e"}, + {file = "fastrlock-0.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e27c3cd27fbd25e5223c5c992b300cd4ee8f0a75c6f222ce65838138d853712c"}, + {file = "fastrlock-0.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:dd961a32a7182c3891cdebca417fda67496d5d5de6ae636962254d22723bdf52"}, + {file = "fastrlock-0.8.2.tar.gz", hash = "sha256:644ec9215cf9c4df8028d8511379a15d9c1af3e16d80e47f1b6fdc6ba118356a"}, +] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "flatten-dict" +version = "0.4.2" +description = "A flexible utility for flattening and unflattening dict-like objects in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "flatten-dict-0.4.2.tar.gz", hash = "sha256:506a96b6e6f805b81ae46a0f9f31290beb5fa79ded9d80dbe1b7fa236ab43076"}, + {file = "flatten_dict-0.4.2-py2.py3-none-any.whl", hash = "sha256:7e245b20c4c718981212210eec4284a330c9f713e632e98765560e05421e48ad"}, +] + +[package.dependencies] +six = ">=1.12,<2.0" + +[[package]] +name = "flufl-lock" +version = "8.0.2" +description = "NFS-safe file locking with timeouts for POSIX and Windows" +optional = false +python-versions = ">=3.8" +files = [ + {file = "flufl_lock-8.0.2-py3-none-any.whl", hash = "sha256:ca33fb581122d651e4f24775bebed1e58cd1ea85a95a505881902ba050ed170b"}, + {file = "flufl_lock-8.0.2.tar.gz", hash = "sha256:61c7246b34d6e5544c8a1fa4dae396d10e16ceb23371a31db22e0a2993d01432"}, +] + +[package.dependencies] +atpublic = "*" +psutil = "*" + +[[package]] +name = "frozenlist" +version = "1.4.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62"}, + {file = "frozenlist-1.4.0-cp310-cp310-win32.whl", hash = "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0"}, + {file = "frozenlist-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb"}, + {file = "frozenlist-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431"}, + {file = "frozenlist-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8"}, + {file = "frozenlist-1.4.0-cp38-cp38-win32.whl", hash = "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc"}, + {file = "frozenlist-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3"}, + {file = "frozenlist-1.4.0-cp39-cp39-win32.whl", hash = "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f"}, + {file = "frozenlist-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167"}, + {file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"}, +] + +[[package]] +name = "fsspec" +version = "2022.11.0" +description = "File-system specification" +optional = false +python-versions = ">=3.7" +files = [ + {file = "fsspec-2022.11.0-py3-none-any.whl", hash = "sha256:d6e462003e3dcdcb8c7aa84c73a228f8227e72453cd22570e2363e8844edfe7b"}, + {file = "fsspec-2022.11.0.tar.gz", hash = "sha256:259d5fd5c8e756ff2ea72f42e7613c32667dc2049a4ac3d84364a7ca034acb8b"}, +] + +[package.dependencies] +aiohttp = {version = "<4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1", optional = true, markers = "extra == \"http\""} +requests = {version = "*", optional = true, markers = "extra == \"http\""} + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +entrypoints = ["importlib-metadata"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "requests"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + +[[package]] +name = "funcy" +version = "2.0" +description = "A fancy and practical functional tools" +optional = false +python-versions = "*" +files = [ + {file = "funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0"}, + {file = "funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb"}, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "gitdb" +version = "4.0.11" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, + {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.40" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, + {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] + +[[package]] +name = "grandalf" +version = "0.8" +description = "Graph and drawing algorithms framework" +optional = false +python-versions = "*" +files = [ + {file = "grandalf-0.8-py3-none-any.whl", hash = "sha256:793ca254442f4a79252ea9ff1ab998e852c1e071b863593e5383afee906b4185"}, + {file = "grandalf-0.8.tar.gz", hash = "sha256:2813f7aab87f0d20f334a3162ccfbcbf085977134a17a5b516940a93a77ea974"}, +] + +[package.dependencies] +pyparsing = "*" + +[package.extras] +full = ["numpy", "ply"] + +[[package]] +name = "griffe" +version = "0.30.1" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.7" +files = [ + {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"}, + {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"}, +] + +[package.dependencies] +cached-property = {version = "*", markers = "python_version < \"3.8\""} +colorama = ">=0.4" + +[[package]] +name = "huggingface-hub" +version = "0.16.4" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "huggingface_hub-0.16.4-py3-none-any.whl", hash = "sha256:0d3df29932f334fead024afc7cb4cc5149d955238b8b5e42dcf9740d6995a349"}, + {file = "huggingface_hub-0.16.4.tar.gz", hash = "sha256:608c7d4f3d368b326d1747f91523dbd1f692871e8e2e7a4750314a2dd8b63e14"}, +] + +[package.dependencies] +filelock = "*" +fsspec = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "black (>=23.1,<24.0)", "gradio", "jedi", "mypy (==0.982)", "numpy", "pydantic", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-vcr", "pytest-xdist", "ruff (>=0.0.241)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "black (>=23.1,<24.0)", "gradio", "jedi", "mypy (==0.982)", "numpy", "pydantic", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-vcr", "pytest-xdist", "ruff (>=0.0.241)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +inference = ["aiohttp", "pydantic"] +quality = ["black (>=23.1,<24.0)", "mypy (==0.982)", "ruff (>=0.0.241)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["torch"] +typing = ["pydantic", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"] + +[[package]] +name = "hydra-core" +version = "1.3.2" +description = "A framework for elegantly configuring complex applications" +optional = false +python-versions = "*" +files = [ + {file = "hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824"}, + {file = "hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b"}, +] + +[package.dependencies] +antlr4-python3-runtime = "==4.9.*" +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} +omegaconf = ">=2.2,<2.4" +packaging = "*" + +[[package]] +name = "identify" +version = "2.5.24" +description = "File identification library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "importlib-resources" +version = "6.1.1" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, + {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "iterative-telemetry" +version = "0.0.8" +description = "Common library for sending telemetry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "iterative-telemetry-0.0.8.tar.gz", hash = "sha256:5bed9d19109c892cff2a4712a2fb18ad727079a7ab260a28b1e2f6934eec652d"}, + {file = "iterative_telemetry-0.0.8-py3-none-any.whl", hash = "sha256:af0a37ec727c1fd728df6e8103e4c89557b99869218e668dce5ca99e6e51231f"}, +] + +[package.dependencies] +appdirs = "*" +distro = "*" +filelock = "*" +requests = "*" + +[package.extras] +dev = ["mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-cov (==3.0.0)", "pytest-mock (==3.8.2)", "pytest-sugar (==0.9.5)", "types-requests"] +tests = ["mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-cov (==3.0.0)", "pytest-mock (==3.8.2)", "pytest-sugar (==0.9.5)", "types-requests"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "kombu" +version = "5.3.3" +description = "Messaging library for Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "kombu-5.3.3-py3-none-any.whl", hash = "sha256:6cd5c5d5ef77538434b8f81f3e265c414269418645dbb47dbf130a8a05c3e357"}, + {file = "kombu-5.3.3.tar.gz", hash = "sha256:1491df826cfc5178c80f3e89dd6dfba68e484ef334db81070eb5cb8094b31167"}, +] + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +"backports.zoneinfo" = {version = ">=0.2.1", extras = ["tzdata"], markers = "python_version < \"3.9\""} +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} +vine = "*" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] +consul = ["python-consul2"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +mongodb = ["pymongo (>=4.1.1)"] +msgpack = ["msgpack"] +pyro = ["pyro4"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + +[[package]] +name = "langcodes" +version = "3.3.0" +description = "Tools for labeling human languages with IETF language tags" +optional = false +python-versions = ">=3.6" +files = [ + {file = "langcodes-3.3.0-py3-none-any.whl", hash = "sha256:4d89fc9acb6e9c8fdef70bcdf376113a3db09b67285d9e1d534de6d8818e7e69"}, + {file = "langcodes-3.3.0.tar.gz", hash = "sha256:794d07d5a28781231ac335a1561b8442f8648ca07cd518310aeb45d6f0807ef6"}, +] + +[package.extras] +data = ["language-data (>=1.1,<2.0)"] + +[[package]] +name = "latexcodec" +version = "2.0.1" +description = "A lexer and codec to work with LaTeX code in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "latexcodec-2.0.1-py2.py3-none-any.whl", hash = "sha256:c277a193638dc7683c4c30f6684e3db728a06efb0dc9cf346db8bd0aa6c5d271"}, + {file = "latexcodec-2.0.1.tar.gz", hash = "sha256:2aa2551c373261cefe2ad3a8953a6d6533e68238d180eb4bb91d7964adb3fe9a"}, +] + +[package.dependencies] +six = ">=1.4.1" + +[[package]] +name = "loguru" +version = "0.6.0" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, + {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] + +[[package]] +name = "markdown" +version = "3.4.4" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, + {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mike" +version = "1.1.2" +description = "Manage multiple versions of your MkDocs-powered documentation" +optional = false +python-versions = "*" +files = [ + {file = "mike-1.1.2-py3-none-any.whl", hash = "sha256:4c307c28769834d78df10f834f57f810f04ca27d248f80a75f49c6fa2d1527ca"}, + {file = "mike-1.1.2.tar.gz", hash = "sha256:56c3f1794c2d0b5fdccfa9b9487beb013ca813de2e3ad0744724e9d34d40b77b"}, +] + +[package.dependencies] +jinja2 = "*" +mkdocs = ">=1.0" +pyyaml = ">=5.1" +verspec = "*" + +[package.extras] +dev = ["coverage", "flake8 (>=3.0)", "shtab"] +test = ["coverage", "flake8 (>=3.0)", "shtab"] + +[[package]] +name = "mkdocs" +version = "1.5.3" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "0.4.1" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, + {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, +] + +[package.dependencies] +Markdown = ">=3.3" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-bibtex" +version = "2.11.0" +description = "An MkDocs plugin that enables managing citations with BibTex" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mkdocs-bibtex-2.11.0.tar.gz", hash = "sha256:9ed78e1e7cfc8cd6f3f5ca75641dbcea8a011c36dbefcde041e36f8e6d0ed10f"}, +] + +[package.dependencies] +mkdocs = ">=1" +pybtex = ">=0.22" +pypandoc = ">=1.5" +requests = ">=2.8.1" +validators = ">=0.19.0" + +[[package]] +name = "mkdocs-gen-files" +version = "0.3.5" +description = "MkDocs plugin to programmatically generate documentation pages during the build" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "mkdocs-gen-files-0.3.5.tar.gz", hash = "sha256:d90d9e1676531a0bb96b1287dc28aa41162986de4dc3c00400214724761ff6ef"}, + {file = "mkdocs_gen_files-0.3.5-py3-none-any.whl", hash = "sha256:69562fddc662482e8f54a00a8b4ede5166ad5384ae4dbb0469f1f338ef3285ca"}, +] + +[package.dependencies] +mkdocs = ">=1.0.3,<2.0.0" + +[[package]] +name = "mkdocs-glightbox" +version = "0.1.7" +description = "MkDocs plugin supports image lightbox with GLightbox." +optional = false +python-versions = "*" +files = [ + {file = "mkdocs-glightbox-0.1.7.tar.gz", hash = "sha256:7e86e107349eacfd17a4f25673482bd2f033b21c272beaffae430b2c9c5aead6"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.11.1" + +[[package]] +name = "mkdocs-literate-nav" +version = "0.4.1" +description = "MkDocs plugin to specify the navigation in Markdown instead of YAML" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "mkdocs-literate-nav-0.4.1.tar.gz", hash = "sha256:9efe26b662f2f901cae5807bfd51446d30ea7e033c2bc43a15d6282c7dfac1ab"}, + {file = "mkdocs_literate_nav-0.4.1-py3-none-any.whl", hash = "sha256:a4b761792ba21defbe2dfd5e0de6ba451639e1ca0f0661c37eda83cc6261e4f9"}, +] + +[package.dependencies] +mkdocs = ">=1.0.3,<2.0.0" + +[[package]] +name = "mkdocs-material" +version = "8.5.11" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs_material-8.5.11-py3-none-any.whl", hash = "sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"}, + {file = "mkdocs_material-8.5.11.tar.gz", hash = "sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7"}, +] + +[package.dependencies] +jinja2 = ">=3.0.2" +markdown = ">=3.2" +mkdocs = ">=1.4.0" +mkdocs-material-extensions = ">=1.1" +pygments = ">=2.12" +pymdown-extensions = ">=9.4" +requests = ">=2.26" + +[[package]] +name = "mkdocs-material-extensions" +version = "1.2" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs_material_extensions-1.2-py3-none-any.whl", hash = "sha256:c767bd6d6305f6420a50f0b541b0c9966d52068839af97029be14443849fb8a1"}, + {file = "mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.18.1" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocstrings-0.18.1-py3-none-any.whl", hash = "sha256:4053929356df8cd69ed32eef71d8f676a472ef72980c9ffd4f933ead1debcdad"}, + {file = "mkdocstrings-0.18.1.tar.gz", hash = "sha256:fb7c91ce7e3ab70488d3fa6c073a4f827cdc319042f682ef8ea95459790d64fc"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.2" +mkdocs-autorefs = ">=0.3.1" +mkdocstrings-python-legacy = ">=0.2" +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "0.6.6" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocstrings-python-0.6.6.tar.gz", hash = "sha256:37281696b9f199624ae420e0625b6659b7fdfbea736618bce7fd978682dea3b1"}, + {file = "mkdocstrings_python-0.6.6-py3-none-any.whl", hash = "sha256:c118438d3cb4b14c492a51d109f4e5b27ab06ba19b099d624430dfd904926152"}, +] + +[package.dependencies] +griffe = ">=0.11.1" +mkdocstrings = ">=0.18" + +[[package]] +name = "mkdocstrings-python-legacy" +version = "0.2.2" +description = "A legacy Python handler for mkdocstrings." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocstrings-python-legacy-0.2.2.tar.gz", hash = "sha256:f0e7ec6a19750581b752acb38f6b32fcd1efe006f14f6703125d2c2c9a5c6f02"}, + {file = "mkdocstrings_python_legacy-0.2.2-py3-none-any.whl", hash = "sha256:379107a3a5b8db9b462efc4493c122efe21e825e3702425dbd404621302a563a"}, +] + +[package.dependencies] +mkdocstrings = ">=0.18" +pytkdocs = ">=0.14" + +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + +[[package]] +name = "murmurhash" +version = "1.0.10" +description = "Cython bindings for MurmurHash" +optional = false +python-versions = ">=3.6" +files = [ + {file = "murmurhash-1.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e90eef568adca5e17a91f96975e9a782ace3a617bbb3f8c8c2d917096e9bfeb"}, + {file = "murmurhash-1.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f8ecb00cc1ab57e4b065f9fb3ea923b55160c402d959c69a0b6dbbe8bc73efc3"}, + {file = "murmurhash-1.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3310101004d9e2e0530c2fed30174448d998ffd1b50dcbfb7677e95db101aa4b"}, + {file = "murmurhash-1.0.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65401a6f1778676253cbf89c1f45a8a7feb7d73038e483925df7d5943c08ed9"}, + {file = "murmurhash-1.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:f23f2dfc7174de2cdc5007c0771ab8376a2a3f48247f32cac4a5563e40c6adcc"}, + {file = "murmurhash-1.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90ed37ee2cace9381b83d56068334f77e3e30bc521169a1f886a2a2800e965d6"}, + {file = "murmurhash-1.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22e9926fdbec9d24ced9b0a42f0fee68c730438be3cfb00c2499fd495caec226"}, + {file = "murmurhash-1.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54bfbfd68baa99717239b8844600db627f336a08b1caf4df89762999f681cdd1"}, + {file = "murmurhash-1.0.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b9d200a09d48ef67f6840b77c14f151f2b6c48fd69661eb75c7276ebdb146c"}, + {file = "murmurhash-1.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:e5d7cfe392c0a28129226271008e61e77bf307afc24abf34f386771daa7b28b0"}, + {file = "murmurhash-1.0.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:96f0a070344d4802ea76a160e0d4c88b7dc10454d2426f48814482ba60b38b9e"}, + {file = "murmurhash-1.0.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9f61862060d677c84556610ac0300a0776cb13cb3155f5075ed97e80f86e55d9"}, + {file = "murmurhash-1.0.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3b6d2d877d8881a08be66d906856d05944be0faf22b9a0390338bcf45299989"}, + {file = "murmurhash-1.0.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f54b0031d8696fed17ed6e9628f339cdea0ba2367ca051e18ff59193f52687"}, + {file = "murmurhash-1.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:97e09d675de2359e586f09de1d0de1ab39f9911edffc65c9255fb5e04f7c1f85"}, + {file = "murmurhash-1.0.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b64e5332932993fef598e78d633b1ba664789ab73032ed511f3dc615a631a1a"}, + {file = "murmurhash-1.0.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2a38437a8497e082408aa015c6d90554b9e00c2c221fdfa79728a2d99a739e"}, + {file = "murmurhash-1.0.10-cp36-cp36m-win_amd64.whl", hash = "sha256:55f4e4f9291a53c36070330950b472d72ba7d331e4ce3ce1ab349a4f458f7bc4"}, + {file = "murmurhash-1.0.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:16ef9f0855952493fe08929d23865425906a8c0c40607ac8a949a378652ba6a9"}, + {file = "murmurhash-1.0.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cc3351ae92b89c2fcdc6e41ac6f17176dbd9b3554c96109fd0713695d8663e7"}, + {file = "murmurhash-1.0.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6559fef7c2e7349a42a63549067709b656d6d1580752bd76be1541d8b2d65718"}, + {file = "murmurhash-1.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:8bf49e3bb33febb7057ae3a5d284ef81243a1e55eaa62bdcd79007cddbdc0461"}, + {file = "murmurhash-1.0.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f1605fde07030516eb63d77a598dd164fb9bf217fd937dbac588fe7e47a28c40"}, + {file = "murmurhash-1.0.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4904f7e68674a64eb2b08823c72015a5e14653e0b4b109ea00c652a005a59bad"}, + {file = "murmurhash-1.0.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0438f0cb44cf1cd26251f72c1428213c4197d40a4e3f48b1efc3aea12ce18517"}, + {file = "murmurhash-1.0.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db1171a3f9a10571931764cdbfaa5371f4cf5c23c680639762125cb075b833a5"}, + {file = "murmurhash-1.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:1c9fbcd7646ad8ba67b895f71d361d232c6765754370ecea473dd97d77afe99f"}, + {file = "murmurhash-1.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7024ab3498434f22f8e642ae31448322ad8228c65c8d9e5dc2d563d57c14c9b8"}, + {file = "murmurhash-1.0.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a99dedfb7f0cc5a4cd76eb409ee98d3d50eba024f934e705914f6f4d765aef2c"}, + {file = "murmurhash-1.0.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b580b8503647de5dd7972746b7613ea586270f17ac92a44872a9b1b52c36d68"}, + {file = "murmurhash-1.0.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75840212bf75eb1352c946c3cf1622dacddd6d6bdda34368237d1eb3568f23a"}, + {file = "murmurhash-1.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:a4209962b9f85de397c3203ea4b3a554da01ae9fd220fdab38757d4e9eba8d1a"}, + {file = "murmurhash-1.0.10.tar.gz", hash = "sha256:5282aab1317804c6ebd6dd7f69f15ba9075aee671c44a34be2bde0f1b11ef88a"}, +] + +[[package]] +name = "mypy" +version = "0.950" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mypy-0.950-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b"}, + {file = "mypy-0.950-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0"}, + {file = "mypy-0.950-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22"}, + {file = "mypy-0.950-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb"}, + {file = "mypy-0.950-cp310-cp310-win_amd64.whl", hash = "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334"}, + {file = "mypy-0.950-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f"}, + {file = "mypy-0.950-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc"}, + {file = "mypy-0.950-cp36-cp36m-win_amd64.whl", hash = "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2"}, + {file = "mypy-0.950-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed"}, + {file = "mypy-0.950-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075"}, + {file = "mypy-0.950-cp37-cp37m-win_amd64.whl", hash = "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"}, + {file = "mypy-0.950-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d"}, + {file = "mypy-0.950-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a"}, + {file = "mypy-0.950-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605"}, + {file = "mypy-0.950-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2"}, + {file = "mypy-0.950-cp38-cp38-win_amd64.whl", hash = "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff"}, + {file = "mypy-0.950-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8"}, + {file = "mypy-0.950-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038"}, + {file = "mypy-0.950-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2"}, + {file = "mypy-0.950-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519"}, + {file = "mypy-0.950-cp39-cp39-win_amd64.whl", hash = "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef"}, + {file = "mypy-0.950-py3-none-any.whl", hash = "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb"}, + {file = "mypy-0.950.tar.gz", hash = "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de"}, +] + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nanotime" +version = "0.5.2" +description = "nanotime python implementation" +optional = false +python-versions = "*" +files = [ + {file = "nanotime-0.5.2.tar.gz", hash = "sha256:c7cc231fc5f6db401b448d7ab51c96d0a4733f4b69fabe569a576f89ffdf966b"}, +] + +[[package]] +name = "networkx" +version = "3.1" +description = "Python package for creating and manipulating graphs and networks" +optional = false +python-versions = ">=3.8" +files = [ + {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"}, + {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"}, +] + +[package.extras] +default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] +developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"] +doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] +test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "numpy" +version = "1.21.1" +description = "NumPy is the fundamental package for array computing with Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, + {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, + {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, + {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, + {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, + {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, + {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, + {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, + {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +description = "A flexible configuration library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b"}, + {file = "omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7"}, +] + +[package.dependencies] +antlr4-python3-runtime = "==4.9.*" +PyYAML = ">=5.1.0" + +[[package]] +name = "orjson" +version = "3.9.10" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.9.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c18a4da2f50050a03d1da5317388ef84a16013302a5281d6f64e4a3f406aabc4"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5148bab4d71f58948c7c39d12b14a9005b6ab35a0bdf317a8ade9a9e4d9d0bd5"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cf7837c3b11a2dfb589f8530b3cff2bd0307ace4c301e8997e95c7468c1378e"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c62b6fa2961a1dcc51ebe88771be5319a93fd89bd247c9ddf732bc250507bc2b"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb3922a7a804755bbe6b5be9b312e746137a03600f488290318936c1a2d4dc"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1234dc92d011d3554d929b6cf058ac4a24d188d97be5e04355f1b9223e98bbe9"}, + {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:06ad5543217e0e46fd7ab7ea45d506c76f878b87b1b4e369006bdb01acc05a83"}, + {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fd72fab7bddce46c6826994ce1e7de145ae1e9e106ebb8eb9ce1393ca01444d"}, + {file = "orjson-3.9.10-cp310-none-win32.whl", hash = "sha256:b5b7d4a44cc0e6ff98da5d56cde794385bdd212a86563ac321ca64d7f80c80d1"}, + {file = "orjson-3.9.10-cp310-none-win_amd64.whl", hash = "sha256:61804231099214e2f84998316f3238c4c2c4aaec302df12b21a64d72e2a135c7"}, + {file = "orjson-3.9.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cff7570d492bcf4b64cc862a6e2fb77edd5e5748ad715f487628f102815165e9"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8bc367f725dfc5cabeed1ae079d00369900231fbb5a5280cf0736c30e2adf7"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c812312847867b6335cfb264772f2a7e85b3b502d3a6b0586aa35e1858528ab1"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edd2856611e5050004f4722922b7b1cd6268da34102667bd49d2a2b18bafb81"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:674eb520f02422546c40401f4efaf8207b5e29e420c17051cddf6c02783ff5ca"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0dc4310da8b5f6415949bd5ef937e60aeb0eb6b16f95041b5e43e6200821fb"}, + {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99c625b8c95d7741fe057585176b1b8783d46ed4b8932cf98ee145c4facf499"}, + {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec6f18f96b47299c11203edfbdc34e1b69085070d9a3d1f302810cc23ad36bf3"}, + {file = "orjson-3.9.10-cp311-none-win32.whl", hash = "sha256:ce0a29c28dfb8eccd0f16219360530bc3cfdf6bf70ca384dacd36e6c650ef8e8"}, + {file = "orjson-3.9.10-cp311-none-win_amd64.whl", hash = "sha256:cf80b550092cc480a0cbd0750e8189247ff45457e5a023305f7ef1bcec811616"}, + {file = "orjson-3.9.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:602a8001bdf60e1a7d544be29c82560a7b49319a0b31d62586548835bbe2c862"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f295efcd47b6124b01255d1491f9e46f17ef40d3d7eabf7364099e463fb45f0f"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92af0d00091e744587221e79f68d617b432425a7e59328ca4c496f774a356071"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5a02360e73e7208a872bf65a7554c9f15df5fe063dc047f79738998b0506a14"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858379cbb08d84fe7583231077d9a36a1a20eb72f8c9076a45df8b083724ad1d"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666c6fdcaac1f13eb982b649e1c311c08d7097cbda24f32612dae43648d8db8d"}, + {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3fb205ab52a2e30354640780ce4587157a9563a68c9beaf52153e1cea9aa0921"}, + {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7ec960b1b942ee3c69323b8721df2a3ce28ff40e7ca47873ae35bfafeb4555ca"}, + {file = "orjson-3.9.10-cp312-none-win_amd64.whl", hash = "sha256:3e892621434392199efb54e69edfff9f699f6cc36dd9553c5bf796058b14b20d"}, + {file = "orjson-3.9.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8b9ba0ccd5a7f4219e67fbbe25e6b4a46ceef783c42af7dbc1da548eb28b6531"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e2ecd1d349e62e3960695214f40939bbfdcaeaaa62ccc638f8e651cf0970e5f"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f433be3b3f4c66016d5a20e5b4444ef833a1f802ced13a2d852c637f69729c1"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4689270c35d4bb3102e103ac43c3f0b76b169760aff8bcf2d401a3e0e58cdb7f"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd176f528a8151a6efc5359b853ba3cc0e82d4cd1fab9c1300c5d957dc8f48c"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a2ce5ea4f71681623f04e2b7dadede3c7435dfb5e5e2d1d0ec25b35530e277b"}, + {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:49f8ad582da6e8d2cf663c4ba5bf9f83cc052570a3a767487fec6af839b0e777"}, + {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a11b4b1a8415f105d989876a19b173f6cdc89ca13855ccc67c18efbd7cbd1f8"}, + {file = "orjson-3.9.10-cp38-none-win32.whl", hash = "sha256:a353bf1f565ed27ba71a419b2cd3db9d6151da426b61b289b6ba1422a702e643"}, + {file = "orjson-3.9.10-cp38-none-win_amd64.whl", hash = "sha256:e28a50b5be854e18d54f75ef1bb13e1abf4bc650ab9d635e4258c58e71eb6ad5"}, + {file = "orjson-3.9.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ee5926746232f627a3be1cc175b2cfad24d0170d520361f4ce3fa2fd83f09e1d"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a73160e823151f33cdc05fe2cea557c5ef12fdf276ce29bb4f1c571c8368a60"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c338ed69ad0b8f8f8920c13f529889fe0771abbb46550013e3c3d01e5174deef"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5869e8e130e99687d9e4be835116c4ebd83ca92e52e55810962446d841aba8de"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2c1e559d96a7f94a4f581e2a32d6d610df5840881a8cba8f25e446f4d792df3"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a3a3a72c9811b56adf8bcc829b010163bb2fc308877e50e9910c9357e78521"}, + {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f8fb7f5ecf4f6355683ac6881fd64b5bb2b8a60e3ccde6ff799e48791d8f864"}, + {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c943b35ecdf7123b2d81d225397efddf0bce2e81db2f3ae633ead38e85cd5ade"}, + {file = "orjson-3.9.10-cp39-none-win32.whl", hash = "sha256:fb0b361d73f6b8eeceba47cd37070b5e6c9de5beaeaa63a1cb35c7e1a73ef088"}, + {file = "orjson-3.9.10-cp39-none-win_amd64.whl", hash = "sha256:b90f340cb6397ec7a854157fac03f0c82b744abdd1c0941a024c3c29d1340aff"}, + {file = "orjson-3.9.10.tar.gz", hash = "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "3.1.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"}, + {file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "preshed" +version = "3.0.9" +description = "Cython hash table that trusts the keys are pre-hashed" +optional = false +python-versions = ">=3.6" +files = [ + {file = "preshed-3.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f96ef4caf9847b2bb9868574dcbe2496f974e41c2b83d6621c24fb4c3fc57e3"}, + {file = "preshed-3.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a61302cf8bd30568631adcdaf9e6b21d40491bd89ba8ebf67324f98b6c2a2c05"}, + {file = "preshed-3.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99499e8a58f58949d3f591295a97bca4e197066049c96f5d34944dd21a497193"}, + {file = "preshed-3.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea6b6566997dc3acd8c6ee11a89539ac85c77275b4dcefb2dc746d11053a5af8"}, + {file = "preshed-3.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:bfd523085a84b1338ff18f61538e1cfcdedc4b9e76002589a301c364d19a2e36"}, + {file = "preshed-3.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7c2364da27f2875524ce1ca754dc071515a9ad26eb5def4c7e69129a13c9a59"}, + {file = "preshed-3.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182138033c0730c683a6d97e567ceb8a3e83f3bff5704f300d582238dbd384b3"}, + {file = "preshed-3.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:345a10be3b86bcc6c0591d343a6dc2bfd86aa6838c30ced4256dfcfa836c3a64"}, + {file = "preshed-3.0.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51d0192274aa061699b284f9fd08416065348edbafd64840c3889617ee1609de"}, + {file = "preshed-3.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:96b857d7a62cbccc3845ac8c41fd23addf052821be4eb987f2eb0da3d8745aa1"}, + {file = "preshed-3.0.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4fe6720012c62e6d550d6a5c1c7ad88cacef8388d186dad4bafea4140d9d198"}, + {file = "preshed-3.0.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e04f05758875be9751e483bd3c519c22b00d3b07f5a64441ec328bb9e3c03700"}, + {file = "preshed-3.0.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a55091d0e395f1fdb62ab43401bb9f8b46c7d7794d5b071813c29dc1ab22fd0"}, + {file = "preshed-3.0.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7de8f5138bcac7870424e09684dc3dd33c8e30e81b269f6c9ede3d8c7bb8e257"}, + {file = "preshed-3.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:24229c77364628743bc29c5620c5d6607ed104f0e02ae31f8a030f99a78a5ceb"}, + {file = "preshed-3.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73b0f7ecc58095ebbc6ca26ec806008ef780190fe685ce471b550e7eef58dc2"}, + {file = "preshed-3.0.9-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cb90ecd5bec71c21d95962db1a7922364d6db2abe284a8c4b196df8bbcc871e"}, + {file = "preshed-3.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:e304a0a8c9d625b70ba850c59d4e67082a6be9c16c4517b97850a17a282ebee6"}, + {file = "preshed-3.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1fa6d3d5529b08296ff9b7b4da1485c080311fd8744bbf3a86019ff88007b382"}, + {file = "preshed-3.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1e5173809d85edd420fc79563b286b88b4049746b797845ba672cf9435c0e7"}, + {file = "preshed-3.0.9-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fe81eb21c7d99e8b9a802cc313b998c5f791bda592903c732b607f78a6b7dc4"}, + {file = "preshed-3.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:78590a4a952747c3766e605ce8b747741005bdb1a5aa691a18aae67b09ece0e6"}, + {file = "preshed-3.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3452b64d97ce630e200c415073040aa494ceec6b7038f7a2a3400cbd7858e952"}, + {file = "preshed-3.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ac970d97b905e9e817ec13d31befd5b07c9cfec046de73b551d11a6375834b79"}, + {file = "preshed-3.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eebaa96ece6641cd981491cba995b68c249e0b6877c84af74971eacf8990aa19"}, + {file = "preshed-3.0.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d473c5f6856e07a88d41fe00bb6c206ecf7b34c381d30de0b818ba2ebaf9406"}, + {file = "preshed-3.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:0de63a560f10107a3f0a9e252cc3183b8fdedcb5f81a86938fd9f1dcf8a64adf"}, + {file = "preshed-3.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3a9ad9f738084e048a7c94c90f40f727217387115b2c9a95c77f0ce943879fcd"}, + {file = "preshed-3.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a671dfa30b67baa09391faf90408b69c8a9a7f81cb9d83d16c39a182355fbfce"}, + {file = "preshed-3.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23906d114fc97c17c5f8433342495d7562e96ecfd871289c2bb2ed9a9df57c3f"}, + {file = "preshed-3.0.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:778cf71f82cedd2719b256f3980d556d6fb56ec552334ba79b49d16e26e854a0"}, + {file = "preshed-3.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:a6e579439b329eb93f32219ff27cb358b55fbb52a4862c31a915a098c8a22ac2"}, + {file = "preshed-3.0.9.tar.gz", hash = "sha256:721863c5244ffcd2651ad0928951a2c7c77b102f4e11a251ad85d37ee7621660"}, +] + +[package.dependencies] +cymem = ">=2.0.2,<2.1.0" +murmurhash = ">=0.28.0,<1.1.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.41" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.41-py3-none-any.whl", hash = "sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"}, + {file = "prompt_toolkit-3.0.41.tar.gz", hash = "sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "5.9.6" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fb8a697f11b0f5994550555fcfe3e69799e5b060c8ecf9e2f75c69302cc35c0d"}, + {file = "psutil-5.9.6-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:91ecd2d9c00db9817a4b4192107cf6954addb5d9d67a969a4f436dbc9200f88c"}, + {file = "psutil-5.9.6-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:10e8c17b4f898d64b121149afb136c53ea8b68c7531155147867b7b1ac9e7e28"}, + {file = "psutil-5.9.6-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:18cd22c5db486f33998f37e2bb054cc62fd06646995285e02a51b1e08da97017"}, + {file = "psutil-5.9.6-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ca2780f5e038379e520281e4c032dddd086906ddff9ef0d1b9dcf00710e5071c"}, + {file = "psutil-5.9.6-cp27-none-win32.whl", hash = "sha256:70cb3beb98bc3fd5ac9ac617a327af7e7f826373ee64c80efd4eb2856e5051e9"}, + {file = "psutil-5.9.6-cp27-none-win_amd64.whl", hash = "sha256:51dc3d54607c73148f63732c727856f5febec1c7c336f8f41fcbd6315cce76ac"}, + {file = "psutil-5.9.6-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c69596f9fc2f8acd574a12d5f8b7b1ba3765a641ea5d60fb4736bf3c08a8214a"}, + {file = "psutil-5.9.6-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92e0cc43c524834af53e9d3369245e6cc3b130e78e26100d1f63cdb0abeb3d3c"}, + {file = "psutil-5.9.6-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:748c9dd2583ed86347ed65d0035f45fa8c851e8d90354c122ab72319b5f366f4"}, + {file = "psutil-5.9.6-cp36-cp36m-win32.whl", hash = "sha256:3ebf2158c16cc69db777e3c7decb3c0f43a7af94a60d72e87b2823aebac3d602"}, + {file = "psutil-5.9.6-cp36-cp36m-win_amd64.whl", hash = "sha256:ff18b8d1a784b810df0b0fff3bcb50ab941c3b8e2c8de5726f9c71c601c611aa"}, + {file = "psutil-5.9.6-cp37-abi3-win32.whl", hash = "sha256:a6f01f03bf1843280f4ad16f4bde26b817847b4c1a0db59bf6419807bc5ce05c"}, + {file = "psutil-5.9.6-cp37-abi3-win_amd64.whl", hash = "sha256:6e5fb8dc711a514da83098bc5234264e551ad980cec5f85dabf4d38ed6f15e9a"}, + {file = "psutil-5.9.6-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:daecbcbd29b289aac14ece28eca6a3e60aa361754cf6da3dfb20d4d32b6c7f57"}, + {file = "psutil-5.9.6.tar.gz", hash = "sha256:e4b92ddcd7dd4cdd3f900180ea1e104932c7bce234fb88976e2a3b296441225a"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "py4j" +version = "0.10.7" +description = "Enables Python programs to dynamically access arbitrary Java objects" +optional = false +python-versions = "*" +files = [ + {file = "py4j-0.10.7-py2.py3-none-any.whl", hash = "sha256:a950fe7de1bfd247a0a4dddb9118f332d22a89e01e0699135ea8038c15ee1293"}, + {file = "py4j-0.10.7.zip", hash = "sha256:721189616b3a7d28212dfb2e7c6a1dd5147b03105f1fc37ff2432acd0e863fa5"}, +] + +[[package]] +name = "pybtex" +version = "0.24.0" +description = "A BibTeX-compatible bibliography processor in Python" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" +files = [ + {file = "pybtex-0.24.0-py2.py3-none-any.whl", hash = "sha256:e1e0c8c69998452fea90e9179aa2a98ab103f3eed894405b7264e517cc2fcc0f"}, + {file = "pybtex-0.24.0.tar.gz", hash = "sha256:818eae35b61733e5c007c3fcd2cfb75ed1bc8b4173c1f70b56cc4c0802d34755"}, +] + +[package.dependencies] +latexcodec = ">=1.0.4" +PyYAML = ">=3.01" +six = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "1.10.13" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, + {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, + {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, + {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, + {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, + {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, + {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, + {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, + {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pydot" +version = "1.4.2" +description = "Python interface to Graphviz's Dot" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pydot-1.4.2-py2.py3-none-any.whl", hash = "sha256:66c98190c65b8d2e2382a441b4c0edfdb4f4c025ef9cb9874de478fb0793a451"}, + {file = "pydot-1.4.2.tar.gz", hash = "sha256:248081a39bcb56784deb018977e428605c1c758f10897a339fce1dd728ff007d"}, +] + +[package.dependencies] +pyparsing = ">=2.1.4" + +[[package]] +name = "pygit2" +version = "1.13.2" +description = "Python bindings for libgit2." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygit2-1.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:781aefab7efc464852e688965bf3b4acc7af951cebea174d69f86b213aa5d5fb"}, + {file = "pygit2-1.13.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3038b5ecef43e2c853e7cf405676241e0395bb37b37ae477ef3b73a91f12378"}, + {file = "pygit2-1.13.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c00927a2626325b64ebc9f860f024a3ae0b4c036663f6ada8d5de0e2393560ca"}, + {file = "pygit2-1.13.2-cp310-cp310-win32.whl", hash = "sha256:6988fc6cf99a3dbc03bd64060888c3b194ee27c810cb61624519ee3813f2da3d"}, + {file = "pygit2-1.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:aec3df351b722ec7cdf7a7e642e421e3a15f3f2e3a51e57380d62d4992acf36d"}, + {file = "pygit2-1.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0eb53cc5212fad90e36693c0cd2ffd0d470efaea2506ce1c0d04f8d7fcf6767c"}, + {file = "pygit2-1.13.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32803ec881cd8f7dba91e03927e1fb13857e795bbe85cd3ec156b4798b933294"}, + {file = "pygit2-1.13.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba7297204e72c5cfdcd7a0c0d318af9d654a1d79b1cfe6cc8330570c749bec1f"}, + {file = "pygit2-1.13.2-cp311-cp311-win32.whl", hash = "sha256:2291707e648f5bba5b5c5e7ed652bc4563bd520718eb31e19525ccaceba5503c"}, + {file = "pygit2-1.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:96e534e92e485c4c1d4c3e151ce960655fed38ab9a1d65e2b16650cf24b3e088"}, + {file = "pygit2-1.13.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75f3b6d754d91dbe47b27b53d5a4440d861906b2f476284e6fb7c46cafe244d7"}, + {file = "pygit2-1.13.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30e145730dc65a9b902a889efdca0126d6b274c0b14427ebb085e090b50f6470"}, + {file = "pygit2-1.13.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2311ca16e1d0b77bc3694407c367391c7f2f78e2f725dc858721a5d4e3635fdd"}, + {file = "pygit2-1.13.2-cp312-cp312-win32.whl", hash = "sha256:a027e06c44f987a217c6197970bb29de9fbc78524c81b1f37888711978a64ce2"}, + {file = "pygit2-1.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:9844fb5a38119a34b31012dddc9b439f81bb0411cbf4a4f8e92a044f6f3e7462"}, + {file = "pygit2-1.13.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2f3a5362c702a42e28c3bc84ff324b57676c8bfdbfab445c96f5e776873630a6"}, + {file = "pygit2-1.13.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7d5d1c3508b66e5e13883ff472b616d2d60feb7a4afea52d3b501e9f5ee5d08"}, + {file = "pygit2-1.13.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2507d99584c7e3976342566adf6bc48aca825c170b86f999fe7bd32f8aa1858e"}, + {file = "pygit2-1.13.2-cp38-cp38-win32.whl", hash = "sha256:acda61b726c33ada3639cac5ddc5898678f7bb7b8415e84e3ff07a2af94b1ac3"}, + {file = "pygit2-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:4a86c4cae2e717acdd9d7ff00d196395fafe1abfc5efab5ada63650b49d5d47f"}, + {file = "pygit2-1.13.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ae9a77be5c5df5f4c9e586fbd53f1095bced2bba86ec669ead92c4c1e02f8373"}, + {file = "pygit2-1.13.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ef1dcfb59e73f6a59491343393b6e843739cbc92e8088a551c73cd367a54d0"}, + {file = "pygit2-1.13.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b88d21ed961fe422275c9c20d2613e6ecff2fa8127ac7620a29aba1d001fc41"}, + {file = "pygit2-1.13.2-cp39-cp39-win32.whl", hash = "sha256:14b458af1e8c6b634d55110edeab055e3bd9075543792cb75d2fdb8b434c202a"}, + {file = "pygit2-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:565b311c783a07768b91382620ad2b474fe40778411cb18c576f667be43d1299"}, + {file = "pygit2-1.13.2.tar.gz", hash = "sha256:75c7eb86b47c70f6f1434bcf3b5eb41f4e8006a15cee6bef606651b97d23788c"}, +] + +[package.dependencies] +cffi = ">=1.16.0" +setuptools = {version = "*", markers = "python_version >= \"3.12\""} + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pygtrie" +version = "2.5.0" +description = "A pure Python trie data structure implementation." +optional = false +python-versions = "*" +files = [ + {file = "pygtrie-2.5.0-py3-none-any.whl", hash = "sha256:8795cda8105493d5ae159a5bef313ff13156c5d4d72feddefacaad59f8c8ce16"}, + {file = "pygtrie-2.5.0.tar.gz", hash = "sha256:203514ad826eb403dab1d2e2ddd034e0d1534bbe4dbe0213bb0593f66beba4e2"}, +] + +[[package]] +name = "pymdown-extensions" +version = "10.2.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pymdown_extensions-10.2.1-py3-none-any.whl", hash = "sha256:bded105eb8d93f88f2f821f00108cb70cef1269db6a40128c09c5f48bfc60ea4"}, + {file = "pymdown_extensions-10.2.1.tar.gz", hash = "sha256:d0c534b4a5725a4be7ccef25d65a4c97dba58b54ad7c813babf0eb5ba9c81591"}, +] + +[package.dependencies] +markdown = ">=3.2" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pypandoc" +version = "1.12" +description = "Thin wrapper for pandoc." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pypandoc-1.12-py3-none-any.whl", hash = "sha256:efb4f7d68ead8bec32e22b62f02d5608a1700978b51bfc4af286fd6acfe9d218"}, + {file = "pypandoc-1.12.tar.gz", hash = "sha256:8f44740a9f074e121d81b489f073160421611d4ead62d1b306aeb11aab3c32df"}, +] + +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyspark" +version = "2.4.3" +description = "Apache Spark Python API" +optional = false +python-versions = "*" +files = [ + {file = "pyspark-2.4.3.tar.gz", hash = "sha256:6839718ce9f779e81153d8a14a843a5c4b2d5e6574f3c916aec241022d717cb2"}, +] + +[package.dependencies] +py4j = "0.10.7" + +[package.extras] +ml = ["numpy (>=1.7)"] +mllib = ["numpy (>=1.7)"] +sql = ["pandas (>=0.19.2)", "pyarrow (>=0.8.0)"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytkdocs" +version = "0.16.1" +description = "Load Python objects documentation." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytkdocs-0.16.1-py3-none-any.whl", hash = "sha256:a8c3f46ecef0b92864cc598e9101e9c4cf832ebbf228f50c84aa5dd850aac379"}, + {file = "pytkdocs-0.16.1.tar.gz", hash = "sha256:e2ccf6dfe9dbbceb09818673f040f1a7c32ed0bffb2d709b06be6453c4026045"}, +] + +[package.dependencies] +astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} +cached-property = {version = ">=1.5", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.7", markers = "python_version < \"3.8\""} + +[package.extras] +numpy-style = ["docstring_parser (>=0.7)"] + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2023.10.3" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, + {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, + {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, + {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, + {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, + {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, + {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, + {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, + {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, + {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, + {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, + {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, + {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, + {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, + {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, + {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, + {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruamel-yaml" +version = "0.18.5" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruamel.yaml-0.18.5-py3-none-any.whl", hash = "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada"}, + {file = "ruamel.yaml-0.18.5.tar.gz", hash = "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.6" +files = [ + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, + {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, + {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, +] + +[[package]] +name = "safetensors" +version = "0.4.0" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "safetensors-0.4.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:2289ae6dbe6d027ecee016b28ced13a2e21a0b3a3a757a23033a2d1c0b1bad55"}, + {file = "safetensors-0.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bf6458959f310f551cbbeef2255527ade5f783f952738e73e4d0136198cc3bfe"}, + {file = "safetensors-0.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6b60a58a8f7cc7aed3b5b73dce1f5259a53c83d9ba43a76a874e6ad868c1b4d"}, + {file = "safetensors-0.4.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:491b3477e4d0d4599bb75d79da4b75af2e6ed9b1f6ec2b715991f0bc927bf09a"}, + {file = "safetensors-0.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d2e10b7e0cd18bb73ed7c17c624a5957b003b81345e18159591771c26ee428"}, + {file = "safetensors-0.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f667a4c12fb593f5f66ce966cb1b14a7148898b2b1a7f79e0761040ae1e3c51"}, + {file = "safetensors-0.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9909512bcb6f712bdd04c296cdfb0d8ff73d258ffc5af884bb62ea02d221e0"}, + {file = "safetensors-0.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d33d29e846821f0e4f92614022949b09ccf063cb36fe2f9fe099cde1efbfbb87"}, + {file = "safetensors-0.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4d512525a8e05a045ce6698066ba0c5378c174a83e0b3720a8c7799dc1bb06f3"}, + {file = "safetensors-0.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0219cea445177f6ad1f9acd3a8d025440c8ff436d70a4a7c7ba9c36066aa9474"}, + {file = "safetensors-0.4.0-cp310-none-win32.whl", hash = "sha256:67ab171eeaad6972d3971c53d29d53353c67f6743284c6d637b59fa3e54c8a94"}, + {file = "safetensors-0.4.0-cp310-none-win_amd64.whl", hash = "sha256:7ffc736039f08a9ca1f09816a7481b8e4469c06e8f8a5ffa8cb67ddd79e6d77f"}, + {file = "safetensors-0.4.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4fe9e3737b30de458225a23926219ca30b902ee779b6a3df96eaab2b6d625ec2"}, + {file = "safetensors-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7916e814a90008de767b1c164a1d83803693c661ffe9af5a697b22e2752edb0"}, + {file = "safetensors-0.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbc4a4da01143472323c145f3c289e5f6fabde0ac0a3414dabf912a21692fff4"}, + {file = "safetensors-0.4.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a54c21654a47669b38e359e8f852af754b786c9da884bb61ad5e9af12bd71ccb"}, + {file = "safetensors-0.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25cd407955bad5340ba17f9f8ac789a0d751601a311e2f7b2733f9384478c95e"}, + {file = "safetensors-0.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82e8fc4e3503cd738fd40718a430fe0e5ce6e7ff91a73d6ce628bbb89c41e8ce"}, + {file = "safetensors-0.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48b92059b1a4ad163024d4f526e0e73ebe2bb3ae70537e15e347820b4de5dc27"}, + {file = "safetensors-0.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5daa05058f7dce85b5f9f60c4eab483ed7859d63978f08a76e52e78859ff20ca"}, + {file = "safetensors-0.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a86565a5c112dd855909e20144947b4f53abb78c4de207f36ca71ee63ba5b90d"}, + {file = "safetensors-0.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38032078ed9fea52d06584e441bccc73fb475c4581600c6d6166de2fe2deb3d1"}, + {file = "safetensors-0.4.0-cp311-none-win32.whl", hash = "sha256:2f99d90c91b7c76b40a862acd9085bc77f7974a27dee7cfcebe46149af5a99a1"}, + {file = "safetensors-0.4.0-cp311-none-win_amd64.whl", hash = "sha256:74e2a448ffe19be188b457b130168190ee73b5a75e45ba96796320c1f5ae35d2"}, + {file = "safetensors-0.4.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:1e2f9c69b41d03b4826ffb96b29e07444bb6b34a78a7bafd0b88d59e8ec75b8a"}, + {file = "safetensors-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3910fb5bf747413b59f1a34e6d2a993b589fa7d919709518823c70efaaa350bd"}, + {file = "safetensors-0.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8fdca709b2470a35a59b1e6dffea75cbe1214b22612b5dd4c93947697aea8b"}, + {file = "safetensors-0.4.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f27b8ef814c5fb43456caeb7f3cbb889b76115180aad1f42402839c14a47c5b"}, + {file = "safetensors-0.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b2d6101eccc43c7be0cb052f13ceda64288b3d8b344b988ed08d7133cbce2f3"}, + {file = "safetensors-0.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdc34027b545a69be3d4220c140b276129523e4e46db06ad1a0b60d6a4cf9214"}, + {file = "safetensors-0.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db7bb48ca9e90bb9526c71b388d38d8de160c0354f4c5126df23e8701a870dcb"}, + {file = "safetensors-0.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a78ffc0795d3595cd9e4d453502e35f764276c49e434b25556a15a337db4dafc"}, + {file = "safetensors-0.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8e735b0f79090f6855b55e205e820b7b595502ffca0009a5c13eef3661ce465b"}, + {file = "safetensors-0.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f8d2416734e850d5392afffbcb2b8985ea29fb171f1cb197e2ae51b8e35d6438"}, + {file = "safetensors-0.4.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:e853e189ba7d47eaf561094586692ba2bbdd258c096f1755805cac098de0e6ab"}, + {file = "safetensors-0.4.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:4b2aa57b5a4d576f3d1dd6e56980026340f156f8a13c13016bfac4e25295b53f"}, + {file = "safetensors-0.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b6c1316ffde6cb4bf22c7445bc9fd224b4d1b9dd7320695f5611c89e802e4b6"}, + {file = "safetensors-0.4.0-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:003077ec85261d00061058fa12e3c1d2055366b02ce8f2938929359ffbaff2b8"}, + {file = "safetensors-0.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd63d83a92f1437a8b0431779320376030ae43ace980bea5686d515de0784100"}, + {file = "safetensors-0.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2077801800b4b13301d8d6290c7fb5bd60737320001717153ebc4371776643b5"}, + {file = "safetensors-0.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7abe0e157a49a75aeeccfbc4f3dac38d8f98512d3cdb35c200f8e628dc5773cf"}, + {file = "safetensors-0.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bfed574f6b1e7e7fe1f17213278875ef6c6e8b1582ab6eda93947db1178cae6"}, + {file = "safetensors-0.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:964ef166a286ce3b023d0d0bd0e21d440a1c8028981c8abdb136bc7872ba9b3d"}, + {file = "safetensors-0.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:44f84373e42183bd56a13a1f2d8acb1db7fedaeffbd83e79cec861477eee1af4"}, + {file = "safetensors-0.4.0-cp37-none-win32.whl", hash = "sha256:c68132727dd86fb641102e494d445f705efe402f4d5e24b278183a15499ab400"}, + {file = "safetensors-0.4.0-cp37-none-win_amd64.whl", hash = "sha256:1db87155454c168aef118d5657a403aee48a4cb08d8851a981157f07351ea317"}, + {file = "safetensors-0.4.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:9e583fa68e5a07cc859c4e13c1ebff12029904aa2e27185cf04a1f57fe9a81c4"}, + {file = "safetensors-0.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73e7696dcf3f72f99545eb1abe6106ad65ff1f62381d6ce4b34be3272552897a"}, + {file = "safetensors-0.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4936096a57c62e84e200f92620a536be067fc5effe46ecc7f230ebb496ecd579"}, + {file = "safetensors-0.4.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87b328ee1591adac332543e1f5fc2c2d7f149b745ebb0d58d7850818ff9cee27"}, + {file = "safetensors-0.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b69554c143336256260eceff1d3c0969172a641b54d4668489a711b05f92a2c0"}, + {file = "safetensors-0.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ebf6bcece5d5d1bd6416472f94604d2c834ca752ac60ed42dba7157e595a990"}, + {file = "safetensors-0.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6686ce01b8602d55a7d9903c90d4a6e6f90aeb6ddced7cf4605892d0ba94bcb8"}, + {file = "safetensors-0.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b8fd6cc2f3bda444a048b541c843c7b7fefc89c4120d7898ea7d5b026e93891"}, + {file = "safetensors-0.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8a6abfe67692f81b8bdb99c837f28351c17e624ebf136970c850ee989c720446"}, + {file = "safetensors-0.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:27a24ca8822c469ee452db4c13418ba983315a0d863c018a9af15f2305eac38c"}, + {file = "safetensors-0.4.0-cp38-none-win32.whl", hash = "sha256:c4a0a47c8640167792d8261ee21b26430bbc39130a7edaad7f4c0bc05669d00e"}, + {file = "safetensors-0.4.0-cp38-none-win_amd64.whl", hash = "sha256:a738970a367f39249e2abb900d9441a8a86d7ff50083e5eaa6e7760a9f216014"}, + {file = "safetensors-0.4.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:806379f37e1abd5d302288c4b2f4186dd7ea7143d4c7811f90a8077f0ae8967b"}, + {file = "safetensors-0.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b9b94133ed2ae9dda0e95dcace7b7556eba023ffa4c4ae6df8f99377f571d6a"}, + {file = "safetensors-0.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b563a14c43614815a6b524d2e4edeaace50b717f7e7487bb227dd5b68350f5a"}, + {file = "safetensors-0.4.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00a9b157be660fb7ba88fa2eedd05ec93793a5b61e43e783e10cb0b995372802"}, + {file = "safetensors-0.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8f194f45ab6aa767993c24f0aeb950af169dbc5d611b94c9021a1d13b8a1a34"}, + {file = "safetensors-0.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:469360b9451db10bfed3881378d5a71b347ecb1ab4f42367d77b8164a13af70b"}, + {file = "safetensors-0.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5f75fa97ccf32a3c7af476c6a0e851023197d3c078f6de3612008fff94735f9"}, + {file = "safetensors-0.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acf0180283c2efae72f1d8c0a4a7974662091df01be3aa43b5237b1e52ed0a01"}, + {file = "safetensors-0.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cd02b495ba0814619f40bda46771bb06dbbf1d42524b66fa03b2a736c77e4515"}, + {file = "safetensors-0.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c42bdea183dbaa99e2f0e6120dc524df79cf4289a6f90f30a534444ef20f49fa"}, + {file = "safetensors-0.4.0-cp39-none-win32.whl", hash = "sha256:cef7bb5d9feae7146c3c3c7b3aef7d2c8b39ba7f5ff4252d368eb69462a47076"}, + {file = "safetensors-0.4.0-cp39-none-win_amd64.whl", hash = "sha256:79dd46fb1f19282fd12f544471efb97823ede927cedbf9cf35550d92b349fdd2"}, + {file = "safetensors-0.4.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:002301c1afa32909f83745b0c124d002e7ae07e15671f3b43cbebd0ffc5e6037"}, + {file = "safetensors-0.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:67762d36ae088c73d4a3c96bfc4ea8d31233554f35b6cace3a18533238d462ea"}, + {file = "safetensors-0.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f45230f20a206e5e4c7f7bbf9342178410c6f8b0af889843aa99045a76f7691"}, + {file = "safetensors-0.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f2ca939bbd8fb2f4dfa28e39a146dad03bc9325e9fc831b68f7b98f69a5a2f1"}, + {file = "safetensors-0.4.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61a00f281391fae5ce91df70918bb61c12d2d514a493fd8056e12114be729911"}, + {file = "safetensors-0.4.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:435fd136a42492b280cb55126f9ce9535b35dd49df2c5d572a5945455a439448"}, + {file = "safetensors-0.4.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f0daa788273d683258fb1e4a5e16bef4486b2fca536451a2591bc0f4a6488895"}, + {file = "safetensors-0.4.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0620ab0d41e390ccb1c4ea8f63dc00cb5f0b96a5cdd3cd0d64c21765720c074a"}, + {file = "safetensors-0.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1fa8d067733cb67f22926689ee808f08afacf7700d2ffb44efae90a0693eb1"}, + {file = "safetensors-0.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaa40bc363edda145db75cd030f3b1822e5478d550c3500a42502ecef32c959"}, + {file = "safetensors-0.4.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b561fbc044db7beff2ece0ec219a291809d45a38d30c6b38e7cc46482582f4ba"}, + {file = "safetensors-0.4.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:79a983b09782dacf9a1adb19bb98f4a8f6c3144108939f572c047b5797e43cf5"}, + {file = "safetensors-0.4.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:10b65cd3ad79f5d0daf281523b4146bc271a34bb7430d4e03212e0de8622dab8"}, + {file = "safetensors-0.4.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:114decacc475a6a9e2f9102a00c171d113ddb5d35cb0bda0db2c0c82b2eaa9ce"}, + {file = "safetensors-0.4.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:72ddb741dd5fe42521db76a70e012f76995516a12e7e0ef26be03ea9be77802a"}, + {file = "safetensors-0.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c5556c2ec75f5a6134866eddd7341cb36062e6edaea343478a279591b63ddba"}, + {file = "safetensors-0.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed50f239b0ce7ae85b078395593b4a351ede7e6f73af25f4873e3392336f64c9"}, + {file = "safetensors-0.4.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495dcaea8fbab70b927d2274e2547824462737acbf98ccd851a71124f779a5c6"}, + {file = "safetensors-0.4.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3f4d90c79a65ba2fe2ff0876f6140748f0a3ce6a21e27a35190f4f96321803f8"}, + {file = "safetensors-0.4.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7a524382b5c55b5fbb168e0e9d3f502450c8cf3fb81b93e880018437c206a482"}, + {file = "safetensors-0.4.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:9849ea60c7e840bfdd6030ad454d4a6ba837b3398c902f15a30460dd6961c28c"}, + {file = "safetensors-0.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:6c42623ae7045615d9eaa6877b9df1db4e9cc71ecc14bcc721ea1e475dddd595"}, + {file = "safetensors-0.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80cb8342f00f3c41b3b93b1a599b84723280d3ac90829bc62262efc03ab28793"}, + {file = "safetensors-0.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c4f5ed4ede384dea8c99bae76b0718a828dbf7b2c8ced1f44e3b9b1a124475"}, + {file = "safetensors-0.4.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40d7cf03493bfe75ef62e2c716314474b28d9ba5bf4909763e4b8dd14330c01a"}, + {file = "safetensors-0.4.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:232029f0a9fa6fa1f737324eda98a700409811186888536a2333cbbf64e41741"}, + {file = "safetensors-0.4.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:9ed55f4a20c78ff3e8477efb63c8303c2152cdfb3bfea4d025a80f54d38fd628"}, + {file = "safetensors-0.4.0.tar.gz", hash = "sha256:b985953c3cf11e942eac4317ef3db3da713e274109cf7cfb6076d877054f013e"}, +] + +[package.extras] +all = ["safetensors[jax]", "safetensors[numpy]", "safetensors[paddlepaddle]", "safetensors[pinned-tf]", "safetensors[quality]", "safetensors[testing]", "safetensors[torch]"] +dev = ["safetensors[all]"] +jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "safetensors[numpy]"] +numpy = ["numpy (>=1.21.6)"] +paddlepaddle = ["paddlepaddle (>=2.4.1)", "safetensors[numpy]"] +pinned-tf = ["safetensors[numpy]", "tensorflow (==2.11.0)"] +quality = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "isort (>=5.5.4)"] +tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] +testing = ["h5py (>=3.7.0)", "huggingface_hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools_rust (>=1.5.2)"] +torch = ["safetensors[numpy]", "torch (>=1.10)"] + +[[package]] +name = "scmrepo" +version = "1.4.1" +description = "SCM wrapper and fsspec filesystem for Git for use in DVC" +optional = false +python-versions = ">=3.8" +files = [ + {file = "scmrepo-1.4.1-py3-none-any.whl", hash = "sha256:025844fc27d2cc4b5056d3a89bcfdce361525ccf7a88bf52c05fba8a27372465"}, + {file = "scmrepo-1.4.1.tar.gz", hash = "sha256:a5b2c0fa35e529e036ce362edc7493f0d196af23412d85485ded7518ea7afb6b"}, +] + +[package.dependencies] +asyncssh = ">=2.13.1,<3" +dulwich = ">=0.21.6" +fsspec = ">=2021.7.0" +funcy = ">=1.14" +gitpython = ">3" +pathspec = ">=0.9.0" +pygit2 = ">=1.13.0" +pygtrie = ">=2.3.2" +shortuuid = ">=0.5.0" + +[package.extras] +dev = ["mock (==5.1.0)", "mypy (==0.971)", "paramiko (==3.3.1)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-asyncio (==0.18.3)", "pytest-cov (==3.0.0)", "pytest-docker (==0.12.0)", "pytest-mock (==3.8.2)", "pytest-sugar (==0.9.5)", "pytest-test-utils (==0.0.8)", "types-certifi (==2021.10.8.3)", "types-mock (==5.1.0.2)", "types-paramiko (==3.3.0.0)"] +tests = ["mock (==5.1.0)", "mypy (==0.971)", "paramiko (==3.3.1)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-asyncio (==0.18.3)", "pytest-cov (==3.0.0)", "pytest-docker (==0.12.0)", "pytest-mock (==3.8.2)", "pytest-sugar (==0.9.5)", "pytest-test-utils (==0.0.8)", "types-certifi (==2021.10.8.3)", "types-mock (==5.1.0.2)", "types-paramiko (==3.3.0.0)"] + +[[package]] +name = "sentencepiece" +version = "0.1.99" +description = "SentencePiece python wrapper" +optional = false +python-versions = "*" +files = [ + {file = "sentencepiece-0.1.99-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0eb528e70571b7c02723e5804322469b82fe7ea418c96051d0286c0fa028db73"}, + {file = "sentencepiece-0.1.99-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d7fafb2c4e4659cbdf303929503f37a26eabc4ff31d3a79bf1c5a1b338caa7"}, + {file = "sentencepiece-0.1.99-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be9cf5b9e404c245aeb3d3723c737ba7a8f5d4ba262ef233a431fa6c45f732a0"}, + {file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baed1a26464998f9710d20e52607c29ffd4293e7c71c6a1f83f51ad0911ec12c"}, + {file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9832f08bb372d4c8b567612f8eab9e36e268dff645f1c28f9f8e851be705f6d1"}, + {file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:019e7535108e309dae2b253a75834fc3128240aa87c00eb80732078cdc182588"}, + {file = "sentencepiece-0.1.99-cp310-cp310-win32.whl", hash = "sha256:fa16a830416bb823fa2a52cbdd474d1f7f3bba527fd2304fb4b140dad31bb9bc"}, + {file = "sentencepiece-0.1.99-cp310-cp310-win_amd64.whl", hash = "sha256:14b0eccb7b641d4591c3e12ae44cab537d68352e4d3b6424944f0c447d2348d5"}, + {file = "sentencepiece-0.1.99-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6d3c56f24183a1e8bd61043ff2c58dfecdc68a5dd8955dc13bab83afd5f76b81"}, + {file = "sentencepiece-0.1.99-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed6ea1819fd612c989999e44a51bf556d0ef6abfb553080b9be3d347e18bcfb7"}, + {file = "sentencepiece-0.1.99-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2a0260cd1fb7bd8b4d4f39dc2444a8d5fd4e0a0c4d5c899810ef1abf99b2d45"}, + {file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a1abff4d1ff81c77cac3cc6fefa34fa4b8b371e5ee51cb7e8d1ebc996d05983"}, + {file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:004e6a621d4bc88978eecb6ea7959264239a17b70f2cbc348033d8195c9808ec"}, + {file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db361e03342c41680afae5807590bc88aa0e17cfd1a42696a160e4005fcda03b"}, + {file = "sentencepiece-0.1.99-cp311-cp311-win32.whl", hash = "sha256:2d95e19168875b70df62916eb55428a0cbcb834ac51d5a7e664eda74def9e1e0"}, + {file = "sentencepiece-0.1.99-cp311-cp311-win_amd64.whl", hash = "sha256:f90d73a6f81248a909f55d8e6ef56fec32d559e1e9af045f0b0322637cb8e5c7"}, + {file = "sentencepiece-0.1.99-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:62e24c81e74bd87a6e0d63c51beb6527e4c0add67e1a17bac18bcd2076afcfeb"}, + {file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57efcc2d51caff20d9573567d9fd3f854d9efe613ed58a439c78c9f93101384a"}, + {file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a904c46197993bd1e95b93a6e373dca2f170379d64441041e2e628ad4afb16f"}, + {file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d89adf59854741c0d465f0e1525b388c0d174f611cc04af54153c5c4f36088c4"}, + {file = "sentencepiece-0.1.99-cp36-cp36m-win32.whl", hash = "sha256:47c378146928690d1bc106fdf0da768cebd03b65dd8405aa3dd88f9c81e35dba"}, + {file = "sentencepiece-0.1.99-cp36-cp36m-win_amd64.whl", hash = "sha256:9ba142e7a90dd6d823c44f9870abdad45e6c63958eb60fe44cca6828d3b69da2"}, + {file = "sentencepiece-0.1.99-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7b1a9ae4d7c6f1f867e63370cca25cc17b6f4886729595b885ee07a58d3cec3"}, + {file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0f644c9d4d35c096a538507b2163e6191512460035bf51358794a78515b74f7"}, + {file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8843d23a0f686d85e569bd6dcd0dd0e0cbc03731e63497ca6d5bacd18df8b85"}, + {file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6f690a1caebb4867a2e367afa1918ad35be257ecdb3455d2bbd787936f155"}, + {file = "sentencepiece-0.1.99-cp37-cp37m-win32.whl", hash = "sha256:8a321866c2f85da7beac74a824b4ad6ddc2a4c9bccd9382529506d48f744a12c"}, + {file = "sentencepiece-0.1.99-cp37-cp37m-win_amd64.whl", hash = "sha256:c42f753bcfb7661c122a15b20be7f684b61fc8592c89c870adf52382ea72262d"}, + {file = "sentencepiece-0.1.99-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b476406da69c70586f0bb682fcca4c9b40e5059814f2db92303ea4585c650c"}, + {file = "sentencepiece-0.1.99-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfbcfe13c69d3f87b7fcd5da168df7290a6d006329be71f90ba4f56bc77f8561"}, + {file = "sentencepiece-0.1.99-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:445b0ec381af1cd4eef95243e7180c63d9c384443c16c4c47a28196bd1cda937"}, + {file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6890ea0f2b4703f62d0bf27932e35808b1f679bdb05c7eeb3812b935ba02001"}, + {file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb71af492b0eefbf9f2501bec97bcd043b6812ab000d119eaf4bd33f9e283d03"}, + {file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b866b5bd3ddd54166bbcbf5c8d7dd2e0b397fac8537991c7f544220b1f67bc"}, + {file = "sentencepiece-0.1.99-cp38-cp38-win32.whl", hash = "sha256:b133e8a499eac49c581c3c76e9bdd08c338cc1939e441fee6f92c0ccb5f1f8be"}, + {file = "sentencepiece-0.1.99-cp38-cp38-win_amd64.whl", hash = "sha256:0eaf3591dd0690a87f44f4df129cf8d05d8a4029b5b6709b489b8e27f9a9bcff"}, + {file = "sentencepiece-0.1.99-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38efeda9bbfb55052d482a009c6a37e52f42ebffcea9d3a98a61de7aee356a28"}, + {file = "sentencepiece-0.1.99-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c030b081dc1e1bcc9fadc314b19b740715d3d566ad73a482da20d7d46fd444c"}, + {file = "sentencepiece-0.1.99-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84dbe53e02e4f8a2e45d2ac3e430d5c83182142658e25edd76539b7648928727"}, + {file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b0f55d0a0ee1719b4b04221fe0c9f0c3461dc3dabd77a035fa2f4788eb3ef9a"}, + {file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e800f206cd235dc27dc749299e05853a4e4332e8d3dfd81bf13d0e5b9007d9"}, + {file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae1c40cda8f9d5b0423cfa98542735c0235e7597d79caf318855cdf971b2280"}, + {file = "sentencepiece-0.1.99-cp39-cp39-win32.whl", hash = "sha256:c84ce33af12ca222d14a1cdd37bd76a69401e32bc68fe61c67ef6b59402f4ab8"}, + {file = "sentencepiece-0.1.99-cp39-cp39-win_amd64.whl", hash = "sha256:350e5c74d739973f1c9643edb80f7cc904dc948578bcb1d43c6f2b173e5d18dd"}, + {file = "sentencepiece-0.1.99.tar.gz", hash = "sha256:189c48f5cb2949288f97ccdb97f0473098d9c3dcf5a3d99d4eabe719ec27297f"}, +] + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "shortuuid" +version = "1.0.11" +description = "A generator library for concise, unambiguous and URL-safe UUIDs." +optional = false +python-versions = ">=3.5" +files = [ + {file = "shortuuid-1.0.11-py3-none-any.whl", hash = "sha256:27ea8f28b1bd0bf8f15057a3ece57275d2059d2b0bb02854f02189962c13b6aa"}, + {file = "shortuuid-1.0.11.tar.gz", hash = "sha256:fc75f2615914815a8e4cb1501b3a513745cb66ef0fd5fc6fb9f8c3fa3481f789"}, +] + +[[package]] +name = "shtab" +version = "1.6.4" +description = "Automagic shell tab completion for Python CLI applications" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shtab-1.6.4-py3-none-any.whl", hash = "sha256:4be38887a912091a1640e06f5ccbcbd24e176cf2fcb9ef0c2e011ee22d63834f"}, + {file = "shtab-1.6.4.tar.gz", hash = "sha256:aba9e049bed54ffdb650cb2e02657282d8c0148024b0f500277052df124d47de"}, +] + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "smart-open" +version = "6.4.0" +description = "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "smart_open-6.4.0-py3-none-any.whl", hash = "sha256:8d3ef7e6997e8e42dd55c74166ed21e6ac70664caa32dd940b26d54a8f6b4142"}, + {file = "smart_open-6.4.0.tar.gz", hash = "sha256:be3c92c246fbe80ebce8fbacb180494a481a77fcdcb7c1aadb2ea5b9c2bee8b9"}, +] + +[package.extras] +all = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "paramiko", "requests"] +azure = ["azure-common", "azure-core", "azure-storage-blob"] +gcs = ["google-cloud-storage (>=2.6.0)"] +http = ["requests"] +s3 = ["boto3"] +ssh = ["paramiko"] +test = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "moto[server]", "paramiko", "pytest", "pytest-rerunfailures", "requests", "responses"] +webhdfs = ["requests"] + +[[package]] +name = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +files = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, +] + +[[package]] +name = "soupsieve" +version = "2.4.1" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, + {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, +] + +[[package]] +name = "spacy" +version = "3.7.2" +description = "Industrial-strength Natural Language Processing (NLP) in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "spacy-3.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4e285366d36c85f784d606a2d966912a18f4d24d47330c1c6acbdd9f19ee373"}, + {file = "spacy-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f132c05368781be5d3be3d706afce7e7a9a0c9edc0dbb7c616162c37bc386561"}, + {file = "spacy-3.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3767b2cabbe337d62779ae4fdc4d57a39755c17dfc499de3ad2bae622caa43"}, + {file = "spacy-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a748ade269bdbea9baaa49ec00882404e7e921163cdc14f5612320d0a957dfd"}, + {file = "spacy-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:66467128e494bfa4dc9c3996e4cbb26bac4741bca4cdd8dd83a6e71182148945"}, + {file = "spacy-3.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5af30aea578e7414fb0eb4dbad0ff0fa0a7d8e833c3e733eceb2617534714c7d"}, + {file = "spacy-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7293de33b1e9ede151555070ad0fee3bac98aefcaac9e615eeeb4296846bd479"}, + {file = "spacy-3.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26940681cf20c8831c558e2c3d345ff20b5bc3c5e6d41c66172d0c5136042f0b"}, + {file = "spacy-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a334667625153f7aaf188c20af7e82c886e41a88483a056accba5a7d51095c6"}, + {file = "spacy-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:43e6147d3583b62a2d3af0cd913ac025068196d587345751e198391ff0b8c1e9"}, + {file = "spacy-3.7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2558df8c11905a0f77a2a3639a12ef8a522d171bcd88eaec039bedf6c60d7e01"}, + {file = "spacy-3.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:df1b9c4bbadc89bad10dba226d52c113e231ea6ad35c8a916ab138b31f69fa24"}, + {file = "spacy-3.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbbe055d2170ac7505a9f580bbdcd2146d0701bdbd6cea2333e18b0db655b97a"}, + {file = "spacy-3.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d35129b16ae2ca4212bf22a5c88b67b1e019e434fc48b69d3b95f80bc9e14e42"}, + {file = "spacy-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:a7419682aba99624cc4df7df66764b6ec62ff415f32c3682c1af2a37bd11a913"}, + {file = "spacy-3.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b12ab9c4923ffd38da84baf09464982da44e8275d680fb3c5da2051d7dd7bd2d"}, + {file = "spacy-3.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c5c9db529dc1caa908813c58ba1643e929d2c811768596a2b64e2e01a882b1"}, + {file = "spacy-3.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcaad95e3e7d0ea8f381f3e2d9e80b7f346ecb6566de9bd55361736fa563fc22"}, + {file = "spacy-3.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5d9b12284871ca5daa7774604a964486957567a86f1af898da0260e94b815e0d"}, + {file = "spacy-3.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2bd89770f61d5980e788ef382297322cceb7dcc4b848d68cb1da8af7d80d6eb6"}, + {file = "spacy-3.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d42f9151a2f01b34227ed31c8db8b7c67889ebcc637eae390faec8093ea1fb12"}, + {file = "spacy-3.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3d25d2f22ba1d2dd46d103e4a54826582de2b853b6f95dfb97b005563b38838"}, + {file = "spacy-3.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:730f23340dd157817d2da6df21f69966791b0bdbd6ea108845a65f3e1c0e981c"}, + {file = "spacy-3.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:9c2f3f04b4b894a6c42ee93cec2f2b158f246f344927e65d9d19b72c5a6493ea"}, + {file = "spacy-3.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b22e0e8dac76740d55556fa13ebb9e1c829779ea0b7ec7a9e04f32efc66f74b9"}, + {file = "spacy-3.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ad7f378350104ca1f9e81180485d8b094aad7acb9b4bce84f1387b905cf230a2"}, + {file = "spacy-3.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ccbffb7825c08c0586ef7384d0aa23196f9ac106b5c7b3c551907316930f94f"}, + {file = "spacy-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:111955d7f4786b952672e9c5cfd9f8b74d81e64b62d479f71efe9cfc2a027a1d"}, + {file = "spacy-3.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:e8a7291e7e1cfcb6041b26f96d0a66b603725c1beff4e0391c3d9226fae16e04"}, + {file = "spacy-3.7.2.tar.gz", hash = "sha256:cedf4927bf0d3fec773a6ce48d5d2c91bdb02fed3c7d5ec07bdb873f1126f1a0"}, +] + +[package.dependencies] +catalogue = ">=2.0.6,<2.1.0" +cymem = ">=2.0.2,<2.1.0" +jinja2 = "*" +langcodes = ">=3.2.0,<4.0.0" +murmurhash = ">=0.28.0,<1.1.0" +numpy = [ + {version = ">=1.15.0", markers = "python_version < \"3.9\""}, + {version = ">=1.19.0", markers = "python_version >= \"3.9\""}, +] +packaging = ">=20.0" +preshed = ">=3.0.2,<3.1.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +requests = ">=2.13.0,<3.0.0" +setuptools = "*" +smart-open = ">=5.2.1,<7.0.0" +spacy-legacy = ">=3.0.11,<3.1.0" +spacy-loggers = ">=1.0.0,<2.0.0" +srsly = ">=2.4.3,<3.0.0" +thinc = ">=8.1.8,<8.3.0" +tqdm = ">=4.38.0,<5.0.0" +typer = ">=0.3.0,<0.10.0" +typing-extensions = {version = ">=3.7.4.1,<4.5.0", markers = "python_version < \"3.8\""} +wasabi = ">=0.9.1,<1.2.0" +weasel = ">=0.1.0,<0.4.0" + +[package.extras] +apple = ["thinc-apple-ops (>=0.1.0.dev0,<1.0.0)"] +cuda = ["cupy (>=5.0.0b4,<13.0.0)"] +cuda-autodetect = ["cupy-wheel (>=11.0.0,<13.0.0)"] +cuda100 = ["cupy-cuda100 (>=5.0.0b4,<13.0.0)"] +cuda101 = ["cupy-cuda101 (>=5.0.0b4,<13.0.0)"] +cuda102 = ["cupy-cuda102 (>=5.0.0b4,<13.0.0)"] +cuda110 = ["cupy-cuda110 (>=5.0.0b4,<13.0.0)"] +cuda111 = ["cupy-cuda111 (>=5.0.0b4,<13.0.0)"] +cuda112 = ["cupy-cuda112 (>=5.0.0b4,<13.0.0)"] +cuda113 = ["cupy-cuda113 (>=5.0.0b4,<13.0.0)"] +cuda114 = ["cupy-cuda114 (>=5.0.0b4,<13.0.0)"] +cuda115 = ["cupy-cuda115 (>=5.0.0b4,<13.0.0)"] +cuda116 = ["cupy-cuda116 (>=5.0.0b4,<13.0.0)"] +cuda117 = ["cupy-cuda117 (>=5.0.0b4,<13.0.0)"] +cuda11x = ["cupy-cuda11x (>=11.0.0,<13.0.0)"] +cuda12x = ["cupy-cuda12x (>=11.5.0,<13.0.0)"] +cuda80 = ["cupy-cuda80 (>=5.0.0b4,<13.0.0)"] +cuda90 = ["cupy-cuda90 (>=5.0.0b4,<13.0.0)"] +cuda91 = ["cupy-cuda91 (>=5.0.0b4,<13.0.0)"] +cuda92 = ["cupy-cuda92 (>=5.0.0b4,<13.0.0)"] +ja = ["sudachidict-core (>=20211220)", "sudachipy (>=0.5.2,!=0.6.1)"] +ko = ["natto-py (>=0.9.0)"] +lookups = ["spacy-lookups-data (>=1.0.3,<1.1.0)"] +th = ["pythainlp (>=2.0)"] +transformers = ["spacy-transformers (>=1.1.2,<1.4.0)"] + +[[package]] +name = "spacy-alignments" +version = "0.9.1" +description = "A spaCy package for the Rust tokenizations library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "spacy-alignments-0.9.1.tar.gz", hash = "sha256:7e020ec4797d6179060818d01cdb4e0013a52dba544b9bbfb5efcff8851926dc"}, + {file = "spacy_alignments-0.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2d9b8da21d7924f4b5e6cfd89234b27f7939c4211c0fa866b3dde4110b96dd6"}, + {file = "spacy_alignments-0.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12402e2eea5c4b21b197c43c9bed2629ab1324ae46bd92f7b8e4630dec14ea3a"}, + {file = "spacy_alignments-0.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd0279610d5047205c8d10368a600fa6b9c6d995efdfb093708d54c9ad7efc1f"}, + {file = "spacy_alignments-0.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c152d78b25a88487145a6bb82aefc938e503c28c4249fd723390409deeb3f04"}, + {file = "spacy_alignments-0.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:61b42ba12222c1ea0e659ae5834e494f25492e7649425d0cef65aa8948818dd1"}, + {file = "spacy_alignments-0.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:285babdffd85840164446fbc40435c57510d4b90f12e893bbecb55c690b23c51"}, + {file = "spacy_alignments-0.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb9cc7efe494468e61038f91269d66ca9a4aa3395250f60eb942368c19a6e11"}, + {file = "spacy_alignments-0.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dccd315b0d083dfae0c82f845e647ead16f04d2ec1c15c9fc05281d6ae00cf7"}, + {file = "spacy_alignments-0.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1fe1ad0bcc9f365746c4031d0523b52da79dd87f9c0e6e977c6c8fd4032a82b"}, + {file = "spacy_alignments-0.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a58ce17fd919c3719529df17c34f82bbaec600130655294aa05effd2308baaeb"}, + {file = "spacy_alignments-0.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bf5a5d7b65f9c7dfbf9c9ac1d1a2ab3e1cdcfc93a1f52cef0d666c29b416fe7d"}, + {file = "spacy_alignments-0.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:20644e71b2d685fc31013ac8a806224a9de4a4dd2c03ded621a95a95efc6000d"}, + {file = "spacy_alignments-0.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36825157fbd7b96e6bfeb3a0076dd36d8d1f560624b824c2873d10a1a0d70fd2"}, + {file = "spacy_alignments-0.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35fa7444dd7117e45cfca51335a4eb737627c9a9dfd191c8291cf9f5fb0557ae"}, + {file = "spacy_alignments-0.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:adb04d06cf417f5df56a80f1a54f9eedaab3e4165b4fcb50bf7c3680eb549fc6"}, + {file = "spacy_alignments-0.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1264e21f7fbba166ed02c8b495e99f2d92e43335a476f4afa498c02e32566b4e"}, + {file = "spacy_alignments-0.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fd8a59fe7d75a61d303e8a290cba53b82d85f3bfecaf267343ef47df5555e9d"}, + {file = "spacy_alignments-0.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b97b879d614f1c37f330c0c0c2fcffacd6bf5322473169748aa76e4acbe484"}, + {file = "spacy_alignments-0.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c70df885671f75ed33371984ac156e5002c1245f0c64eb5a0b2aef20805b835b"}, + {file = "spacy_alignments-0.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c4e68df531d177d5b07ee9396f22c085e54685a6c4ab349f0ce5c8f55b54dde0"}, + {file = "spacy_alignments-0.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:365c44a5f76d789af82d174235333f31cf0e151c28d56b886a1223a961b47ba4"}, + {file = "spacy_alignments-0.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c913af4e0e3da4acbd9265697fb86a2c8370b2e70d984ef8f7238efa2922ec9"}, + {file = "spacy_alignments-0.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4582d242808c4c5c44380e3543e6a53225bf6db2ae9b4d9d58e2a671442e1b60"}, + {file = "spacy_alignments-0.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:69d8081654b310390aa037c6caee70fdf6825c4474f84dbe42d58cc44874c9f5"}, + {file = "spacy_alignments-0.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:992e2768b6f2432922b616ca893fe7a66d3e865cf457352dc250bc16ab016633"}, + {file = "spacy_alignments-0.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10ecfb8e42adf0d39fec87bed9f344e0f85be893d2258d0b7d81134d5b110525"}, + {file = "spacy_alignments-0.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f36d49431d6d6067c57caaabe1aca501bbe8df39c9ffa92daf386bdc239074"}, + {file = "spacy_alignments-0.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62c1d70bfb6fc12ce2a7a92f1c1725abaa87a0e06bc2c4bf2b3b5b43f5a3f59"}, + {file = "spacy_alignments-0.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:0b3cd95356f27fa4dc41448e131b6b44eb065d11e4c4c4fbcbfc0ef20ad4e513"}, +] + +[[package]] +name = "spacy-legacy" +version = "3.0.12" +description = "Legacy registered functions for spaCy backwards compatibility" +optional = false +python-versions = ">=3.6" +files = [ + {file = "spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774"}, + {file = "spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f"}, +] + +[[package]] +name = "spacy-loggers" +version = "1.0.5" +description = "Logging utilities for SpaCy" +optional = false +python-versions = ">=3.6" +files = [ + {file = "spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24"}, + {file = "spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645"}, +] + +[[package]] +name = "spacy-transformers" +version = "1.3.3" +description = "spaCy pipelines for pre-trained BERT and other transformers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "spacy-transformers-1.3.3.tar.gz", hash = "sha256:43ca15d26c3c3ce16c70571db1865f4e46af355432a9674b01d6f89435f86a13"}, + {file = "spacy_transformers-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d36d58963f1ac8366052f18e881ee7d7705acd464b6b5a73d3756a599a572c8"}, + {file = "spacy_transformers-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae91eadf268da4ee2c01f203524abd83f837e56f0f8f3ec3f26ffe1200fc0794"}, + {file = "spacy_transformers-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:2fc524dd4706d196440572d715f884d0ad20a7e579cbc131a994981c871c1652"}, + {file = "spacy_transformers-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20dfdea11df788ca8c2a2869ffc5af14bd58c020965e9c95fc6fdf56e934b3cd"}, + {file = "spacy_transformers-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9ba4d29ff7c8a2d1000487c966f4c6d6782616d51cad06ebb770278edcb8a541"}, + {file = "spacy_transformers-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:06903d2c50b559da2e460c513a9b0d13dbf5a036aede14dd26d18f7271bbd1a1"}, + {file = "spacy_transformers-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e3c38cdb2c22724337fa383cc44632950da9e851366172e2d8218fa3e8895370"}, + {file = "spacy_transformers-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026467613cefb6f9fb23c19d8b9acba608065014fa82515f2cefe4b5976f38cf"}, + {file = "spacy_transformers-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22b058fba486db5df0d9aee0cbd9e142fb716e281b94ba45502ff9e4ee1cefac"}, + {file = "spacy_transformers-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:33f338a64e7f77abf4dc763ebe2baf138a77a02414f7b424c61903f920bf141d"}, + {file = "spacy_transformers-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:4d348ab0ce1983b75f35476117b0bc530feac377739c985b9c95c36a2ac3feb1"}, + {file = "spacy_transformers-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0d56ffd59c03e75fab2c555982f8395539b913848c20f82f173a1ce69674bd12"}, + {file = "spacy_transformers-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e28b011d847f5ce7ebf9773b76d96024d9f08c3c69cd034ce2ba067f23fb82b3"}, + {file = "spacy_transformers-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:891d323dc7e343b543279165c985167b6213cd95d5d6f37084690bc9b8dbc341"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.15.0", markers = "python_version < \"3.9\""}, + {version = ">=1.19.0", markers = "python_version >= \"3.9\""}, +] +spacy = ">=3.5.0,<4.0.0" +spacy-alignments = ">=0.7.2,<1.0.0" +srsly = ">=2.4.0,<3.0.0" +torch = ">=1.8.0" +transformers = ">=3.4.0,<4.36.0" + +[package.extras] +cuda = ["cupy (>=5.0.0b4)"] +cuda100 = ["cupy-cuda100 (>=5.0.0b4)"] +cuda101 = ["cupy-cuda101 (>=5.0.0b4)"] +cuda102 = ["cupy-cuda102 (>=5.0.0b4)"] +cuda110 = ["cupy-cuda110 (>=5.0.0b4)"] +cuda111 = ["cupy-cuda111 (>=5.0.0b4)"] +cuda112 = ["cupy-cuda112 (>=5.0.0b4)"] +cuda80 = ["cupy-cuda80 (>=5.0.0b4)"] +cuda90 = ["cupy-cuda90 (>=5.0.0b4)"] +cuda91 = ["cupy-cuda91 (>=5.0.0b4)"] +cuda92 = ["cupy-cuda92 (>=5.0.0b4)"] + +[[package]] +name = "sqltrie" +version = "0.8.0" +description = "SQL-based prefix tree inspired by pygtrie and python-diskcache" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqltrie-0.8.0-py3-none-any.whl", hash = "sha256:80a708960fd9468b645f527b39ea6beae30e57d5d5dd284f54a49ac267d240eb"}, + {file = "sqltrie-0.8.0.tar.gz", hash = "sha256:a773e41f00ae9215a79d3e0537526eaf5e37100037a2ef042d09edcc209abc9e"}, +] + +[package.dependencies] +attrs = "*" +orjson = {version = "*", markers = "implementation_name == \"cpython\""} +pygtrie = "*" + +[package.extras] +dev = ["mypy (==0.971)", "pyinstaller", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-benchmark", "pytest-cov (==3.0.0)", "pytest-mock (==3.8.2)", "pytest-sugar (==0.9.5)"] +tests = ["mypy (==0.971)", "pyinstaller", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-benchmark", "pytest-cov (==3.0.0)", "pytest-mock (==3.8.2)", "pytest-sugar (==0.9.5)"] + +[[package]] +name = "srsly" +version = "2.4.8" +description = "Modern high-performance serialization utilities for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "srsly-2.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:17f3bcb418bb4cf443ed3d4dcb210e491bd9c1b7b0185e6ab10b6af3271e63b2"}, + {file = "srsly-2.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b070a58e21ab0e878fd949f932385abb4c53dd0acb6d3a7ee75d95d447bc609"}, + {file = "srsly-2.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98286d20014ed2067ad02b0be1e17c7e522255b188346e79ff266af51a54eb33"}, + {file = "srsly-2.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18685084e2e0cc47c25158cbbf3e44690e494ef77d6418c2aae0598c893f35b0"}, + {file = "srsly-2.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:980a179cbf4eb5bc56f7507e53f76720d031bcf0cef52cd53c815720eb2fc30c"}, + {file = "srsly-2.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5472ed9f581e10c32e79424c996cf54c46c42237759f4224806a0cd4bb770993"}, + {file = "srsly-2.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50f10afe9230072c5aad9f6636115ea99b32c102f4c61e8236d8642c73ec7a13"}, + {file = "srsly-2.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c994a89ba247a4d4f63ef9fdefb93aa3e1f98740e4800d5351ebd56992ac75e3"}, + {file = "srsly-2.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace7ed4a0c20fa54d90032be32f9c656b6d75445168da78d14fe9080a0c208ad"}, + {file = "srsly-2.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:7a919236a090fb93081fbd1cec030f675910f3863825b34a9afbcae71f643127"}, + {file = "srsly-2.4.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7583c03d114b4478b7a357a1915305163e9eac2dfe080da900555c975cca2a11"}, + {file = "srsly-2.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ccdd2f6db824c31266aaf93e0f31c1c43b8bc531cd2b3a1d924e3c26a4f294"}, + {file = "srsly-2.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72d2974f91aee652d606c7def98744ca6b899bd7dd3009fd75ebe0b5a51034"}, + {file = "srsly-2.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a60c905fd2c15e848ce1fc315fd34d8a9cc72c1dee022a0d8f4c62991131307"}, + {file = "srsly-2.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:e0b8d5722057000694edf105b8f492e7eb2f3aa6247a5f0c9170d1e0d074151c"}, + {file = "srsly-2.4.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:196b4261f9d6372d1d3d16d1216b90c7e370b4141471322777b7b3c39afd1210"}, + {file = "srsly-2.4.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4750017e6d78590b02b12653e97edd25aefa4734281386cc27501d59b7481e4e"}, + {file = "srsly-2.4.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa034cd582ba9e4a120c8f19efa263fcad0f10fc481e73fb8c0d603085f941c4"}, + {file = "srsly-2.4.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5a78ab9e9d177ee8731e950feb48c57380036d462b49e3fb61a67ce529ff5f60"}, + {file = "srsly-2.4.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:087e36439af517e259843df93eb34bb9e2d2881c34fa0f541589bcfbc757be97"}, + {file = "srsly-2.4.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad141d8a130cb085a0ed3a6638b643e2b591cb98a4591996780597a632acfe20"}, + {file = "srsly-2.4.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d05367b2571c0d08d00459636b951e3ca2a1e9216318c157331f09c33489d3"}, + {file = "srsly-2.4.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3fd661a1c4848deea2849b78f432a70c75d10968e902ca83c07c89c9b7050ab8"}, + {file = "srsly-2.4.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec37233fe39af97b00bf20dc2ceda04d39b9ea19ce0ee605e16ece9785e11f65"}, + {file = "srsly-2.4.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d2fd4bc081f1d6a6063396b6d97b00d98e86d9d3a3ac2949dba574a84e148080"}, + {file = "srsly-2.4.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7347cff1eb4ef3fc335d9d4acc89588051b2df43799e5d944696ef43da79c873"}, + {file = "srsly-2.4.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9dc1da5cc94d77056b91ba38365c72ae08556b6345bef06257c7e9eccabafe"}, + {file = "srsly-2.4.8-cp38-cp38-win_amd64.whl", hash = "sha256:dc0bf7b6f23c9ecb49ec0924dc645620276b41e160e9b283ed44ca004c060d79"}, + {file = "srsly-2.4.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff8df21d00d73c371bead542cefef365ee87ca3a5660de292444021ff84e3b8c"}, + {file = "srsly-2.4.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ac3e340e65a9fe265105705586aa56054dc3902789fcb9a8f860a218d6c0a00"}, + {file = "srsly-2.4.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d1733f4275eff4448e96521cc7dcd8fdabd68ba9b54ca012dcfa2690db2644"}, + {file = "srsly-2.4.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be5b751ad88fdb58fb73871d456248c88204f213aaa3c9aab49b6a1802b3fa8d"}, + {file = "srsly-2.4.8-cp39-cp39-win_amd64.whl", hash = "sha256:822a38b8cf112348f3accbc73274a94b7bf82515cb14a85ba586d126a5a72851"}, + {file = "srsly-2.4.8.tar.gz", hash = "sha256:b24d95a65009c2447e0b49cda043ac53fecf4f09e358d87a57446458f91b8a91"}, +] + +[package.dependencies] +catalogue = ">=2.0.3,<2.1.0" + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "thinc" +version = "8.2.1" +description = "A refreshing functional take on deep learning, compatible with your favorite libraries" +optional = false +python-versions = ">=3.6" +files = [ + {file = "thinc-8.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67948bbcf86c3ace8838ca4cdb72977b051d8ee024eeb631d94467be18b15271"}, + {file = "thinc-8.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e1a558b323f15f60bd79ba3cb95f78945e76748684db00052587270217b96a5"}, + {file = "thinc-8.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca97679f14f3cd73be76375d6792ac2685c7eca50260cef1810415a2c75ac6c5"}, + {file = "thinc-8.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:228dabcb8667ff19b2576718e4201b203c3f78dfbed4fa79caab8eef6d5fed48"}, + {file = "thinc-8.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b02dadc3e41dd5cfd515f0c60aa3e5c472e02c12613a1bb9d837ce5f49cf9d34"}, + {file = "thinc-8.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0afbcd243d27c076b8c47aded8e5e0aff2ff683af6b95a39839fe3aea862cfd9"}, + {file = "thinc-8.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4704354879abb052fbd2c658cd6df20d7bba40790ded0e81e994c879849b62f4"}, + {file = "thinc-8.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d6257369950002abe09d64b4f161d10d73af5df3764aea89f70cae018cca14b"}, + {file = "thinc-8.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2ce2f93a06f8e56796fd2b9d237b6f6ef36ccd9dec66cb38d0092a3947c875"}, + {file = "thinc-8.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bbefd9939302ebed6d48f57b959be899b23a0c85f1afaf50c82e7b493e5de04"}, + {file = "thinc-8.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:70fabf9e3d7f4da9804be9d29800dab7506cac12598735edb05ed1cec7b2ee50"}, + {file = "thinc-8.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0fe6f36faa5a0a69d267d7196d821a9730b3bf1817941db2a83780a199599cd5"}, + {file = "thinc-8.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a1bc995cace52503c906b87ff0cf428b94435b8b70539c6e6ad29b526925c5"}, + {file = "thinc-8.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be1f169f01451010822cde5052db3fee25a0793abebe8fbd48d02955a33d0692"}, + {file = "thinc-8.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9cf766fac7e845e96e509ac9545ea1a60034a069aee3d75068b6e46da084c206"}, + {file = "thinc-8.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0ad99b6d1f7c149137497c6ae9345304fd7465c0c290c00cedd504ff5ae5485d"}, + {file = "thinc-8.2.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beda7380017df1fbdf8de1733851464886283786c3c9149e2ac7cef612eff6ed"}, + {file = "thinc-8.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e6ae6309f110440bcbd6a03b5b4b940d7c607afd2027a6b638336cc42a2171"}, + {file = "thinc-8.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:aaad5532c3abd2fe69500426a102a3b53725a78eba5ba6867bed9e6b8de0bcba"}, + {file = "thinc-8.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3c32c1e1e60b5e676f1f618915fbb20547b573998693704d0b4987d972e35a62"}, + {file = "thinc-8.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eae5a3415ff9be0fa21671a58166e82fe6c9ee832252779fd92c31c03692fb7"}, + {file = "thinc-8.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79e66eed14c2e7b333d69b376f8a091efad366e172b11e39c04814b54969b399"}, + {file = "thinc-8.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8a1a2ef7061e23507f8172adb7978f7b7bc0bd4ccb266149de7065ee5331e1ea"}, + {file = "thinc-8.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0216e17be5ddcc1014af55d2e02388698fb64dbc9f32a4782df0a3860615057"}, + {file = "thinc-8.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:16e7c0988df852cbae40ac03f45e11e3c39300b05dff87267c6fc13108723985"}, + {file = "thinc-8.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:637fafb7d3b51f2aa611371761578fe9999d2675f4fc87eb09e736648d12be30"}, + {file = "thinc-8.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27bab1026284fba355eda7d83ebc0612ace437fb50ddc9d390e71d732b67e20"}, + {file = "thinc-8.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:88dab842c68c8e9f0b75a7b4352b53eaa385db2a1de91e276219bfcfda27e47b"}, + {file = "thinc-8.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5978a97b35a36adb133a83b9fc6cbb9f0c364f8db8525fa0ef5c4fc03f25b889"}, + {file = "thinc-8.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8181d86b1c8de8dae154ad02399a8d59beb62881c172926594a5f3d7dc0e625"}, + {file = "thinc-8.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab83ade836933e34a82c61ff9fe0cb3ea9103165935ce9ea12102aff270dad9"}, + {file = "thinc-8.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19387a23ef2ce2714572040c15f0896b6e0d3751e37ccc1d927c0447f8eac7a1"}, + {file = "thinc-8.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:229efc84666901730e5575d5ec3c852d02009478411b24c0640f45b42e87a21c"}, + {file = "thinc-8.2.1.tar.gz", hash = "sha256:cd7fdb3d883a15e6906254e7fb0162f69878e9ccdd1f8519db6ffbfe46bf6f49"}, +] + +[package.dependencies] +blis = ">=0.7.8,<0.8.0" +catalogue = ">=2.0.4,<2.1.0" +confection = ">=0.0.1,<1.0.0" +cymem = ">=2.0.2,<2.1.0" +murmurhash = ">=1.0.2,<1.1.0" +numpy = [ + {version = ">=1.15.0", markers = "python_version < \"3.9\""}, + {version = ">=1.19.0", markers = "python_version >= \"3.9\""}, +] +packaging = ">=20.0" +preshed = ">=3.0.2,<3.1.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +setuptools = "*" +srsly = ">=2.4.0,<3.0.0" +typing-extensions = {version = ">=3.7.4.1,<4.5.0", markers = "python_version < \"3.8\""} +wasabi = ">=0.8.1,<1.2.0" + +[package.extras] +cuda = ["cupy (>=5.0.0b4)"] +cuda-autodetect = ["cupy-wheel (>=11.0.0)"] +cuda100 = ["cupy-cuda100 (>=5.0.0b4)"] +cuda101 = ["cupy-cuda101 (>=5.0.0b4)"] +cuda102 = ["cupy-cuda102 (>=5.0.0b4)"] +cuda110 = ["cupy-cuda110 (>=5.0.0b4)"] +cuda111 = ["cupy-cuda111 (>=5.0.0b4)"] +cuda112 = ["cupy-cuda112 (>=5.0.0b4)"] +cuda113 = ["cupy-cuda113 (>=5.0.0b4)"] +cuda114 = ["cupy-cuda114 (>=5.0.0b4)"] +cuda115 = ["cupy-cuda115 (>=5.0.0b4)"] +cuda116 = ["cupy-cuda116 (>=5.0.0b4)"] +cuda117 = ["cupy-cuda117 (>=5.0.0b4)"] +cuda11x = ["cupy-cuda11x (>=11.0.0)"] +cuda80 = ["cupy-cuda80 (>=5.0.0b4)"] +cuda90 = ["cupy-cuda90 (>=5.0.0b4)"] +cuda91 = ["cupy-cuda91 (>=5.0.0b4)"] +cuda92 = ["cupy-cuda92 (>=5.0.0b4)"] +datasets = ["ml-datasets (>=0.2.0,<0.3.0)"] +mxnet = ["mxnet (>=1.5.1,<1.6.0)"] +tensorflow = ["tensorflow (>=2.0.0,<2.6.0)"] +torch = ["torch (>=1.6.0)"] + +[[package]] +name = "thinc-apple-ops" +version = "0.1.4" +description = "Improve Thinc's performance on Apple devices with native libraries" +optional = true +python-versions = ">=3.7" +files = [ + {file = "thinc_apple_ops-0.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ad03f4247f8243164d4faba79aa157899d2a3ab22e2964f01be93a9a889895ee"}, + {file = "thinc_apple_ops-0.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec3bd84e0c5ef5d9ee1a10de07c5bfd477f936029ebc698b3b418f82963dbd0b"}, + {file = "thinc_apple_ops-0.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6935883f055a040a41d6af8f8597044de570c8229361886727d9b6b85bb18788"}, + {file = "thinc_apple_ops-0.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dd098f13733e11f74444e6bd882068c35d7e9d15f40535526e420eb7a2ef56c9"}, + {file = "thinc_apple_ops-0.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:16cf0bd28a640177c62b1652915e225aa04458682bf698d9a51351654d206907"}, + {file = "thinc_apple_ops-0.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:30ebe3deecf0fb156b74ba4f600d52ec3fa96fb6efb8a00ca14d01e712a85cc4"}, + {file = "thinc_apple_ops-0.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fa9c53717b00f49de091046100e69c03556e209902c07eb50d0d4bbd26ba0f5d"}, + {file = "thinc_apple_ops-0.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9d7864bbee3e13a6c91ec59634810378d2dd11b808380ee40c11b3eb11aae1c6"}, + {file = "thinc_apple_ops-0.1.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f203451c5c0dead351cd7fdc495afab6f7b6f40e27651e9f1d5f8839be127210"}, + {file = "thinc_apple_ops-0.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:31533f28e1af497edb246afa88cd7287ff49a365a1a21dfab2c6d2b19aea3990"}, + {file = "thinc_apple_ops-0.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:60cd002ceb2406991dda5adedc79e4494e19ba45a72d01c53124ecc86a00861f"}, + {file = "thinc_apple_ops-0.1.4.tar.gz", hash = "sha256:992dfc4805ab964131c9d9c71bcc4330becabe41916665ec424c035311347d05"}, +] + +[package.dependencies] +numpy = ">=1.21.0" +thinc = ">=8.1.0,<9.1.0" + +[[package]] +name = "tokenizers" +version = "0.13.3" +description = "Fast and Customizable Tokenizers" +optional = false +python-versions = "*" +files = [ + {file = "tokenizers-0.13.3-cp310-cp310-macosx_10_11_x86_64.whl", hash = "sha256:f3835c5be51de8c0a092058a4d4380cb9244fb34681fd0a295fbf0a52a5fdf33"}, + {file = "tokenizers-0.13.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4ef4c3e821730f2692489e926b184321e887f34fb8a6b80b8096b966ba663d07"}, + {file = "tokenizers-0.13.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5fd1a6a25353e9aa762e2aae5a1e63883cad9f4e997c447ec39d071020459bc"}, + {file = "tokenizers-0.13.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee0b1b311d65beab83d7a41c56a1e46ab732a9eed4460648e8eb0bd69fc2d059"}, + {file = "tokenizers-0.13.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ef4215284df1277dadbcc5e17d4882bda19f770d02348e73523f7e7d8b8d396"}, + {file = "tokenizers-0.13.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4d53976079cff8a033f778fb9adca2d9d69d009c02fa2d71a878b5f3963ed30"}, + {file = "tokenizers-0.13.3-cp310-cp310-win32.whl", hash = "sha256:1f0e3b4c2ea2cd13238ce43548959c118069db7579e5d40ec270ad77da5833ce"}, + {file = "tokenizers-0.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:89649c00d0d7211e8186f7a75dfa1db6996f65edce4b84821817eadcc2d3c79e"}, + {file = "tokenizers-0.13.3-cp311-cp311-macosx_10_11_universal2.whl", hash = "sha256:56b726e0d2bbc9243872b0144515ba684af5b8d8cd112fb83ee1365e26ec74c8"}, + {file = "tokenizers-0.13.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:cc5c022ce692e1f499d745af293ab9ee6f5d92538ed2faf73f9708c89ee59ce6"}, + {file = "tokenizers-0.13.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55c981ac44ba87c93e847c333e58c12abcbb377a0c2f2ef96e1a266e4184ff2"}, + {file = "tokenizers-0.13.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f247eae99800ef821a91f47c5280e9e9afaeed9980fc444208d5aa6ba69ff148"}, + {file = "tokenizers-0.13.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b3e3215d048e94f40f1c95802e45dcc37c5b05eb46280fc2ccc8cd351bff839"}, + {file = "tokenizers-0.13.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ba2b0bf01777c9b9bc94b53764d6684554ce98551fec496f71bc5be3a03e98b"}, + {file = "tokenizers-0.13.3-cp311-cp311-win32.whl", hash = "sha256:cc78d77f597d1c458bf0ea7c2a64b6aa06941c7a99cb135b5969b0278824d808"}, + {file = "tokenizers-0.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:ecf182bf59bd541a8876deccf0360f5ae60496fd50b58510048020751cf1724c"}, + {file = "tokenizers-0.13.3-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:0527dc5436a1f6bf2c0327da3145687d3bcfbeab91fed8458920093de3901b44"}, + {file = "tokenizers-0.13.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07cbb2c307627dc99b44b22ef05ff4473aa7c7cc1fec8f0a8b37d8a64b1a16d2"}, + {file = "tokenizers-0.13.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4560dbdeaae5b7ee0d4e493027e3de6d53c991b5002d7ff95083c99e11dd5ac0"}, + {file = "tokenizers-0.13.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64064bd0322405c9374305ab9b4c07152a1474370327499911937fd4a76d004b"}, + {file = "tokenizers-0.13.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8c6e2ab0f2e3d939ca66aa1d596602105fe33b505cd2854a4c1717f704c51de"}, + {file = "tokenizers-0.13.3-cp37-cp37m-win32.whl", hash = "sha256:6cc29d410768f960db8677221e497226e545eaaea01aa3613fa0fdf2cc96cff4"}, + {file = "tokenizers-0.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fc2a7fdf864554a0dacf09d32e17c0caa9afe72baf9dd7ddedc61973bae352d8"}, + {file = "tokenizers-0.13.3-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:8791dedba834c1fc55e5f1521be325ea3dafb381964be20684b92fdac95d79b7"}, + {file = "tokenizers-0.13.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:d607a6a13718aeb20507bdf2b96162ead5145bbbfa26788d6b833f98b31b26e1"}, + {file = "tokenizers-0.13.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3791338f809cd1bf8e4fee6b540b36822434d0c6c6bc47162448deee3f77d425"}, + {file = "tokenizers-0.13.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2f35f30e39e6aab8716f07790f646bdc6e4a853816cc49a95ef2a9016bf9ce6"}, + {file = "tokenizers-0.13.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310204dfed5aa797128b65d63538a9837cbdd15da2a29a77d67eefa489edda26"}, + {file = "tokenizers-0.13.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0f9b92ea052305166559f38498b3b0cae159caea712646648aaa272f7160963"}, + {file = "tokenizers-0.13.3-cp38-cp38-win32.whl", hash = "sha256:9a3fa134896c3c1f0da6e762d15141fbff30d094067c8f1157b9fdca593b5806"}, + {file = "tokenizers-0.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:8e7b0cdeace87fa9e760e6a605e0ae8fc14b7d72e9fc19c578116f7287bb873d"}, + {file = "tokenizers-0.13.3-cp39-cp39-macosx_10_11_x86_64.whl", hash = "sha256:00cee1e0859d55507e693a48fa4aef07060c4bb6bd93d80120e18fea9371c66d"}, + {file = "tokenizers-0.13.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:a23ff602d0797cea1d0506ce69b27523b07e70f6dda982ab8cf82402de839088"}, + {file = "tokenizers-0.13.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ce07445050b537d2696022dafb115307abdffd2a5c106f029490f84501ef97"}, + {file = "tokenizers-0.13.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:280ffe95f50eaaf655b3a1dc7ff1d9cf4777029dbbc3e63a74e65a056594abc3"}, + {file = "tokenizers-0.13.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97acfcec592f7e9de8cadcdcda50a7134423ac8455c0166b28c9ff04d227b371"}, + {file = "tokenizers-0.13.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7730c98a3010cd4f523465867ff95cd9d6430db46676ce79358f65ae39797b"}, + {file = "tokenizers-0.13.3-cp39-cp39-win32.whl", hash = "sha256:48625a108029cb1ddf42e17a81b5a3230ba6888a70c9dc14e81bc319e812652d"}, + {file = "tokenizers-0.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:bc0a6f1ba036e482db6453571c9e3e60ecd5489980ffd95d11dc9f960483d783"}, + {file = "tokenizers-0.13.3.tar.gz", hash = "sha256:2e546dbb68b623008a5442353137fbb0123d311a6d7ba52f2667c8862a75af2e"}, +] + +[package.extras] +dev = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"] +docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] +testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.2-py3-none-any.whl", hash = "sha256:eeea7ac7563faeab0a1ed8fe12c2e5a51c61f933f2502f7e9db0241a65163ad0"}, + {file = "tomlkit-0.12.2.tar.gz", hash = "sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977"}, +] + +[[package]] +name = "torch" +version = "1.12.1" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "torch-1.12.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:9c038662db894a23e49e385df13d47b2a777ffd56d9bcd5b832593fab0a7e286"}, + {file = "torch-1.12.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:4e1b9c14cf13fd2ab8d769529050629a0e68a6fc5cb8e84b4a3cc1dd8c4fe541"}, + {file = "torch-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:e9c8f4a311ac29fc7e8e955cfb7733deb5dbe1bdaabf5d4af2765695824b7e0d"}, + {file = "torch-1.12.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:976c3f997cea38ee91a0dd3c3a42322785414748d1761ef926b789dfa97c6134"}, + {file = "torch-1.12.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:68104e4715a55c4bb29a85c6a8d57d820e0757da363be1ba680fa8cc5be17b52"}, + {file = "torch-1.12.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:743784ccea0dc8f2a3fe6a536bec8c4763bd82c1352f314937cb4008d4805de1"}, + {file = "torch-1.12.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b5dbcca369800ce99ba7ae6dee3466607a66958afca3b740690d88168752abcf"}, + {file = "torch-1.12.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f3b52a634e62821e747e872084ab32fbcb01b7fa7dbb7471b6218279f02a178a"}, + {file = "torch-1.12.1-cp37-none-macosx_10_9_x86_64.whl", hash = "sha256:8a34a2fbbaa07c921e1b203f59d3d6e00ed379f2b384445773bd14e328a5b6c8"}, + {file = "torch-1.12.1-cp37-none-macosx_11_0_arm64.whl", hash = "sha256:42f639501928caabb9d1d55ddd17f07cd694de146686c24489ab8c615c2871f2"}, + {file = "torch-1.12.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0b44601ec56f7dd44ad8afc00846051162ef9c26a8579dda0a02194327f2d55e"}, + {file = "torch-1.12.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:cd26d8c5640c3a28c526d41ccdca14cf1cbca0d0f2e14e8263a7ac17194ab1d2"}, + {file = "torch-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:42e115dab26f60c29e298559dbec88444175528b729ae994ec4c65d56fe267dd"}, + {file = "torch-1.12.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:a8320ba9ad87e80ca5a6a016e46ada4d1ba0c54626e135d99b2129a4541c509d"}, + {file = "torch-1.12.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:03e31c37711db2cd201e02de5826de875529e45a55631d317aadce2f1ed45aa8"}, + {file = "torch-1.12.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:9b356aea223772cd754edb4d9ecf2a025909b8615a7668ac7d5130f86e7ec421"}, + {file = "torch-1.12.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:6cf6f54b43c0c30335428195589bd00e764a6d27f3b9ba637aaa8c11aaf93073"}, + {file = "torch-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:f00c721f489089dc6364a01fd84906348fe02243d0af737f944fddb36003400d"}, + {file = "torch-1.12.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:bfec2843daa654f04fda23ba823af03e7b6f7650a873cdb726752d0e3718dada"}, + {file = "torch-1.12.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:69fe2cae7c39ccadd65a123793d30e0db881f1c1927945519c5c17323131437e"}, +] + +[package.dependencies] +typing-extensions = "*" + +[[package]] +name = "tqdm" +version = "4.66.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "transformers" +version = "4.30.2" +description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "transformers-4.30.2-py3-none-any.whl", hash = "sha256:c332e3a3097f9ed89ce556b403251235931c00237b8bc2d7adaa19d226c13f1d"}, + {file = "transformers-4.30.2.tar.gz", hash = "sha256:f4a8aac4e1baffab4033f4a345b0d7dc7957d12a4f1ba969afea08205a513045"}, +] + +[package.dependencies] +filelock = "*" +huggingface-hub = ">=0.14.1,<1.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +numpy = ">=1.17" +packaging = ">=20.0" +pyyaml = ">=5.1" +regex = "!=2019.12.17" +requests = "*" +safetensors = ">=0.3.1" +tokenizers = ">=0.11.1,<0.11.3 || >0.11.3,<0.14" +tqdm = ">=4.27" + +[package.extras] +accelerate = ["accelerate (>=0.20.2)"] +agents = ["Pillow", "accelerate (>=0.20.2)", "datasets (!=2.5.0)", "diffusers", "opencv-python", "sentencepiece (>=0.1.91,!=0.1.92)", "torch (>=1.9,!=1.12.0)"] +all = ["Pillow", "accelerate (>=0.20.2)", "av (==9.2.0)", "codecarbon (==1.2.0)", "decord (==0.6.0)", "flax (>=0.4.1,<=0.6.9)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf (<=3.20.3)", "pyctcdecode (>=0.4.0)", "ray[tune]", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "torchaudio", "torchvision"] +audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +codecarbon = ["codecarbon (==1.2.0)"] +deepspeed = ["accelerate (>=0.20.2)", "deepspeed (>=0.8.3)"] +deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.20.2)", "beautifulsoup4", "black (>=23.1,<24.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.8.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "optuna", "parameterized", "protobuf (<=3.20.3)", "psutil", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "timeout-decorator"] +dev = ["GitPython (<3.1.19)", "Pillow", "accelerate (>=0.20.2)", "av (==9.2.0)", "beautifulsoup4", "black (>=23.1,<24.0)", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "decord (==0.6.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flax (>=0.4.1,<=0.6.9)", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "nltk", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "parameterized", "phonemizer", "protobuf (<=3.20.3)", "psutil", "pyctcdecode (>=0.4.0)", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "ray[tune]", "rhoknp (>=1.1.0,<1.3.1)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (>=0.0.241,<=0.0.259)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx", "timeout-decorator", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] +dev-tensorflow = ["GitPython (<3.1.19)", "Pillow", "beautifulsoup4", "black (>=23.1,<24.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "nltk", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf (<=3.20.3)", "psutil", "pyctcdecode (>=0.4.0)", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (>=0.0.241,<=0.0.259)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx", "timeout-decorator", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "urllib3 (<2.0.0)"] +dev-torch = ["GitPython (<3.1.19)", "Pillow", "accelerate (>=0.20.2)", "beautifulsoup4", "black (>=23.1,<24.0)", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "librosa", "nltk", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf (<=3.20.3)", "psutil", "pyctcdecode (>=0.4.0)", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "ray[tune]", "rhoknp (>=1.1.0,<1.3.1)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (>=0.0.241,<=0.0.259)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "timeout-decorator", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] +docs = ["Pillow", "accelerate (>=0.20.2)", "av (==9.2.0)", "codecarbon (==1.2.0)", "decord (==0.6.0)", "flax (>=0.4.1,<=0.6.9)", "hf-doc-builder", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf (<=3.20.3)", "pyctcdecode (>=0.4.0)", "ray[tune]", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "torchaudio", "torchvision"] +docs-specific = ["hf-doc-builder"] +fairscale = ["fairscale (>0.3)"] +flax = ["flax (>=0.4.1,<=0.6.9)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "optax (>=0.0.8,<=0.1.4)"] +flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +ftfy = ["ftfy"] +integrations = ["optuna", "ray[tune]", "sigopt"] +ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0,<1.3.1)", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"] +modelcreation = ["cookiecutter (==1.7.3)"] +natten = ["natten (>=0.14.6)"] +onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"] +onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"] +optuna = ["optuna"] +quality = ["GitPython (<3.1.19)", "black (>=23.1,<24.0)", "datasets (!=2.5.0)", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)", "ruff (>=0.0.241,<=0.0.259)", "urllib3 (<2.0.0)"] +ray = ["ray[tune]"] +retrieval = ["datasets (!=2.5.0)", "faiss-cpu"] +sagemaker = ["sagemaker (>=2.31.0)"] +sentencepiece = ["protobuf (<=3.20.3)", "sentencepiece (>=0.1.91,!=0.1.92)"] +serving = ["fastapi", "pydantic", "starlette", "uvicorn"] +sigopt = ["sigopt"] +sklearn = ["scikit-learn"] +speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] +testing = ["GitPython (<3.1.19)", "beautifulsoup4", "black (>=23.1,<24.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "parameterized", "protobuf (<=3.20.3)", "psutil", "pytest (>=7.2.0)", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "timeout-decorator"] +tf = ["keras-nlp (>=0.3.1)", "onnxconverter-common", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx"] +tf-cpu = ["keras-nlp (>=0.3.1)", "onnxconverter-common", "tensorflow-cpu (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx"] +tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +timm = ["timm"] +tokenizers = ["tokenizers (>=0.11.1,!=0.11.3,<0.14)"] +torch = ["accelerate (>=0.20.2)", "torch (>=1.9,!=1.12.0)"] +torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] +torch-vision = ["Pillow", "torchvision"] +torchhub = ["filelock", "huggingface-hub (>=0.14.1,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf (<=3.20.3)", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "tqdm (>=4.27)"] +video = ["av (==9.2.0)", "decord (==0.6.0)"] +vision = ["Pillow"] + +[[package]] +name = "typed-ast" +version = "1.5.5" +description = "a fork of Python 2 and 3 ast modules with type comment support" +optional = false +python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, + {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, + {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, + {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, + {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, + {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, + {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, + {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, + {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, + {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, + {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, +] + +[[package]] +name = "typer" +version = "0.4.2" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.6" +files = [ + {file = "typer-0.4.2-py3-none-any.whl", hash = "sha256:023bae00d1baf358a6cc7cea45851639360bb716de687b42b0a4641cd99173f1"}, + {file = "typer-0.4.2.tar.gz", hash = "sha256:b8261c6c0152dd73478b5ba96ba677e5d6948c715c310f7c91079f311f62ec03"}, +] + +[package.dependencies] +click = ">=7.1.1,<9.0.0" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "validators" +version = "0.20.0" +description = "Python Data Validation for Humans™." +optional = false +python-versions = ">=3.4" +files = [ + {file = "validators-0.20.0.tar.gz", hash = "sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a"}, +] + +[package.dependencies] +decorator = ">=3.4.0" + +[package.extras] +test = ["flake8 (>=2.4.0)", "isort (>=4.2.2)", "pytest (>=2.2.3)"] + +[[package]] +name = "verspec" +version = "0.1.0" +description = "Flexible version handling" +optional = false +python-versions = "*" +files = [ + {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, + {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, +] + +[package.extras] +test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] + +[[package]] +name = "vine" +version = "5.1.0" +description = "Python promises." +optional = false +python-versions = ">=3.6" +files = [ + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, +] + +[[package]] +name = "virtualenv" +version = "20.21.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.21.1-py3-none-any.whl", hash = "sha256:09ddbe1af0c8ed2bb4d6ed226b9e6415718ad18aef9fa0ba023d96b7a8356049"}, + {file = "virtualenv-20.21.1.tar.gz", hash = "sha256:4c104ccde994f8b108163cf9ba58f3d11511d9403de87fb9b4f52bf33dbc8668"}, +] + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} +platformdirs = ">=2.4,<4" + +[package.extras] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "voluptuous" +version = "0.14.0" +description = "Python data validation library" +optional = false +python-versions = "*" +files = [ + {file = "voluptuous-0.14.0-py3-none-any.whl", hash = "sha256:f9d63353f270c87d5f3aea29f8c251beddfd164d74d934e54dad7b8f84ad7a74"}, + {file = "voluptuous-0.14.0.tar.gz", hash = "sha256:145384a9613f7520b70e214e5c06de0809e069ab56716685446450f9bee15e2e"}, +] + +[[package]] +name = "wasabi" +version = "1.1.2" +description = "A lightweight console printing and formatting toolkit" +optional = false +python-versions = ">=3.6" +files = [ + {file = "wasabi-1.1.2-py3-none-any.whl", hash = "sha256:0a3f933c4bf0ed3f93071132c1b87549733256d6c8de6473c5f7ed2e171b5cf9"}, + {file = "wasabi-1.1.2.tar.gz", hash = "sha256:1aaef3aceaa32edb9c91330d29d3936c0c39fdb965743549c173cb54b16c30b5"}, +] + +[package.dependencies] +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\" and python_version >= \"3.7\""} +typing-extensions = {version = ">=3.7.4.1,<4.5.0", markers = "python_version < \"3.8\""} + +[[package]] +name = "watchdog" +version = "3.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.7" +files = [ + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, + {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, + {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, + {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, + {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, + {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, + {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, + {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, + {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.10" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.10-py2.py3-none-any.whl", hash = "sha256:aec5179002dd0f0d40c456026e74a729661c9d468e1ed64405e3a6c2176ca36f"}, + {file = "wcwidth-0.2.10.tar.gz", hash = "sha256:390c7454101092a6a5e43baad8f83de615463af459201709556b6e4b1c861f97"}, +] + +[[package]] +name = "weasel" +version = "0.3.4" +description = "Weasel: A small and easy workflow system" +optional = false +python-versions = ">=3.6" +files = [ + {file = "weasel-0.3.4-py3-none-any.whl", hash = "sha256:ee48a944f051d007201c2ea1661d0c41035028c5d5a8bcb29a0b10f1100206ae"}, + {file = "weasel-0.3.4.tar.gz", hash = "sha256:eb16f92dc9f1a3ffa89c165e3a9acd28018ebb656e0da4da02c0d7d8ae3f6178"}, +] + +[package.dependencies] +cloudpathlib = ">=0.7.0,<0.17.0" +confection = ">=0.0.4,<0.2.0" +packaging = ">=20.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +requests = ">=2.13.0,<3.0.0" +smart-open = ">=5.2.1,<7.0.0" +srsly = ">=2.4.3,<3.0.0" +typer = ">=0.3.0,<0.10.0" +wasabi = ">=0.9.1,<1.2.0" + +[[package]] +name = "wheel" +version = "0.41.3" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "wheel-0.41.3-py3-none-any.whl", hash = "sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942"}, + {file = "wheel-0.41.3.tar.gz", hash = "sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[[package]] +name = "yarl" +version = "1.9.2" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, + {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, + {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, + {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, + {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, + {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, + {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, + {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, + {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, + {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, + {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, + {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, + {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[[package]] +name = "zc-lockfile" +version = "3.0.post1" +description = "Basic inter-process locks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zc.lockfile-3.0.post1-py3-none-any.whl", hash = "sha256:ddb2d71088c061dc8a5edbaa346b637d742ca1e1564be75cb98e7dcae715de19"}, + {file = "zc.lockfile-3.0.post1.tar.gz", hash = "sha256:adb2ee6d9e6a2333c91178dcb2c9b96a5744c78edb7712dc784a7d75648e81ec"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +test = ["zope.testing"] + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[extras] +apple = ["thinc-apple-ops"] +cuda = ["cupy"] +cuda100 = ["cupy-cuda100"] +cuda101 = ["cupy-cuda101"] +cuda102 = ["cupy-cuda102"] +cuda110 = ["cupy-cuda110"] +cuda111 = ["cupy-cuda111"] +cuda112 = ["cupy-cuda112"] +cuda113 = ["cupy-cuda113"] +cuda114 = ["cupy-cuda114"] +cuda115 = ["cupy-cuda115"] +cuda116 = ["cupy-cuda116"] +cuda117 = ["cupy-cuda117"] +cuda80 = ["cupy-cuda80"] +cuda90 = ["cupy-cuda90"] +cuda91 = ["cupy-cuda91"] +cuda92 = ["cupy-cuda92"] + +[metadata] +lock-version = "2.0" +python-versions = ">3.7.6,<4.0,!=3.8.1" +content-hash = "bb2663c5bce60bd6807b15a6a890186b8b46eba3c330f52ef71e004dbe903a03" diff --git a/NER_model/project.lock b/NER_model/project.lock new file mode 100644 index 000000000..e380c9cdd --- /dev/null +++ b/NER_model/project.lock @@ -0,0 +1,108 @@ +convert: + cmd: python -m spacy run convert + script: + - python scripts/convert.py --lang eds --input-path data/NLP_diabeto/train --output-path + corpus/train.spacy + - python scripts/convert.py --lang eds --input-path data/NLP_diabeto/val --output-path + corpus/dev.spacy + - python scripts/convert.py --lang eds --input-path data/NLP_diabeto/test --output-path + corpus/test.spacy + deps: + - path: data/NLP_diabeto/train + md5: 30f19f2f900601d4ba8be1732ff81279 + - path: data/NLP_diabeto/val + md5: 98194594382fa1039782f651ad0110af + - path: data/NLP_diabeto/test + md5: ddec584923d06f2685a232976a5a0eca + - path: scripts/convert.py + md5: 951ecd34bc4ef64d218a90225a5c2aca + outs: + - path: corpus/train.spacy + md5: bd6c7c646cf288ee49f855177841c399 + - path: corpus/dev.spacy + md5: 97ad6c031ae1b3522131d2675a1464a0 + - path: corpus/test.spacy + md5: 0812cf1d19ab475be07eb65162e0ce2c + spacy_version: 3.5.1 + spacy_git_version: Unknown +train: + cmd: python -m spacy run train + script: + - python -m spacy train configs/config.cfg --output training --paths.train corpus/train.spacy + --paths.dev corpus/dev.spacy --nlp.lang eds --gpu-id 0 + deps: + - path: configs/config.cfg + md5: 0714cebf0e60346234c2a244d1924198 + - path: corpus/train.spacy + md5: bd6c7c646cf288ee49f855177841c399 + - path: corpus/dev.spacy + md5: 97ad6c031ae1b3522131d2675a1464a0 + outs: + - path: training/model-best + md5: dc2f4b221dc791390bc17c84aeae714d + spacy_version: 3.5.1 + spacy_git_version: Unknown +evaluate: + cmd: python -m spacy run evaluate + script: + - python scripts/evaluate.py training/model-best corpus/test.spacy --output training/test_metrics.json + --docbin corpus/output.spacy --gpu-id 0 + deps: + - path: corpus/test.spacy + md5: 0812cf1d19ab475be07eb65162e0ce2c + - path: training/model-best + md5: dc2f4b221dc791390bc17c84aeae714d + outs: + - path: corpus/output.spacy + md5: aa7d595acc52e9ff79323f24fd0f106c + - path: training/test_metrics.json + md5: b7c84305723d1917301506545e41f5db + spacy_version: 3.5.1 + spacy_git_version: Unknown +package: + cmd: python -m spacy run package + script: + - python scripts/package.py training/model-best packages --name medic --version + 0.1.0 --force --build wheel --code eds_medic + deps: + - path: training/model-best + md5: dc2f4b221dc791390bc17c84aeae714d + outs: + - path: packages/eds_medic-0.1.0/dist/eds_medic-0.1.0-py3-none-any.whl + md5: 1b491e3d4ce4f7822f381adc6525f0ba + spacy_version: 3.5.1 + spacy_git_version: Unknown +save: + cmd: python -m spacy run save + script: + - python scripts/save_to_brat.py training/model-best corpus/test.spacy --output + training/test_metrics.json --docbin corpus/output.spacy --gpu-id 0 + deps: + - path: corpus/test.spacy + md5: 6f167a761337976d4cc34309a1dccbf2 + - path: training/model-best + md5: 36b40cd2d45edf99e9bc14b4e8080b17 + outs: + - path: corpus/output.spacy + md5: null + - path: training/test_metrics.json + md5: null + spacy_version: 3.5.1 + spacy_git_version: Unknown +save_to_brat: + cmd: python -m spacy run save_to_brat + script: + - python scripts/save_to_brat.py training/model-best corpus/test.spacy --output + training/test_metrics.json --docbin corpus/output.spacy --gpu-id 0 + deps: + - path: corpus/test.spacy + md5: a5390dbbaa344bac5c71c7cdc3141849 + - path: training/model-best + md5: c9319bca2767deb0868c641da5302bb5 + outs: + - path: corpus/output.spacy + md5: 25794a6a530648bd92bd82882de9e730 + - path: training/test_metrics.json + md5: de7b607948f789e8069088592314eb0d + spacy_version: 3.5.1 + spacy_git_version: Unknown diff --git a/NER_model/project.yml b/NER_model/project.yml new file mode 100644 index 000000000..4b305f59a --- /dev/null +++ b/NER_model/project.yml @@ -0,0 +1,138 @@ +title: "EDS-Medic" +description: | + EDS-Medic is a spaCy-based project used at APHP to extract drug prescriptions from clinical reports + + To run the full pipeline (download, split and format the dataset, train the pipeline and package it), simply run : + ```shell + spacy project run all + ``` + +# Variables can be referenced across the project.yml using ${vars.var_name} +vars: + name: "medic" + lang: "eds" + version: "0.1.0" + train: "data/NLP_diabeto/train" + test: "data/NLP_diabeto/test" + dev: "data/NLP_diabeto/val" +# jeu suyr lequel tester le modèle : +# brat_data/QUAERO_FrenchMed/corpus/train/EMEA/ + + corpus: "corpus" + training: "training" + seed: 0 + fraction: 200 + gpu_id: 0 + +env: + registry_token: GITLAB_REGISTRY_TOKEN + +# These are the directories that the project needs. The project CLI will make +# sure that they always exist. +directories: + ["data", "corpus", "configs", "training", "scripts", "packages", "output"] + +# Workflows are sequences of commands (see below) executed in order. You can +# run them via "spacy project run [workflow]". If a commands's inputs/outputs +# haven't changed, it won't be re-run. +workflows: + all: + - train + - evaluate + - package + xp: + - convert + - train + - evaluate + +# Project commands, specified in a style similar to CI config files (e.g. Azure +# pipelines). The name is the command name that lets you trigger the command +# via "spacy project run [command] [path]". The help message is optional and +# shown when executing "spacy project run [optional command] [path] --help". +commands: + - name: "convert" + help: "Convert the data to spaCy's binary format" + script: + - "python scripts/convert.py --lang ${vars.lang} --input-path ${vars.train} --output-path ${vars.corpus}/train.spacy" + - "python scripts/convert.py --lang ${vars.lang} --input-path ${vars.dev} --output-path ${vars.corpus}/dev.spacy" + - "python scripts/convert.py --lang ${vars.lang} --input-path ${vars.test} --output-path ${vars.corpus}/test.spacy" + deps: + - "${vars.train}" + - "${vars.dev}" + - "${vars.test}" + - "scripts/convert.py" + outputs: + - "${vars.corpus}/train.spacy" + - "${vars.corpus}/dev.spacy" + - "${vars.corpus}/test.spacy" + + - name: "create-config" + help: "Create a new config with an NER pipeline component" + script: + - "python -m spacy init config --lang ${vars.lang} --pipeline ner configs/config.cfg --force --gpu" + outputs: + - "configs/config.cfg" + + - name: "train" + help: "Train the NER model" + script: + - "python -m spacy train configs/config.cfg --output ${vars.training} --paths.train ${vars.corpus}/train.spacy --paths.dev ${vars.corpus}/dev.spacy --nlp.lang ${vars.lang} --gpu-id ${vars.gpu_id}" + deps: + - "configs/config.cfg" + - "${vars.corpus}/train.spacy" + - "${vars.corpus}/dev.spacy" + outputs: + - "${vars.training}/model-best" + + # - name: "evaluate" + # help: "Evaluate the model and export metrics" + # script: + # - "python scripts/new_evaluate.py ${vars.training}/model-best ${vars.corpus}/test.spacy --output ${vars.training}/test_metrics.json --docbin ${vars.corpus}/output.spacy --gpu-id ${vars.gpu_id}" + # deps: + # - "${vars.corpus}/test.spacy" + # - "${vars.training}/model-best" + # outputs: + # - "${vars.corpus}/output.spacy" + # - "${vars.training}/test_metrics.json" + + - name: "evaluate" + help: "Evaluate the model and export metrics" + script: + - "python scripts/evaluate.py ${vars.training}/model-best ${vars.corpus}/test.spacy --output ${vars.training}/test_metrics.json --docbin ${vars.corpus}/output.spacy --gpu-id ${vars.gpu_id}" + deps: + - "${vars.corpus}/test.spacy" + - "${vars.training}/model-best" + outputs: + - "${vars.corpus}/output.spacy" + - "${vars.training}/test_metrics.json" + + + - name: "infer" + help: "Infer the model on test documents" + script: + - "python scripts/infer.py --model ${vars.training}/model-best --data ${vars.corpus}/test.spacy --output ${vars.corpus}/output.spacy" + deps: + - "${vars.corpus}/test.spacy" + - "${vars.training}/model-best" + outputs: + - "${vars.corpus}/output.spacy" + + - name: "package" + help: "Package the trained model as a pip package" + script: + - "python scripts/package.py ${vars.training}/model-best packages --name ${vars.name} --version ${vars.version} --force --build wheel --code eds_medic" + deps: + - "${vars.training}/model-best" + outputs_no_cache: + - "packages/${vars.lang}_${vars.name}-${vars.version}/dist/${vars.lang}_${vars.name}-${vars.version}-py3-none-any.whl" + + - name: "save_to_brat" + help: "Save the test set with prediction on a BRAT format" + script: + - "python scripts/save_to_brat.py ${vars.training}/model-best ${vars.corpus}/test.spacy --output ${vars.training}/test_metrics.json --docbin ${vars.corpus}/output.spacy --gpu-id ${vars.gpu_id}" + deps: + - "${vars.corpus}/test.spacy" + - "${vars.training}/model-best" + outputs: + - "${vars.corpus}/output.spacy" + - "${vars.training}/test_metrics.json" diff --git a/NER_model/pyproject.toml b/NER_model/pyproject.toml new file mode 100644 index 000000000..8c4b8de76 --- /dev/null +++ b/NER_model/pyproject.toml @@ -0,0 +1,121 @@ +[tool.poetry] +name = "NER_model" +version = "0.1.0" +description = "" +authors = ["Perceval Wajsburt "] + +[tool.poetry.dependencies] +python = ">3.7.6,<4.0,!=3.8.1" +spacy = "^3.2.4" +spacy-transformers = "^1.1.5" +thinc = "^8.0.13" +sentencepiece = "^0.1.96" +cupy = {version = "^11.0.0", optional = true} +cupy-cuda80 = {version = "^7.8.0", optional = true} +cupy-cuda90 = {version = "^8.6.0", optional = true} +cupy-cuda91 = {version = "^7.8.0", optional = true} +cupy-cuda92 = {version = "^8.6.0", optional = true} +cupy-cuda100 = {version = "^9.6.0", optional = true} +cupy-cuda101 = {version = "^9.6.0", optional = true} +cupy-cuda102 = {version = "^11.0.0", optional = true} +cupy-cuda110 = {version = "^11.0.0", optional = true} +cupy-cuda111 = {version = "^10.6.0", optional = true} +cupy-cuda112 = {version = "^10.6.0", optional = true} +cupy-cuda113 = {version = "^10.6.0", optional = true} +cupy-cuda114 = {version = "^10.6.0", optional = true} +cupy-cuda115 = {version = "^10.6.0", optional = true} +cupy-cuda116 = {version = "^10.6.0", optional = true} +cupy-cuda117 = {version = "^10.6.0", optional = true} +thinc-apple-ops = {version = "^0.1.0", optional = true} +torch = "<1.13" + +[tool.poetry.group.dev.dependencies] +dvc = {version = "^2.37.0", markers="python_version >= '3.8'"} +loguru = "^0.6.0" +typer = "^0.4.1" +fsspec = "^2022.3.0" +pre-commit = "^2.18.1" +pytest = "^7.1.1" +pytest-cov = "^3.0.0" +mypy = "^0.950" +coverage = "^6.5.0" +jupyter_black = "0.3.4" + +[tool.poetry.group.docs] +optional = true + +[tool.poetry.group.docs.dependencies] +mike = "^1.1.2" +mkdocs-bibtex = "^2.0.3" +mkdocs-gen-files = "^0.3.4" +mkdocs-literate-nav = "^0.4.1" +mkdocs-material = "^8.2.8" +mkdocstrings = "^0.18.1" +mkdocstrings-python = "^0.6.6" +mkdocs-glightbox = "^0.1.6" +mkdocs-autorefs = "^0.4.1" + +[tool.poetry.extras] +cuda = ["cupy"] +cuda80 = ["cupy-cuda80"] +cuda90 = ["cupy-cuda90"] +cuda91 = ["cupy-cuda91"] +cuda92 = ["cupy-cuda92"] +cuda100 = ["cupy-cuda100"] +cuda101 = ["cupy-cuda101"] +cuda102 = ["cupy-cuda102"] +cuda110 = ["cupy-cuda110"] +cuda111 = ["cupy-cuda111"] +cuda112 = ["cupy-cuda112"] +cuda113 = ["cupy-cuda113"] +cuda114 = ["cupy-cuda114"] +cuda115 = ["cupy-cuda115"] +cuda116 = ["cupy-cuda116"] +cuda117 = ["cupy-cuda117"] +apple = ["thinc-apple-ops"] + +[tool.poetry.group.spark] +optional = true + +[tool.poetry.group.spark.dependencies] +pyspark = "2.4.3" + +[tool.interrogate] +ignore-init-method = true +ignore-init-module = true +ignore-magic = false +ignore-semiprivate = true +ignore-private = true +ignore-property-decorators = false +ignore-module = true +ignore-nested-functions = true +ignore-nested-classes = true +ignore-setters = false +fail-under = 10 +exclude = ["docs", "build", "tests", "packages", "setup.py"] +verbose = 0 +quiet = false +whitelist-regex = [] +color = true +omit-covered-files = false + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] + +[tool.mypy] +plugins = "pydantic.mypy" + +[tool.ruff] +exclude = [".git", "__pycache__", "__init__.py", ".mypy_cache", ".pytest_cache", ".venv", "build", "packages"] +ignore = [] +line-length = 88 +select = ["E", "F", "W"] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] + +[build-system] +requires = ["poetry-core>=1.0.0", "pypandoc<1.8.0"] +build-backend = "poetry.core.masonry.api" diff --git a/NER_model/scripts/convert.py b/NER_model/scripts/convert.py new file mode 100644 index 000000000..6cae21498 --- /dev/null +++ b/NER_model/scripts/convert.py @@ -0,0 +1,153 @@ +import os +from pathlib import Path +from typing import Dict, List, Union + +import spacy +import srsly +import typer +from edsnlp.connectors.brat import BratConnector +from spacy.language import Language +from spacy.tokens import Doc, DocBin +from spacy.util import filter_spans + +if not Doc.has_extension("context"): + Doc.set_extension("context", default=dict()) +if not Doc.has_extension("note_id"): + Doc.set_extension("note_id", default=None) +if not Doc.has_extension("note_datetime"): + Doc.set_extension("note_datetime", default=None) +if not Doc.has_extension("note_class_source_value"): + Doc.set_extension("note_class_source_value", default=None) +if not Doc.has_extension("split"): + Doc.set_extension("split", default=None) + + +def add_entities(doc: Doc, entities: List[Dict[str, Union[int, str]]]): + """ + Add annotations as Doc entities, re-tokenizing the document if need be. + + Parameters + ---------- + doc : Doc + spaCy Doc object + entities : List[Dict[str, Union[int, str]]] + List of annotations. + """ + + ents = [] + + for entity in entities: + start, end, label = entity["start"], entity["end"], entity["label"] + span = doc.char_span(start, end, label=label, alignment_mode="expand") + if span: + ents.append(span) + + doc.ents = filter_spans(ents) + + +def get_nlp(lang: str) -> Language: + nlp = spacy.blank(lang) + nlp.add_pipe("sentencizer") + + return nlp + + +def convert_jsonl( + nlp: spacy.Language, + input_path: Path, + n_limit: int, +) -> spacy.tokens.DocBin: + db = DocBin(store_user_data=True) + + if n_limit is not None: + nb_docs = 0 + for annot in srsly.read_jsonl(input_path): + if n_limit is not None: + if nb_docs >= n_limit: + break + nb_docs += 1 + ( + text, + note_id, + # note_datetime, + note_class_source_value, + entities, + context, + split, + ) = ( + annot["note_text"], + annot["note_id"], + # annot["note_datetime"], + annot["note_class_source_value"], + annot.get("entities", []), + annot.get("context", {}), + annot.get("split", None), + ) + + doc = nlp(text) + doc._.note_id = note_id + # doc._.note_datetime = note_datetime + doc._.note_class_source_value = note_class_source_value + doc._.context = context + doc._.split = split + + add_entities(doc, entities) + + db.add(doc) + + + return db + + +def convert_brat( + nlp: spacy.Language, + input_path: Path, + n_limit: int, +) -> spacy.tokens.DocBin: + db = DocBin(store_user_data=True) + + connector = BratConnector(input_path) + docs = connector.brat2docs(nlp, run_pipe=True) + if n_limit is not None: + docs = docs[:n_limit] + for doc in docs: + if hasattr(doc, "text"): + db.add(doc) + + + return db + + +def convert( + lang: str = typer.Option( + "fr", + help="Language to use", + ), + input_path: Path = typer.Option( + ..., + help="Path to the JSONL file", + ), + output_path: Path = typer.Option( + ..., + help="Path to the output spacy DocBin", + ), + n_limit: int = typer.Option( + None, + help="Limit number of documents to convert", + ), +) -> None: + nlp = get_nlp(lang) + + if os.path.isdir(input_path): + db = convert_brat(nlp, input_path, n_limit) + else: + db = convert_jsonl(nlp, input_path, n_limit) + + typer.echo(f"The saved dataset contains {len(db)} documents.") + if not os.path.exists(output_path.parent): + os.makedirs(output_path.parent) + print(f"Folder: {output_path.parent} has been created") + db.to_disk(output_path) + +if __name__ == "__main__": + typer.run(convert) diff --git a/NER_model/scripts/evaluate.py b/NER_model/scripts/evaluate.py new file mode 100644 index 000000000..3f833d0a4 --- /dev/null +++ b/NER_model/scripts/evaluate.py @@ -0,0 +1,214 @@ +import re +from pathlib import Path +from typing import Any, Dict, Optional + +import srsly +import typer +from spacy import util +from spacy.cli._util import Arg, Opt, import_code, setup_gpu +from spacy.cli.evaluate import ( + print_prf_per_type, + print_textcats_auc_per_cat, + render_parses, +) +from spacy.tokens import DocBin +from thinc.api import fix_random_seed +from wasabi import Printer + +from edsnlp.corpus_reader import Corpus + + +# fmt: off +def evaluate_cli( + model: str = Arg(..., help="Model name or path"), # noqa: E501 + data_path: Path = Arg(..., help="Location of binary evaluation data in .spacy format", exists=True), # noqa: E501 + output: Optional[Path] = Opt(None, "--output", "-o", help="Output JSON file for metrics", dir_okay=False), # noqa: E501 + docbin: Optional[Path] = Opt(None, "--docbin", help="Output Doc Bin path", dir_okay=False), # noqa: E501 + code_path: Optional[Path] = Opt(None, "--code", "-c", help="Path to Python file with additional code (registered functions) to be imported"), # noqa: E501 + use_gpu: int = Opt(-1, "--gpu-id", "-g", help="GPU ID or -1 for CPU"), # noqa: E501 + gold_preproc: bool = Opt(False, "--gold-preproc", "-G", help="Use gold preprocessing"), # noqa: E501 + displacy_path: Optional[Path] = Opt(None, "--displacy-path", "-dp", help="Directory to output rendered parses as HTML", exists=True, file_okay=False), # noqa: E501 + displacy_limit: int = Opt(25, "--displacy-limit", "-dl", help="Limit of parses to render as HTML"), # noqa: E501 +): + # fmt: on + + """ + Evaluate a trained pipeline. Expects a loadable spaCy pipeline and evaluation + data in the binary .spacy format. The --gold-preproc option sets up the + evaluation examples with gold-standard sentences and tokens for the + predictions. Gold preprocessing helps the annotations align to the + tokenization, and may result in sequences of more consistent length. However, + it may reduce runtime accuracy due to train/test skew. To render a sample of + dependency parses in a HTML file, set as output directory as the + displacy_path argument. + DOCS: https://spacy.io/api/cli#evaluate + """ + import_code(code_path) + evaluate( + model, + data_path, + output=output, + docbin=docbin, + use_gpu=use_gpu, + gold_preproc=gold_preproc, + displacy_path=displacy_path, + displacy_limit=displacy_limit, + silent=False, + ) + + +def evaluate( + model: str, + data_path: Path, + output: Optional[Path] = None, + docbin: Optional[Path] = None, + use_gpu: int = -1, + gold_preproc: bool = False, + displacy_path: Optional[Path] = None, + displacy_limit: int = 25, + silent: bool = True, + spans_key: str = "sc", +) -> Dict[str, Any]: + msg = Printer(no_print=silent, pretty=not silent) + fix_random_seed() + setup_gpu(use_gpu, silent=silent) + data_path = util.ensure_path(data_path) + output_path = util.ensure_path(output) + displacy_path = util.ensure_path(displacy_path) + if not data_path.exists(): + msg.fail("Evaluation data not found", data_path, exits=1) + if displacy_path and not displacy_path.exists(): + msg.fail("Visualization output directory not found", displacy_path, exits=1) + corpus = Corpus(data_path, gold_preproc=gold_preproc) + nlp = util.load_model(model) + nlp.batch_size = 1 + # nlp.remove_pipe("dates") + # nlp.remove_pipe("addresses") + # nlp.remove_pipe("rules") + # nlp.remove_pipe("structured") + + dev_dataset = [ + eg + for eg in corpus(nlp) # if getattr(eg.reference._, "split", "test") == "test" + ] + print(f"Evaluating {len(dev_dataset)} docs") + + if docbin is not None: + output_db = DocBin(store_user_data=True) + input_docs = DocBin().from_disk(data_path).get_docs(nlp.vocab) + for doc in input_docs: + doc.ents = [] + doc.spans.clear() + for doc in nlp.pipe(input_docs): + doc.user_data = { + k: v + for k, v in doc.user_data.items() + if "note_id" in k or "context" in k or "split" in k or "Action" in k or "Allergie" in k or "Certainty" in k or "Temporality" in k or "Family" in k or "Negation" in k + } + output_db.add(doc) + output_db.to_disk(docbin) + + scores = nlp.evaluate(dev_dataset) + metrics = { + "TOK": "token_acc", + "TAG": "tag_acc", + "POS": "pos_acc", + "MORPH": "morph_acc", + "LEMMA": "lemma_acc", + "UAS": "dep_uas", + "LAS": "dep_las", + "NER P": "ents_p", + "NER R": "ents_r", + "NER F": "ents_f", + "TEXTCAT": "cats_score", + "SENT P": "sents_p", + "SENT R": "sents_r", + "SENT F": "sents_f", + "SPAN P": f"spans_{spans_key}_p", + "SPAN R": f"spans_{spans_key}_r", + "SPAN F": f"spans_{spans_key}_f", + "SPEED": "speed", + "QUAL": "qual", + } + results = {} + data = {} + for metric, key in metrics.items(): + if key in scores: + if key == "cats_score": + metric = metric + " (" + scores.get("cats_score_desc", "unk") + ")" + if isinstance(scores[key], (int, float)): + if key == "speed": + results[metric] = f"{scores[key]:.0f}" + else: + results[metric] = f"{scores[key]*100:.2f}" + else: + results[metric] = "-" + data[re.sub(r"[\s/]", "_", key.lower())] = scores[key] + + msg.table(results, title="Results") + data = handle_scores_per_type(scores, data, spans_key=spans_key, silent=silent) + + if displacy_path: + factory_names = [nlp.get_pipe_meta(pipe).factory for pipe in nlp.pipe_names] + docs = list(nlp.pipe(ex.reference.text for ex in dev_dataset[:displacy_limit])) + render_deps = "parser" in factory_names + render_ents = "ner" in factory_names + render_parses( + docs, + displacy_path, + model_name=model, + limit=displacy_limit, + deps=render_deps, + ents=render_ents, + ) + msg.good(f"Generated {displacy_limit} parses as HTML", displacy_path) + + if output_path is not None: + srsly.write_json(output_path, data) + msg.good(f"Saved results to {output_path}") + return data + + +def handle_scores_per_type( + scores: Dict[str, Any], + data: Dict[str, Any] = {}, + *, + spans_key: str = "sc", + silent: bool = False, +) -> Dict[str, Any]: + msg = Printer(no_print=silent, pretty=not silent) + if "morph_per_feat" in scores: + if scores["morph_per_feat"]: + print_prf_per_type(msg, scores["morph_per_feat"], "MORPH", "feat") + data["morph_per_feat"] = scores["morph_per_feat"] + if "dep_las_per_type" in scores: + if scores["dep_las_per_type"]: + print_prf_per_type(msg, scores["dep_las_per_type"], "LAS", "type") + data["dep_las_per_type"] = scores["dep_las_per_type"] + if "ents_per_type" in scores: + if scores["ents_per_type"]: + print_prf_per_type(msg, scores["ents_per_type"], "NER", "type") + data["ents_per_type"] = scores["ents_per_type"] + if f"spans_{spans_key}_per_type" in scores: + if scores[f"spans_{spans_key}_per_type"]: + print_prf_per_type( + msg, scores[f"spans_{spans_key}_per_type"], "SPANS", "type" + ) + data[f"spans_{spans_key}_per_type"] = scores[f"spans_{spans_key}_per_type"] + if "cats_f_per_type" in scores: + if scores["cats_f_per_type"]: + print_prf_per_type(msg, scores["cats_f_per_type"], "Textcat F", "label") + data["cats_f_per_type"] = scores["cats_f_per_type"] + if "cats_auc_per_type" in scores: + if scores["cats_auc_per_type"]: + print_textcats_auc_per_cat(msg, scores["cats_auc_per_type"]) + data["cats_auc_per_type"] = scores["cats_auc_per_type"] + if "qual_per_type" in scores: + if scores["qual_per_type"]: + print_prf_per_type(msg, scores["qual_per_type"], "Qualifiers", "qualifier") + data["qual_per_type"] = scores["qual_per_type"] + return scores + + +if __name__ == "__main__": + typer.run(evaluate_cli) diff --git a/NER_model/scripts/infer.py b/NER_model/scripts/infer.py new file mode 100644 index 000000000..f99783277 --- /dev/null +++ b/NER_model/scripts/infer.py @@ -0,0 +1,63 @@ +import os +from pathlib import Path +from typing import Optional + +import spacy +import typer +from edsnlp.connectors.brat import BratConnector +from spacy.tokens import DocBin +from tqdm import tqdm + + +def main( + model: Optional[Path] = typer.Option(None, help="Path to the model"), + input: Path = typer.Option( + ..., help="Path to the evaluation dataset, in spaCy format" + ), + output: Path = typer.Option(..., help="Path to the output dataset"), + format: str = typer.Option(..., help="spacy or brat"), +): + """Partition the data into train/test/dev split.""" + + assert format in ("spacy", "brat") + + spacy.require_gpu() + + nlp = spacy.load(model) + + if os.path.isdir(input): + print("Input format is BRAT") + input_docs = list(BratConnector(input).brat2docs(nlp)) + else: + print("Input format is spaCy") + input_docs = DocBin().from_disk(input).get_docs(nlp.vocab) + + print("Number of docs:", len(input_docs)) + + for doc in input_docs: + doc.ents = [] + doc.spans.clear() + + predicted = [] + + nlp.batch_size = 1 + + for doc in tqdm(nlp.pipe(input_docs), total=len(input_docs)): + doc.user_data = { + k: v + for k, v in doc.user_data.items() + if "note_id" in k or "context" in k or "split" in k or "Action" in k or "Allergie" in k or "Certainty" in k or "Temporality" in k or "Family" in k or "Negation" in k + } + predicted.append(doc) + # predicted[0].ents[i]._.negation donne None au lieu de False/True + if format == "spacy": + print("Output format is spaCy") + out_db = DocBin(store_user_data=True, docs=predicted) + out_db.to_disk(output) + elif format == "brat": + print("Output format is BRAT") + BratConnector(output, attributes=["Negation", "Family", "Temporality", "Certainty", "Action", "Allergie"]).docs2brat(predicted) + + +if __name__ == "__main__": + typer.run(main) diff --git a/NER_model/scripts/new_evaluate.py b/NER_model/scripts/new_evaluate.py new file mode 100644 index 000000000..31b82e373 --- /dev/null +++ b/NER_model/scripts/new_evaluate.py @@ -0,0 +1,233 @@ +import copy +import re +from pathlib import Path +from typing import Any, Dict, Optional + +import srsly +import typer +from spacy.scorer import Scorer +from spacy.training import Example +from spacy import util +from spacy.cli._util import Arg, Opt, import_code, setup_gpu +from spacy.cli.evaluate import ( + print_prf_per_type, + print_textcats_auc_per_cat, + render_parses, +) +from spacy.tokens import DocBin +from thinc.api import fix_random_seed +from wasabi import Printer +from edsnlp.evaluate import evaluate_test + +# fmt: off +def evaluate_cli( + model: str = Arg(..., help="Model name or path"), # noqa: E501 + data_path: Path = Arg(..., help="Location of binary evaluation data in .spacy format", exists=True), # noqa: E501 + output: Optional[Path] = Opt(None, "--output", "-o", help="Output JSON file for metrics", dir_okay=False), # noqa: E501 + docbin: Optional[Path] = Opt(None, "--docbin", help="Output Doc Bin path", dir_okay=False), # noqa: E501 + code_path: Optional[Path] = Opt(None, "--code", "-c", help="Path to Python file with additional code (registered functions) to be imported"), # noqa: E501 + use_gpu: int = Opt(-1, "--gpu-id", "-g", help="GPU ID or -1 for CPU"), # noqa: E501 + gold_preproc: bool = Opt(False, "--gold-preproc", "-G", help="Use gold preprocessing"), # noqa: E501 + displacy_path: Optional[Path] = Opt(None, "--displacy-path", "-dp", help="Directory to output rendered parses as HTML", exists=True, file_okay=False), # noqa: E501 + displacy_limit: int = Opt(25, "--displacy-limit", "-dl", help="Limit of parses to render as HTML"), # noqa: E501 +): + # fmt: on + + """ + Evaluate a trained pipeline. Expects a loadable spaCy pipeline and evaluation + data in the binary .spacy format. The --gold-preproc option sets up the + evaluation examples with gold-standard sentences and tokens for the + predictions. Gold preprocessing helps the annotations align to the + tokenization, and may result in sequences of more consistent length. However, + it may reduce runtime accuracy due to train/test skew. To render a sample of + dependency parses in a HTML file, set as output directory as the + displacy_path argument. + DOCS: https://spacy.io/api/cli#evaluate + """ + import_code(code_path) + evaluate( + model, + data_path, + output=output, + docbin=docbin, + use_gpu=use_gpu, + gold_preproc=gold_preproc, + displacy_path=displacy_path, + displacy_limit=displacy_limit, + silent=False, + ) + + +def evaluate( + model: str, + data_path: Path, + output: Optional[Path] = None, + docbin: Optional[Path] = None, + use_gpu: int = -1, + gold_preproc: bool = False, + displacy_path: Optional[Path] = None, + displacy_limit: int = 25, + silent: bool = True, + spans_key: str = "sc", +) -> Dict[str, Any]: + msg = Printer(no_print=silent, pretty=not silent) + fix_random_seed() + setup_gpu(use_gpu, silent=silent) + data_path = util.ensure_path(data_path) + output_path = util.ensure_path(output) + displacy_path = util.ensure_path(displacy_path) + if not data_path.exists(): + msg.fail("Evaluation data not found", data_path, exits=1) + if displacy_path and not displacy_path.exists(): + msg.fail("Visualization output directory not found", displacy_path, exits=1) + nlp = util.load_model(model) + nlp.batch_size = 1 + # nlp.remove_pipe("dates") + # nlp.remove_pipe("addresses") + # nlp.remove_pipe("rules") + # nlp.remove_pipe("structured") + + gold_docs = list(DocBin().from_disk(data_path).get_docs(nlp.vocab)) + gold_docs.sort(key=lambda doc: doc._.note_id) + input_docs = copy.deepcopy(gold_docs) + for doc in input_docs: + doc.ents = [] + doc.spans.clear() + print(f"Evaluating {len(gold_docs)} docs") + pred_docs = [] + for doc in tqdm(nlp.pipe(input_docs), total=len(input_docs)): + doc.user_data = { + k: v + for k, v in doc.user_data.items() + if "note_id" in k or "context" in k or "split" in k or "Action" in k or "Certainty" in k or "Temporality" in k or "Family" in k or "Negation" in k + } + pred_docs.append(doc) + pred_docs.sort(key=lambda doc: doc._.note_id) + + if docbin is not None: + output_db = DocBin(store_user_data=True) + for doc in pred_docs: + output_db.add(doc) + output_db.to_disk(docbin) + + scores = evaluate_test(gold_docs, pred_docs) + + metrics = { + "TOK": "token_acc", + "TAG": "tag_acc", + "POS": "pos_acc", + "MORPH": "morph_acc", + "LEMMA": "lemma_acc", + "UAS": "dep_uas", + "LAS": "dep_las", + "NER P": "ents_p", + "NER R": "ents_r", + "NER F": "ents_f", + "TEXTCAT": "cats_score", + "SENT P": "sents_p", + "SENT R": "sents_r", + "SENT F": "sents_f", + "SPAN P": f"spans_{spans_key}_p", + "SPAN R": f"spans_{spans_key}_r", + "SPAN F": f"spans_{spans_key}_f", + "SPEED": "speed", + "QUAL": "qual", + } + results = {} + data = {} + for metric, key in metrics.items(): + if key in scores: + if key == "cats_score": + metric = metric + " (" + scores.get("cats_score_desc", "unk") + ")" + if isinstance(scores[key], (int, float)): + if key == "speed": + results[metric] = f"{scores[key]:.0f}" + else: + results[metric] = f"{scores[key]*100:.2f}" + else: + results[metric] = "-" + data[re.sub(r"[\s/]", "_", key.lower())] = scores[key] + + msg.table(results, title="Results") + data = handle_scores_per_type(scores, data, spans_key=spans_key, silent=silent) + + if displacy_path: + factory_names = [nlp.get_pipe_meta(pipe).factory for pipe in nlp.pipe_names] + docs = list(nlp.pipe(ex.reference.text for ex in dev_dataset[:displacy_limit])) + render_deps = "parser" in factory_names + render_ents = "ner" in factory_names + render_parses( + docs, + displacy_path, + model_name=model, + limit=displacy_limit, + deps=render_deps, + ents=render_ents, + ) + msg.good(f"Generated {displacy_limit} parses as HTML", displacy_path) + + if output_path is not None: + srsly.write_json(output_path, data) + msg.good(f"Saved results to {output_path}") + return data + +def print_prf_per_type( + msg: Printer, scores: Dict[str, Dict[str, float]], name: str, type: str +) -> None: + data = [] + for key, value in scores.items(): + row = [key] + for k in ("p", "r", "f", "n_entity"): + v = value[k] + row.append(f"{v * 100:.2f}" if isinstance(v, (int, float)) else v) + data.append(row) + msg.table( + data, + header=("", "P", "R", "F", "N_entity"), + aligns=("l", "r", "r", "r", "r"), + title=f"{name} (per {type})", + ) + +def handle_scores_per_type( + scores: Dict[str, Any], + data: Dict[str, Any] = {}, + *, + spans_key: str = "sc", + silent: bool = False, +) -> Dict[str, Any]: + msg = Printer(no_print=silent, pretty=not silent) + if "morph_per_feat" in scores: + if scores["morph_per_feat"]: + print_prf_per_type(msg, scores["morph_per_feat"], "MORPH", "feat") + data["morph_per_feat"] = scores["morph_per_feat"] + if "dep_las_per_type" in scores: + if scores["dep_las_per_type"]: + print_prf_per_type(msg, scores["dep_las_per_type"], "LAS", "type") + data["dep_las_per_type"] = scores["dep_las_per_type"] + if "ents_per_type" in scores: + if scores["ents_per_type"]: + print_prf_per_type(msg, scores["ents_per_type"], "NER", "type") + data["ents_per_type"] = scores["ents_per_type"] + if f"spans_{spans_key}_per_type" in scores: + if scores[f"spans_{spans_key}_per_type"]: + print_prf_per_type( + msg, scores[f"spans_{spans_key}_per_type"], "SPANS", "type" + ) + data[f"spans_{spans_key}_per_type"] = scores[f"spans_{spans_key}_per_type"] + if "cats_f_per_type" in scores: + if scores["cats_f_per_type"]: + print_prf_per_type(msg, scores["cats_f_per_type"], "Textcat F", "label") + data["cats_f_per_type"] = scores["cats_f_per_type"] + if "cats_auc_per_type" in scores: + if scores["cats_auc_per_type"]: + print_textcats_auc_per_cat(msg, scores["cats_auc_per_type"]) + data["cats_auc_per_type"] = scores["cats_auc_per_type"] + if "qual_per_type" in scores: + if scores["qual_per_type"]: + print_prf_per_type(msg, scores["qual_per_type"], "Qualifiers", "qualifier") + data["qual_per_type"] = scores["qual_per_type"] + return scores + + +if __name__ == "__main__": + typer.run(evaluate_cli) diff --git a/NER_model/scripts/package.py b/NER_model/scripts/package.py new file mode 100644 index 000000000..deb499bd1 --- /dev/null +++ b/NER_model/scripts/package.py @@ -0,0 +1,324 @@ +# This script exists because spaCy's package script cannot natively +# - export a package (for medic rules) as well as the model weights +# - keep the same requirements as those listed in the pyproject.toml file +# - include package_data for the medic rules, listed also in the pyproject +# Therefore, we in this script, we build the model package using most of spaCy's code +# and build a new pyproject.toml derived from the main one (at the root of the repo) +# and use poetry to build the model instead of `python setup.py`. + +import os.path +import re +import shutil +import sys +from pathlib import Path +from typing import List, Optional + +import srsly +import toml +from spacy import util +from spacy.cli._util import SDIST_SUFFIX, WHEEL_SUFFIX, Arg, Opt, string_to_list +from spacy.cli.package import ( + FILENAMES_DOCS, + TEMPLATE_MANIFEST, + _is_permitted_package_name, + create_file, + generate_meta, + generate_readme, + get_build_formats, + get_meta, + has_wheel, +) +from spacy.schemas import ModelMetaSchema, validate +from typer import Typer +from wasabi import Printer + +app = Typer() + + +TEMPLATE_INIT = """ +from pathlib import Path +from spacy.util import load_model_from_init_py, get_model_meta + +{imports} + +__version__ = {version} + + +def load(**overrides): + return load_model_from_init_py(__file__, **overrides) +""".lstrip() + + +def make_pyproject_toml( + pyproject_path: str, + name: str, + package_name: str, + package_data: str, +) -> str: + """ + Creates a new pyproject.toml config for the generated model + from the root poetry-based pyproject.toml file + For that we: + - adapt paths to the new structure (nested under the new model name) + - change the pyproject name + - include any original included files as well as the model weights + + Parameters + ---------- + pyproject_path: str + Path to the root level pyproject.toml path + name: str + Model name + package_name: str + Package directory name + package_data: str + Package data name in the above directory + + Returns + ------- + str + The string content of the new pyproject.toml file + """ + package_name = Path(package_name) + pyproject_text = Path(pyproject_path).read_text() + pyproject_data = toml.loads(pyproject_text) + print(pyproject_data) + pyproject_data["tool"]["poetry"]["name"] = name + new_includes = [ + str(package_name / include) + for include in pyproject_data["tool"]["poetry"]["include"] + ] + [ + str(package_name / "**/*.py"), + str(package_name / package_data / "**/*"), + str("**/meta.json"), + ] + pyproject_data["tool"]["poetry"]["include"] = new_includes + for key, plugins in pyproject_data["tool"]["poetry"]["plugins"].items(): + new_plugins = {} + for value, path in plugins.items(): + new_plugins[value] = f"{package_name}.{path}" + plugins.clear() + plugins.update(new_plugins) + return toml.dumps(pyproject_data) + + +# fmt: off +@app.command() +def package_medic_cli( + input_dir: Path = Arg(..., help="Directory with pipeline data", exists=True, file_okay=False), # noqa: E501 + output_dir: Path = Arg(..., help="Output parent directory", exists=True, file_okay=False), # noqa: E501 + code_paths: str = Opt("", "--code", "-c", help="Comma-separated paths to Python file with additional code (registered functions) to be included in the package"), # noqa: E501 + meta_path: Optional[Path] = Opt(None, "--meta-path", "--meta", "-m", help="Path to meta.json", exists=True, dir_okay=False), # noqa: E501 + create_meta: bool = Opt(False, "--create-meta", "-C", help="Create meta.json, even if one exists"), # noqa: E501 + name: Optional[str] = Opt(None, "--name", "-n", help="Package name to override meta"), # noqa: E501 + version: Optional[str] = Opt(None, "--version", "-v", help="Package version to override meta"), # noqa: E501 + build: str = Opt("sdist", "--build", "-b", help="Comma-separated formats to build: sdist and/or wheel, or none."), # noqa: E501 + force: bool = Opt(False, "--force", "-f", "-F", help="Force overwriting existing data in output directory"), # noqa: E501 +): + # fmt: on + """ + Adapted from spaCy package CLI command (documentation copied below) + This script exists spaCy's standard script cannot natively + - export a package (for medic rules) as well as the model weights + - keep the same requirements as those listed in the pyproject.toml file + - include package_data for the medic rules, listed also in the pyproject + Therefore, we in this script, we build the model package using most of spaCy's code + and build a new pyproject.toml derived from the main one (at the root of the repo) + and use poetry to build the model instead of `python setup.py`. + + SpaCy's original docstring: + Generate an installable Python package for a pipeline. Includes binary data, + meta and required installation files. A new directory will be created in the + specified output directory, and the data will be copied over. If + --create-meta is set and a meta.json already exists in the output directory, + the existing values will be used as the defaults in the command-line prompt. + After packaging, "python setup.py sdist" is run in the package directory, + which will create a .tar.gz archive that can be installed via "pip install". + If additional code files are provided (e.g. Python files containing custom + registered functions like pipeline components), they are copied into the + package and imported in the __init__.py. + DOCS: https://spacy.io/api/cli#package + """ + create_sdist, create_wheel = get_build_formats(string_to_list(build)) + code_paths = [Path(p.strip()) for p in string_to_list(code_paths)] + package( + input_dir, + output_dir, + meta_path=meta_path, + code_paths=code_paths, + name=name, + version=version, + create_meta=create_meta, + create_sdist=create_sdist, + create_wheel=create_wheel, + force=force, + silent=False, + ) + + +def package( + input_dir: Path, + output_dir: Path, + meta_path: Optional[Path] = None, + code_paths: List[Path] = [], + name: Optional[str] = None, + version: Optional[str] = None, + create_meta: bool = False, + create_sdist: bool = True, + create_wheel: bool = False, + force: bool = False, + silent: bool = True, +) -> None: + msg = Printer(no_print=silent, pretty=not silent) + input_path = util.ensure_path(input_dir) + output_path = util.ensure_path(output_dir) + meta_path = util.ensure_path(meta_path) + if create_wheel and not has_wheel(): + err = "Generating a binary .whl file requires wheel to be installed" + msg.fail(err, "pip install wheel", exits=1) + if not input_path or not input_path.exists(): + msg.fail("Can't locate pipeline data", input_path, exits=1) + if not output_path or not output_path.exists(): + msg.fail("Output directory not found", output_path, exits=1) + if create_sdist or create_wheel: + opts = ["sdist" if create_sdist else "", "wheel" if create_wheel else ""] + msg.info(f"Building package artifacts: {', '.join(opt for opt in opts if opt)}") + for code_path in code_paths: + if not code_path.exists(): + msg.fail("Can't find code file", code_path, exits=1) + if os.path.isdir(code_path): + print("Will import", code_path.stem, "but did not test it before packaging") + # Import the code here so it's available when model is loaded (via + # get_meta helper). Also verifies that everything works + else: + util.import_file(code_path.stem, code_path) + if code_paths: + msg.good(f"Including {len(code_paths)} Python module(s) with custom code") + if meta_path and not meta_path.exists(): + msg.fail("Can't find pipeline meta.json", meta_path, exits=1) + meta_path = meta_path or input_dir / "meta.json" + if not meta_path.exists() or not meta_path.is_file(): + msg.fail("Can't load pipeline meta.json", meta_path, exits=1) + meta = srsly.read_json(meta_path) + meta = get_meta(input_dir, meta) + if meta["requirements"]: + msg.good( + f"Including {len(meta['requirements'])} package requirement(s) from " + f"meta and config", + ", ".join(meta["requirements"]), + ) + if name is not None: + if not name.isidentifier(): + msg.fail( + f"Model name ('{name}') is not a valid module name. " + "This is required so it can be imported as a module.", + "We recommend names that use ASCII A-Z, a-z, _ (underscore), " + "and 0-9. " + "For specific details see: " + "https://docs.python.org/3/reference/lexical_analysis.html#identifiers", + exits=1, + ) + if not _is_permitted_package_name(name): + msg.fail( + f"Model name ('{name}') is not a permitted package name. " + "This is required to correctly load the model with spacy.load.", + "We recommend names that use ASCII A-Z, a-z, _ (underscore), " + "and 0-9. " + "For specific details see: " + "https://www.python.org/dev/peps/pep-0426/#name", + exits=1, + ) + meta["name"] = name + if version is not None: + meta["version"] = version + if not create_meta: # only print if user doesn't want to overwrite + msg.good("Loaded meta.json from file", meta_path) + else: + meta = generate_meta(meta, msg) + errors = validate(ModelMetaSchema, meta) + if errors: + msg.fail("Invalid pipeline meta.json") + print("\n".join(errors)) + sys.exit(1) + model_name = meta["name"] + if not model_name.startswith(meta["lang"] + "_"): + model_name = f"{meta['lang']}_{model_name}" + model_name_v = model_name + "-" + meta["version"] + main_path = output_dir / model_name_v + package_path = main_path / model_name + if package_path.exists(): + if force: + shutil.rmtree(str(package_path)) + else: + msg.fail( + "Package directory already exists", + "Please delete the directory and try again, or use the " + "`--force` flag to overwrite existing directories.", + exits=1, + ) + Path.mkdir(package_path, parents=True) + shutil.copytree(str(input_dir), str(package_path / model_name_v)) + for file_name in FILENAMES_DOCS: + file_path = package_path / model_name_v / file_name + if file_path.exists(): + shutil.copy(str(file_path), str(main_path)) + readme_path = main_path / "README.md" + if not readme_path.exists(): + readme = generate_readme(meta) + create_file(readme_path, readme) + create_file(package_path / model_name_v / "README.md", readme) + msg.good("Generated README.md from meta.json") + else: + msg.info("Using existing README.md from pipeline directory") + imports = [] + for code_path in code_paths: + imports.append(code_path.stem) + if os.path.isdir(code_path): + print("Copying module", code_path, "to", str(package_path / code_path.stem)) + shutil.copytree(str(code_path), str(package_path / code_path.stem)) + else: + shutil.copy(str(code_path), str(package_path)) + + # no more top level meta.json, it was only used to load version + # number and toplevel resources are not compatible with poetry + create_file(main_path / model_name / "meta.json", srsly.json_dumps(meta, indent=2)) + + # no more setup.py, we use poetry now + # create_file(main_path / "setup.py", TEMPLATE_SETUP) + + create_file( + main_path / "pyproject.toml", + make_pyproject_toml( + "pyproject.toml", + model_name, + model_name, + model_name_v, + ), + ) + create_file(main_path / "MANIFEST.in", TEMPLATE_MANIFEST) + init_py = TEMPLATE_INIT.format( + imports="\n".join(f"from . import {m}" for m in imports), + version=repr(version), + ) + create_file(package_path / "__init__.py", init_py) + msg.good(f"Successfully created package directory '{model_name_v}'", main_path) + if create_sdist: + with util.working_dir(main_path): + util.run_command(["poetry", "build", "-f", "sdist"]) + zip_file = main_path / "dist" / f"{model_name_v}{SDIST_SUFFIX}" + msg.good(f"Successfully created zipped Python package {zip_file}") + if create_wheel: + with util.working_dir(main_path): + util.run_command(["poetry", "build", "-f", "wheel"]) + wheel_name_squashed = re.sub("_+", "_", model_name_v) + wheel = main_path / "dist" / f"{wheel_name_squashed}{WHEEL_SUFFIX}" + msg.good(f"Successfully created binary wheel {wheel}") + if "__" in model_name: + msg.warn( + f"Model name ('{model_name}') contains a run of underscores. " + "Runs of underscores are not significant in installed package names.", + ) + + +if __name__ == "__main__": + app() diff --git a/NER_model/scripts/save_to_brat.py b/NER_model/scripts/save_to_brat.py new file mode 100644 index 000000000..0588c6574 --- /dev/null +++ b/NER_model/scripts/save_to_brat.py @@ -0,0 +1,129 @@ +import pandas as pd +import numpy as np +import spacy +from edsnlp.connectors.brat import BratConnector +import re +import srsly +import typer +from spacy.scorer import Scorer + +from spacy.tokens import Doc +from spacy.training import Example +from spacy import util +from spacy.cli._util import Arg, Opt, import_code, setup_gpu +from spacy.cli.evaluate import ( + print_prf_per_type, + print_textcats_auc_per_cat, + render_parses, +) + +import re +from pathlib import Path +from typing import Any, Dict, Optional +from tqdm import tqdm + +import os +from spacy.tokens import DocBin +from thinc.api import fix_random_seed +from wasabi import Printer + +from eds_medic.corpus_reader import Corpus + + + + +def evaluate_cli( + model: str = Arg(..., help="Model name or path"), # noqa: E501 + data_path: Path = Arg(..., help="Location of binary evaluation data in .spacy format", exists=True), # noqa: E501 + output: Optional[Path] = Opt(None, "--output", "-o", help="Output JSON file for metrics", dir_okay=False), # noqa: E501 + docbin: Optional[Path] = Opt(None, "--docbin", help="Output Doc Bin path", dir_okay=False), # noqa: E501 + code_path: Optional[Path] = Opt(None, "--code", "-c", help="Path to Python file with additional code (registered functions) to be imported"), # noqa: E501 + use_gpu: int = Opt(-1, "--gpu-id", "-g", help="GPU ID or -1 for CPU"), # noqa: E501 + gold_preproc: bool = Opt(False, "--gold-preproc", "-G", help="Use gold preprocessing"), # noqa: E501 + displacy_path: Optional[Path] = Opt(None, "--displacy-path", "-dp", help="Directory to output rendered parses as HTML", exists=True, file_okay=False), # noqa: E501 + displacy_limit: int = Opt(25, "--displacy-limit", "-dl", help="Limit of parses to render as HTML"), # noqa: E501 +): + + save(model, + ### A DECOMMENTER ### + #data_path = '../data/attr2/test', + #output_brat ='../data/attr2/pred', + # data_path = "/export/home/"cse200093/Jacques_Bio/data_bio/brat_annotated_bio_val/test", + # output_brat = "/export/home/cse200093/Jacques_Bio/data_bio/brat_annotated_bio_val/test_eds-medic", + #data_path = '/export/home/cse200093/RV_Inter_conf/unnested_sosydiso_qualifiers_final/test_, + #output_brat = '/export/home/cse200093/RV_Inter_conf/unnested_sosydiso_qualifiers_final/pred', + #data_path = '/export/home/cse200093/RV_Inter_conf/unnested_final/test', + #output_brat = '/export/home/cse200093/RV_Inter_conf/unnested_final/pred', + data_path='/export/home/cse200093/Jacques_Bio/data_bio/super_pipe_get_stats_by_section_on_cim10/pred/syndrome_des_anti-phospholipides_init', + output_brat='/export/home/cse200093/Jacques_Bio/data_bio/super_pipe_get_stats_by_section_on_cim10/pred/syndrome_des_anti-phospholipides_pred2', + output=output, + docbin=docbin, + use_gpu=use_gpu, + gold_preproc=gold_preproc, + displacy_path=displacy_path, + displacy_limit=displacy_limit, + silent=False, + ) + + +def save( + model: str, + output_brat: str, + data_path: Path, + output: Optional[Path] = None, + docbin: Optional[Path] = None, + use_gpu: int = -1, + gold_preproc: bool = False, + displacy_path: Optional[Path] = None, + displacy_limit: int = 25, + silent: bool = True, + spans_key: str = "sc", +): + setup_gpu(use_gpu, silent) + + #brat = BratConnector(data_path, attributes = {"Disorders_type":"Disorders_type",'SOSY_type':'SOSY_type','Chemical_and_drugs_type':'Chemical_and_drugs_type', + # 'Concept_type':'Concept_type','negation':'negation','hypothetique':'hypothetique', 'family':'family','Medical_Procedure_type':'Medical_Procedure_type','gender_type':'gender_type'}) + brat = BratConnector(data_path, attributes = {"Negation":"Negation","Family": "Family", "Temporality":"Temporality","Certainty":"Certainty","Action":"Action"}) + empty = spacy.blank("fr") + df_gold = brat.brat2docs(empty) + df_gold.sort(key=lambda doc: doc.text) + + + + + print('-- Model running --') + #model_path = '/export/home/cse200093/Pierre_Medic/NEURAL_BASED_NER/inference_model/model-best' + df_txt = [doc.text for doc in df_gold] + model = spacy.load(model) + model.add_pipe('clean-entities') + #caanot find clean entities... --> from edsmedic.... import clean_entites + df_txt_pred = [] + for doc in tqdm(df_txt, desc="Processing documents"): + doc = model(doc) + doc._.trf_data = None + df_txt_pred.append(doc) + + for i in range(len(df_txt_pred)): + df_txt_pred[i]._.note_id = df_gold[i]._.note_id + + + #for doc in df_txt: + #for ent in doc.ents: + #if ent._.Action: + #print(ent, ent._.Action) + + print('-- try saving --') + + print('path: ',output_brat) + brat = BratConnector(output_brat, attributes = {"Negation":"Negation","Family": "Family", "Temporality":"Temporality","Certainty":"Certainty","Action":"Action"}) + + brat.docs2brat(df_txt_pred) + + print('-- saved -- ') + + + + + +if __name__ == "__main__": + typer.run(evaluate_cli) diff --git a/NER_model/scripts/visualize_model.py b/NER_model/scripts/visualize_model.py new file mode 100644 index 000000000..f204d7903 --- /dev/null +++ b/NER_model/scripts/visualize_model.py @@ -0,0 +1,14 @@ +import spacy_streamlit +import typer + + +def main(models: str, default_text: str): + models = [name.strip() for name in models.split(",")] + spacy_streamlit.visualize(models, default_text, visualizers=["ner"]) + + +if __name__ == "__main__": + try: + typer.run(main) + except SystemExit: + pass diff --git a/Normalisation/drugs/exception.py b/Normalisation/drugs/exception.py new file mode 100644 index 000000000..d01134991 --- /dev/null +++ b/Normalisation/drugs/exception.py @@ -0,0 +1,14 @@ +exception_list = { + 'glucocorticoid': ['cortico', + 'corticoides', + 'corticoide' + 'corticos', + 'corticotherapie', + 'corticotherapies', + 'glucocorticoides', + 'glucocorticoide', + 'corticostéroides', + 'corticostéroide'], + 'rituximab': ['ritux','rtx'], + 'prevenar': ['prevenar13'], +} diff --git a/Normalisation/drugs/normalisation.py b/Normalisation/drugs/normalisation.py new file mode 100644 index 000000000..17ce3499d --- /dev/null +++ b/Normalisation/drugs/normalisation.py @@ -0,0 +1,246 @@ +import pandas as pd +import numpy as np +import re +import spacy +import Levenshtein +from unidecode import unidecode +from tqdm import tqdm +import duckdb +from edsnlp.connectors import BratConnector +from collections import defaultdict +from exception import exception_list + +from sklearn.preprocessing import MultiLabelBinarizer +import numpy as np +from sklearn.metrics import precision_score, recall_score, f1_score +from levenpandas import fuzzymerge + +class DrugNormaliser: + def __init__(self, df_path, drug_dict, method="exact", max_pred=5, atc_len = 7): + if df_path.endswith('json'): + self.df = pd.read_json(df_path) + else: + self.df = self.gold_generation(df_path) + self.drug_dict = drug_dict + # self.df['drug'] = self.df['drug'].apply(lambda x: re.sub(r'\W+', '',x.lower())) + self.df['norm_term'] = self.df['norm_term'].apply(lambda x: unidecode(x)) + # self.df['score'] = None + + merged_dict = {} + for atc_code, values in self.drug_dict.items(): + # Shorten the ATC code + shortened_code = atc_code[:atc_len] + + # Check if the shortened ATC code already exists in the merged dictionary + if shortened_code in merged_dict: + # Merge the arrays + merged_dict[shortened_code] = list(set(merged_dict[shortened_code] + values)) + + else: + # Add a new entry for the shortened ATC code + merged_dict[shortened_code] = values + + merged_dict = pd.DataFrame.from_dict({"norm_term": merged_dict}, "index").T.explode("norm_term").reset_index().rename(columns={"index": "label"}) + merged_dict.norm_term = merged_dict.norm_term.str.split(",") + merged_dict = merged_dict.explode("norm_term").reset_index(drop=True) + self.drug_dict = merged_dict + self.method = method + self.max_pred = max_pred + + + + + def get_gold(self): + return self.df + + def get_dict(self): + return self.drug_dict + + def gold_generation(self, df_path): + doc_list = BratConnector(df_path).brat2docs(spacy.blank("fr")) + drug_list = [] + for doc in doc_list: + for ent in doc.ents: + if ent.label_ == 'Chemical_and_drugs': + if not ent._.Tech: + drug_list.append([ent.text, doc._.note_id, [ent.start, ent.end], ent.text.lower().strip()]) + + drug_list_df = pd.DataFrame(drug_list, columns=['term', 'source', 'span_converted', 'norm_term']) + drug_list_df.span_converted = drug_list_df.span_converted.astype(str) + return drug_list_df + + + +# def exact_match(self, drug_name, atc, names): +# matching_atc = [] +# matching_names = [] +# for name in names: +# if drug_name == name: +# matching_atc.append(atc) +# matching_names.append(name) +# return matching_atc, matching_names + +# def levenshtein_match(self, drug_name, name): +# return Levenshtein.ratio(drug_name, name) + +# def dice_match(self, word1, word2): +# intersection = len(set(word1) & set(word2)) +# coefficient = (2 * intersection) / (len(word1) + len(word2)) +# return coefficient + + + def normalize(self, threshold=0.85): + # self.df['pred_atc'] = [None]*len(self.df) + # self.df['pred_string'] = [None]*len(self.df) + + for index, row in self.df.iterrows(): + for k, v in exception_list.items(): + if row["norm_term"] in v: + self.df.at[index, "norm_term"] = k + + if self.method == "exact": + self.df = self.df.merge(self.drug_dict, how="left", on="norm_term") + if self.method == "lev": + df_1 = self.df.copy() + df_2 = self.drug_dict.copy() + merged_df = duckdb.query( + f"""select *, jaro_winkler_similarity(df_1.norm_term, df_2.norm_term) score from df_1, df_2 where score > {threshold}""" + ).to_df() + idx = ( + merged_df.groupby(["term", "source", "span_converted", "norm_term"])[ + "score" + ].transform(max) + == merged_df["score"] + ) + self.df = merged_df[idx] + self.df = self.df.groupby( + ["term", "source", "span_converted", "norm_term"], as_index=False + ).agg({"label": list}) + return self.df + +# if self.method =='lev': +# for index, row in self.df.iterrows(): +# drug_name = row['drug'] +# matching_atc = [] +# matching_names = [] +# matching_scores = [] +# for atc, names in self.drug_dict.items(): +# names = [name for name in names if name is not np.nan] +# Levenshtein_distance = [] +# Levenshtein_distance_name = [] +# for name in names: +# Levenshtein_distance.append(self.levenshtein_match(drug_name,name)) +# Levenshtein_distance_name.append(name) +# if len(Levenshtein_distance) > 0: +# max_value_id = Levenshtein_distance.index(max(Levenshtein_distance)) +# max_value = Levenshtein_distance[max_value_id] +# max_value_name = Levenshtein_distance_name[max_value_id] +# if max_value >= treshold: +# matching_atc.append(atc) +# matching_names.append(max_value_name) +# matching_scores.append(max_value) +# #sort matching_atc by score +# matching_atc = [x for _,x in sorted(zip(matching_scores,matching_atc), reverse=True)] +# matching_names = [x for _,x in sorted(zip(matching_scores,matching_names), reverse=True)] +# matching_scores = sorted(matching_scores, reverse=True) +# if 1 in matching_scores: +# self.df.at[index, 'pred_atc'] = [matching_atc[i] for i, score in enumerate(matching_scores) if score == 1] +# self.df.at[index, 'pred_string'] = [matching_names[i] for i, score in enumerate(matching_scores) if score == 1] +# self.df.at[index, 'score'] = [score for score in matching_scores if score == 1] +# else: +# self.df.at[index, 'pred_atc'] = matching_atc[:self.max_pred] +# self.df.at[index, 'pred_string'] = matching_names[:self.max_pred] +# self.df.at[index, 'score'] = matching_scores[:self.max_pred] +# return self.df + + + +# def acc(self, verbose = False): +# correct_predictions = self.df.apply(lambda row: row['ATC'][:len(row['ATC'])] in [x[:len(row['ATC'])] for x in row['pred_atc']], axis=1).sum() +# total_predictions = len(self.df) +# accuracy = correct_predictions / total_predictions +# if verbose: +# return f'{accuracy}, ({correct_predictions}/{total_predictions})' +# else: +# return accuracy + +# def get_good_predictions(self): +# good_predictions = self.df.apply(lambda row: row['ATC'][:len(row['ATC'])] in [x[:len(row['ATC'])] for x in row['pred_atc']], axis=1) +# return self.df[good_predictions] + +# def get_bad_predictions(self): +# bad_predictions = self.df.apply(lambda row: row['ATC'][:len(row['ATC'])] not in [x[:len(row['ATC'])] for x in row['pred_atc']], axis=1) +# return self.df[bad_predictions] + +# def get_no_predictions(self): +# no_predictions = self.df.apply(lambda row: len(row['pred_atc'])==0, axis=1) +# return self.df[no_predictions] + + +# def metrics(self, verbose = True): +# y_true = self.df['ATC'] +# y_pred = self.df['pred_atc'] + +# unique_atc = set([atc for atc in y_true]) + +# results = {atc: {'TP': 0, 'FP': 0, 'FN': 0} for atc in unique_atc} + +# for atc in unique_atc: +# TP = 0 +# FP = 0 +# FN = 0 +# for c,atc_gold in enumerate(y_true): +# if atc_gold == atc: +# if atc_gold in y_pred[c]: +# TP += 1 +# else: +# FN += 1 +# for c, atcs_pred in enumerate(y_pred): +# for atc_pred in atcs_pred: +# if atc_pred == atc: +# if atc_pred not in y_true[c]: +# FP += 1 + +# results[atc]['TP'] = TP +# results[atc]['FP'] = FP +# results[atc]['FN'] = FN + +# #we get the micro_average +# total_TP = sum([results[atc]['TP'] for atc in unique_atc]) +# total_FP = sum([results[atc]['FP'] for atc in unique_atc]) +# total_FN = sum([results[atc]['FN'] for atc in unique_atc]) + +# precision_micro = total_TP/(total_TP+total_FP) +# recall_micro = total_TP/(total_TP+total_FN) +# f1_micro = 2*precision_micro*recall_micro/(precision_micro+recall_micro) + +# #we get the macro_average +# total_precision = 0 +# total_recall = 0 +# total_f1 = 0 + +# for atc in unique_atc: +# if results[atc]['TP']+results[atc]['FP'] != 0: +# precision = results[atc]['TP']/(results[atc]['TP']+results[atc]['FP']) +# else: +# precision = 0 +# if results[atc]['TP']+results[atc]['FN'] != 0: +# recall = results[atc]['TP']/(results[atc]['TP']+results[atc]['FN']) +# else: +# recall = 0 +# if precision+recall != 0: +# f1 = 2*precision*recall/(precision+recall) +# else: +# f1 = 0 + +# total_precision += precision +# total_recall += recall +# total_f1 += f1 + +# total_precision = total_precision/len(unique_atc) +# total_recall = total_recall/len(unique_atc) +# total_f1 = total_f1/len(unique_atc) + + +# print(f' MICRO : The precision is {precision_micro}, the recall is {recall_micro} and the f1 score is {f1_micro}') +# print(f' MACRO : The precision is {total_precision}, the recall is {total_recall} and the f1 score is {total_f1}') diff --git a/Normalisation/extract_measurement/config.py b/Normalisation/extract_measurement/config.py new file mode 100644 index 000000000..8a319e172 --- /dev/null +++ b/Normalisation/extract_measurement/config.py @@ -0,0 +1,16 @@ +from measurements_patterns import * +import pandas as pd + + +################################ +# ## MEASUREMENTS PIPE CONFIG ### +# ############################### +measurements_pipe_regex_convert_spans = regex_convert_spans +measurements_pipe_label_key = label_key +measurements_pipe_labels_to_remove = labels_to_remove +measurements_pipe_labels_linkable_to_measurement = labels_linkable_to_measurement +measurements_pipe_config_normalizer_from_label_key = config_normalizer_from_label_key +measurements_pipe_config_measurements_from_label_key = config_measurements_from_label_key +measurements_pipe_config_normalizer_from_tables = config_normalizer_from_tables +measurements_pipe_config_measurements_from_tables = config_measurements_from_tables +measurements_only_tables = False diff --git a/Normalisation/extract_measurement/extract_measurements_from_brat.py b/Normalisation/extract_measurement/extract_measurements_from_brat.py new file mode 100644 index 000000000..06f3e2856 --- /dev/null +++ b/Normalisation/extract_measurement/extract_measurements_from_brat.py @@ -0,0 +1,348 @@ +import time +from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union +from typing_extensions import TypedDict + +from measurements_patterns import regex_convert_spans, label_key, labels_to_remove, labels_linkable_to_measurement, config_normalizer_from_label_key, config_measurements_from_label_key, config_normalizer_from_tables, config_measurements_from_tables +from scipy.stats import bootstrap +import spacy +import pandas as pd +import re +import random +import matplotlib.pyplot as plt +import numpy as np +import math +from statistics import mean +from os.path import isfile, isdir, join, basename +from os import listdir +from itertools import combinations +from edsnlp.processing import pipe +from tqdm import tqdm + +import sys +from extract_pandas_from_brat import extract_pandas + + +class UnitConfig(TypedDict): + scale: float + terms: List[str] + followed_by: Optional[str] = None + ui_decomposition: Dict[str, int] + +class UnitlessRange(TypedDict): + min: Optional[int] + max: Optional[int] + unit: str + +class UnitlessPatternConfig(TypedDict): + terms: List[str] + ranges: List[UnitlessRange] + +class SimpleMeasurementConfigWithoutRegistry(TypedDict): + value_range: str + value: Union[float, int] + unit: str + +class ValuelessPatternConfig(TypedDict): + terms: Optional[List[str]] + regex: Optional[List[str]] + measurement: SimpleMeasurementConfigWithoutRegistry + +class MeasureConfig(TypedDict): + unit: str + unitless_patterns: Optional[List[UnitlessPatternConfig]] + valueless_patterns: Optional[List[ValuelessPatternConfig]] + +class MeasurementsPipeConfig(TypedDict): + measurements: Union[List[str], Tuple[str], Dict[str, MeasureConfig]] + units_config: Dict[str, UnitConfig] + number_terms: Dict[str, List[str]] + value_range_terms: Dict[str, List[str]] + all_measurements: bool + parse_tables: bool + parse_doc: bool + stopwords_unitless: List[str] + stopwords_measure_unit: List[str] + measure_before_unit: bool + unit_divisors: List[str] + name: str + ignore_excluded: bool + attr: str + + +class ExtractMeasurements: + def __init__( + self, + regex_convert_spans: Optional[str] = regex_convert_spans, + label_key: Optional[str] = label_key, + labels_to_remove: Optional[List[str]] = labels_to_remove, + labels_linkable_to_measurement: Optional[List[str]] = labels_linkable_to_measurement, + config_normalizer_from_label_key: Optional[Dict[str, bool]] = config_normalizer_from_label_key, + config_measurements_from_label_key: Optional[MeasurementsPipeConfig] = config_measurements_from_label_key, + config_normalizer_from_tables: Optional[Dict[str, bool]] = config_normalizer_from_tables, + config_measurements_from_tables: Optional[MeasurementsPipeConfig] = config_measurements_from_tables + ): + print("--------------- Loading extraction pipe ---------------") + self.regex_convert_spans = re.compile(regex_convert_spans) + self.label_key = label_key + self.labels_to_remove = labels_to_remove + self.labels_linkable_to_measurement = labels_linkable_to_measurement + self.nlp_from_label_key = self.load_nlp(config_normalizer_from_label_key, config_measurements_from_label_key) + self.nlp_from_tables = self.load_nlp(config_normalizer_from_tables, config_measurements_from_tables) + print("--------------- Extraction pipe loaded ---------------") + + def load_nlp(self, config_normalizer, config_measurements): + nlp = spacy.blank("eds") + nlp.add_pipe("eds.normalizer", config=config_normalizer) + nlp.add_pipe("eds.dates") + nlp.add_pipe("eds.tables") + nlp.add_pipe("eds.measurements", config=config_measurements) + return nlp + + def extract_pandas_labels_of_interest(self, brat_dir): + # Convert span to list with span_start, span_end. It considers the new lines by adding one character. + def convert_spans(span): + span_match = self.regex_convert_spans.match(span) + span_start = int(span_match.group(1)) + span_end = int(span_match.group(2)) + return [span_start, span_end] + + df = extract_pandas(IN_BRAT_DIR=brat_dir) + df = df.loc[df["label"].isin([self.label_key] + self.labels_to_remove + self.labels_linkable_to_measurement)] + df["span_converted"] = df["span"].apply(convert_spans) + df = df[["term", "source", "span_converted", "label"]] + return df + + @classmethod + def is_overlapping(cls, a, b): + # Return crop parts in a if a and b overlaps, 0, 0 if not + if max(0, 1 + min(a[1], b[1]) - max(a[0], b[0])): + return max(a[0], b[0]), min(a[1], b[1]) + else: + return 0, 0 + + def remove_labels_from_label_key(self, df): + def get_parts_to_crop(old_parts_to_crop, new_part_to_crop): + # From old_part_to_crops which is a list of segments and new_part_to_crop + # which is a segment, return the union all the segments. + # All segments in old_parts_to_crop are disjunct + for old_part in old_parts_to_crop: + crop_start, crop_end = self.is_overlapping(old_part, new_part_to_crop) + if crop_start or crop_end: + return get_parts_to_crop( + old_parts_to_crop[1:], + [ + min(old_part[0], new_part_to_crop[0]), + max(old_part[1], new_part_to_crop[1]), + ], + ) + old_parts_to_crop.append(new_part_to_crop) + return old_parts_to_crop + + + def crop_with_parts_to_crop(parts_to_crop, to_crop, to_crop_span): + # parts_to_crop contains a list of segments to crop in to_crop (str) + parts_to_crop.insert(0, [to_crop_span[0], to_crop_span[0]]) + parts_to_crop.append([to_crop_span[1], to_crop_span[1]]) + res = [ + to_crop[ + parts_to_crop[i][1] + - to_crop_span[0] : parts_to_crop[i + 1][0] + - to_crop_span[0] + ] + for i in range(len(parts_to_crop) - 1) + ] + return "".join(res) + + label_key_df = df.loc[df["label"] == self.label_key].sort_values("source") + specific_label_df = df.loc[df["label"].isin(self.labels_to_remove + self.labels_linkable_to_measurement)] + res = {"term_labels_removed": [], "terms_linked_to_measurement": []} + label_keys = [] + source = None + for label_key in tqdm(label_key_df.itertuples(index=False), total = label_key_df.shape[0]): + new_source = label_key.source + if new_source != source: + temp_df = specific_label_df.loc[(specific_label_df["source"] == new_source)] + source = new_source + labels_linkable_to_measurement = [] + parts_to_crop = [] + + for label in temp_df.itertuples(index=False): + + crop_start, crop_end = self.is_overlapping( + label_key.span_converted, label.span_converted + ) + + if crop_start or crop_end: + + if label.label in self.labels_to_remove: + if not parts_to_crop: + parts_to_crop.append([crop_start, crop_end]) + + else: + parts_to_crop = get_parts_to_crop( + parts_to_crop, [crop_start, crop_end] + ) + if label.label in self.labels_linkable_to_measurement: + labels_linkable_to_measurement.append(label.term) + res["term_labels_removed"].append(crop_with_parts_to_crop( + parts_to_crop, label_key.term, label_key.span_converted + )) + res["terms_linked_to_measurement"].append(labels_linkable_to_measurement) + label_keys.append(label_key) + res = pd.DataFrame(label_keys).join(pd.DataFrame(res)) + return res.reset_index(drop=True) + + def get_measurements_from_label_key(self, df): + df_for_nlp_from_label_key = pd.DataFrame({ + "note_text": df["term_labels_removed"], + "note_id": df.index + }) + df_for_nlp_from_label_key = pipe( + note=df_for_nlp_from_label_key, + nlp = self.nlp_from_label_key, + n_jobs=-1, + additional_spans = ["measurements"], + extensions = ["value"], + ) + df_for_nlp_from_label_key = df_for_nlp_from_label_key.groupby("note_id").agg({"note_id":"first", "value":list, "lexical_variant":list}).reset_index(drop=True).rename(columns={"value":"found"}) + df = pd.merge(df, df_for_nlp_from_label_key, left_index=True, right_on="note_id") + df["found"] = df["found"].fillna("").apply(list) + return df.reset_index(drop=True) + + def get_measurements_from_tables(self, df, df_labels_of_interest, brat_dir, only_tables): + + # Treat each txt files + txt_files = [ + f for f in listdir(brat_dir) if isfile(join(brat_dir, f)) if f.endswith(".txt") + ] + ann_files = [f[:-3] + "ann" for f in txt_files] + text_df = {"note_text": [], "note_id": []} + for i, txt_file in enumerate(txt_files): + with open(join(brat_dir, txt_file), "r") as file: + text = file.read() + text_df["note_text"].append(text) + text_df["note_id"].append(txt_file[:-3] + "ann") + text_df = pd.DataFrame(text_df) + df_for_nlp_from_table = pipe( + note=text_df, + nlp = self.nlp_from_tables, + n_jobs=-1, + additional_spans = ["measurements"], + extensions = ["value"], + ) + # Load discriminative dataframe (in other words df containing terms with a label to remove) so that we can drop matches when one overlaps one of these words + discriminative_df = df_labels_of_interest.loc[ + df_labels_of_interest["label"].isin(self.labels_to_remove) + ] + + def get_measurements_from_tables_one_file(df_for_nlp_from_table, ann_file): + # Select label_keys from the ann_file + # and check if our matcher from tables + # finds measurements with a span overlapping + # one of these label_keys. If yes, then we keep this measurement + # and throw all the ones found by our first matcher + # from the label_key at stake. + df_part = df.loc[df["source"] == ann_file].copy().reset_index(drop=True) + df_part["new_found"] = [[] for _ in range(len(df_part))] + discriminative_df_part = ( + discriminative_df.loc[discriminative_df["source"] == ann_file] + .copy() + .reset_index(drop=True) + ) + df_for_nlp_from_table_part = df_for_nlp_from_table.loc[df_for_nlp_from_table["note_id"] == ann_file].copy().reset_index(drop=True) + for measurement_from_table in df_for_nlp_from_table_part.itertuples(index=False): + measurement_span = [ + measurement_from_table.start, + measurement_from_table.end, + ] + + # Check if a match is in a term with a label to remove + overlapping_discriminative_indexes = discriminative_df_part.loc[ + discriminative_df_part["span_converted"].apply( + lambda x: self.is_overlapping(x, measurement_span) + ) + != (0, 0) + ].index.values.tolist() + if overlapping_discriminative_indexes: + continue + # Link the match measure to a label_key - label_linkable_to_measurement + overlapping_label_key_indexes = df_part.loc[ + df_part["span_converted"].apply( + lambda x: self.is_overlapping(x, measurement_span) + ) + != (0, 0) + ].index.values.tolist() + for i in overlapping_label_key_indexes: + df_part.iloc[i]["new_found"].append(measurement_from_table.value) + return df_part + + # DataFrame with merged doc and tables matches + result_df_per_file = [] + for ann_file in tqdm(ann_files, total = len(ann_files)): + result_df_per_file.append(get_measurements_from_tables_one_file(df_for_nlp_from_table, ann_file)) + + result_df = pd.concat(result_df_per_file, ignore_index=True) + if only_tables: + result_df = result_df.loc[result_df["new_found"].astype(bool)].reset_index(drop=True) + result_df = result_df.drop(columns=["found"]).rename(columns={"new_found":"found"}) + return result_df + else: + result_df["found"] = result_df.apply( + lambda x: x["found"] * (not x["new_found"]) + + x["new_found"] * (len(x["new_found"]) > 0), + axis=1, + ) + return result_df.drop(columns=["new_found"]) + + def prepare_df_for_normalization(self, df): + # This method converts SimpleMeasurement objects to strings + # So that It can be exported to json + # Moreover, for each term, if any terms_linked_to_measurement are found, + # We fill the cell with a list of 1 item: + # this term cropped by the found measures from It + + # Fill empty terms_linked_to_measurement + mask_empty_labels_linkable_to_measurement = (df["terms_linked_to_measurement"].str.len() == 0) + df_empty_labels_linkable_to_measurement = df[mask_empty_labels_linkable_to_measurement][["term", "lexical_variant"]] + df.loc[mask_empty_labels_linkable_to_measurement, "terms_linked_to_measurement"] = df_empty_labels_linkable_to_measurement.apply(lambda row: + [re + .compile( + r"\b(?:" + "|".join(row["lexical_variant"]) + r")\b", + re.IGNORECASE + ) + .sub("", row["term"])], + axis=1) + + # Convert SimpleMeasurement to str + df["found"] = df["found"].apply(lambda measurements: [measurement.value_range + " " + str(measurement.value) + " " + measurement.unit + for measurement in measurements]) + df = df.drop(columns=["label", "term_labels_removed", "note_id", "lexical_variant"]) + return df + + def __call__(self, brat_dir, only_tables): + print("--------------- Converting BRAT files to Pandas DataFrame... ---------------") + tic = time.time() + df_labels_of_interest = self.extract_pandas_labels_of_interest(brat_dir) + tac=time.time() + print(f"Converting BRAT files to Pandas DataFrame : {tac-tic:.2f} sec") + print("--------------- Removing labels from label keys... ---------------") + tic = time.time() + df = self.remove_labels_from_label_key(df_labels_of_interest) + tac=time.time() + print(f"Removing labels from label keys : {tac-tic:.2f} sec") + print("--------------- Extracting measurements from label keys... ---------------") + tic = time.time() + df = self.get_measurements_from_label_key(df) + tac=time.time() + print(f"Extracting measurements from label keys : {tac-tic:.2f} sec") + print("--------------- Extracting measurements from tables... ---------------") + tic = time.time() + df = self.get_measurements_from_tables(df, df_labels_of_interest, brat_dir, only_tables) + tac=time.time() + print(f"Extracting measurements from tables : {tac-tic:.2f} sec") + print("--------------- Formatting table for normalization... ---------------") + tic = time.time() + df = self.prepare_df_for_normalization(df) + tac=time.time() + print(f"Formatting table for normalization : {tac-tic:.2f} sec") + return df diff --git a/Normalisation/extract_measurement/extract_pandas_from_brat.py b/Normalisation/extract_measurement/extract_pandas_from_brat.py new file mode 100644 index 000000000..b1836f95d --- /dev/null +++ b/Normalisation/extract_measurement/extract_pandas_from_brat.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# coding: utf-8 +# %% + +# # Build prediction file +# +# From our files with NER prediction, extract a pandas data frame to work on entities easily +# + +# %% + + +from os.path import isfile, isdir, join, basename +from os import listdir +import pandas as pd +import numpy as np +import re + +import collections +import math + + +def extract_pandas(IN_BRAT_DIR, + OUT_DF = None, + labels = None): + + assert isdir(IN_BRAT_DIR) + + ENTITY_REGEX = re.compile('^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$') + + + data = [] + patients = [] + + # extract all ann_files from IN_BRAT_DIR + ann_files = [f for f in listdir(IN_BRAT_DIR) if isfile(join(IN_BRAT_DIR, f)) if f.endswith('.ann')] + for ann_file in ann_files: + ann_path = join(IN_BRAT_DIR, ann_file) + txt_path = ann_path[:-4]+'.txt' + + # sanity check + assert isfile(ann_path) + assert isfile(txt_path) + + # Read text file to get patient number : + with open(txt_path, 'r', encoding='utf-8') as f_txt:lines_txt = f_txt.readlines() + patient_num = lines_txt[0][:-1] + patients.append(patient_num) + + # Read ann file + with open(ann_path, 'r', encoding='utf-8') as f_in:lines = f_in.readlines() + + for line in lines: + entity_match = ENTITY_REGEX.match(line.strip()) + if entity_match is not None: + ann_id = entity_match.group(1) + label = entity_match.group(2) + offsets = entity_match.group(3) + term = entity_match.group(4) + if labels is None: + data.append([ann_id, term, label, basename(ann_path), offsets]) + elif label in labels: + data.append([ann_id, term, label, basename(ann_path), offsets]) + + columns = ['ann_id', 'term', 'label', 'source', 'span'] + dataset_df = pd.DataFrame(data=list(data), columns=columns) + if OUT_DF: dataset_df.to_csv(OUT_DF) + + return dataset_df diff --git a/Normalisation/extract_measurement/main.py b/Normalisation/extract_measurement/main.py new file mode 100644 index 000000000..6db8294fd --- /dev/null +++ b/Normalisation/extract_measurement/main.py @@ -0,0 +1,31 @@ +import os +import typer +os.environ["OMP_NUM_THREADS"] = "16" +from extract_measurements_from_brat import ExtractMeasurements +from config import * +from pathlib import Path + +import pandas as pd + + +def extract_measurements_cli( + input_dir: Path, + output_dir: Path, +): + df = ExtractMeasurements( + regex_convert_spans = measurements_pipe_regex_convert_spans, + label_key = measurements_pipe_label_key, + labels_to_remove = measurements_pipe_labels_to_remove, + labels_linkable_to_measurement = measurements_pipe_labels_linkable_to_measurement, + config_normalizer_from_label_key = measurements_pipe_config_normalizer_from_label_key, + config_measurements_from_label_key = measurements_pipe_config_measurements_from_label_key, + config_normalizer_from_tables = measurements_pipe_config_normalizer_from_tables, + config_measurements_from_tables = measurements_pipe_config_measurements_from_tables, + )(brat_dir = input_dir, only_tables = measurements_only_tables) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + df.to_json(output_dir / "pred_with_extraction.json") + + +if __name__ == "__main__": + typer.run(extract_measurements_cli) diff --git a/Normalisation/extract_measurement/measurements_patterns.py b/Normalisation/extract_measurement/measurements_patterns.py new file mode 100644 index 000000000..cb02efed0 --- /dev/null +++ b/Normalisation/extract_measurement/measurements_patterns.py @@ -0,0 +1,332 @@ +from edsnlp.pipelines.misc.measurements.patterns import common_measurements, number_terms, value_range_terms, units_config, unit_divisors, stopwords_unitless, stopwords_measure_unit + + +####################################### +# ## CONFIG TO PRETREAT THE BRAT DIR ### +# ###################################### + +# Regex which identifies in group 1 the beginning of a span and in group 2 +# the end of the same span +regex_convert_spans = r"^(\d+).*\s(\d+)$" + +# Label of the entities containing the measurement and possibly +# other random entities +label_key = "BIO_comp" + +# Labels of the entities It is possible not to consider during matching +labels_to_remove = ["BIO"] + +# Labels of the entities which can be linked to a measurement +labels_linkable_to_measurement = ["BIO"] + + +########################################################## +# ## PIPE TO MATCH MEASUREMENTS IN `label_key` ENTITIES ### +# ######################################################### + +# Config of the normalizer pipe used in entities labeled `label_key` +config_normalizer_from_label_key = dict( + lowercase=True, + accents=True, + quotes=True, + pollution=True, +) + +# Terms which will make the measurements pipe match a positive measurement +positive_terms_from_label_key = ("positifs", "positives", "positivites") +# We create a list to match abbreviations of the positive words. This list will +# be the final dictionnary used to match the positive measurements. +positive_terms_from_label_key = [ + word[: i + 1] + for word in positive_terms_from_label_key + for i in range(min(len(word) - 1, 1), len(word)) +] +# Symbols which will make the measurements pipe match a positive measurement +positive_symbols_from_label_key = ("\+", "p") +# To match symbols, we create regex +positive_regex_from_label_key = [ + r"^[^a-zA-Z0-9]*(?:% s)" % "|".join(positive_symbols_from_label_key) + + r"[^a-zA-Z0-9]*$" +] + +# Terms which will make the measurements pipe match a negative measurement +negative_terms_from_label_key = ( + "negatifs", + "negatives", + "negativites", + "absences", + "absents", +) +# We create a list to match abbreviations of the negative words. This list will +# be the final dictionnary used to match the negative measurements. +negative_terms_from_label_key = [ + word[: i + 1] + for word in negative_terms_from_label_key + for i in range(min(len(word) - 1, 1), len(word)) +] +# Symbols which will make the measurements pipe match a positive measurement +negative_symbols_from_label_key = ("\-", "n") +# To match symbols, we create regex +negative_regex_from_label_key = [ + r"^[^a-zA-Z0-9]*(?:% s)" % "|".join(negative_symbols_from_label_key) + + r"[^a-zA-Z0-9]*$" +] + +# Terms which will make the measurements pipe match a normal measurement +normal_terms_from_label_key = ("normales", "normaux", "normalisations", "normalites") +# We create a list to match abbreviations of the normal words. This list will +# be the final dictionnary used to match the normal measurements. +normal_terms_from_label_key = [ + word[: i + 1] + for word in normal_terms_from_label_key + for i in range(min(len(word) - 1, 1), len(word)) +] + +# Custom mesurements mainly to include custom positive, negative +# and normal measurements +measurements_from_label_key = { + "eds.weight": { + "unit": "kg", + "unitless_patterns": [ + { + "terms": ["poids", "poid", "pese", "pesant", "pesait", "pesent"], + "ranges": [ + {"min": 0, "max": 200, "unit": "kg"}, + {"min": 200, "unit": "g"}, + ], + } + ], + }, + "eds.size": { + "unit": "m", + "unitless_patterns": [ + { + "terms": [ + "mesure", + "taille", + "mesurant", + "mesurent", + "mesurait", + "mesuree", + "hauteur", + "largeur", + "longueur", + ], + "ranges": [ + {"min": 0, "max": 3, "unit": "m"}, + {"min": 3, "unit": "cm"}, + ], + } + ], + }, + "eds.bmi": { + "unit": "kg_per_m2", + "unitless_patterns": [ + {"terms": ["imc", "bmi"], "ranges": [{"unit": "kg_per_m2"}]} + ], + }, + "eds.volume": {"unit": "m3", "unitless_patterns": []}, + "eds.bool": { + "unit": "bool", + "valueless_patterns": [ + { + "terms": positive_terms_from_label_key, + "regex": positive_regex_from_label_key, + "measurement": { + "value_range": "=", + "value": 1, + "unit": "bool", + }, + }, + { + "terms": negative_terms_from_label_key, + "regex": negative_regex_from_label_key, + "measurement": { + "value_range": "=", + "value": 0, + "unit": "bool", + }, + }, + { + "terms": normal_terms_from_label_key, + "measurement": { + "value_range": "=", + "value": 0.5, + "unit": "bool", + }, + }, + ], + }, +} + +# Config of the measurement pipe used in entities labeled `label_key` +config_measurements_from_label_key = dict( + measurements=measurements_from_label_key, + units_config=units_config, + number_terms=number_terms, + value_range_terms=value_range_terms, + unit_divisors=unit_divisors, + stopwords_unitless=stopwords_unitless, + stopwords_measure_unit=stopwords_measure_unit, + measure_before_unit=False, + ignore_excluded=True, + attr="NORM", + all_measurements=True, + parse_tables=False, + parse_doc=True, +) + + +############################################ +# ## PIPE TO MATCH MEASUREMENTS IN TABLES ### +# ########################################### + +# Config of the normalizer pipe used in tables +config_normalizer_from_tables = dict( + lowercase=True, + accents=True, + quotes=True, + pollution=True, +) + +# Terms which will make the measurements pipe match a positive measurement +positive_terms_from_tables = ("positifs", "positives", "positivites") +# We create a list to match abbreviations of the positive words. This list will +# be the final dictionnary used to match the positive measurements. +positive_terms_from_tables = [ + word[: i + 1] + for word in positive_terms_from_tables + for i in range(min(len(word) - 1, 1), len(word)) +] +# Symbols which will make the measurements pipe match a positive measurement +positive_symbols_from_tables = ("\+", "p") +# To match symbols, we create regex +positive_regex_from_tables = [ + r"^[^a-zA-Z0-9]*(?:% s)" % "|".join(positive_symbols_from_tables) + r"[^a-zA-Z0-9]*$" +] + +# Terms which will make the measurements pipe match a negative measurement +negative_terms_from_tables = ( + "negatifs", + "negatives", + "negativites", + "absences", + "absents", +) +# We create a list to match abbreviations of the negative words. This list will +# be the final dictionnary used to match the negative measurements. +negative_terms_from_tables = [ + word[: i + 1] + for word in negative_terms_from_tables + for i in range(min(len(word) - 1, 1), len(word)) +] +# Symbols which will make the measurements pipe match a positive measurement +negative_symbols_from_tables = ("\-", "n") +# To match symbols, we create regex +negative_regex_from_tables = [ + r"^[^a-zA-Z0-9]*(?:% s)" % "|".join(negative_symbols_from_tables) + r"[^a-zA-Z0-9]*$" +] + +# Terms which will make the measurements pipe match a normal measurement +normal_terms_from_tables = ("normales", "normaux", "normalisations", "normalites") +# We create a list to match abbreviations of the normal words. This list will +# be the final dictionnary used to match the normal measurements. +normal_terms_from_tables = [ + word[: i + 1] + for word in normal_terms_from_tables + for i in range(min(len(word) - 1, 1), len(word)) +] + +# Custom mesurements mainly to include custom positive, negative +# and normal measurements +measurements_from_tables = { + "eds.weight": { + "unit": "kg", + "unitless_patterns": [ + { + "terms": ["poids", "poid", "pese", "pesant", "pesait", "pesent"], + "ranges": [ + {"min": 0, "max": 200, "unit": "kg"}, + {"min": 200, "unit": "g"}, + ], + } + ], + }, + "eds.size": { + "unit": "m", + "unitless_patterns": [ + { + "terms": [ + "mesure", + "taille", + "mesurant", + "mesurent", + "mesurait", + "mesuree", + "hauteur", + "largeur", + "longueur", + ], + "ranges": [ + {"min": 0, "max": 3, "unit": "m"}, + {"min": 3, "unit": "cm"}, + ], + } + ], + }, + "eds.bmi": { + "unit": "kg_per_m2", + "unitless_patterns": [ + {"terms": ["imc", "bmi"], "ranges": [{"unit": "kg_per_m2"}]} + ], + }, + "eds.volume": {"unit": "m3", "unitless_patterns": []}, + "eds.bool": { + "unit": "bool", + "valueless_patterns": [ + { + "terms": positive_terms_from_tables, + "regex": positive_regex_from_tables, + "measurement": { + "value_range": "=", + "value": 1, + "unit": "bool", + }, + }, + { + "terms": negative_terms_from_tables, + "regex": negative_regex_from_tables, + "measurement": { + "value_range": "=", + "value": 0, + "unit": "bool", + }, + }, + { + "terms": normal_terms_from_tables, + "measurement": { + "value_range": "=", + "value": 0.5, + "unit": "bool", + }, + }, + ], + }, +} + +# Config of the measurement pipe used in tables +config_measurements_from_tables = dict( + measurements=measurements_from_tables, + units_config=units_config, + number_terms=number_terms, + value_range_terms=value_range_terms, + unit_divisors=unit_divisors, + stopwords_unitless=stopwords_unitless, + stopwords_measure_unit=stopwords_measure_unit, + measure_before_unit=False, + ignore_excluded=True, + attr="NORM", + all_measurements=True, + parse_tables=True, + parse_doc=False, +) diff --git a/Normalisation/inference/config.py b/Normalisation/inference/config.py new file mode 100644 index 000000000..a86a34cff --- /dev/null +++ b/Normalisation/inference/config.py @@ -0,0 +1,84 @@ +import pandas as pd + + +###################### +# ## GENERAL CONFIG ### +# ##################### +umls_path = "/export/home/cse200093/scratch/BioMedics/data/umls/bio_str_SNOMEDCT_US.json" +labels_column_name = "CUI" +# Name of the column which contains the CUIs +synonyms_column_name = "STR" +# Name of the column which contains the synonyms +res_path = "/export/home/cse200093/Jacques_Bio/BioMedics/data/maladie_de_takayasu_norm/res.json" + + +#################### +# ## CODER CONFIG ### +# ################### +column_name_to_normalize = "term" +# Name of the preceding column of interest. Default should be +# "terms_linked_to_measurement" to make the entire pipe work +coder_model_name_or_path = "/export/home/cse200093/scratch/word-embedding/coder_eds/model_967662.pth" +coder_tokenizer_name_or_path = "/export/home/cse200093/scratch/word-embedding/finetuning-camembert-2021-07-29" +coder_device = "cuda:0" +coder_save_umls_embeddings_dir = False +# set to False if you don't want to save +coder_save_umls_des_dir = False +# set to False if you don't want to save +coder_save_umls_labels_dir = False +# set to False if you don't want to save +coder_save_data_embeddings_dir = False +# set to False if you don't want to save +coder_normalize=True +coder_summary_method="CLS" +coder_tqdm_bar=True +coder_cased = True +coder_batch_size = 128 +coder_stopwords = [ + "for", + "assay", + "by", + "tests", + "minute", + "exam", + "with", + "human", + "moyenne", + "in", + "to", + "from", + "analyse", + "test", + "level", + "fluid", + "laboratory", + "determination", + "examination", + "releasing", + "quantitative", + "screening", + "and", + "exploration", + "factor", + "method", + "analysis", + "laboratoire", + "specimen", + "or", + "typing", + "of", + "concentration", + "measurement", + "detection", + "procedure", + "identification", + "numeration", + "hour", + "retired", + "technique", + "count", +] +coder_remove_stopwords_terms = False +coder_remove_special_characters_terms = False +coder_remove_stopwords_umls = True +coder_remove_special_characters_umls = True diff --git a/Normalisation/inference/extract_pandas_from_brat.py b/Normalisation/inference/extract_pandas_from_brat.py new file mode 100644 index 000000000..c9a959582 --- /dev/null +++ b/Normalisation/inference/extract_pandas_from_brat.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# coding: utf-8 +# %% + +# # Build prediction file +# +# From our files with NER prediction, extract a pandas data frame to work on entities easily +# + +# %% + + +from os.path import isfile, isdir, join, basename +from os import listdir +import pandas as pd +import numpy as np +import re + +import collections +import math + + +def extract_pandas(IN_BRAT_DIR, + OUT_DF = None, + labels = None): + + assert isdir(IN_BRAT_DIR) + + ENTITY_REGEX = re.compile('^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$') + + + data = [] + patients = [] + + # extract all ann_files from IN_BRAT_DIR + ann_files = [f for f in listdir(IN_BRAT_DIR) if isfile(join(IN_BRAT_DIR, f)) if f.endswith('.ann')] + + for ann_file in ann_files: + ann_path = join(IN_BRAT_DIR, ann_file) + txt_path = ann_path[:-4]+'.txt' + + # sanity check + assert isfile(ann_path) + assert isfile(txt_path) + + # Read text file to get patient number : + with open(txt_path, 'r', encoding='utf-8') as f_txt:lines_txt = f_txt.readlines() + patient_num = lines_txt[0][:-1] + patients.append(patient_num) + + # Read ann file + with open(ann_path, 'r', encoding='utf-8') as f_in:lines = f_in.readlines() + + for line in lines: + entity_match = ENTITY_REGEX.match(line.strip()) + if entity_match is not None: + ann_id = entity_match.group(1) + label = entity_match.group(2) + offsets = entity_match.group(3) + term = entity_match.group(4) + if labels is None: + data.append([ann_id, term, label, basename(ann_path), offsets]) + elif label in labels: + data.append([ann_id, term, label, basename(ann_path), offsets]) + + columns = ['ann_id', 'term', 'label', 'source', 'span'] + dataset_df = pd.DataFrame(data=list(data), columns=columns) + if OUT_DF: dataset_df.to_csv(OUT_DF) + + return dataset_df diff --git a/Normalisation/inference/get_normalization_with_coder.py b/Normalisation/inference/get_normalization_with_coder.py new file mode 100644 index 000000000..c3a0a5ed3 --- /dev/null +++ b/Normalisation/inference/get_normalization_with_coder.py @@ -0,0 +1,125 @@ +import os +import sys +import time +import pathlib +import argparse +import torch +from torch import nn +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig, AdamW, get_linear_schedule_with_warmup, get_cosine_schedule_with_warmup, get_constant_schedule_with_warmup +from tqdm import tqdm, trange +import sys +sys.path.append("/export/home/cse200093/scratch/BioMedics/normalisation/training") + +class CoderNormalizer(): + def __init__( + self, + model_name_or_path: str, + tokenizer_name_or_path: str, + device: str = "cuda:0", + ): + self.device = device + try: + self.model = AutoModel.from_pretrained(model_name_or_path).to(self.device) + self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_name_or_path) + self.model_from_transformers = True + except: + self.model = torch.load(model_name_or_path).to(self.device) + self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_name_or_path) + self.model_from_transformers = False + + def get_bert_embed( + self, + phrase_list, + normalize=True, + summary_method="CLS", + tqdm_bar=False, + coder_batch_size=128, + ): + input_ids = [] + for phrase in phrase_list: + input_ids.append( + self.tokenizer.encode_plus( + phrase, + max_length=32, + add_special_tokens=True, + truncation=True, + pad_to_max_length=True, + )["input_ids"] + ) + self.model.eval() + + count = len(input_ids) + now_count = 0 + with torch.no_grad(): + if tqdm_bar: + pbar = tqdm(total=count) + while now_count < count: + input_gpu_0 = torch.LongTensor( + input_ids[now_count : min(now_count + coder_batch_size, count)] + ).to(self.device) + if summary_method == "CLS": + if self.model_from_transformers: + embed = self.model(input_gpu_0)[1] + else: + embed = self.model.bert(input_gpu_0)[1] + if summary_method == "MEAN": + if self.model_from_transformers: + embed = torch.mean(self.model(input_gpu_0)[0], dim=1) + else: + embed = torch.mean(self.model.bert(input_gpu_0)[0], dim=1) + if normalize: + embed_norm = torch.norm(embed, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) + embed = embed / embed_norm + if now_count == 0: + output = embed + else: + output = torch.cat((output, embed), dim=0) + if tqdm_bar: + pbar.update(min(now_count + coder_batch_size, count) - now_count) + now_count = min(now_count + coder_batch_size, count) + if tqdm_bar: + pbar.close() + return output + + def get_sim_results( + self, + res_embeddings, + umls_embeddings, + umls_labels, + umls_des, + split_size=200 + ): + label_matches = [] + des_matches = [] + print(f"Number of split: {len(torch.split(res_embeddings, split_size))}") + for split_res_embeddings in torch.split(res_embeddings, split_size): + sim = torch.matmul(split_res_embeddings, umls_embeddings.t()) + most_similar = torch.max(sim, dim=1)[1].tolist() + label_matches_split = [umls_labels[idx] for idx in most_similar] + des_matches_split = [umls_des[idx] for idx in most_similar] + label_matches.extend(label_matches_split) + des_matches.extend(des_matches_split) + return label_matches, des_matches + + def __call__( + self, + umls_labels_list, + umls_des_list, + data_list, + save_umls_embeddings_dir = False, + save_data_embeddings_dir = False, + normalize=True, + summary_method="CLS", + tqdm_bar=False, + coder_batch_size = 128, + ): + umls_embeddings = self.get_bert_embed(umls_des_list, normalize, summary_method, tqdm_bar, coder_batch_size) + res_embeddings = self.get_bert_embed(data_list, normalize, summary_method, tqdm_bar, coder_batch_size) + if save_umls_embeddings_dir: + torch.save(umls_embeddings, save_umls_embeddings_dir) + if save_data_embeddings_dir: + torch.save(res_embeddings, save_data_embeddings_dir) + return self.get_sim_results(res_embeddings, umls_embeddings, umls_labels_list, umls_des_list) diff --git a/Normalisation/inference/main.py b/Normalisation/inference/main.py new file mode 100644 index 000000000..f628bc561 --- /dev/null +++ b/Normalisation/inference/main.py @@ -0,0 +1,111 @@ +import os +import typer +os.environ["OMP_NUM_THREADS"] = "16" +from text_preprocessor import TextPreprocessor +from get_normalization_with_coder import CoderNormalizer +from config import * +import pickle +from pathlib import Path + +import pandas as pd + +def coder_wrapper(df): + # This wrapper is needed to preprocess terms + # and in case the cells contains list of terms instead of one unique term + df = df.reset_index(drop=True) + text_preprocessor = TextPreprocessor( + cased=coder_cased, + stopwords=coder_stopwords + ) + coder_normalizer = CoderNormalizer( + model_name_or_path = coder_model_name_or_path, + tokenizer_name_or_path = coder_tokenizer_name_or_path, + device = coder_device + ) + + # Preprocess UMLS + print("--- Preprocessing UMLS ---") + umls_df = pd.read_json(umls_path) + + umls_df[synonyms_column_name] = umls_df[synonyms_column_name].apply(lambda term: + text_preprocessor( + text = term, + remove_stopwords = coder_remove_stopwords_umls, + remove_special_characters = coder_remove_special_characters_umls) + ) + umls_df = ( + umls_df.loc[(~umls_df[synonyms_column_name].str.isnumeric()) & (umls_df[synonyms_column_name] != "")] + .groupby([synonyms_column_name]) + .agg({labels_column_name: set, synonyms_column_name: "first"}) + .reset_index(drop=True) + ) + coder_umls_des_list = umls_df[synonyms_column_name] + coder_umls_labels_list = umls_df[labels_column_name] + if coder_save_umls_des_dir: + with open(coder_save_umls_des_dir, "wb") as f: + pickle.dump(coder_umls_des_list, f) + if coder_save_umls_labels_dir: + with open(coder_save_umls_labels_dir, "wb") as f: + pickle.dump(coder_umls_labels_list, f) + + # Preprocessing and inference on terms + print("--- Preprocessing terms ---") + if type(df[column_name_to_normalize].iloc[0]) == str: + coder_data_list = df[column_name_to_normalize].apply(lambda term: + text_preprocessor( + text = term, + remove_stopwords = coder_remove_stopwords_terms, + remove_special_characters = coder_remove_special_characters_terms) + ).tolist() + print("--- CODER inference ---") + coder_res = coder_normalizer( + umls_labels_list = coder_umls_labels_list, + umls_des_list = coder_umls_des_list, + data_list = coder_data_list, + save_umls_embeddings_dir = coder_save_umls_embeddings_dir, + save_data_embeddings_dir = coder_save_data_embeddings_dir, + normalize = coder_normalize, + summary_method = coder_summary_method, + tqdm_bar = coder_tqdm_bar, + coder_batch_size = coder_batch_size, + ) + df[["label", "des"]] = pd.DataFrame(zip(*coder_res)) + else: + exploded_term_df = pd.DataFrame({ + "id": df.index, + column_name_to_normalize: df[column_name_to_normalize] + }).explode(column_name_to_normalize).reset_index(drop=True) + coder_data_list = exploded_term_df[column_name_to_normalize].apply(lambda term: + text_preprocessor( + text = term, + remove_stopwords = coder_remove_stopwords_terms, + remove_special_characters = coder_remove_special_characters_terms) + ).tolist() + print("--- CODER inference ---") + coder_res = coder_normalizer( + umls_labels_list = coder_umls_labels_list, + umls_des_list = coder_umls_des_list, + data_list = coder_data_list, + save_umls_embeddings_dir = coder_save_umls_embeddings_dir, + save_data_embeddings_dir = coder_save_data_embeddings_dir, + normalize = coder_normalize, + summary_method = coder_summary_method, + tqdm_bar = coder_tqdm_bar, + coder_batch_size = coder_batch_size, + ) + exploded_term_df[["label", "des"]] = pd.DataFrame(zip(*coder_res)) + df = pd.merge(df.drop(columns=[column_name_to_normalize]), exploded_term_df, left_index = True, right_on = "id").drop(columns=["id"]).reset_index(drop=True) + return df + + +def coder_inference_cli( + input_dir: Path, + output_dir: Path, +): + df = pd.read_json(input_dir) + df = coder_wrapper(df) + if res_path: + df.to_json(output_dir) + +if __name__ == "__main__": + typer.run(coder_inference_cli) diff --git a/Normalisation/inference/text_preprocessor.py b/Normalisation/inference/text_preprocessor.py new file mode 100644 index 000000000..269e0bb58 --- /dev/null +++ b/Normalisation/inference/text_preprocessor.py @@ -0,0 +1,40 @@ +from unidecode import unidecode +import re + +class TextPreprocessor(): + def __init__( + self, + cased, + stopwords + ): + self.cased = cased + self.regex_stopwords = re.compile(r"\b(?:" + "|".join(stopwords) + r")\b", re.IGNORECASE) + self.regex_special_characters = re.compile(r"[^a-zA-Z0-9\s]", re.IGNORECASE) + + def normalize(self, txt, remove_stopwords, remove_special_characters): + if not self.cased: + txt = unidecode( + txt.lower() + .replace("-", " ") + .replace("ag ", "antigene ") + .replace("ac ", "anticorps ") + .replace("antigenes ", "antigene ") + ) + else: + txt = unidecode( + txt.replace("-", " ") + .replace("ag ", "antigene ") + .replace("ac ", "anticorps ") + .replace("antigenes ", "antigene ") + .replace("Ag ", "Antigene ") + .replace("Ac ", "Anticorps ") + .replace("Antigenes ", "Antigene ") + ) + if remove_stopwords: + txt = self.regex_stopwords.sub("", txt) + if remove_special_characters: + txt = self.regex_special_characters.sub(" ", txt) + return re.sub(" +", " ", txt).strip() + + def __call__(self, text, remove_stopwords = False, remove_special_characters = False): + return self.normalize(text, remove_stopwords, remove_special_characters) diff --git a/edsnlp/matchers/__init__.py b/Normalisation/training/__init__.py similarity index 100% rename from edsnlp/matchers/__init__.py rename to Normalisation/training/__init__.py diff --git a/Normalisation/training/data_util.py b/Normalisation/training/data_util.py new file mode 100644 index 000000000..196878228 --- /dev/null +++ b/Normalisation/training/data_util.py @@ -0,0 +1,174 @@ +import os +import numpy as np +import pandas as pd +from transformers import AutoTokenizer +from load_umls import UMLS +from torch.utils.data import Dataset, DataLoader +from random import sample +from sampler_util import FixedLengthBatchSampler, my_collate_fn +from torch.utils.data.sampler import RandomSampler +import ipdb +from time import time +import json +from pathlib import Path + + +def pad(list_ids, pad_length, pad_mark=0): + output = [] + for l in list_ids: + if len(l) > pad_length: + output.append(l[0:pad_length]) + else: + output.append(l + [pad_mark] * (pad_length - len(l))) + return output + + +def my_sample(lst, lst_length, start, length): + start = start % lst_length + if start + length < lst_length: + return lst[start:start + length] + return lst[start:] + lst[0:start + length - lst_length] + + +class UMLSDataset(Dataset): + def __init__(self, umls_folder, model_name_or_path, lang, json_save_path=None, max_lui_per_cui=8, max_length=32): + self.umls = UMLS(umls_folder, lang_range=lang) + self.len = len(self.umls.rel) + self.max_lui_per_cui = max_lui_per_cui + self.max_length = max_length + self.tokenizer = AutoTokenizer.from_pretrained("/export/home/cse200093/scratch/word-embedding/finetuning-camembert-2021-07-29") + self.json_save_path = json_save_path + self.calculate_class_count() + + def calculate_class_count(self): + print("Calculate class count") + + self.cui2id = {cui: index for index, + cui in enumerate(self.umls.cui2str.keys())} + + self.re_set = set() + self.rel_set = set() + for r in self.umls.rel: + _, _, re, rel = r.split("\t") + self.re_set.update([re]) + self.rel_set.update([rel]) + self.re_set = list(self.re_set) + self.rel_set = list(self.rel_set) + self.re_set.sort() + self.rel_set.sort() + + self.re2id = {re: index for index, re in enumerate(self.re_set)} + self.rel2id = {rel: index for index, rel in enumerate(self.rel_set)} + + sty_list = list(set(self.umls.cui2sty.values())) + sty_list.sort() + self.sty2id = {sty: index for index, sty in enumerate(sty_list)} + + if self.json_save_path: + with open(os.path.join(self.json_save_path, "re2id.json"), "w") as f: + json.dump(self.re2id, f) + with open(os.path.join(self.json_save_path, "rel2id.json"), "w") as f: + json.dump(self.rel2id, f) + with open(os.path.join(self.json_save_path, "sty2id.json"), "w") as f: + json.dump(self.sty2id, f) + + print("CUI:", len(self.cui2id)) + print("RE:", len(self.re2id)) + print("REL:", len(self.rel2id)) + print("STY:", len(self.sty2id)) + + def tokenize_one(self, string): + return self.tokenizer.encode_plus(string, max_length=self.max_length, truncation=True)['input_ids'] + + # @profile + def __getitem__(self, index): + cui0, cui1, re, rel = self.umls.rel[index].split("\t") + + str0_list = list(self.umls.cui2str[cui0]) + str1_list = list(self.umls.cui2str[cui1]) + if len(str0_list) > self.max_lui_per_cui: + str0_list = sample(str0_list, self.max_lui_per_cui) + if len(str1_list) > self.max_lui_per_cui: + str1_list = sample(str1_list, self.max_lui_per_cui) + use_len = min(len(str0_list), len(str1_list)) + str0_list = str0_list[0:use_len] + str1_list = str1_list[0:use_len] + + sty0_index = self.sty2id[self.umls.cui2sty[cui0]] + sty1_index = self.sty2id[self.umls.cui2sty[cui1]] + + str2_list = [] + cui2_index_list = [] + sty2_index_list = [] + + cui2 = my_sample(self.umls.cui, self.umls.cui_count, + index * self.max_lui_per_cui, use_len * 2) + sample_index = 0 + while len(str2_list) < use_len: + if sample_index < len(cui2): + use_cui2 = cui2[sample_index] + else: + sample_index = 0 + cui2 = my_sample(self.umls.cui, self.umls.cui_count, + index * self.max_lui_per_cui, use_len * 2) + use_cui2 = cui2[sample_index] + # if not "\t".join([cui0, use_cui2, re, rel]) in self.umls.rel: # TOO SLOW! + if True: + cui2_index_list.append(self.cui2id[use_cui2]) + sty2_index_list.append( + self.sty2id[self.umls.cui2sty[use_cui2]]) + str2_list.append(sample(self.umls.cui2str[use_cui2], 1)[0]) + sample_index += 1 + + # print(str0_list) + # print(str1_list) + # print(str2_list) + + input_ids = [self.tokenize_one(s) + for s in str0_list + str1_list + str2_list] + input_ids = pad(input_ids, self.max_length) + input_ids_0 = input_ids[0:use_len] + input_ids_1 = input_ids[use_len:2 * use_len] + input_ids_2 = input_ids[2 * use_len:] + + cui0_index = self.cui2id[cui0] + cui1_index = self.cui2id[cui1] + + re_index = self.re2id[re] + rel_index = self.rel2id[rel] + return input_ids_0, input_ids_1, input_ids_2, \ + [cui0_index] * use_len, [cui1_index] * use_len, cui2_index_list, \ + [sty0_index] * use_len, [sty1_index] * use_len, sty2_index_list, \ + [re_index] * use_len, \ + [rel_index] * use_len + + def __len__(self): + return self.len + + +def fixed_length_dataloader(umls_dataset, fixed_length=96, num_workers=0): + base_sampler = RandomSampler(umls_dataset) + batch_sampler = FixedLengthBatchSampler( + sampler=base_sampler, fixed_length=fixed_length, drop_last=True) + dataloader = DataLoader(umls_dataset, batch_sampler=batch_sampler, + collate_fn=my_collate_fn, num_workers=num_workers, pin_memory=True) + return dataloader + + +if __name__ == "__main__": + umls_dataset = UMLSDataset(umls_folder="../umls", + model_name_or_path="../biobert_v1.1", + lang=None) + ipdb.set_trace() + umls_dataloader = fixed_length_dataloader(umls_dataset, num_workers=4) + now_time = time() + for index, batch in enumerate(umls_dataloader): + print(time() - now_time) + now_time = time() + if index < 10: + for item in batch: + print(item.shape) + #print(batch) + else: + import sys + sys.exit() diff --git a/Normalisation/training/extract_bert.py b/Normalisation/training/extract_bert.py new file mode 100644 index 000000000..be4a7fd6c --- /dev/null +++ b/Normalisation/training/extract_bert.py @@ -0,0 +1,8 @@ +import torch +import sys +import os + + +model = torch.load(sys.argv[1], map_location=torch.device('cpu')) +bert_model = model.bert +torch.save(bert_model, sys.argv[2]) diff --git a/Normalisation/training/extract_pandas_from_brat.py b/Normalisation/training/extract_pandas_from_brat.py new file mode 100644 index 000000000..c9a959582 --- /dev/null +++ b/Normalisation/training/extract_pandas_from_brat.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# coding: utf-8 +# %% + +# # Build prediction file +# +# From our files with NER prediction, extract a pandas data frame to work on entities easily +# + +# %% + + +from os.path import isfile, isdir, join, basename +from os import listdir +import pandas as pd +import numpy as np +import re + +import collections +import math + + +def extract_pandas(IN_BRAT_DIR, + OUT_DF = None, + labels = None): + + assert isdir(IN_BRAT_DIR) + + ENTITY_REGEX = re.compile('^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$') + + + data = [] + patients = [] + + # extract all ann_files from IN_BRAT_DIR + ann_files = [f for f in listdir(IN_BRAT_DIR) if isfile(join(IN_BRAT_DIR, f)) if f.endswith('.ann')] + + for ann_file in ann_files: + ann_path = join(IN_BRAT_DIR, ann_file) + txt_path = ann_path[:-4]+'.txt' + + # sanity check + assert isfile(ann_path) + assert isfile(txt_path) + + # Read text file to get patient number : + with open(txt_path, 'r', encoding='utf-8') as f_txt:lines_txt = f_txt.readlines() + patient_num = lines_txt[0][:-1] + patients.append(patient_num) + + # Read ann file + with open(ann_path, 'r', encoding='utf-8') as f_in:lines = f_in.readlines() + + for line in lines: + entity_match = ENTITY_REGEX.match(line.strip()) + if entity_match is not None: + ann_id = entity_match.group(1) + label = entity_match.group(2) + offsets = entity_match.group(3) + term = entity_match.group(4) + if labels is None: + data.append([ann_id, term, label, basename(ann_path), offsets]) + elif label in labels: + data.append([ann_id, term, label, basename(ann_path), offsets]) + + columns = ['ann_id', 'term', 'label', 'source', 'span'] + dataset_df = pd.DataFrame(data=list(data), columns=columns) + if OUT_DF: dataset_df.to_csv(OUT_DF) + + return dataset_df diff --git a/Normalisation/training/generate_term_embeddings.py b/Normalisation/training/generate_term_embeddings.py new file mode 100644 index 000000000..01cd978f7 --- /dev/null +++ b/Normalisation/training/generate_term_embeddings.py @@ -0,0 +1,84 @@ +from gensim import models +import os +import sys +sys.path.append("/export/home/cse200093/Jacques_Bio/normalisation/py_files") +from load_umls import UMLS +import torch +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig +import tqdm +import pickle +import pandas as pd +from unidecode import unidecode + +batch_size = 128 +device = "cuda:0" + +# Defining the model +# coder_all +model_checkpoint = '/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29' +tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) +model = AutoModel.from_pretrained(model_checkpoint).to(device) + +# Defining data paths +DATA_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/json_annotated_normalisation_formatted/annotated_normalisation_formatted_train_umls_snomed.json" +EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/data_embeddings_normalized_snomed_camembert_eds.pt" + +# Method to generate embeddings +def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False): + input_ids = [] + for phrase in phrase_list: + input_ids.append(tok.encode_plus( + phrase, max_length=32, add_special_tokens=True, + truncation=True, pad_to_max_length=True)['input_ids']) + m.eval() + + count = len(input_ids) + now_count = 0 + with torch.no_grad(): + if tqdm_bar: + pbar = tqdm.tqdm(total=count) + while now_count < count: + input_gpu_0 = torch.LongTensor(input_ids[now_count:min( + now_count + batch_size, count)]).to(device) + if summary_method == "CLS": + embed = m(input_gpu_0)[1] + if summary_method == "MEAN": + embed = torch.mean(m(input_gpu_0)[0], dim=1) + if normalize: + embed_norm = torch.norm( + embed, p=2, dim=1, keepdim=True).clamp(min=1e-12) + embed = embed / embed_norm + if now_count == 0: + output = embed + else: + output = torch.cat((output, embed), dim=0) + if tqdm_bar: + pbar.update(min(now_count + batch_size, count) - now_count) + now_count = min(now_count + batch_size, count) + if tqdm_bar: + pbar.close() + return output + +# Normalisation of words +def normalize(txt): + return unidecode( + txt.lower() + .replace("-", " ") + .replace("ag ", "antigene ") + .replace("ac ", "anticorps ") + .replace("antigenes ", "antigene ") + ) + +# Loading our data to match +data_df = pd.read_json(DATA_DIR) +data_df["term"] = data_df["term"].apply(normalize) +# Merge same terms and keep all possible loincs +data_df = ( + data_df.groupby("term") + .agg({"term": "first", "annotation": set, "source": set}) + .reset_index(drop=True) +) + +umls_embedding = get_bert_embed(data_df["term"].tolist(), model, tokenizer, tqdm_bar=False) +torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) \ No newline at end of file diff --git a/Normalisation/training/generate_term_embeddings.sh b/Normalisation/training/generate_term_embeddings.sh new file mode 100644 index 000000000..63ee366bb --- /dev/null +++ b/Normalisation/training/generate_term_embeddings.sh @@ -0,0 +1,22 @@ +#!/bin/bash +#SBATCH --job-name=generate_umls_embeddings.sh +#SBATCH -t 48:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH -N1-1 +#SBATCH -c2 +#SBATCH --mem=40000 +#SBATCH -p gpuT4 +#SBATCH --output=./log/%x-%j.out +#SBATCH --error=./log/%x-%j.err +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable + +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : +echo starting +conda activate pierrenv +cd /export/home/cse200093/Jacques_Bio/normalisation/py_files +which python +python ./generate_term_embeddings.py +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#which python +#python ./main.py diff --git a/Normalisation/training/generate_term_embeddings_coder_eds.py b/Normalisation/training/generate_term_embeddings_coder_eds.py new file mode 100644 index 000000000..8eb8aada0 --- /dev/null +++ b/Normalisation/training/generate_term_embeddings_coder_eds.py @@ -0,0 +1,85 @@ +from gensim import models +import os +import sys +sys.path.append("/export/home/cse200093/Jacques_Bio/normalisation/py_files") +from load_umls import UMLS +import torch +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig +import tqdm +import pickle +import pandas as pd +from unidecode import unidecode + +batch_size = 128 +device = "cuda:0" + +# Defining the model +# coder_all +model_checkpoint = "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +tokenizer_path = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" +model = torch.load(model_checkpoint).to(device) +tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) + +# Defining data paths +DATA_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/json_annotated_normalisation_formatted/annotated_normalisation_formatted_train_umls_snomed.json" +EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/data_embeddings_normalized_snomed_coder_eds.pt" + +# Method to generate embeddings +def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False): + input_ids = [] + for phrase in phrase_list: + input_ids.append(tok.encode_plus( + phrase, max_length=32, add_special_tokens=True, + truncation=True, pad_to_max_length=True)['input_ids']) + m.eval() + + count = len(input_ids) + now_count = 0 + with torch.no_grad(): + if tqdm_bar: + pbar = tqdm.tqdm(total=count) + while now_count < count: + input_gpu_0 = torch.LongTensor(input_ids[now_count:min( + now_count + batch_size, count)]).to(device) + if summary_method == "CLS": + embed = m.bert(input_gpu_0)[1] + if summary_method == "MEAN": + embed = torch.mean(m.bert(input_gpu_0)[0], dim=1) + if normalize: + embed_norm = torch.norm( + embed, p=2, dim=1, keepdim=True).clamp(min=1e-12) + embed = embed / embed_norm + if now_count == 0: + output = embed + else: + output = torch.cat((output, embed), dim=0) + if tqdm_bar: + pbar.update(min(now_count + batch_size, count) - now_count) + now_count = min(now_count + batch_size, count) + if tqdm_bar: + pbar.close() + return output + +# Normalisation of words +def normalize(txt): + return unidecode( + txt.lower() + .replace("-", " ") + .replace("ag ", "antigene ") + .replace("ac ", "anticorps ") + .replace("antigenes ", "antigene ") + ) + +# Loading our data to match +data_df = pd.read_json(DATA_DIR) +data_df["term"] = data_df["term"].apply(normalize) +# Merge same terms and keep all possible loincs +data_df = ( + data_df.groupby("term") + .agg({"term": "first", "annotation": set, "source": set}) + .reset_index(drop=True) +) + +umls_embedding = get_bert_embed(data_df["term"].tolist(), model, tokenizer, tqdm_bar=False) +torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) diff --git a/Normalisation/training/generate_term_embeddings_coder_eds.sh b/Normalisation/training/generate_term_embeddings_coder_eds.sh new file mode 100644 index 000000000..5a62c2140 --- /dev/null +++ b/Normalisation/training/generate_term_embeddings_coder_eds.sh @@ -0,0 +1,22 @@ +#!/bin/bash +#SBATCH --job-name=generate_umls_embeddings_coder_eds.sh +#SBATCH -t 48:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH -N1-1 +#SBATCH -c2 +#SBATCH --mem=40000 +#SBATCH -p gpuT4 +#SBATCH --output=./log/%x-%j.out +#SBATCH --error=./log/%x-%j.err +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable + +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : +echo starting +conda activate pierrenv +cd /export/home/cse200093/Jacques_Bio/normalisation/py_files +which python +python ./generate_term_embeddings_coder_eds.py +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#which python +#python ./main.py diff --git a/Normalisation/training/generate_term_embeddings_coder_eds_cased.py b/Normalisation/training/generate_term_embeddings_coder_eds_cased.py new file mode 100644 index 000000000..47a070e38 --- /dev/null +++ b/Normalisation/training/generate_term_embeddings_coder_eds_cased.py @@ -0,0 +1,87 @@ +from gensim import models +import os +import sys +sys.path.append("/export/home/cse200093/Jacques_Bio/normalisation/py_files") +from load_umls import UMLS +import torch +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig +import tqdm +import pickle +import pandas as pd +from unidecode import unidecode + +batch_size = 128 +device = "cuda:0" + +# Defining the model +# coder_all +model_checkpoint = "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +tokenizer_path = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" +model = torch.load(model_checkpoint).to(device) +tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) + +# Defining data paths +DATA_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/json_annotated_normalisation_formatted/annotated_normalisation_formatted_train_umls_snomed.json" +EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/data_embeddings_normalized_snomed_coder_eds_2_cased.pt" + +# Method to generate embeddings +def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False): + input_ids = [] + for phrase in phrase_list: + input_ids.append(tok.encode_plus( + phrase, max_length=32, add_special_tokens=True, + truncation=True, pad_to_max_length=True)['input_ids']) + m.eval() + + count = len(input_ids) + now_count = 0 + with torch.no_grad(): + if tqdm_bar: + pbar = tqdm.tqdm(total=count) + while now_count < count: + input_gpu_0 = torch.LongTensor(input_ids[now_count:min( + now_count + batch_size, count)]).to(device) + if summary_method == "CLS": + embed = m.bert(input_gpu_0)[1] + if summary_method == "MEAN": + embed = torch.mean(m.bert(input_gpu_0)[0], dim=1) + if normalize: + embed_norm = torch.norm( + embed, p=2, dim=1, keepdim=True).clamp(min=1e-12) + embed = embed / embed_norm + if now_count == 0: + output = embed + else: + output = torch.cat((output, embed), dim=0) + if tqdm_bar: + pbar.update(min(now_count + batch_size, count) - now_count) + now_count = min(now_count + batch_size, count) + if tqdm_bar: + pbar.close() + return output + +# Normalisation of words +def normalize(txt): + return unidecode( + txt.replace("-", " ") + .replace("ag ", "antigene ") + .replace("ac ", "anticorps ") + .replace("antigenes ", "antigene ") + .replace("Ag ", "Antigene ") + .replace("Ac ", "Anticorps ") + .replace("Antigenes ", "Antigene ") + ) + +# Loading our data to match +data_df = pd.read_json(DATA_DIR) +data_df["term"] = data_df["term"].apply(normalize) +# Merge same terms and keep all possible loincs +data_df = ( + data_df.groupby("term") + .agg({"term": "first", "annotation": set, "source": set}) + .reset_index(drop=True) +) + +umls_embedding = get_bert_embed(data_df["term"].tolist(), model, tokenizer, tqdm_bar=False) +torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) diff --git a/Normalisation/training/generate_term_embeddings_coder_eds_cased.sh b/Normalisation/training/generate_term_embeddings_coder_eds_cased.sh new file mode 100644 index 000000000..71b9f609e --- /dev/null +++ b/Normalisation/training/generate_term_embeddings_coder_eds_cased.sh @@ -0,0 +1,22 @@ +#!/bin/bash +#SBATCH --job-name=generate_umls_embeddings_coder_eds_cased.sh +#SBATCH -t 48:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH -N1-1 +#SBATCH -c2 +#SBATCH --mem=40000 +#SBATCH -p gpuT4 +#SBATCH --output=./log/%x-%j.out +#SBATCH --error=./log/%x-%j.err +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable + +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : +echo starting +conda activate pierrenv +cd /export/home/cse200093/Jacques_Bio/normalisation/py_files +which python +python ./generate_term_embeddings_coder_eds_cased.py +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#which python +#python ./main.py diff --git a/Normalisation/training/generate_umls_embeddings.py b/Normalisation/training/generate_umls_embeddings.py new file mode 100644 index 000000000..b9ec1dbdc --- /dev/null +++ b/Normalisation/training/generate_umls_embeddings.py @@ -0,0 +1,94 @@ +print("BONJOUR") + +from gensim import models +import os +import sys +sys.path.append("/export/home/cse200093/Jacques_Bio/normalisation/py_files") +from load_umls import UMLS +import torch +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig +import tqdm +import pickle + +batch_size = 128 +device = "cuda:0" + +# Defining the model +# coder_all +model_checkpoint = '/export/home/cse200093/coder_all' +tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) +model = AutoModel.from_pretrained(model_checkpoint).to(device) + +# Defining save paths +DES_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_des.pkl" +LABEL_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_label.pkl" +EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_embeddings.pt" + +# Method to generate embeddings +def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False): + input_ids = [] + for phrase in phrase_list: + input_ids.append(tok.encode_plus( + phrase, max_length=32, add_special_tokens=True, + truncation=True, pad_to_max_length=True)['input_ids']) + m.eval() + + count = len(input_ids) + now_count = 0 + with torch.no_grad(): + if tqdm_bar: + pbar = tqdm.tqdm(total=count) + while now_count < count: + input_gpu_0 = torch.LongTensor(input_ids[now_count:min( + now_count + batch_size, count)]).to(device) + if summary_method == "CLS": + embed = m(input_gpu_0)[1] + if summary_method == "MEAN": + embed = torch.mean(m(input_gpu_0)[0], dim=1) + if normalize: + embed_norm = torch.norm( + embed, p=2, dim=1, keepdim=True).clamp(min=1e-12) + embed = embed / embed_norm + if now_count == 0: + output = embed + else: + output = torch.cat((output, embed), dim=0) + if tqdm_bar: + pbar.update(min(now_count + batch_size, count) - now_count) + now_count = min(now_count + batch_size, count) + if tqdm_bar: + pbar.close() + return output + +def get_umls(): + umls_label = [] + umls_label_set = set() + umls_des = [] + umls = UMLS("/export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB", lang_range=['ENG', 'FRA'], only_load_dict=True) + umls.load_sty() + umls_sty = umls.cui2sty + for cui in tqdm.tqdm(umls.cui2str): + if not cui in umls_label_set and umls_sty[cui] == 'Laboratory Procedure': + tmp_str = list(umls.cui2str[cui]) + umls_label.extend([cui] * len(tmp_str)) + umls_des.extend(tmp_str) + umls_label_set.update([cui]) + print(len(umls_des)) + return umls_label, umls_des + +umls_label, umls_des = get_umls() +umls_embedding = get_bert_embed(umls_des, model, tokenizer, tqdm_bar=False) + +"""# Save embeddings +torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) + +# Save umls_des +open_file = open(DES_SAVE_DIR, "wb") +pickle.dump(umls_des, open_file) +open_file.close() + +# Save umls_labels +open_file = open(LABEL_SAVE_DIR, "wb") +pickle.dump(umls_label, open_file) +open_file.close()""" \ No newline at end of file diff --git a/Normalisation/training/generate_umls_embeddings.sh b/Normalisation/training/generate_umls_embeddings.sh new file mode 100644 index 000000000..6d5fb80b9 --- /dev/null +++ b/Normalisation/training/generate_umls_embeddings.sh @@ -0,0 +1,22 @@ +#!/bin/bash +#SBATCH --job-name=generate_umls_embeddings.sh +#SBATCH -t 48:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH -N1-1 +#SBATCH -c2 +#SBATCH --mem=40000 +#SBATCH -p gpuT4 +#SBATCH --output=./log/%x-%j.out +#SBATCH --error=./log/%x-%j.err +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable + +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : +echo starting +conda activate pierrenv +cd /export/home/cse200093/Jacques_Bio/normalisation/py_files +which python +python ./generate_umls_embeddings.py +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#which python +#python ./main.py diff --git a/Normalisation/training/generate_umls_normalized_embeddings.py b/Normalisation/training/generate_umls_normalized_embeddings.py new file mode 100644 index 000000000..5aece0b85 --- /dev/null +++ b/Normalisation/training/generate_umls_normalized_embeddings.py @@ -0,0 +1,171 @@ +from gensim import models +import os +import sys +import torch +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig +import tqdm +import pickle +import pandas as pd +from unidecode import unidecode +import re + +batch_size = 128 +device = "cuda:0" + +# Defining the model +# coder_all +model_checkpoint = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" +tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) +model = AutoModel.from_pretrained(model_checkpoint).to(device) + +# UMLS path +UMLS_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_umls_synonyms/bio_str_SNOMEDCT_US.json" + +# Defining save paths +DES_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_des_camembert_eds.pkl" +LABEL_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_label_camembert_eds.pkl" +EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_embeddings_camembert_eds.pt" + +# Method to generate embeddings +def get_bert_embed( + phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False +): + input_ids = [] + for phrase in phrase_list: + input_ids.append( + tok.encode_plus( + phrase, + max_length=32, + add_special_tokens=True, + truncation=True, + pad_to_max_length=True, + )["input_ids"] + ) + m.eval() + + count = len(input_ids) + now_count = 0 + with torch.no_grad(): + if tqdm_bar: + pbar = tqdm.tqdm(total=count) + while now_count < count: + input_gpu_0 = torch.LongTensor( + input_ids[now_count : min(now_count + batch_size, count)] + ).to(device) + if summary_method == "CLS": + embed = m(input_gpu_0)[1] + if summary_method == "MEAN": + embed = torch.mean(m(input_gpu_0)[0], dim=1) + if normalize: + embed_norm = torch.norm(embed, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) + embed = embed / embed_norm + if now_count == 0: + output = embed + else: + output = torch.cat((output, embed), dim=0) + if tqdm_bar: + pbar.update(min(now_count + batch_size, count) - now_count) + now_count = min(now_count + batch_size, count) + if tqdm_bar: + pbar.close() + return output + + +# Normalisation of words +def normalize(txt): + return unidecode( + txt.lower() + .replace("-", " ") + .replace("ag ", "antigene ") + .replace("ac ", "anticorps ") + .replace("antigenes ", "antigene ") + ) + + +umls_raw = pd.read_json(UMLS_DIR) +umls_raw["STR"] = umls_raw["STR"].apply(normalize) + +words_to_remove = [ + "for", + "assay", + "by", + "tests", + "minute", + "exam", + "with", + "human", + "moyenne", + "in", + "to", + "from", + "analyse", + "test", + "level", + "fluid", + "laboratory", + "determination", + "examination", + "releasing", + "quantitative", + "screening", + "and", + "exploration", + "factor", + "method", + "analysis", + "laboratoire", + "specimen", + "or", + "typing", + "of", + "concentration", + "measurement", + "detection", + "procedure", + "identification", + "numeration", + "hour", + "retired", + "technique", + "count", +] + +# Second step of normalization: we remove stop words and special characters with regex +regex_words_to_remove = r"\b(?:" + "|".join(words_to_remove) + r")\b" +regex_remove_special_characters = "[^a-zA-Z0-9\s]" + +umls_raw["STR"] = umls_raw["STR"].apply( + lambda syn: re.compile(regex_words_to_remove).sub("", syn) +) +umls_raw["STR"] = umls_raw["STR"].apply( + lambda syn: re.compile(regex_remove_special_characters).sub("", syn) +) +umls_raw["STR"] = umls_raw["STR"].apply(lambda syn: re.sub(" +", " ", syn).strip()) + +umls_raw = ( + umls_raw.loc[(~umls_raw["STR"].str.isnumeric()) & (umls_raw["STR"] != "")] + .groupby(["STR"]) + .agg({"CUI": set, "STR": "first"}) + .reset_index(drop=True) +) +umls_label = umls_raw["CUI"] +umls_des = umls_raw["STR"] + +print("Starting embeddings generation...") +umls_embedding = get_bert_embed(umls_des, model, tokenizer, tqdm_bar=True) + +# Save embeddings +torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) + +# Save umls_des +open_file = open(DES_SAVE_DIR, "wb") +pickle.dump(umls_des, open_file) +open_file.close() + +# Save umls_labels +open_file = open(LABEL_SAVE_DIR, "wb") +pickle.dump(umls_label, open_file) +open_file.close() \ No newline at end of file diff --git a/Normalisation/training/generate_umls_normalized_embeddings.sh b/Normalisation/training/generate_umls_normalized_embeddings.sh new file mode 100644 index 000000000..cb4f53ce2 --- /dev/null +++ b/Normalisation/training/generate_umls_normalized_embeddings.sh @@ -0,0 +1,25 @@ +#!/bin/bash +#SBATCH --job-name=generate_umls_normalized_embeddings.sh +#SBATCH -t 24:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH -N1-1 +#SBATCH -c2 +#SBATCH --mem=40000 +#SBATCH -p gpuT4 +#SBATCH -w bbs-edsg28-p012 +#SBATCH --output=./log/%x-%j.out +#SBATCH --error=./log/%x-%j.err +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable + +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : +echo starting +conda activate pierrenv +#cd /export/home/cse200093/Jacques_Bio/normalisation/py_files +which python +pwd +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/generate_umls_normalized_embeddings.py +echo end +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#which python +#python ./main.py diff --git a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.py b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.py new file mode 100644 index 000000000..d24864c01 --- /dev/null +++ b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.py @@ -0,0 +1,172 @@ +from gensim import models +import os +import sys +import torch +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig +import tqdm +import pickle +import pandas as pd +from unidecode import unidecode +import re + +batch_size = 128 +device = "cuda:0" + +# Defining the model +# coder_eds +model_checkpoint = "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +tokenizer_path = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" +model = torch.load(model_checkpoint).to(device) +tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) + +# UMLS path +UMLS_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_umls_synonyms/bio_str_SNOMEDCT_US.json" + +# Defining save paths +DES_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_des_coder_eds.pkl" +LABEL_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_label_coder_eds.pkl" +EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_embeddings_coder_eds.pt" + +# Method to generate embeddings +def get_bert_embed( + phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False +): + input_ids = [] + for phrase in phrase_list: + input_ids.append( + tok.encode_plus( + phrase, + max_length=32, + add_special_tokens=True, + truncation=True, + pad_to_max_length=True, + )["input_ids"] + ) + m.eval() + + count = len(input_ids) + now_count = 0 + with torch.no_grad(): + if tqdm_bar: + pbar = tqdm.tqdm(total=count) + while now_count < count: + input_gpu_0 = torch.LongTensor( + input_ids[now_count : min(now_count + batch_size, count)] + ).to(device) + if summary_method == "CLS": + embed = m.bert(input_gpu_0)[1] + if summary_method == "MEAN": + embed = torch.mean(m.bert(input_gpu_0)[0], dim=1) + if normalize: + embed_norm = torch.norm(embed, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) + embed = embed / embed_norm + if now_count == 0: + output = embed + else: + output = torch.cat((output, embed), dim=0) + if tqdm_bar: + pbar.update(min(now_count + batch_size, count) - now_count) + now_count = min(now_count + batch_size, count) + if tqdm_bar: + pbar.close() + return output + + +# Normalisation of words +def normalize(txt): + return unidecode( + txt.lower() + .replace("-", " ") + .replace("ag ", "antigene ") + .replace("ac ", "anticorps ") + .replace("antigenes ", "antigene ") + ) + + +umls_raw = pd.read_json(UMLS_DIR) +umls_raw["STR"] = umls_raw["STR"].apply(normalize) + +words_to_remove = [ + "for", + "assay", + "by", + "tests", + "minute", + "exam", + "with", + "human", + "moyenne", + "in", + "to", + "from", + "analyse", + "test", + "level", + "fluid", + "laboratory", + "determination", + "examination", + "releasing", + "quantitative", + "screening", + "and", + "exploration", + "factor", + "method", + "analysis", + "laboratoire", + "specimen", + "or", + "typing", + "of", + "concentration", + "measurement", + "detection", + "procedure", + "identification", + "numeration", + "hour", + "retired", + "technique", + "count", +] + +# Second step of normalization: we remove stop words and special characters with regex +regex_words_to_remove = r"\b(?:" + "|".join(words_to_remove) + r")\b" +regex_remove_special_characters = "[^a-zA-Z0-9\s]" + +umls_raw["STR"] = umls_raw["STR"].apply( + lambda syn: re.compile(regex_words_to_remove).sub("", syn) +) +umls_raw["STR"] = umls_raw["STR"].apply( + lambda syn: re.compile(regex_remove_special_characters).sub("", syn) +) +umls_raw["STR"] = umls_raw["STR"].apply(lambda syn: re.sub(" +", " ", syn).strip()) + +umls_raw = ( + umls_raw.loc[(~umls_raw["STR"].str.isnumeric()) & (umls_raw["STR"] != "")] + .groupby(["STR"]) + .agg({"CUI": set, "STR": "first"}) + .reset_index(drop=True) +) +umls_label = umls_raw["CUI"] +umls_des = umls_raw["STR"] + +print("Starting embeddings generation...") +umls_embedding = get_bert_embed(umls_des, model, tokenizer, tqdm_bar=True) + +# Save embeddings +torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) + +# Save umls_des +open_file = open(DES_SAVE_DIR, "wb") +pickle.dump(umls_des, open_file) +open_file.close() + +# Save umls_labels +open_file = open(LABEL_SAVE_DIR, "wb") +pickle.dump(umls_label, open_file) +open_file.close() \ No newline at end of file diff --git a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.sh b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.sh new file mode 100644 index 000000000..095730118 --- /dev/null +++ b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.sh @@ -0,0 +1,25 @@ +#!/bin/bash +#SBATCH --job-name=generate_umls_normalized_embeddings_coder_eds.sh +#SBATCH -t 24:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH -N1-1 +#SBATCH -c2 +#SBATCH --mem=40000 +#SBATCH -p gpuT4 +#SBATCH -w bbs-edsg28-p009 +#SBATCH --output=./log/%x-%j.out +#SBATCH --error=./log/%x-%j.err +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable + +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : +echo starting +conda activate pierrenv +#cd /export/home/cse200093/Jacques_Bio/normalisation/py_files +which python +pwd +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/generate_umls_normalized_embeddings_coder_eds.py +echo end +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#which python +#python ./main.py diff --git a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.py b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.py new file mode 100644 index 000000000..562853917 --- /dev/null +++ b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.py @@ -0,0 +1,174 @@ +from gensim import models +import os +import sys +import torch +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig +import tqdm +import pickle +import pandas as pd +from unidecode import unidecode +import re + +batch_size = 128 +device = "cuda:0" + +# Defining the model +# coder_eds +model_checkpoint = "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +tokenizer_path = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" +model = torch.load(model_checkpoint).to(device) +tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) + +# UMLS path +UMLS_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_umls_synonyms/bio_str_SNOMEDCT_US.json" + +# Defining save paths +DES_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_des_coder_eds_2_cased.pkl" +LABEL_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_label_coder_eds_2_cased.pkl" +EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_embeddings_coder_eds_2_cased.pt" + +# Method to generate embeddings +def get_bert_embed( + phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False +): + input_ids = [] + for phrase in phrase_list: + input_ids.append( + tok.encode_plus( + phrase, + max_length=32, + add_special_tokens=True, + truncation=True, + pad_to_max_length=True, + )["input_ids"] + ) + m.eval() + + count = len(input_ids) + now_count = 0 + with torch.no_grad(): + if tqdm_bar: + pbar = tqdm.tqdm(total=count) + while now_count < count: + input_gpu_0 = torch.LongTensor( + input_ids[now_count : min(now_count + batch_size, count)] + ).to(device) + if summary_method == "CLS": + embed = m.bert(input_gpu_0)[1] + if summary_method == "MEAN": + embed = torch.mean(m.bert(input_gpu_0)[0], dim=1) + if normalize: + embed_norm = torch.norm(embed, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) + embed = embed / embed_norm + if now_count == 0: + output = embed + else: + output = torch.cat((output, embed), dim=0) + if tqdm_bar: + pbar.update(min(now_count + batch_size, count) - now_count) + now_count = min(now_count + batch_size, count) + if tqdm_bar: + pbar.close() + return output + + +# Normalisation of words +def normalize(txt): + return unidecode( + txt.replace("-", " ") + .replace("ag ", "antigene ") + .replace("ac ", "anticorps ") + .replace("antigenes ", "antigene ") + .replace("Ag ", "Antigene ") + .replace("Ac ", "Anticorps ") + .replace("Antigenes ", "Antigene ") + ) + + +umls_raw = pd.read_json(UMLS_DIR) +umls_raw["STR"] = umls_raw["STR"].apply(normalize) + +words_to_remove = [ + "for", + "assay", + "by", + "tests", + "minute", + "exam", + "with", + "human", + "moyenne", + "in", + "to", + "from", + "analyse", + "test", + "level", + "fluid", + "laboratory", + "determination", + "examination", + "releasing", + "quantitative", + "screening", + "and", + "exploration", + "factor", + "method", + "analysis", + "laboratoire", + "specimen", + "or", + "typing", + "of", + "concentration", + "measurement", + "detection", + "procedure", + "identification", + "numeration", + "hour", + "retired", + "technique", + "count", +] + +# Second step of normalization: we remove stop words and special characters with regex +regex_words_to_remove = r"\b(?:" + "|".join(words_to_remove) + r")\b" +regex_remove_special_characters = "[^a-zA-Z0-9\s]" + +umls_raw["STR"] = umls_raw["STR"].apply( + lambda syn: re.compile(regex_words_to_remove, re.IGNORECASE).sub("", syn) +) +umls_raw["STR"] = umls_raw["STR"].apply( + lambda syn: re.compile(regex_remove_special_characters, re.IGNORECASE).sub("", syn) +) +umls_raw["STR"] = umls_raw["STR"].apply(lambda syn: re.sub(" +", " ", syn).strip()) + +umls_raw = ( + umls_raw.loc[(~umls_raw["STR"].str.isnumeric()) & (umls_raw["STR"] != "")] + .groupby(["STR"]) + .agg({"CUI": set, "STR": "first"}) + .reset_index(drop=True) +) +umls_label = umls_raw["CUI"] +umls_des = umls_raw["STR"] + +print("Starting embeddings generation...") +umls_embedding = get_bert_embed(umls_des, model, tokenizer, tqdm_bar=True) + +# Save embeddings +torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) + +# Save umls_des +open_file = open(DES_SAVE_DIR, "wb") +pickle.dump(umls_des, open_file) +open_file.close() + +# Save umls_labels +open_file = open(LABEL_SAVE_DIR, "wb") +pickle.dump(umls_label, open_file) +open_file.close() diff --git a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.sh b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.sh new file mode 100644 index 000000000..1fc64c7ec --- /dev/null +++ b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.sh @@ -0,0 +1,25 @@ +#!/bin/bash +#SBATCH --job-name=generate_umls_normalized_embeddings_coder_eds_cased.sh +#SBATCH -t 24:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH -N1-1 +#SBATCH -c2 +#SBATCH --mem=40000 +#SBATCH -p gpuT4 +#SBATCH -w bbs-edsg28-p009 +#SBATCH --output=./log/%x-%j.out +#SBATCH --error=./log/%x-%j.err +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable + +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : +echo starting +conda activate pierrenv +#cd /export/home/cse200093/Jacques_Bio/normalisation/py_files +which python +pwd +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/generate_umls_normalized_embeddings_coder_eds_cased.py +echo end +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#which python +#python ./main.py diff --git a/Normalisation/training/get_matches.py b/Normalisation/training/get_matches.py new file mode 100644 index 000000000..3cad3241c --- /dev/null +++ b/Normalisation/training/get_matches.py @@ -0,0 +1,34 @@ +from gensim import models +import os +import sys +import torch +import numpy as np +from transformers import AutoTokenizer, AutoModel, AutoConfig +import tqdm +import pickle +import pandas as pd + +device = "cpu" +EMBEDDINGS_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_embeddings_coder_eds.pt" +RES_EMBEDDINGS_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/data_embeddings_normalized_snomed_coder_eds.pt" +LABEL_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_label_coder_eds.pkl" +DES_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_des_coder_eds.pkl" + +# LOAD UMLS EMBEDDINGS ALREADY GENERATED +umls_embeddings = torch.load(EMBEDDINGS_DIR, map_location=torch.device(device)) +# LOAD EMBEDDINGS FROM ANNOTATED DATASET ALREADY GENERATED +res_embeddings = torch.load(RES_EMBEDDINGS_DIR, map_location=torch.device(device)) + +# LOAD CORRESPONDANCE FILE BETWEEN EMBEDDINGS AND CUIS AND DES +with open(LABEL_DIR, "rb") as f: + umls_labels= pickle.load(f) + +with open(DES_DIR, "rb") as f: + umls_des= pickle.load(f) + +sim = torch.matmul(res_embeddings, umls_embeddings.t()) +most_similar = torch.max(sim, dim=1)[1].tolist() +label_matches = [umls_labels[idx] for idx in most_similar] +des_matches = [umls_des[idx] for idx in most_similar] +print(label_matches) +print(des_matches) \ No newline at end of file diff --git a/Normalisation/training/get_matches.sh b/Normalisation/training/get_matches.sh new file mode 100644 index 000000000..d80a6937b --- /dev/null +++ b/Normalisation/training/get_matches.sh @@ -0,0 +1,22 @@ +#!/bin/bash +#SBATCH --job-name=get_matches.sh +#SBATCH -t 48:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH -N1-1 +#SBATCH -c2 +#SBATCH --mem=40000 +#SBATCH -p gpuT4 +#SBATCH --output=./log/%x-%j.out +#SBATCH --error=./log/%x-%j.err +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable + +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : +echo starting +conda activate pierrenv +cd /export/home/cse200093/Jacques_Bio/normalisation/py_files +which python +python ./get_matches.py +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#which python +#python ./main.py diff --git a/Normalisation/training/load_umls.py b/Normalisation/training/load_umls.py new file mode 100644 index 000000000..a3ab6c84c --- /dev/null +++ b/Normalisation/training/load_umls.py @@ -0,0 +1,177 @@ +import os +from tqdm import tqdm +import re +from random import shuffle +#import ipdb + +def byLineReader(filename): + with open(filename, "r", encoding="utf-8") as f: + line = f.readline() + while line: + yield line + line = f.readline() + return + + +class UMLS(object): + def __init__(self, umls_path, source_range=None, lang_range=['ENG'], only_load_dict=False): + self.umls_path = umls_path + self.source_range = source_range + self.lang_range = lang_range + self.detect_type() + self.load() + if not only_load_dict: + self.load_rel() + self.load_sty() + + def detect_type(self): + if os.path.exists(os.path.join(self.umls_path, "MRCONSO.RRF")): + self.type = "RRF" + else: + self.type = "txt" + + def load(self): + reader = byLineReader(os.path.join(self.umls_path, "MRCONSO." + self.type)) + self.lui_set = set() + self.cui2str = {} + self.str2cui = {} + self.code2cui = {} + #self.lui_status = {} + read_count = 0 + for line in tqdm(reader, ascii=True): + if self.type == "txt": + l = [t.replace("\"", "") for t in line.split(",")] + else: + l = line.strip().split("|") + if len(l) < 3: + continue + cui = l[0] + lang = l[1] + # lui_status = l[2].lower() # p -> preferred + lui = l[3] + source = l[11] + code = l[13] + string = l[14] + + if (self.source_range is None or source in self.source_range) and (self.lang_range is None or lang in self.lang_range): + if not lui in self.lui_set: + read_count += 1 + self.str2cui[string] = cui + self.str2cui[string.lower()] = cui + clean_string = self.clean(string) + self.str2cui[clean_string] = cui + + if not cui in self.cui2str: + self.cui2str[cui] = set() + self.cui2str[cui].update([clean_string]) + self.code2cui[code] = cui + self.lui_set.update([lui]) + + # For debug + # if read_count > 1000: + # break + + self.cui = list(self.cui2str.keys()) + shuffle(self.cui) + self.cui_count = len(self.cui) + + print("cui count:", self.cui_count) + print("str2cui count:", len(self.str2cui)) + print("MRCONSO count:", read_count) + + def load_rel(self): + reader = byLineReader(os.path.join(self.umls_path, "MRREL." + self.type)) + self.rel = set() + for line in tqdm(reader, ascii=True): + if self.type == "txt": + l = [t.replace("\"", "") for t in line.split(",")] + else: + l = line.strip().split("|") + cui0 = l[0] + re = l[3] + cui1 = l[4] + rel = l[7] + if cui0 in self.cui2str and cui1 in self.cui2str: + str_rel = "\t".join([cui0, cui1, re, rel]) + if not str_rel in self.rel and cui0 != cui1: + self.rel.update([str_rel]) + + # For debug + # if len(self.rel) > 1000: + # break + self.rel = list(self.rel) + + print("rel count:", len(self.rel)) + + def load_sty(self): + reader = byLineReader(os.path.join(self.umls_path, "MRSTY." + self.type)) + self.cui2sty = {} + for line in tqdm(reader, ascii=True): + if self.type == "txt": + l = [t.replace("\"", "") for t in line.split(",")] + else: + l = line.strip().split("|") + cui = l[0] + sty = l[3] + if cui in self.cui2str: + self.cui2sty[cui] = sty + + print("sty count:", len(self.cui2sty)) + + def clean(self, term, lower=True, clean_NOS=True, clean_bracket=True, clean_dash=True): + term = " " + term + " " + if lower: + term = term.lower() + if clean_NOS: + term = term.replace(" NOS ", " ").replace(" nos ", " ") + if clean_bracket: + term = re.sub(u"\\(.*?\\)", "", term) + if clean_dash: + term = term.replace("-", " ") + term = " ".join([w for w in term.split() if w]) + return term + + def search_by_code(self, code): + if code in self.cui2str: + return list(self.cui2str[code]) + if code in self.code2cui: + return list(self.cui2str[self.code2cui[code]]) + return None + + def search_by_string_list(self, string_list): + for string in string_list: + if string in self.str2cui: + find_string = self.cui2str[self.str2cui[string]] + return [string for string in find_string if not string in string_list] + if string.lower() in self.str2cui: + find_string = self.cui2str[self.str2cui[string.lower()]] + return [string for string in find_string if not string in string_list] + return None + + def search(self, code=None, string_list=None, max_number=-1): + result_by_code = self.search_by_code(code) + if result_by_code is not None: + if max_number > 0: + return result_by_code[0:min(len(result_by_code), max_number)] + return result_by_code + return None + result_by_string = self.search_by_string_list(string_list) + if result_by_string is not None: + if max_number > 0: + return result_by_string[0:min(len(result_by_string), max_number)] + return result_by_string + return None + + +if __name__ == "__main__": + umls = UMLS("E:\\code\\research\\umls") + # print(umls.search_by_code("282299006")) + #print(umls.search_by_string_list(["Backache", "aching muscles in back"])) + #print(umls.search(code="95891005", max_number=10)) + # ipdb.set_trace() + +""" +['unable to balance', 'loss of balance'] +['backache', 'back pain', 'dorsalgi', 'dorsodynia', 'pain over the back', 'back pain [disease/finding]', 'back ache', 'dorsal back pain', 'backach', 'dorsalgia', 'dorsal pain', 'notalgia', 'unspecified back pain', 'backpain', 'backache symptom'] +['influenza like illness', 'flu-like illness', 'influenza-like illness'] +""" diff --git a/Normalisation/training/load_umls_normalized.py b/Normalisation/training/load_umls_normalized.py new file mode 100644 index 000000000..38910d638 --- /dev/null +++ b/Normalisation/training/load_umls_normalized.py @@ -0,0 +1,199 @@ +import os +from tqdm import tqdm +import re +from random import shuffle +#import ipdb + +### THIS LOADING PROCESS DEFERS FROM load_umls.py IN THE WAY THAT source_range LETS YOU +### SELECT ALL SYNONYMS FROM ONE CUI IF THIS CUI HAVE AT LEAST ONE SYNONYM +### FROM A SOURCE OF source_range +### THUS, ALL SYNONYMS ARE NOT FROM THE SOURCES OF source_range + +def byLineReader(filename): + with open(filename, "r", encoding="utf-8") as f: + line = f.readline() + while line: + yield line + line = f.readline() + return + + +class UMLS(object): + def __init__(self, umls_path, source_range=None, lang_range=['ENG'], only_load_dict=False): + self.umls_path = umls_path + self.source_range = source_range + self.lang_range = lang_range + self.detect_type() + self.load() + if not only_load_dict: + self.load_rel() + self.load_sty() + + def detect_type(self): + if os.path.exists(os.path.join(self.umls_path, "MRCONSO.RRF")): + self.type = "RRF" + else: + self.type = "txt" + + def load(self): + reader = byLineReader(os.path.join(self.umls_path, "MRCONSO." + self.type)) + self.lui_set = set() + self.cui2str = {} + self.str2cui = {} + self.code2cui = {} + #self.lui_status = {} + + # Select all CUIs which have at least one synonym in source_range + cuis2keep = [] + if self.source_range is not None: + for line in tqdm(reader, ascii=True): + if self.type == "txt": + l = [t.replace("\"", "") for t in line.split(",")] + else: + l = line.strip().split("|") + if len(l) < 3: + continue + cui = l[0] + source = l[11] + if source in self.source_range: + cuis2keep.append(cui) + + reader = byLineReader(os.path.join(self.umls_path, "MRCONSO." + self.type)) + read_count = 0 + for line in tqdm(reader, ascii=True): + if self.type == "txt": + l = [t.replace("\"", "") for t in line.split(",")] + else: + l = line.strip().split("|") + if len(l) < 3: + continue + cui = l[0] + lang = l[1] + # lui_status = l[2].lower() # p -> preferred + lui = l[3] + source = l[11] + code = l[13] + string = l[14] + + if (self.source_range is None or source in cuis2keep) and (self.lang_range is None or lang in self.lang_range): + if not lui in self.lui_set: + read_count += 1 + self.str2cui[string] = cui + self.str2cui[string.lower()] = cui + clean_string = self.clean(string) + self.str2cui[clean_string] = cui + + if not cui in self.cui2str: + self.cui2str[cui] = set() + self.cui2str[cui].update([clean_string]) + self.code2cui[code] = cui + self.lui_set.update([lui]) + + # For debug + # if read_count > 1000: + # break + + self.cui = list(self.cui2str.keys()) + shuffle(self.cui) + self.cui_count = len(self.cui) + + print("cui count:", self.cui_count) + print("str2cui count:", len(self.str2cui)) + print("MRCONSO count:", read_count) + + def load_rel(self): + reader = byLineReader(os.path.join(self.umls_path, "MRREL." + self.type)) + self.rel = set() + for line in tqdm(reader, ascii=True): + if self.type == "txt": + l = [t.replace("\"", "") for t in line.split(",")] + else: + l = line.strip().split("|") + cui0 = l[0] + re = l[3] + cui1 = l[4] + rel = l[7] + if cui0 in self.cui2str and cui1 in self.cui2str: + str_rel = "\t".join([cui0, cui1, re, rel]) + if not str_rel in self.rel and cui0 != cui1: + self.rel.update([str_rel]) + + # For debug + # if len(self.rel) > 1000: + # break + self.rel = list(self.rel) + + print("rel count:", len(self.rel)) + + def load_sty(self): + reader = byLineReader(os.path.join(self.umls_path, "MRSTY." + self.type)) + self.cui2sty = {} + for line in tqdm(reader, ascii=True): + if self.type == "txt": + l = [t.replace("\"", "") for t in line.split(",")] + else: + l = line.strip().split("|") + cui = l[0] + sty = l[3] + if cui in self.cui2str: + self.cui2sty[cui] = sty + + print("sty count:", len(self.cui2sty)) + + def clean(self, term, lower=True, clean_NOS=True, clean_bracket=True, clean_dash=True): + term = " " + term + " " + if lower: + term = term.lower() + if clean_NOS: + term = term.replace(" NOS ", " ").replace(" nos ", " ") + if clean_bracket: + term = re.sub(u"\\(.*?\\)", "", term) + if clean_dash: + term = term.replace("-", " ") + term = " ".join([w for w in term.split() if w]) + return term + + def search_by_code(self, code): + if code in self.cui2str: + return list(self.cui2str[code]) + if code in self.code2cui: + return list(self.cui2str[self.code2cui[code]]) + return None + + def search_by_string_list(self, string_list): + for string in string_list: + if string in self.str2cui: + find_string = self.cui2str[self.str2cui[string]] + return [string for string in find_string if not string in string_list] + if string.lower() in self.str2cui: + find_string = self.cui2str[self.str2cui[string.lower()]] + return [string for string in find_string if not string in string_list] + return None + + def search(self, code=None, string_list=None, max_number=-1): + result_by_code = self.search_by_code(code) + if result_by_code is not None: + if max_number > 0: + return result_by_code[0:min(len(result_by_code), max_number)] + return result_by_code + return None + result_by_string = self.search_by_string_list(string_list) + if result_by_string is not None: + if max_number > 0: + return result_by_string[0:min(len(result_by_string), max_number)] + return result_by_string + return None + + +if __name__ == "__main__": + umls = UMLS("E:\\code\\research\\umls") + # print(umls.search_by_code("282299006")) + #print(umls.search_by_string_list(["Backache", "aching muscles in back"])) + #print(umls.search(code="95891005", max_number=10)) + # ipdb.set_trace() + +""" +['unable to balance', 'loss of balance'] +['backache', 'back pain', 'dorsalgi', 'dorsodynia', 'pain over the back', 'back pain [disease/finding]', 'back ache', 'dorsal back pain', 'backach', 'dorsalgia', 'dorsal pain', 'notalgia', 'unspecified back pain', 'backpain', 'backache symptom'] +['influenza like illness', 'flu-like illness', 'influenza-like illness'] +""" diff --git a/Normalisation/training/log/generate_umls_embeddings_coder_eds_cased.sh-11575.err b/Normalisation/training/log/generate_umls_embeddings_coder_eds_cased.sh-11575.err new file mode 100644 index 000000000..6d050726f --- /dev/null +++ b/Normalisation/training/log/generate_umls_embeddings_coder_eds_cased.sh-11575.err @@ -0,0 +1,2 @@ +/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/lib/python3.7/site-packages/transformers/tokenization_utils_base.py:2345: FutureWarning: The `pad_to_max_length` argument is deprecated and will be removed in a future version, use `padding=True` or `padding='longest'` to pad to the longest sequence in the batch, or use `padding='max_length'` to pad to a max length. In this case, you can give a specific length with `max_length` (e.g. `max_length=45`) or leave max_length to None to pad to the maximal input size of the model (e.g. 512 for Bert). + FutureWarning, diff --git a/Normalisation/training/log/generate_umls_embeddings_coder_eds_cased.sh-11575.out b/Normalisation/training/log/generate_umls_embeddings_coder_eds_cased.sh-11575.out new file mode 100644 index 000000000..9bb6e53d0 --- /dev/null +++ b/Normalisation/training/log/generate_umls_embeddings_coder_eds_cased.sh-11575.out @@ -0,0 +1,2 @@ +starting +/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python diff --git a/Normalisation/training/loss.py b/Normalisation/training/loss.py new file mode 100644 index 000000000..9f2d8ca56 --- /dev/null +++ b/Normalisation/training/loss.py @@ -0,0 +1,108 @@ +import torch +import torch.nn as nn + + +class AMSoftmax(nn.Module): + def __init__(self, + in_feats, + n_classes=10, + m=0.35, + s=30): + super(AMSoftmax, self).__init__() + self.m = m + self.s = s + self.in_feats = in_feats + self.W = torch.nn.Parameter(torch.randn(in_feats, n_classes), requires_grad=True) + self.ce = nn.CrossEntropyLoss() + nn.init.xavier_normal_(self.W, gain=1) + + def forward(self, x, label): + #print(x.shape, lb.shape, self.in_feats) + #assert x.size()[0] == label.size()[0] + #assert x.size()[1] == self.in_feats + x_norm = torch.norm(x, p=2, dim=1, keepdim=True).clamp(min=1e-12) + x_norm = torch.div(x, x_norm) + w_norm = torch.norm(self.W, p=2, dim=0, keepdim=True).clamp(min=1e-12) + w_norm = torch.div(self.W, w_norm) + costh = torch.mm(x_norm, w_norm) + # print(x_norm.shape, w_norm.shape, costh.shape) + lb_view = label.view(-1, 1).to(x.device) + delt_costh = torch.zeros(costh.size()).to(x.device).scatter_(1, lb_view, self.m) + costh_m = costh - delt_costh + costh_m_s = self.s * costh_m + loss = self.ce(costh_m_s, label) + return loss, costh_m_s + + def predict(self, x): + x_norm = torch.norm(x, p=2, dim=1, keepdim=True).clamp(min=1e-12) + x_norm = torch.div(x, x_norm) + w_norm = torch.norm(self.W, p=2, dim=0, keepdim=True).clamp(min=1e-12) + w_norm = torch.div(self.W, w_norm) + costh = torch.mm(x_norm, w_norm) + return costh + +class MultiSimilarityLoss(nn.Module): + def __init__(self): + super(MultiSimilarityLoss, self).__init__() + self.thresh = 0.5 + self.margin = 0.1 + + self.scale_pos = 2.0 + self.scale_neg = 50.0 + + def forward(self, feats, labels): + #assert feats.size(0) == labels.size(0), \ + # f"feats.size(0): {feats.size(0)} is not equal to labels.size(0): {labels.size(0)}" + batch_size = feats.size(0) + + # Feature normalize + x_norm = torch.norm(feats, p=2, dim=1, keepdim=True).clamp(min=1e-12) + x_norm = torch.div(feats, x_norm) + + sim_mat = torch.matmul(x_norm, torch.t(x_norm)) + + epsilon = 1e-5 + loss = [] + + #unique_label, inverse_indices = torch.unique_consecutive(labels, return_inverse=True) + + for i in range(batch_size): + pos_pair_ = sim_mat[i][labels == labels[i]] + pos_pair_ = pos_pair_[pos_pair_ < 1 - epsilon] + neg_pair_ = sim_mat[i][labels != labels[i]] + + #print(pos_pair_) + #print(neg_pair_) + + if len(neg_pair_) >= 1: + pos_pair = pos_pair_[pos_pair_ - self.margin < max(neg_pair_)] + if len(pos_pair) >= 1: + pos_loss = 1.0 / self.scale_pos * torch.log( + 1 + torch.sum(torch.exp(-self.scale_pos * (pos_pair - self.thresh)))) + loss.append(pos_loss) + + if len(pos_pair_) >= 1: + neg_pair = neg_pair_[neg_pair_ + self.margin > min(pos_pair_)] + if len(neg_pair) >= 1: + neg_loss = 1.0 / self.scale_neg * torch.log( + 1 + torch.sum(torch.exp(self.scale_neg * (neg_pair - self.thresh)))) + loss.append(neg_loss) + + #print(labels, len(loss)) + if len(loss) == 0: + return torch.zeros([], requires_grad=True).to(feats.device) + + loss = sum(loss) / batch_size + return loss + +if __name__ == '__main__': + criteria = AMSoftmax(20, 5) + a = torch.randn(10, 20) + lb = torch.randint(0, 5, (10, ), dtype=torch.long) + loss = criteria(a, lb) + loss.backward() + + print(loss.detach().numpy()) + print(list(criteria.parameters())[0].shape) + print(type(next(criteria.parameters()))) + print(lb) diff --git a/Normalisation/training/model.py b/Normalisation/training/model.py new file mode 100644 index 000000000..4de2df06b --- /dev/null +++ b/Normalisation/training/model.py @@ -0,0 +1,178 @@ +#from transformers import BertConfig, BertPreTrainedModel, BertTokenizer, BertModel +from transformers import AutoConfig +from transformers import AutoModelForPreTraining +from transformers import AutoTokenizer +from transformers import AutoModel +from transformers.modeling_utils import SequenceSummary +from torch import nn +import torch.nn.functional as F +import torch +from loss import AMSoftmax +from pytorch_metric_learning import losses, miners +from trans import TransE + + +class UMLSPretrainedModel(nn.Module): + def __init__(self, device, model_name_or_path, + cui_label_count, rel_label_count, sty_label_count, + re_weight=1.0, sty_weight=0.1, + cui_loss_type="ms_loss", + trans_loss_type="TransE", trans_margin=1.0): + super(UMLSPretrainedModel, self).__init__() + + self.device = device + self.model_name_or_path = model_name_or_path + if self.model_name_or_path.find("large") >= 0: + self.feature_dim = 1024 + else: + self.feature_dim = 768 + self.bert = AutoModel.from_pretrained(model_name_or_path) + self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path) + self.dropout = nn.Dropout(0.1) + + self.rel_label_count = rel_label_count + self.re_weight = re_weight + + self.sty_label_count = sty_label_count + self.linear_sty = nn.Linear(self.feature_dim, self.sty_label_count) + self.sty_loss_fn = nn.CrossEntropyLoss() + self.sty_weight = sty_weight + + self.cui_loss_type = cui_loss_type + self.cui_label_count = cui_label_count + + if self.cui_loss_type == "softmax": + self.cui_loss_fn = nn.CrossEntropyLoss() + self.linear = nn.Linear(self.feature_dim, self.cui_label_count) + if self.cui_loss_type == "am_softmax": + self.cui_loss_fn = AMSoftmax( + self.feature_dim, self.cui_label_count) + if self.cui_loss_type == "ms_loss": + self.cui_loss_fn = losses.MultiSimilarityLoss(alpha=2, beta=50) + self.miner = miners.MultiSimilarityMiner(epsilon=0.1) + + self.trans_loss_type = trans_loss_type + if self.trans_loss_type == "TransE": + self.re_loss_fn = TransE(trans_margin) + self.re_embedding = nn.Embedding( + self.rel_label_count, self.feature_dim) + + self.standard_dataloader = None + + self.sequence_summary = SequenceSummary(AutoConfig.from_pretrained(model_name_or_path)) # Now only used for XLNet + + def softmax(self, logits, label): + loss = self.cui_loss_fn(logits, label) + return loss + + def am_softmax(self, pooled_output, label): + loss, _ = self.cui_loss_fn(pooled_output, label) + return loss + + def ms_loss(self, pooled_output, label): + pairs = self.miner(pooled_output, label) + loss = self.cui_loss_fn(pooled_output, label, pairs) + return loss + + def calculate_loss(self, pooled_output=None, logits=None, label=None): + if self.cui_loss_type == "softmax": + return self.softmax(logits, label) + if self.cui_loss_type == "am_softmax": + return self.am_softmax(pooled_output, label) + if self.cui_loss_type == "ms_loss": + return self.ms_loss(pooled_output, label) + + def get_sentence_feature(self, input_ids): + # bert, albert, roberta + if self.model_name_or_path.find("xlnet") < 0: + outputs = self.bert(input_ids) + pooled_output = outputs[1] + return pooled_output + + # xlnet + outputs = self.bert(input_ids) + pooled_output = self.sequence_summary(outputs[0]) + return pooled_output + + + # @profile + def forward(self, + input_ids_0, input_ids_1, input_ids_2, + cui_label_0, cui_label_1, cui_label_2, + sty_label_0, sty_label_1, sty_label_2, + re_label): + input_ids = torch.cat((input_ids_0, input_ids_1, input_ids_2), 0) + cui_label = torch.cat((cui_label_0, cui_label_1, cui_label_2)) + sty_label = torch.cat((sty_label_0, sty_label_1, sty_label_2)) + #print(input_ids.shape, cui_label.shape, sty_label.shape) + + use_len = input_ids_0.shape[0] + + pooled_output = self.get_sentence_feature( + input_ids) # (3 * pair) * re_label + logits_sty = self.linear_sty(pooled_output) + sty_loss = self.sty_loss_fn(logits_sty, sty_label) + + if self.cui_loss_type == "softmax": + logits = self.linear(pooled_output) + else: + logits = None + cui_loss = self.calculate_loss(pooled_output, logits, cui_label) + + cui_0_output = pooled_output[0:use_len] + cui_1_output = pooled_output[use_len:2 * use_len] + cui_2_output = pooled_output[2 * use_len:] + re_output = self.re_embedding(re_label) + re_loss = self.re_loss_fn( + cui_0_output, cui_1_output, cui_2_output, re_output) + + loss = self.sty_weight * sty_loss + cui_loss + self.re_weight * re_loss + #print(sty_loss.device, cui_loss.device, re_loss.device) + + return loss, (sty_loss, cui_loss, re_loss) + + """ + def predict(self, input_ids): + if self.loss_type == "softmax": + return self.predict_by_softmax(input_ids) + if self.loss_type == "am_softmax": + return self.predict_by_amsoftmax(input_ids) + + def predict_by_softmax(self, input_ids): + pooled_output = self.get_sentence_feature(input_ids) + logits = self.linear(pooled_output) + return torch.max(logits, dim=1)[1], logits + + def predict_by_amsoftmax(self, input_ids): + pooled_output = self.get_sentence_feature(input_ids) + logits = self.loss_fn.predict(pooled_output) + return torch.max(logits, dim=1)[1], logits + """ + + def init_standard_feature(self): + if self.standard_dataloader is not None: + for index, batch in enumerate(self.standard_dataloader): + input_ids = batch[0].to(self.device) + outputs = self.get_sentence_feature(input_ids) + normalized_standard_feature = torch.norm( + outputs, p=2, dim=1, keepdim=True).clamp(min=1e-12) + normalized_standard_feature = torch.div( + outputs, normalized_standard_feature) + if index == 0: + self.standard_feature = normalized_standard_feature + else: + self.standard_feature = torch.cat( + (self.standard_feature, normalized_standard_feature), 0) + assert self.standard_feature.shape == ( + self.num_label, self.feature_dim), self.standard_feature.shape + return None + + def predict_by_cosine(self, input_ids): + pooled_output = self.get_sentence_feature(input_ids) + + normalized_feature = torch.norm( + pooled_output, p=2, dim=1, keepdim=True).clamp(min=1e-12) + normalized_feature = torch.div(pooled_output, normalized_feature) + sim_mat = torch.matmul(normalized_feature, torch.t( + self.standard_feature)) # batch_size * num_label + return torch.max(sim_mat, dim=1)[1], sim_mat \ No newline at end of file diff --git a/Normalisation/training/sampler_util.py b/Normalisation/training/sampler_util.py new file mode 100644 index 000000000..01c0259b2 --- /dev/null +++ b/Normalisation/training/sampler_util.py @@ -0,0 +1,53 @@ +import torch +from torch.utils.data import Dataset, DataLoader +from torch.utils.data.sampler import Sampler, RandomSampler + +""" +class TmpDataset(Dataset): + def __init__(self, m=10): + self.len = m + + def __getitem__(self, index): + return (list(range(10)) * index, [0] * index) + + def __len__(self): + return self.len +""" + +class FixedLengthBatchSampler(Sampler): + def __init__(self, sampler, fixed_length, drop_last): + self.sampler = sampler + self.fixed_length = fixed_length + self.drop_last = drop_last + self.rel_sampler_count = 0 + + def __iter__(self): + batch = [] + now_length = 0 + for idx in self.sampler: + #print(batch, now_length) + sample_length = len(self.sampler.data_source[idx][-1]) * 3 + if now_length + sample_length > self.fixed_length: + #print(batch, now_length) + yield batch + batch = [] + now_length = 0 + batch.append(idx) + now_length += sample_length + self.rel_sampler_count += 1 + if len(batch) > 0 and not self.drop_last: + yield batch + +def my_collate_fn(batch): + type_count = len(batch[0]) + batch_size = sum([len(item[-1]) for item in batch]) + output = () + for i in range(type_count): + tmp = [] + for item in batch: + tmp.extend(item[i]) + if len(tmp) <= batch_size: + output += (torch.LongTensor(tmp),) + else: + output += (torch.LongTensor(tmp).reshape(batch_size, -1),) + return output diff --git a/Normalisation/training/train.py b/Normalisation/training/train.py new file mode 100644 index 000000000..584b152aa --- /dev/null +++ b/Normalisation/training/train.py @@ -0,0 +1,324 @@ +from data_util import UMLSDataset, fixed_length_dataloader +from model import UMLSPretrainedModel +from transformers import AdamW, get_linear_schedule_with_warmup, get_cosine_schedule_with_warmup, get_constant_schedule_with_warmup +from tqdm import tqdm, trange +import torch +from torch import nn +import time +import os +import numpy as np +import argparse +import time +import pathlib +#import ipdb +# try: +# from torch.utils.tensorboard import SummaryWriter +# except: +from tensorboardX import SummaryWriter + + +def train(args, model, train_dataloader, umls_dataset): + writer = SummaryWriter(comment='umls') + + t_total = args.max_steps + + no_decay = ["bias", "LayerNorm.weight"] + optimizer_grouped_parameters = [ + { + "params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], + "weight_decay": args.weight_decay, + }, + {"params": [p for n, p in model.named_parameters() if any( + nd in n for nd in no_decay)], "weight_decay": 0.0}, + ] + + optimizer = AdamW(optimizer_grouped_parameters, + lr=args.learning_rate, eps=args.adam_epsilon) + args.warmup_steps = int(args.warmup_steps) + if args.schedule == 'linear': + scheduler = get_linear_schedule_with_warmup( + optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total + ) + if args.schedule == 'constant': + scheduler = get_constant_schedule_with_warmup( + optimizer, num_warmup_steps=args.warmup_steps + ) + if args.schedule == 'cosine': + scheduler = get_cosine_schedule_with_warmup( + optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total + ) + + print("***** Running training *****") + print(" Total Steps =", t_total) + print(" Steps needs to be trained=", t_total - args.shift) + print(" Instantaneous batch size per GPU =", args.train_batch_size) + print( + " Total train batch size (w. parallel, distributed & accumulation) =", + args.train_batch_size + * args.gradient_accumulation_steps, + ) + print(" Gradient Accumulation steps =", args.gradient_accumulation_steps) + + model.zero_grad() + + for i in range(args.shift): + scheduler.step() + global_step = args.shift + + best_batch_loss = 0.033 + + while True: + model.train() + epoch_iterator = tqdm(train_dataloader, desc="Iteration", ascii=True) + batch_loss = 0. + batch_sty_loss = 0. + batch_cui_loss = 0. + batch_re_loss = 0. + for _, batch in enumerate(epoch_iterator): + input_ids_0 = batch[0].to(args.device) + input_ids_1 = batch[1].to(args.device) + input_ids_2 = batch[2].to(args.device) + cui_label_0 = batch[3].to(args.device) + cui_label_1 = batch[4].to(args.device) + cui_label_2 = batch[5].to(args.device) + sty_label_0 = batch[6].to(args.device) + sty_label_1 = batch[7].to(args.device) + sty_label_2 = batch[8].to(args.device) + # use batch[9] for re, use batch[10] for rel + if args.use_re: + re_label = batch[9].to(args.device) + else: + re_label = batch[10].to(args.device) + # for item in batch: + # print(item.shape) + + loss, (sty_loss, cui_loss, re_loss) = \ + model(input_ids_0, input_ids_1, input_ids_2, + cui_label_0, cui_label_1, cui_label_2, + sty_label_0, sty_label_1, sty_label_2, + re_label) + batch_loss = float(loss.item()) + batch_sty_loss = float(sty_loss.item()) + batch_cui_loss = float(cui_loss.item()) + batch_re_loss = float(re_loss.item()) + + # tensorboardX + writer.add_scalar( + 'rel_count', train_dataloader.batch_sampler.rel_sampler_count, global_step=global_step) + writer.add_scalar('batch_loss', batch_loss, + global_step=global_step) + writer.add_scalar('batch_sty_loss', batch_sty_loss, + global_step=global_step) + writer.add_scalar('batch_cui_loss', batch_cui_loss, + global_step=global_step) + writer.add_scalar('batch_re_loss', batch_re_loss, + global_step=global_step) + + if args.gradient_accumulation_steps > 1: + loss = loss / args.gradient_accumulation_steps + loss.backward() + + epoch_iterator.set_description("Rel_count: %s, Loss: %0.4f, Sty: %0.4f, Cui: %0.4f, Re: %0.4f" % + (train_dataloader.batch_sampler.rel_sampler_count, batch_loss, batch_sty_loss, batch_cui_loss, batch_re_loss)) + + if (global_step + 1) % args.gradient_accumulation_steps == 0: + torch.nn.utils.clip_grad_norm_( + model.parameters(), args.max_grad_norm) + optimizer.step() + scheduler.step() # Update learning rate schedule + model.zero_grad() + + global_step += 1 + if batch_loss < best_batch_loss: + best_batch_loss = batch_loss + save_path = os.path.join( + args.output_dir, f'model_{global_step}.pth') + torch.save(model, save_path) + + # re_embedding + if args.use_re: + writer.add_embedding(model.re_embedding.weight, metadata=umls_dataset.re2id.keys( + ), global_step=global_step, tag="re embedding") + else: + # print(len(umls_dataset.rel2id)) + # print(model.re_embedding.weight.shape) + writer.add_embedding(model.re_embedding.weight, metadata=umls_dataset.rel2id.keys( + ), global_step=global_step, tag="rel embedding") + + # sty_parameter + writer.add_embedding(model.linear_sty.weight, metadata=umls_dataset.sty2id.keys( + ), global_step=global_step, tag="sty weight") + + if global_step % args.save_step == 0 and global_step > 0: + save_path = os.path.join( + args.output_dir, f'model_{global_step}.pth') + torch.save(model, save_path) + + # re_embedding + if args.use_re: + writer.add_embedding(model.re_embedding.weight, metadata=umls_dataset.re2id.keys( + ), global_step=global_step, tag="re embedding") + else: + # print(len(umls_dataset.rel2id)) + # print(model.re_embedding.weight.shape) + writer.add_embedding(model.re_embedding.weight, metadata=umls_dataset.rel2id.keys( + ), global_step=global_step, tag="rel embedding") + + # sty_parameter + writer.add_embedding(model.linear_sty.weight, metadata=umls_dataset.sty2id.keys( + ), global_step=global_step, tag="sty weight") + + if args.max_steps > 0 and global_step > args.max_steps: + return None + + return None + + +def run(args): + torch.manual_seed(args.seed) # cpu + torch.cuda.manual_seed(args.seed) # gpu + np.random.seed(args.seed) # numpy + torch.backends.cudnn.deterministic = True # cudnn + + #args.output_dir = args.output_dir + "_" + str(int(time.time())) + + # dataloader + if args.lang == "eng": + lang = ["ENG"] + if args.lang == "all": + lang = None + if args.lang == "eng_fr": + lang = ["ENG","FRE"] + # assert args.model_name_or_path.find("bio") == -1, "Should use multi-language model" + umls_dataset = UMLSDataset( + umls_folder=args.umls_dir, model_name_or_path=args.model_name_or_path, lang=lang, json_save_path=args.output_dir) + umls_dataloader = fixed_length_dataloader( + umls_dataset, fixed_length=args.train_batch_size, num_workers=args.num_workers) + + if args.use_re: + rel_label_count = len(umls_dataset.re2id) + else: + rel_label_count = len(umls_dataset.rel2id) + + model_load = False + if os.path.exists(args.output_dir): + save_list = [] + for f in os.listdir(args.output_dir): + if f[0:5] == "model" and f[-4:] == ".pth": + save_list.append(int(f[6:-4])) + if len(save_list) > 0: + args.shift = max(save_list) + if os.path.exists(os.path.join(args.output_dir, 'last_model.pth')): + model = torch.load(os.path.join( + args.output_dir, 'last_model.pth')).to(args.device) + model_load = True + else: + model = torch.load(os.path.join( + args.output_dir, f'model_{max(save_list)}.pth')).to(args.device) + model_load = True + if not model_load: + if not os.path.exists(args.output_dir): + os.makedirs(args.output_dir) + model = UMLSPretrainedModel(device=args.device, model_name_or_path=args.model_name_or_path, + cui_label_count=len(umls_dataset.cui2id), + rel_label_count=rel_label_count, + sty_label_count=len(umls_dataset.sty2id), + re_weight=args.re_weight, + sty_weight=args.sty_weight).to(args.device) + args.shift = 0 + model_load = True + + if args.do_train: + torch.save(args, os.path.join(args.output_dir, 'training_args.bin')) + train(args, model, umls_dataloader, umls_dataset) + torch.save(model, os.path.join(args.output_dir, 'last_model.pth')) + + return None + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--umls_dir", + default="../umls", + type=str, + help="UMLS dir", + ) + parser.add_argument( + "--model_name_or_path", + default="../biobert_v1.1", + type=str, + help="Path to pre-trained model or shortcut name selected in the list: ", + ) + parser.add_argument( + "--output_dir", + default="output", + type=str, + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument( + "--save_step", + default=10000, + type=int, + help="Save step", + ) + + # Other parameters + parser.add_argument( + "--max_seq_length", + default=32, + type=int, + help="The maximum total input sequence length after tokenization. Sequences longer " + "than this will be truncated, sequences shorter will be padded.", + ) + parser.add_argument("--do_train", default=True, type=bool, help="Whether to run training.") + parser.add_argument( + "--train_batch_size", default=256, type=int, help="Batch size per GPU/CPU for training.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=8, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument("--learning_rate", default=2e-5, + type=float, help="The initial learning rate for Adam.") + parser.add_argument("--weight_decay", default=0.01, + type=float, help="Weight decay if we apply some.") + parser.add_argument("--adam_epsilon", default=1e-8, + type=float, help="Epsilon for Adam optimizer.") + parser.add_argument("--max_grad_norm", default=1.0, + type=float, help="Max gradient norm.") + parser.add_argument( + "--max_steps", + default=1000000, + type=int, + help="If > 0: set total number of training steps to perform. Override num_train_epochs.", + ) + parser.add_argument("--warmup_steps", default=10000, + help="Linear warmup over warmup_steps or a float.") + parser.add_argument("--device", type=str, default='cuda:0', help="device") + parser.add_argument("--seed", type=int, default=72, + help="random seed for initialization") + parser.add_argument("--schedule", type=str, default="linear", + choices=["linear", "cosine", "constant"], help="Schedule.") + parser.add_argument("--trans_margin", type=float, default=1.0, + help="Margin of TransE.") + parser.add_argument("--use_re", default=False, type=bool, + help="Whether to use re or rel.") + parser.add_argument("--num_workers", default=1, type=int, + help="Num workers for data loader, only 0 can be used for Windows") + parser.add_argument("--lang", default='eng', type=str, choices=["eng", "all", "eng_fr"], + help="language range, eng or all") + parser.add_argument("--sty_weight", type=float, default=0.0, + help="Weight of sty.") + parser.add_argument("--re_weight", type=float, default=1.0, + help="Weight of re.") + + args = parser.parse_args() + + run(args) + + +if __name__ == "__main__": + main() diff --git a/Normalisation/training/train_coder_slurm.cfg b/Normalisation/training/train_coder_slurm.cfg new file mode 100644 index 000000000..af65aa9bb --- /dev/null +++ b/Normalisation/training/train_coder_slurm.cfg @@ -0,0 +1,7 @@ +[slurm] +gpu_type = v100 +log_path = "BioMedics/bash_scripts/Coder_model/log_train_coder" +mem = 40G +job_duration = "72:00:00" +n_gpu = 1 +n_cpu = 5 diff --git a/Normalisation/training/trans.py b/Normalisation/training/trans.py new file mode 100644 index 000000000..2987758b3 --- /dev/null +++ b/Normalisation/training/trans.py @@ -0,0 +1,14 @@ +import torch +from torch import nn +import torch.nn.functional as F + + +class TransE(nn.Module): + def __init__(self, margin=1.0): + super(TransE, self).__init__() + self.margin = margin + + def forward(self, cui_0, cui_1, cui_2, re): + pos = cui_0 + re - cui_1 + neg = cui_0 + re - cui_2 + return torch.mean(F.relu(self.margin + torch.norm(pos, p=2, dim=1) - torch.norm(neg, p=2, dim=1))) diff --git a/bash_scripts/NER_model/expe_data_size.sh b/bash_scripts/NER_model/expe_data_size.sh new file mode 100644 index 000000000..80687a7c8 --- /dev/null +++ b/bash_scripts/NER_model/expe_data_size.sh @@ -0,0 +1,55 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 24:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuV100 +#SBATCH --output=logs/slurm-%j-stdout.log +#SBATCH --error=logs/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script + +cd 'data/scratch/cse200093/BioMedics/NER_model' +source ../.venv/bin/activate +conda deactivate + +for i in 5 10 15 20 25 30 35 40 45 50 55 60 62 +do + echo ----------------- + echo CONVERT $i DOCS + echo ----------------- + + python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/train --output-path ./corpus/expe_data_size/train_$i.spacy --n-limit $i + + echo ----------------- + echo TRAIN ON $i DOCS + echo ----------------- + + python -m spacy train configs/config.cfg --output ./training/expe_data_size/model_$i/ --paths.train ./corpus/expe_data_size/train_$i.spacy --paths.dev ./corpus/dev.spacy --nlp.lang eds --gpu-id 0 + + + echo ----------------- + echo REMOVE MODEL LAST + echo ----------------- + + rm -rf ./training/expe_data_size/model_$i/model-last + + echo ----------------- + echo INFER TEST DOCS WITH MODEL TRAINED ON $i DOCS + echo ----------------- + + python ./scripts/infer.py --model ./training/expe_data_size/model_$i/model-best/ --input ./data/NLP_diabeto/test/ --output ./data/NLP_diabeto/expe_data_size/pred_$i/ --format brat + + echo ----------------- + echo EVALUATE MODEL TRAINED ON $i DOCS + echo ----------------- + + python ./scripts/evaluate.py ./training/expe_data_size/model_$i/model-best ./corpus/test.spacy --output ./training/expe_data_size/model_$i/test_metrics.json --docbin ./data/NLP_diabeto/expe_data_size/pred_$i.spacy --gpu-id 0 + +done + + +echo --Training_done--- + +echo --------------- \ No newline at end of file diff --git a/bash_scripts/NER_model/expe_hyperparams.sh b/bash_scripts/NER_model/expe_hyperparams.sh new file mode 100644 index 000000000..8836f08b9 --- /dev/null +++ b/bash_scripts/NER_model/expe_hyperparams.sh @@ -0,0 +1,46 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 24:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuV100 +#SBATCH --output=logs/slurm-%j-stdout.log +#SBATCH --error=logs/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script + +cd 'data/scratch/cse200093/BioMedics/NER_model' +source ../.venv/bin/activate +conda deactivate + +echo ----------------- +echo CONVERT DOCS +echo ----------------- + +python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/train --output-path ./corpus/train.spacy +python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/test --output-path ./corpus/test.spacy +python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/val --output-path ./corpus/dev.spacy + +echo ----------------- +echo TRAIN ON DOCS +echo ----------------- + +python -m spacy train ./configs/config.cfg --output ./training/expe_section/model_all_labels/ --paths.train ./corpus/train.spacy --paths.dev ./corpus/dev.spacy --nlp.lang eds --gpu-id 0 + +echo ----------------- +echo INFER TEST DOCS +echo ----------------- + +python ./scripts/infer.py --model ./training/expe_section/model_all_labels/model-best --input ./data/NLP_diabeto/test/ --output ./data/NLP_diabeto/expe_section/pred_model_all_labels/ --format brat + + +echo ----------------- +echo EVALUATE MODEL +echo ----------------- + +python ./scripts/evaluate.py ./training/expe_section/model_all_labels/model-best ./corpus/test.spacy --output ./training/expe_section/model_all_labels/test_metrics.json --docbin ./data/NLP_diabeto/expe_section/pred_model_all_labels.spacy --gpu-id 0 + +echo --Training_done--- + +echo --------------- diff --git a/bash_scripts/NER_model/expe_model_lang.sh b/bash_scripts/NER_model/expe_model_lang.sh new file mode 100644 index 000000000..5de9f166c --- /dev/null +++ b/bash_scripts/NER_model/expe_model_lang.sh @@ -0,0 +1,60 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 24:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuV100 +#SBATCH --output=logs/slurm-%j-stdout.log +#SBATCH --error=logs/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script + +cd 'data/scratch/cse200093/BioMedics/NER_model' +source ../.venv/bin/activate +conda deactivate + + + +# echo ----------------- +# echo CONVERT DOCS +# echo ----------------- + +# python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/train --output-path ./corpus/train.spacy +# python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/test --output-path ./corpus/test.spacy +# python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/val --output-path ./corpus/dev.spacy + +for lang_model in "camembert_base" "DrBert" "camembert_bio" "eds_finetune" "eds_scratch" +do + + echo ----------------- + echo TRAIN $lang_model ON DOCS + echo ----------------- + + python -m spacy train ./configs/expe_lang_model/config_$lang_model.cfg --output ./training/expe_lang_model/model_$lang_model/ --paths.train ./corpus/train.spacy --paths.dev ./corpus/dev.spacy --nlp.lang eds --gpu-id 0 + + + echo ----------------- + echo REMOVE $lang_model MODEL LAST + echo ----------------- + + rm -rf ./training/expe_lang_model/model_$lang_model/model-last + + echo ----------------- + echo INFER $lang_model TEST DOCS + echo ----------------- + + python ./scripts/infer.py --model ./training/expe_lang_model/model_$lang_model/model-best --input ./data/NLP_diabeto/test/ --output ./data/NLP_diabeto/expe_lang_model/pred_model_$lang_model/ --format brat + + + echo ----------------- + echo EVALUATE $lang_model MODEL + echo ----------------- + + python ./scripts/evaluate.py ./training/expe_lang_model/model_$lang_model/model-best ./corpus/test.spacy --output ./training/expe_lang_model/model_$lang_model/test_metrics.json --docbin ./data/NLP_diabeto/expe_lang_model/pred_model_$lang_model.spacy --gpu-id 0 + +done + +echo --Training_done--- + +echo --------------- diff --git a/bash_scripts/NER_model/infer.sh b/bash_scripts/NER_model/infer.sh new file mode 100644 index 000000000..5c6e88961 --- /dev/null +++ b/bash_scripts/NER_model/infer.sh @@ -0,0 +1,21 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 24:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuT4 +#SBATCH --output=logs/slurm-%j-stdout.log +#SBATCH --error=logs/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : + +echo starting +conda activate pierrenv + +cd '/export/home/cse200093/Jacques_Bio/BioMedics/eds-medic' + +#python ./scripts/infer.py --model ~/RV_Inter_conf/model-best/ --input ~/RV_Inter_conf/unnested_sosydiso_qualifiers_final/test_ --output ~/RV_Inter_conf/usqf_pred/ --format brat + +python ./scripts/infer.py --model ./training/model-best/ --input ../data/lupus_erythemateux_dissemine_raw --output ../data/lupus_erythemateux_dissemine_pred/ --format brat \ No newline at end of file diff --git a/bash_scripts/NER_model/save.sh b/bash_scripts/NER_model/save.sh new file mode 100644 index 000000000..8f2ae5fd3 --- /dev/null +++ b/bash_scripts/NER_model/save.sh @@ -0,0 +1,26 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 24:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuV100 +#SBATCH --output=logs/slurm-%j-stdout.log +#SBATCH --error=logs/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : + +echo starting +conda activate pierrenv +cd '/export/home/cse200093/Jacques_Bio/BioMedics/eds-medic' + +echo ---- Building dvc.yaml ---- + +python -m spacy project dvc + +echo ---- Saving Brat files ---- + +python -m spacy project run save_to_brat --force + +echo --------------- diff --git a/bash_scripts/NER_model/test.sh b/bash_scripts/NER_model/test.sh new file mode 100644 index 000000000..8adda0786 --- /dev/null +++ b/bash_scripts/NER_model/test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 1:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuT4 +#SBATCH --output=logs/slurm-%j-stdout.log +#SBATCH --error=logs/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script +# your code here : + +echo starting +conda activate pierrenv +cd '/export/home/cse200093/Jacques_Bio/BioMedics/eds-medic' + +echo ---- Building dvc.yaml ---- + +python -m spacy project dvc + +echo ---- Testing model ---- + +python -m spacy project run evaluate --force + +echo --------------- diff --git a/bash_scripts/NER_model/train.sh b/bash_scripts/NER_model/train.sh new file mode 100644 index 000000000..30ff967b2 --- /dev/null +++ b/bash_scripts/NER_model/train.sh @@ -0,0 +1,35 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 1:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuV100 +#SBATCH --output=logs/slurm-%j-stdout.log +#SBATCH --error=logs/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script + +conda activate pierrenv +cd '/export/home/cse200093/Jacques_Bio/BioMedics/eds-medic' + + +python -m spacy project dvc + +echo dvc.yml built succesfully + +echo ----------------- +echo CONVERT +echo ----------------- + +python -m spacy project run convert + +echo ----------------- +echo TRAIN +echo ----------------- + +dvc repro -f 2>&1 | tee training/train.log + +echo --Training_done--- + +echo --------------- diff --git a/bash_scripts/NER_model/train_v1.sh b/bash_scripts/NER_model/train_v1.sh new file mode 100644 index 000000000..85ffea383 --- /dev/null +++ b/bash_scripts/NER_model/train_v1.sh @@ -0,0 +1,57 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 48:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuV100 +#SBATCH --output=logs/slurm-%j-stdout.log +#SBATCH --error=logs/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script + +cd 'data/scratch/cse200093/BioMedics/NER_model' +source ../.venv/bin/activate +conda deactivate + +echo ----------------- +echo CONVERT DOCS +echo ----------------- + +python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/train_test --output-path ./corpus/train_test.spacy +python scripts/convert.py --lang eds --input-path ./data/NLP_diabeto/val --output-path ./corpus/dev.spacy + +echo ----------------- +echo TRAIN ON DOCS +echo ----------------- + +python -m spacy train ./configs/config_v1.cfg --output ./training/model_v1/ --paths.train ./corpus/train_test.spacy --paths.dev ./corpus/dev.spacy --nlp.lang eds --gpu-id 0 + +echo ----------------- +echo INFER LUPUS DOCS +echo ----------------- + +python ./scripts/infer.py --model ./training/model_v1/model-best/ --input ../data/CRH/raw/lupus_erythemateux_dissemine/ --output ../data/CRH/pred/lupus_erythemateux_dissemine/ --format brat + +echo ----------------- +echo INFER MALADIE TAKAYASU DOCS +echo ----------------- + +python ./scripts/infer.py --model ./training/model_v1/model-best/ --input ../data/CRH/raw/maladie_de_takayasu/ --output ../data/CRH/pred/maladie_de_takayasu/ --format brat + +echo ----------------- +echo INFER SCLERODERMIE SYSTEMIQUE DOCS +echo ----------------- + +python ./scripts/infer.py --model ./training/model_v1/model-best/ --input ../data/CRH/raw/sclerodermie_systemique/ --output ../data/CRH/pred/sclerodermie_systemique/ --format brat + +echo ----------------- +echo INFER SAPL DOCS +echo ----------------- + +python ./scripts/infer.py --model ./training/model_v1/model-best/ --input ../data/CRH/raw/syndrome_des_anti-phospholipides/ --output ../data/CRH/pred/syndrome_des_anti-phospholipides/ --format brat + + +echo --Inference_done--- + +echo --------------- diff --git a/bash_scripts/Normalisation/extract_measurement.sh b/bash_scripts/Normalisation/extract_measurement.sh new file mode 100644 index 000000000..e4cf11408 --- /dev/null +++ b/bash_scripts/Normalisation/extract_measurement.sh @@ -0,0 +1,39 @@ +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh + +cd ~/scratch/BioMedics +source .venv/bin/activate +conda deactivate + + +echo ----------------- +echo EXTRACTING MEASUREMENT VALUES AND UNITS USING BIO_COMP LABEL AND RULES. +echo ----------------- + +echo ----------------- +echo EXTRACT MEASUREMENT FROM MALADIE TAKAYASU +echo ----------------- + +python extract_measurement/main.py ./data/CRH/pred/maladie_de_takayasu ./data/bio_results/maladie_de_takayasu + +echo ----------------- +echo EXTRACT MEASUREMENT FROM LUPUS +echo ----------------- + +python extract_measurement/main.py ./data/CRH/pred/lupus_erythemateux_dissemine ./data/bio_results/lupus_erythemateux_dissemine + +echo ----------------- +echo EXTRACT MEASUREMENT FROM SCLERODERMIE SYSTEMIQUE +echo ----------------- + +python extract_measurement/main.py ./data/CRH/pred/sclerodermie_systemique ./data/bio_results/sclerodermie_systemique + +echo ----------------- +echo EXTRACT MEASUREMENT FROM SAPL +echo ----------------- + +python extract_measurement/main.py ./data/CRH/pred/syndrome_des_anti-phospholipides ./data/bio_results/syndrome_des_anti-phospholipides + + +echo --EXTRACTION_FINISHED--- + +echo --------------- diff --git a/bash_scripts/Normalisation/infer_coder.sh b/bash_scripts/Normalisation/infer_coder.sh new file mode 100644 index 000000000..0c093f9fe --- /dev/null +++ b/bash_scripts/Normalisation/infer_coder.sh @@ -0,0 +1,43 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 48:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuV100 +#SBATCH --output=log_infer_coder/slurm-%j-stdout.log +#SBATCH --error=log_infer_coder/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script + +cd 'data/scratch/cse200093/BioMedics' +source .venv/bin/activate +conda deactivate + +echo ----------------- +echo NORMALIZE LUPUS DOCS +echo ----------------- + +python normalisation/inference/main.py data/bio_results/lupus_erythemateux_dissemine/pred_with_extraction.json data/bio_results/lupus_erythemateux_dissemine/norm_coder_all.json + +echo ----------------- +echo NORMALIZE MALADIE TAKAYASU DOCS +echo ----------------- + +python normalisation/inference/main.py data/bio_results/maladie_de_takayasu/pred_with_extraction.json data/bio_results/maladie_de_takayasu/norm_coder_all.json + +echo ----------------- +echo NORMALIZE SCLERODERMIE SYSTEMIQUE DOCS +echo ----------------- + +python normalisation/inference/main.py data/bio_results/sclerodermie_systemique/pred_with_extraction.json data/bio_results/sclerodermie_systemique/norm_coder_all.json + +echo ----------------- +echo NORMALIZE SAPL DOCS +echo ----------------- + +python normalisation/inference/main.py data/bio_results/syndrome_des_anti-phospholipides/pred_with_extraction.json data/bio_results/syndrome_des_anti-phospholipides/norm_coder_all.json + +echo --NORMALIZATION_FINISHED--- + +echo --------------- \ No newline at end of file diff --git a/bash_scripts/Normalisation/infer_coder_quaero.sh b/bash_scripts/Normalisation/infer_coder_quaero.sh new file mode 100644 index 000000000..626960a85 --- /dev/null +++ b/bash_scripts/Normalisation/infer_coder_quaero.sh @@ -0,0 +1,32 @@ +#!/bin/bash +#SBATCH --job-name=ner_med_training +#SBATCH -t 48:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --cpus-per-task=2 +#SBATCH --mem=20000 +#SBATCH --partition gpuV100 +#SBATCH --output=log_infer_coder/slurm-%j-stdout.log +#SBATCH --error=log_infer_coder/slurm-%j-stderr.log +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script + +cd 'data/scratch/cse200093/BioMedics' +source .venv/bin/activate +conda deactivate + + +echo ----------------- +echo NORMALIZE ANNOTATED DOCS +echo ----------------- + +python normalisation/inference/main.py normalisation/data/CRH/annotated_umls_snomed_full.json normalisation/data/pred_coder_eds/annotated_bio_micro.json + +echo ----------------- +echo NORMALIZE QUAERO DOCS +echo ----------------- + +python normalisation/inference/main.py normalisation/data/quaero_bio_micro.json normalisation/data/pred_coder_eds/quaero_bio_micro.json + +echo --NORMALIZATION_FINISHED--- + +echo --------------- diff --git a/bash_scripts/Normalisation/train_coder.sh b/bash_scripts/Normalisation/train_coder.sh new file mode 100755 index 000000000..febcf16d9 --- /dev/null +++ b/bash_scripts/Normalisation/train_coder.sh @@ -0,0 +1,7 @@ +source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh + +cd ~/scratch +source BioMedics/.venv/bin/activate +conda deactivate + +eds-toolbox slurm submit --config BioMedics/normalisation/training/train_coder_slurm.cfg -c "python BioMedics/normalisation/training/train.py --umls_dir BioMedics/data/umls/2021AB/ --model_name_or_path word-embedding/coder_eds --output_dir word-embedding/coder_eds --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr" \ No newline at end of file diff --git a/demo/requirements.txt b/demo/requirements.txt deleted file mode 100644 index 49895c26c..000000000 --- a/demo/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -git+https://github.com/aphp/edsnlp.git -streamlit diff --git a/CITATION.cff b/edsnlp/CITATION.cff similarity index 100% rename from CITATION.cff rename to edsnlp/CITATION.cff diff --git a/LICENSE b/edsnlp/LICENSE similarity index 100% rename from LICENSE rename to edsnlp/LICENSE diff --git a/Makefile b/edsnlp/Makefile similarity index 100% rename from Makefile rename to edsnlp/Makefile diff --git a/README.md b/edsnlp/README.md similarity index 100% rename from README.md rename to edsnlp/README.md diff --git a/edsnlp/__init__.py b/edsnlp/__init__.py deleted file mode 100644 index 3a232eac8..000000000 --- a/edsnlp/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -EDS-NLP -""" - -from . import patch_spacy_dot_components # isort: skip -from pathlib import Path - -from . import extensions -from .language import * - -__version__ = "0.8.0" - -BASE_DIR = Path(__file__).parent diff --git a/changelog.md b/edsnlp/changelog.md similarity index 100% rename from changelog.md rename to edsnlp/changelog.md diff --git a/contributing.md b/edsnlp/contributing.md similarity index 100% rename from contributing.md rename to edsnlp/contributing.md diff --git a/demo/app.py b/edsnlp/demo/app.py similarity index 100% rename from demo/app.py rename to edsnlp/demo/app.py diff --git a/docs/advanced-tutorials/fastapi.md b/edsnlp/docs/advanced-tutorials/fastapi.md similarity index 100% rename from docs/advanced-tutorials/fastapi.md rename to edsnlp/docs/advanced-tutorials/fastapi.md diff --git a/docs/advanced-tutorials/index.md b/edsnlp/docs/advanced-tutorials/index.md similarity index 100% rename from docs/advanced-tutorials/index.md rename to edsnlp/docs/advanced-tutorials/index.md diff --git a/docs/advanced-tutorials/word-vectors.md b/edsnlp/docs/advanced-tutorials/word-vectors.md similarity index 100% rename from docs/advanced-tutorials/word-vectors.md rename to edsnlp/docs/advanced-tutorials/word-vectors.md diff --git a/docs/assets/logo/aphp-blue.svg b/edsnlp/docs/assets/logo/aphp-blue.svg similarity index 100% rename from docs/assets/logo/aphp-blue.svg rename to edsnlp/docs/assets/logo/aphp-blue.svg diff --git a/docs/assets/logo/aphp-white.svg b/edsnlp/docs/assets/logo/aphp-white.svg similarity index 100% rename from docs/assets/logo/aphp-white.svg rename to edsnlp/docs/assets/logo/aphp-white.svg diff --git a/docs/assets/logo/edsnlp.svg b/edsnlp/docs/assets/logo/edsnlp.svg similarity index 100% rename from docs/assets/logo/edsnlp.svg rename to edsnlp/docs/assets/logo/edsnlp.svg diff --git a/docs/assets/stylesheets/extra.css b/edsnlp/docs/assets/stylesheets/extra.css similarity index 100% rename from docs/assets/stylesheets/extra.css rename to edsnlp/docs/assets/stylesheets/extra.css diff --git a/docs/assets/templates/python/material/docstring.html b/edsnlp/docs/assets/templates/python/material/docstring.html similarity index 100% rename from docs/assets/templates/python/material/docstring.html rename to edsnlp/docs/assets/templates/python/material/docstring.html diff --git a/docs/assets/templates/python/material/docstring/parameters.html b/edsnlp/docs/assets/templates/python/material/docstring/parameters.html similarity index 100% rename from docs/assets/templates/python/material/docstring/parameters.html rename to edsnlp/docs/assets/templates/python/material/docstring/parameters.html diff --git a/docs/assets/templates/python/material/function.html b/edsnlp/docs/assets/templates/python/material/function.html similarity index 100% rename from docs/assets/templates/python/material/function.html rename to edsnlp/docs/assets/templates/python/material/function.html diff --git a/docs/assets/termynal/termynal.css b/edsnlp/docs/assets/termynal/termynal.css similarity index 100% rename from docs/assets/termynal/termynal.css rename to edsnlp/docs/assets/termynal/termynal.css diff --git a/docs/assets/termynal/termynal.js b/edsnlp/docs/assets/termynal/termynal.js similarity index 100% rename from docs/assets/termynal/termynal.js rename to edsnlp/docs/assets/termynal/termynal.js diff --git a/edsnlp/docs/changelog.md b/edsnlp/docs/changelog.md new file mode 100644 index 000000000..0e156fb04 --- /dev/null +++ b/edsnlp/docs/changelog.md @@ -0,0 +1,304 @@ +# Changelog + +## Unreleased + +### Added + +- Add `eds.spaces` (or `eds.normalizer` with `spaces=True`) to detect space tokens, and add `ignore_space_tokens` to `EDSPhraseMatcher` and `SimstringMatcher` to skip them +- Add `ignore_space_tokens` option in most components + +## v0.8.0 (2023-03-09) + +### Added +- Tokenization exceptions (`Mr.`, `Dr.`, `Mrs.`) and non end-of-sentence periods are now tokenized with the next letter in the `eds` tokenizer + +### Changed + +- Disable `EDSMatcher` preprocessing auto progress tracking by default +- Moved dependencies to a single pyproject.toml: support for `pip install -e '.[dev,docs,setup]'` +- ADICAP matcher now allow dot separators (e.g. `B.H.HP.A7A0`) + +### Fixed + +- `eds.adicap` : reparsed the dictionnary used to decode the ADICAP codes (some of them were wrongly decoded) + +## v0.7.4 (2022-12-12) + +### Added +- `eds.history` : Add the option to consider only the closest dates in the sentence (dates inside the boundaries and if there is not, it takes the closest date in the entire sentence). +- `eds.negation` : It takes into account following past participates and preceding infinitives. +- `eds.hypothesis`: It takes into account following past participates hypothesis verbs. +- `eds.negation` & `eds.hypothesis` : Introduce new patterns and remove unnecessary patterns. +- `eds.dates` : Add a pattern for preceding relative dates (ex: l'embolie qui est survenue **à 10 jours**). +- Improve patterns in the `eds.pollution` component to account for multiline footers +- Add `QuickExample` object to quickly try a pipeline. +- Add UMLS terminology matcher `eds.umls` +- New `RegexMatcher` method to create spans from groupdicts +- New `eds.dates` option to disable time detection + +### Changed + +- Improve date detection by removing false positives + +### Fixed + +- `eds.hypothesis` : Remove too generic patterns. +- `EDSTokenizer` : It now tokenizes `"rechereche d'"` as `["recherche", "d'"]`, instead of `["recherche", "d", "'"]`. +- Fix small typos in the documentation and in the docstring. +- Harmonize processing utils (distributed custom_pipe) to have the same API for Pandas and Pyspark +- Fix BratConnector file loading issues with complex file hierarchies + +## v0.7.2 (2022-10-26) + +### Added + +- Improve the `eds.history` component by taking into account the date extracted from `eds.dates` component. +- New pop up when you click on the copy icon in the termynal widget (docs). +- Add NER `eds.elston-ellis` pipeline to identify Elston Ellis scores +- Add flags=re.MULTILINE to `eds.pollution` and change pattern of footer + +### Fixed + +- Remove the warning in the ``eds.sections`` when ``eds.normalizer`` is in the pipe. +- Fix filter_spans for strictly nested entities +- Fill eds.remove-lowercase "assign" metadata to run the pipeline during EDSPhraseMatcher preprocessing +- Allow back spaCy components whose name contains a dot (forbidden since spaCy v3.4.2) for backward compatibility. + +## v0.7.1 (2022-10-13) + +### Added + +- Add new patterns (footer, web entities, biology tables, coding sections) to pipeline normalisation (pollution) + +### Changed + +- Improved TNM detection algorithm +- Account for more modifiers in ADICAP codes detection + +### Fixed + +- Add nephew, niece and daughter to family qualifier patterns +- EDSTokenizer (`spacy.blank('eds')`) now recognizes non-breaking whitespaces as spaces and does not split float numbers +- `eds.dates` pipeline now allows new lines as space separators in dates + +## v0.7.0 (2022-09-06) + +### Added + +- New nested NER trainable `nested_ner` pipeline component +- Support for nested entities and attributes in BratDataConnector +- Pytorch wrappers and experimental training utils +- Add attribute `section` to entities +- Add new cases for separator pattern when components of the TNM score are separated by a forward slash +- Add NER `eds.adicap` pipeline to identify ADICAP codes +- Add patterns to `pollution` pipeline and simplifies activating or deactivating specific patterns + +### Changed +- Simplified the configuration scheme of the `pollution` pipeline +- Update of the `ContextualMatcher` (and all pipelines depending on it), rendering it more flexible to use +- Rename R component of score TNM as "resection_completeness" + +### Fixed + +- Prevent section titles from capturing surrounding tokens, causing overlaps (#113) +- Enhance existing patterns for section detection and add patterns for previously ignored sections (introduction, evolution, modalites de sortie, vaccination) . +- Fix explain mode, which was always triggered, in `eds.history` factory. +- Fix test in `eds.sections`. Previously, no check was done +- Remove SOFA scores spurious span suffixes + +## v0.6.2 (2022-08-02) + +### Added + +- New `SimstringMatcher` matcher to perform fuzzy term matching, and `algorithm` parameter in terminology components and `eds.matcher` component +- Makefile to install,test the application and see the documentation + +### Changed + +- Add consultation date pattern "CS", and False Positive patterns for dates (namely phone numbers and pagination). +- Update the pipeline score `eds.TNM`. Now it is possible to return a dictionary where the results are either `str` or `int` values + +### Fixed + +- Add new patterns to the negation qualifier +- Numpy header issues with binary distributed packages +- Simstring dependency on Windows + +## v0.6.1 (2022-07-11) + +### Added + +- Now possible to provide regex flags when using the RegexMatcher +- New `ContextualMatcher` pipe, aiming at replacing the `AdvancedRegex` pipe. +- New `as_ents` parameter for `eds.dates`, to save detected dates as entities + +### Changed + +- Faster `eds.sentences` pipeline component with Cython +- Bump version of Pydantic in `requirements.txt` to 1.8.2 to handle an incompatibility with the ContextualMatcher +- Optimise space requirements by using `.csv.gz` compression for verbs + +### Fixed + +- `eds.sentences` behaviour with dot-delimited dates (eg `02.07.2022`, which counted as three sentences) + +## v0.6.0 (2022-06-17) + +### Added + +- Complete revamp of the measurements detection pipeline, with better parsing and more exhaustive matching +- Add new functionality to the method `Span._.date.to_datetime()` to return a result infered from context for those cases with missing information. +- Force a batch size of 2000 when distributing a pipeline with Spark +- New patterns to pipeline `eds.dates` to identify cases where only the month is mentioned +- New `eds.terminology` component for generic terminology matching, using the `kb_id_` attribute to store fine-grained entity label +- New `eds.cim10` terminology matching pipeline +- New `eds.drugs` terminology pipeline that maps brand names and active ingredients to a unique [ATC](https://en.wikipedia.org/wiki/Anatomical_Therapeutic_Chemical_Classification_System) code + +## v0.5.3 (2022-05-04) + +### Added + +- Support for strings in the example utility +- [TNM](https://en.wikipedia.org/wiki/TNM_staging_system) detection and normalisation with the `eds.TNM` pipeline +- Support for arbitrary callback for Pandas multiprocessing, with the `callback` argument + +## v0.5.2 (2022-04-29) + +### Added + +- Support for chained attributes in the `processing` pipelines +- Colour utility with the category20 colour palette + +### Fixed + +- Correct a REGEX on the date detector (both `nov` and `nov.` are now detected, as all other months) + +## v0.5.1 (2022-04-11) + +### Fixed + +- Updated Numpy requirements to be compatible with the `EDSPhraseMatcher` + +## v0.5.0 (2022-04-08) + +### Added + +- New `eds` language to better fit French clinical documents and improve speed +- Testing for markdown codeblocks to make sure the documentation is actually executable + +### Changed + +- Complete revamp of the date detection pipeline, with better parsing and more exhaustive matching +- Reimplementation of the EDSPhraseMatcher in Cython, leading to a x15 speed increase + +## v0.4.4 + +- Add `measures` pipeline +- Cap Jinja2 version to fix mkdocs +- Adding the possibility to add context in the processing module +- Improve the speed of char replacement pipelines (accents and quotes) +- Improve the speed of the regex matcher + +## v0.4.3 + +- Fix regex matching on spans. +- Add fast_parse in date pipeline. +- Add relative_date information parsing + +## v0.4.2 + +- Fix issue with `dateparser` library (see scrapinghub/dateparser#1045) +- Fix `attr` issue in the `advanced-regex` pipelin +- Add documentation for `eds.covid` +- Update the demo with an explanation for the regex + +## v0.4.1 + +- Added support to Koalas DataFrames in the `edsnlp.processing` pipe. +- Added `eds.covid` NER pipeline for detecting COVID19 mentions. + +## v0.4.0 + +- Profound re-write of the normalisation : + - The custom attribute `CUSTOM_NORM` is completely abandoned in favour of a more _spacyfic_ alternative + - The `normalizer` pipeline modifies the `NORM` attribute in place + - Other pipelines can modify the `Token._.excluded` custom attribute +- EDS regex and term matchers can ignore excluded tokens during matching, effectively adding a second dimension to normalisation (choice of the attribute and possibility to skip _pollution_ tokens regardless of the attribute) +- Matching can be performed on custom attributes more easily +- Qualifiers are regrouped together within the `edsnlp.qualifiers` submodule, the inheritance from the `GenericMatcher` is dropped. +- `edsnlp.utils.filter.filter_spans` now accepts a `label_to_remove` parameter. If set, only corresponding spans are removed, along with overlapping spans. Primary use-case: removing pseudo cues for qualifiers. +- Generalise the naming convention for extensions, which keep the same name as the pipeline that created them (eg `Span._.negation` for the `eds.negation` pipeline). The previous convention is kept for now, but calling it issues a warning. +- The `dates` pipeline underwent some light formatting to increase robustness and fix a few issues +- A new `consultation_dates` pipeline was added, which looks for dates preceded by expressions specific to consultation dates +- In rule-based processing, the `terms.py` submodule is replaced by `patterns.py` to reflect the possible presence of regular expressions +- Refactoring of the architecture : + - pipelines are now regrouped by type (`core`, `ner`, `misc`, `qualifiers`) + - `matchers` submodule contains `RegexMatcher` and `PhraseMatcher` classes, which interact with the normalisation + - `multiprocessing` submodule contains `spark` and `local` multiprocessing tools + - `connectors` contains `Brat`, `OMOP` and `LabelTool` connectors + - `utils` contains various utilities +- Add entry points to make pipeline usable directly, removing the need to import `edsnlp.components`. +- Add a `eds` namespace for components: for instance, `negation` becomes `eds.negation`. Using the former pipeline name still works, but issues a deprecation warning. +- Add 3 score pipelines related to emergency +- Add a helper function to use a spaCy pipeline as a Spark UDF. +- Fix alignment issues in RegexMatcher +- Change the alignment procedure, dropping clumsy `numpy` dependency in favour of `bisect` +- Change the name of `eds.antecedents` to `eds.history`. + Calling `eds.antecedents` still works, but issues a deprecation warning and support will be removed in a future version. +- Add a `eds.covid` component, that identifies mentions of COVID +- Change the demo, to include NER components + +## v0.3.2 + +- Major revamp of the normalisation. + - The `normalizer` pipeline **now adds atomic components** (`lowercase`, `accents`, `quotes`, `pollution` & `endlines`) to the processing pipeline, and compiles the results into a new `Doc._.normalized` extension. The latter is itself a spaCy `Doc` object, wherein tokens are normalised and pollution tokens are removed altogether. Components that match on the `CUSTOM_NORM` attribute process the `normalized` document, and matches are brought back to the original document using a token-wise mapping. + - Update the `RegexMatcher` to use the `CUSTOM_NORM` attribute + - Add an `EDSPhraseMatcher`, wrapping spaCy's `PhraseMatcher` to enable matching on `CUSTOM_NORM`. + - Update the `matcher` and `advanced` pipelines to enable matching on the `CUSTOM_NORM` attribute. +- Add an OMOP connector, to help go back and forth between OMOP-formatted pandas dataframes and spaCy documents. +- Add a `reason` pipeline, that extracts the reason for visit. +- Add an `endlines` pipeline, that classifies newline characters between spaces and actual ends of line. +- Add possibility to annotate within entities for qualifiers (`negation`, `hypothesis`, etc), ie if the cue is within the entity. Disabled by default. + +## v0.3.1 + +- Update `dates` to remove miscellaneous bugs. +- Add `isort` pre-commit hook. +- Improve performance for `negation`, `hypothesis`, `antecedents`, `family` and `rspeech` by using spaCy's `filter_spans` and our `consume_spans` methods. +- Add proposition segmentation to `hypothesis` and `family`, enhancing results. + +## v0.3.0 + +- Renamed `generic` to `matcher`. This is a non-breaking change for the average user, adding the pipeline is still : + + + + ```python + nlp.add_pipe("matcher", config=dict(terms=dict(maladie="maladie"))) + ``` + +- Removed `quickumls` pipeline. It was untested, unmaintained. Will be added back in a future release. +- Add `score` pipeline, and `charlson`. +- Add `advanced-regex` pipeline +- Corrected bugs in the `negation` pipeline + +## v0.2.0 + +- Add `negation` pipeline +- Add `family` pipeline +- Add `hypothesis` pipeline +- Add `antecedents` pipeline +- Add `rspeech` pipeline +- Refactor the library : + - Remove the `rules` folder + - Add a `pipelines` folder, containing one subdirectory per component + - Every component subdirectory contains a module defining the component, and a module defining a factory, plus any other utilities (eg `terms.py`) + +## v0.1.0 + +First working version. Available pipelines : + +- `section` +- `sentences` +- `normalization` +- `pollution` diff --git a/edsnlp/docs/contributing.md b/edsnlp/docs/contributing.md new file mode 100644 index 000000000..2ca7634f2 --- /dev/null +++ b/edsnlp/docs/contributing.md @@ -0,0 +1,127 @@ +# Contributing to EDS-NLP + +We welcome contributions ! There are many ways to help. For example, you can: + +1. Help us track bugs by filing issues +2. Suggest and help prioritise new functionalities +3. Develop a new pipeline ! Fork the project and propose a new functionality through a pull request +4. Help us make the library as straightforward as possible, by simply asking questions on whatever does not seem clear to you. + +## Development installation + +To be able to run the test suite, run the example notebooks and develop your own pipeline, you should clone the repo and install it locally. + +

+ +To make sure the pipeline will not fail because of formatting errors, we added pre-commit hooks using the `pre-commit` Python library. To use it, simply install it: + +
+ +```console +$ pre-commit install +``` + +
+ +The pre-commit hooks defined in the [configuration](https://github.com/aphp/edsnlp/blob/master/.pre-commit-config.yaml) will automatically run when you commit your changes, letting you know if something went wrong. + +The hooks only run on staged changes. To force-run it on all files, run: + +
+ +```console +$ pre-commit run --all-files +---> 100% +color:green All good ! +``` + +
+ +## Proposing a merge request + +At the very least, your changes should : + +- Be well-documented ; +- Pass every tests, and preferably implement its own ; +- Follow the style guide. + +### Testing your code + +We use the Pytest test suite. + +The following command will run the test suite. Writing your own tests is encouraged ! + +```shell +python -m pytest +``` + +!!! warning "Testing Cython code" + + Make sure the package is [installed in editable mode](#development-installation). + Otherwise `Pytest` won't be able to find the Cython modules. + +Should your contribution propose a bug fix, we require the bug be thoroughly tested. + +### Architecture of a pipeline + +Pipelines should follow the same pattern : + +``` +edsnlp/pipelines/ + |-- .py # Defines the component logic + |-- patterns.py # Defines matched patterns + |-- factory.py # Declares the pipeline to spaCy +``` + +### Style Guide + +We use [Black](https://github.com/psf/black) to reformat the code. While other formatter only enforce PEP8 compliance, Black also makes the code uniform. In short : + +> Black reformats entire files in place. It is not configurable. + +Moreover, the CI/CD pipeline enforces a number of checks on the "quality" of the code. To wit, non black-formatted code will make the test pipeline fail. We use `pre-commit` to keep our codebase clean. + +Refer to the [development install tutorial](#development-installation) for tips on how to format your files automatically. +Most modern editors propose extensions that will format files on save. + +### Documentation + +Make sure to document your improvements, both within the code with comprehensive docstrings, +as well as in the documentation itself if need be. + +We use `MkDocs` for EDS-NLP's documentation. You can checkout the changes you make with: + +
+ +```console +# Install the requirements +$ pip install -e '.[docs]' +---> 100% +color:green Installation successful + +# Run the documentation +$ mkdocs serve +``` + +
+ +Go to [`localhost:8000`](http://localhost:8000) to see your changes. MkDocs watches for changes in the documentation folder +and automatically reloads the page. diff --git a/docs/index.md b/edsnlp/docs/index.md similarity index 100% rename from docs/index.md rename to edsnlp/docs/index.md diff --git a/docs/pipelines/architecture.md b/edsnlp/docs/pipelines/architecture.md similarity index 100% rename from docs/pipelines/architecture.md rename to edsnlp/docs/pipelines/architecture.md diff --git a/docs/pipelines/core/contextual-matcher.md b/edsnlp/docs/pipelines/core/contextual-matcher.md similarity index 100% rename from docs/pipelines/core/contextual-matcher.md rename to edsnlp/docs/pipelines/core/contextual-matcher.md diff --git a/docs/pipelines/core/endlines.md b/edsnlp/docs/pipelines/core/endlines.md similarity index 100% rename from docs/pipelines/core/endlines.md rename to edsnlp/docs/pipelines/core/endlines.md diff --git a/docs/pipelines/core/index.md b/edsnlp/docs/pipelines/core/index.md similarity index 100% rename from docs/pipelines/core/index.md rename to edsnlp/docs/pipelines/core/index.md diff --git a/docs/pipelines/core/matcher.md b/edsnlp/docs/pipelines/core/matcher.md similarity index 100% rename from docs/pipelines/core/matcher.md rename to edsnlp/docs/pipelines/core/matcher.md diff --git a/docs/pipelines/core/normalisation.md b/edsnlp/docs/pipelines/core/normalisation.md similarity index 100% rename from docs/pipelines/core/normalisation.md rename to edsnlp/docs/pipelines/core/normalisation.md diff --git a/docs/pipelines/core/resources/alignment.svg b/edsnlp/docs/pipelines/core/resources/alignment.svg similarity index 100% rename from docs/pipelines/core/resources/alignment.svg rename to edsnlp/docs/pipelines/core/resources/alignment.svg diff --git a/docs/pipelines/core/resources/span-alignment.svg b/edsnlp/docs/pipelines/core/resources/span-alignment.svg similarity index 100% rename from docs/pipelines/core/resources/span-alignment.svg rename to edsnlp/docs/pipelines/core/resources/span-alignment.svg diff --git a/docs/pipelines/core/sentences.md b/edsnlp/docs/pipelines/core/sentences.md similarity index 100% rename from docs/pipelines/core/sentences.md rename to edsnlp/docs/pipelines/core/sentences.md diff --git a/docs/pipelines/core/terminology.md b/edsnlp/docs/pipelines/core/terminology.md similarity index 100% rename from docs/pipelines/core/terminology.md rename to edsnlp/docs/pipelines/core/terminology.md diff --git a/docs/pipelines/index.md b/edsnlp/docs/pipelines/index.md similarity index 100% rename from docs/pipelines/index.md rename to edsnlp/docs/pipelines/index.md diff --git a/docs/pipelines/misc/consultation-dates.md b/edsnlp/docs/pipelines/misc/consultation-dates.md similarity index 100% rename from docs/pipelines/misc/consultation-dates.md rename to edsnlp/docs/pipelines/misc/consultation-dates.md diff --git a/docs/pipelines/misc/dates.md b/edsnlp/docs/pipelines/misc/dates.md similarity index 100% rename from docs/pipelines/misc/dates.md rename to edsnlp/docs/pipelines/misc/dates.md diff --git a/docs/pipelines/misc/index.md b/edsnlp/docs/pipelines/misc/index.md similarity index 100% rename from docs/pipelines/misc/index.md rename to edsnlp/docs/pipelines/misc/index.md diff --git a/docs/pipelines/misc/measurements.md b/edsnlp/docs/pipelines/misc/measurements.md similarity index 100% rename from docs/pipelines/misc/measurements.md rename to edsnlp/docs/pipelines/misc/measurements.md diff --git a/docs/pipelines/misc/reason.md b/edsnlp/docs/pipelines/misc/reason.md similarity index 100% rename from docs/pipelines/misc/reason.md rename to edsnlp/docs/pipelines/misc/reason.md diff --git a/docs/pipelines/misc/sections.md b/edsnlp/docs/pipelines/misc/sections.md similarity index 100% rename from docs/pipelines/misc/sections.md rename to edsnlp/docs/pipelines/misc/sections.md diff --git a/docs/pipelines/misc/tables.md b/edsnlp/docs/pipelines/misc/tables.md similarity index 100% rename from docs/pipelines/misc/tables.md rename to edsnlp/docs/pipelines/misc/tables.md diff --git a/docs/pipelines/ner/adicap.md b/edsnlp/docs/pipelines/ner/adicap.md similarity index 100% rename from docs/pipelines/ner/adicap.md rename to edsnlp/docs/pipelines/ner/adicap.md diff --git a/docs/pipelines/ner/cim10.md b/edsnlp/docs/pipelines/ner/cim10.md similarity index 100% rename from docs/pipelines/ner/cim10.md rename to edsnlp/docs/pipelines/ner/cim10.md diff --git a/docs/pipelines/ner/covid.md b/edsnlp/docs/pipelines/ner/covid.md similarity index 100% rename from docs/pipelines/ner/covid.md rename to edsnlp/docs/pipelines/ner/covid.md diff --git a/docs/pipelines/ner/drugs.md b/edsnlp/docs/pipelines/ner/drugs.md similarity index 100% rename from docs/pipelines/ner/drugs.md rename to edsnlp/docs/pipelines/ner/drugs.md diff --git a/docs/pipelines/ner/index.md b/edsnlp/docs/pipelines/ner/index.md similarity index 100% rename from docs/pipelines/ner/index.md rename to edsnlp/docs/pipelines/ner/index.md diff --git a/docs/pipelines/ner/score.md b/edsnlp/docs/pipelines/ner/score.md similarity index 100% rename from docs/pipelines/ner/score.md rename to edsnlp/docs/pipelines/ner/score.md diff --git a/docs/pipelines/ner/umls.md b/edsnlp/docs/pipelines/ner/umls.md similarity index 100% rename from docs/pipelines/ner/umls.md rename to edsnlp/docs/pipelines/ner/umls.md diff --git a/docs/pipelines/qualifiers/family.md b/edsnlp/docs/pipelines/qualifiers/family.md similarity index 100% rename from docs/pipelines/qualifiers/family.md rename to edsnlp/docs/pipelines/qualifiers/family.md diff --git a/docs/pipelines/qualifiers/history.md b/edsnlp/docs/pipelines/qualifiers/history.md similarity index 100% rename from docs/pipelines/qualifiers/history.md rename to edsnlp/docs/pipelines/qualifiers/history.md diff --git a/docs/pipelines/qualifiers/hypothesis.md b/edsnlp/docs/pipelines/qualifiers/hypothesis.md similarity index 100% rename from docs/pipelines/qualifiers/hypothesis.md rename to edsnlp/docs/pipelines/qualifiers/hypothesis.md diff --git a/docs/pipelines/qualifiers/index.md b/edsnlp/docs/pipelines/qualifiers/index.md similarity index 100% rename from docs/pipelines/qualifiers/index.md rename to edsnlp/docs/pipelines/qualifiers/index.md diff --git a/docs/pipelines/qualifiers/negation.md b/edsnlp/docs/pipelines/qualifiers/negation.md similarity index 100% rename from docs/pipelines/qualifiers/negation.md rename to edsnlp/docs/pipelines/qualifiers/negation.md diff --git a/docs/pipelines/qualifiers/reported-speech.md b/edsnlp/docs/pipelines/qualifiers/reported-speech.md similarity index 100% rename from docs/pipelines/qualifiers/reported-speech.md rename to edsnlp/docs/pipelines/qualifiers/reported-speech.md diff --git a/docs/pipelines/trainable/edsnlp-ner.svg b/edsnlp/docs/pipelines/trainable/edsnlp-ner.svg similarity index 100% rename from docs/pipelines/trainable/edsnlp-ner.svg rename to edsnlp/docs/pipelines/trainable/edsnlp-ner.svg diff --git a/docs/pipelines/trainable/index.md b/edsnlp/docs/pipelines/trainable/index.md similarity index 100% rename from docs/pipelines/trainable/index.md rename to edsnlp/docs/pipelines/trainable/index.md diff --git a/docs/pipelines/trainable/ner.md b/edsnlp/docs/pipelines/trainable/ner.md similarity index 100% rename from docs/pipelines/trainable/ner.md rename to edsnlp/docs/pipelines/trainable/ner.md diff --git a/edsnlp/docs/reference/components.md b/edsnlp/docs/reference/components.md new file mode 100644 index 000000000..ac3814956 --- /dev/null +++ b/edsnlp/docs/reference/components.md @@ -0,0 +1,3 @@ +# `edsnlp.components` + +::: edsnlp.components diff --git a/edsnlp/docs/reference/conjugator.md b/edsnlp/docs/reference/conjugator.md new file mode 100644 index 000000000..cb00455ce --- /dev/null +++ b/edsnlp/docs/reference/conjugator.md @@ -0,0 +1,3 @@ +# `edsnlp.conjugator` + +::: edsnlp.conjugator diff --git a/edsnlp/docs/reference/connectors/brat.md b/edsnlp/docs/reference/connectors/brat.md new file mode 100644 index 000000000..42b3ff59d --- /dev/null +++ b/edsnlp/docs/reference/connectors/brat.md @@ -0,0 +1,3 @@ +# `edsnlp.connectors.brat` + +::: edsnlp.connectors.brat diff --git a/edsnlp/docs/reference/connectors/index.md b/edsnlp/docs/reference/connectors/index.md new file mode 100644 index 000000000..a43078957 --- /dev/null +++ b/edsnlp/docs/reference/connectors/index.md @@ -0,0 +1,3 @@ +# `edsnlp.connectors` + +::: edsnlp.connectors diff --git a/edsnlp/docs/reference/connectors/labeltool.md b/edsnlp/docs/reference/connectors/labeltool.md new file mode 100644 index 000000000..70a0d11ba --- /dev/null +++ b/edsnlp/docs/reference/connectors/labeltool.md @@ -0,0 +1,3 @@ +# `edsnlp.connectors.labeltool` + +::: edsnlp.connectors.labeltool diff --git a/edsnlp/docs/reference/connectors/omop.md b/edsnlp/docs/reference/connectors/omop.md new file mode 100644 index 000000000..c114883f4 --- /dev/null +++ b/edsnlp/docs/reference/connectors/omop.md @@ -0,0 +1,3 @@ +# `edsnlp.connectors.omop` + +::: edsnlp.connectors.omop diff --git a/edsnlp/docs/reference/extensions.md b/edsnlp/docs/reference/extensions.md new file mode 100644 index 000000000..bd7f97d1d --- /dev/null +++ b/edsnlp/docs/reference/extensions.md @@ -0,0 +1,3 @@ +# `edsnlp.extensions` + +::: edsnlp.extensions diff --git a/edsnlp/docs/reference/index.md b/edsnlp/docs/reference/index.md new file mode 100644 index 000000000..89d0457c5 --- /dev/null +++ b/edsnlp/docs/reference/index.md @@ -0,0 +1,3 @@ +# `edsnlp` + +::: edsnlp diff --git a/edsnlp/docs/reference/language.md b/edsnlp/docs/reference/language.md new file mode 100644 index 000000000..73a67a809 --- /dev/null +++ b/edsnlp/docs/reference/language.md @@ -0,0 +1,3 @@ +# `edsnlp.language` + +::: edsnlp.language diff --git a/edsnlp/docs/reference/matchers/index.md b/edsnlp/docs/reference/matchers/index.md new file mode 100644 index 000000000..6c7e24e6c --- /dev/null +++ b/edsnlp/docs/reference/matchers/index.md @@ -0,0 +1,3 @@ +# `edsnlp.matchers` + +::: edsnlp.matchers diff --git a/edsnlp/docs/reference/matchers/regex.md b/edsnlp/docs/reference/matchers/regex.md new file mode 100644 index 000000000..f94457403 --- /dev/null +++ b/edsnlp/docs/reference/matchers/regex.md @@ -0,0 +1,3 @@ +# `edsnlp.matchers.regex` + +::: edsnlp.matchers.regex diff --git a/edsnlp/docs/reference/matchers/simstring.md b/edsnlp/docs/reference/matchers/simstring.md new file mode 100644 index 000000000..7c59d9a97 --- /dev/null +++ b/edsnlp/docs/reference/matchers/simstring.md @@ -0,0 +1,3 @@ +# `edsnlp.matchers.simstring` + +::: edsnlp.matchers.simstring diff --git a/edsnlp/docs/reference/matchers/utils/index.md b/edsnlp/docs/reference/matchers/utils/index.md new file mode 100644 index 000000000..58dcfd9bb --- /dev/null +++ b/edsnlp/docs/reference/matchers/utils/index.md @@ -0,0 +1,3 @@ +# `edsnlp.matchers.utils` + +::: edsnlp.matchers.utils diff --git a/edsnlp/docs/reference/matchers/utils/offset.md b/edsnlp/docs/reference/matchers/utils/offset.md new file mode 100644 index 000000000..da96a727f --- /dev/null +++ b/edsnlp/docs/reference/matchers/utils/offset.md @@ -0,0 +1,3 @@ +# `edsnlp.matchers.utils.offset` + +::: edsnlp.matchers.utils.offset diff --git a/edsnlp/docs/reference/matchers/utils/text.md b/edsnlp/docs/reference/matchers/utils/text.md new file mode 100644 index 000000000..3d5f05796 --- /dev/null +++ b/edsnlp/docs/reference/matchers/utils/text.md @@ -0,0 +1,3 @@ +# `edsnlp.matchers.utils.text` + +::: edsnlp.matchers.utils.text diff --git a/edsnlp/docs/reference/models/index.md b/edsnlp/docs/reference/models/index.md new file mode 100644 index 000000000..72c9adc2a --- /dev/null +++ b/edsnlp/docs/reference/models/index.md @@ -0,0 +1,3 @@ +# `edsnlp.models` + +::: edsnlp.models diff --git a/edsnlp/docs/reference/models/pytorch_wrapper.md b/edsnlp/docs/reference/models/pytorch_wrapper.md new file mode 100644 index 000000000..f2817cd46 --- /dev/null +++ b/edsnlp/docs/reference/models/pytorch_wrapper.md @@ -0,0 +1,3 @@ +# `edsnlp.models.pytorch_wrapper` + +::: edsnlp.models.pytorch_wrapper diff --git a/edsnlp/docs/reference/models/stack_crf_ner.md b/edsnlp/docs/reference/models/stack_crf_ner.md new file mode 100644 index 000000000..414094c62 --- /dev/null +++ b/edsnlp/docs/reference/models/stack_crf_ner.md @@ -0,0 +1,3 @@ +# `edsnlp.models.stack_crf_ner` + +::: edsnlp.models.stack_crf_ner diff --git a/edsnlp/docs/reference/models/torch/crf.md b/edsnlp/docs/reference/models/torch/crf.md new file mode 100644 index 000000000..8863c1ab8 --- /dev/null +++ b/edsnlp/docs/reference/models/torch/crf.md @@ -0,0 +1,3 @@ +# `edsnlp.models.torch.crf` + +::: edsnlp.models.torch.crf diff --git a/edsnlp/docs/reference/models/torch/index.md b/edsnlp/docs/reference/models/torch/index.md new file mode 100644 index 000000000..c186df0c6 --- /dev/null +++ b/edsnlp/docs/reference/models/torch/index.md @@ -0,0 +1,3 @@ +# `edsnlp.models.torch` + +::: edsnlp.models.torch diff --git a/edsnlp/docs/reference/patch_spacy_dot_components.md b/edsnlp/docs/reference/patch_spacy_dot_components.md new file mode 100644 index 000000000..a94d83c1a --- /dev/null +++ b/edsnlp/docs/reference/patch_spacy_dot_components.md @@ -0,0 +1,3 @@ +# `edsnlp.patch_spacy_dot_components` + +::: edsnlp.patch_spacy_dot_components diff --git a/edsnlp/docs/reference/pipelines/base.md b/edsnlp/docs/reference/pipelines/base.md new file mode 100644 index 000000000..9717e2467 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/base.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.base` + +::: edsnlp.pipelines.base diff --git a/edsnlp/docs/reference/pipelines/core/context/context.md b/edsnlp/docs/reference/pipelines/core/context/context.md new file mode 100644 index 000000000..edcf07e7f --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/context/context.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.context.context` + +::: edsnlp.pipelines.core.context.context diff --git a/edsnlp/docs/reference/pipelines/core/context/factory.md b/edsnlp/docs/reference/pipelines/core/context/factory.md new file mode 100644 index 000000000..096b5d364 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/context/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.context.factory` + +::: edsnlp.pipelines.core.context.factory diff --git a/edsnlp/docs/reference/pipelines/core/context/index.md b/edsnlp/docs/reference/pipelines/core/context/index.md new file mode 100644 index 000000000..eebe999fe --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/context/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.context` + +::: edsnlp.pipelines.core.context diff --git a/edsnlp/docs/reference/pipelines/core/contextual_matcher/contextual_matcher.md b/edsnlp/docs/reference/pipelines/core/contextual_matcher/contextual_matcher.md new file mode 100644 index 000000000..6eea02499 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/contextual_matcher/contextual_matcher.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.contextual_matcher.contextual_matcher` + +::: edsnlp.pipelines.core.contextual_matcher.contextual_matcher diff --git a/edsnlp/docs/reference/pipelines/core/contextual_matcher/factory.md b/edsnlp/docs/reference/pipelines/core/contextual_matcher/factory.md new file mode 100644 index 000000000..eebca8f65 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/contextual_matcher/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.contextual_matcher.factory` + +::: edsnlp.pipelines.core.contextual_matcher.factory diff --git a/edsnlp/docs/reference/pipelines/core/contextual_matcher/index.md b/edsnlp/docs/reference/pipelines/core/contextual_matcher/index.md new file mode 100644 index 000000000..e69d6b620 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/contextual_matcher/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.contextual_matcher` + +::: edsnlp.pipelines.core.contextual_matcher diff --git a/edsnlp/docs/reference/pipelines/core/contextual_matcher/models.md b/edsnlp/docs/reference/pipelines/core/contextual_matcher/models.md new file mode 100644 index 000000000..c11554814 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/contextual_matcher/models.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.contextual_matcher.models` + +::: edsnlp.pipelines.core.contextual_matcher.models diff --git a/edsnlp/docs/reference/pipelines/core/endlines/endlines.md b/edsnlp/docs/reference/pipelines/core/endlines/endlines.md new file mode 100644 index 000000000..bcc1b2e30 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/endlines/endlines.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.endlines.endlines` + +::: edsnlp.pipelines.core.endlines.endlines diff --git a/edsnlp/docs/reference/pipelines/core/endlines/endlinesmodel.md b/edsnlp/docs/reference/pipelines/core/endlines/endlinesmodel.md new file mode 100644 index 000000000..23b1f79f4 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/endlines/endlinesmodel.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.endlines.endlinesmodel` + +::: edsnlp.pipelines.core.endlines.endlinesmodel diff --git a/edsnlp/docs/reference/pipelines/core/endlines/factory.md b/edsnlp/docs/reference/pipelines/core/endlines/factory.md new file mode 100644 index 000000000..634905a3e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/endlines/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.endlines.factory` + +::: edsnlp.pipelines.core.endlines.factory diff --git a/edsnlp/docs/reference/pipelines/core/endlines/functional.md b/edsnlp/docs/reference/pipelines/core/endlines/functional.md new file mode 100644 index 000000000..33045b3df --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/endlines/functional.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.endlines.functional` + +::: edsnlp.pipelines.core.endlines.functional diff --git a/edsnlp/docs/reference/pipelines/core/endlines/index.md b/edsnlp/docs/reference/pipelines/core/endlines/index.md new file mode 100644 index 000000000..36c0db206 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/endlines/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.endlines` + +::: edsnlp.pipelines.core.endlines diff --git a/edsnlp/docs/reference/pipelines/core/index.md b/edsnlp/docs/reference/pipelines/core/index.md new file mode 100644 index 000000000..e611fff2d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core` + +::: edsnlp.pipelines.core diff --git a/edsnlp/docs/reference/pipelines/core/matcher/factory.md b/edsnlp/docs/reference/pipelines/core/matcher/factory.md new file mode 100644 index 000000000..657b6205d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/matcher/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.matcher.factory` + +::: edsnlp.pipelines.core.matcher.factory diff --git a/edsnlp/docs/reference/pipelines/core/matcher/index.md b/edsnlp/docs/reference/pipelines/core/matcher/index.md new file mode 100644 index 000000000..fca207785 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/matcher/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.matcher` + +::: edsnlp.pipelines.core.matcher diff --git a/edsnlp/docs/reference/pipelines/core/matcher/matcher.md b/edsnlp/docs/reference/pipelines/core/matcher/matcher.md new file mode 100644 index 000000000..2ca0e5145 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/matcher/matcher.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.matcher.matcher` + +::: edsnlp.pipelines.core.matcher.matcher diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/accents/accents.md b/edsnlp/docs/reference/pipelines/core/normalizer/accents/accents.md new file mode 100644 index 000000000..1089d7d4d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/accents/accents.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.accents.accents` + +::: edsnlp.pipelines.core.normalizer.accents.accents diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/accents/factory.md b/edsnlp/docs/reference/pipelines/core/normalizer/accents/factory.md new file mode 100644 index 000000000..4f26e980e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/accents/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.accents.factory` + +::: edsnlp.pipelines.core.normalizer.accents.factory diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/accents/index.md b/edsnlp/docs/reference/pipelines/core/normalizer/accents/index.md new file mode 100644 index 000000000..06a49942b --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/accents/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.accents` + +::: edsnlp.pipelines.core.normalizer.accents diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/accents/patterns.md b/edsnlp/docs/reference/pipelines/core/normalizer/accents/patterns.md new file mode 100644 index 000000000..23e986ebe --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/accents/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.accents.patterns` + +::: edsnlp.pipelines.core.normalizer.accents.patterns diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/factory.md b/edsnlp/docs/reference/pipelines/core/normalizer/factory.md new file mode 100644 index 000000000..85109de2d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.factory` + +::: edsnlp.pipelines.core.normalizer.factory diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/index.md b/edsnlp/docs/reference/pipelines/core/normalizer/index.md new file mode 100644 index 000000000..f276c371e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer` + +::: edsnlp.pipelines.core.normalizer diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/lowercase/factory.md b/edsnlp/docs/reference/pipelines/core/normalizer/lowercase/factory.md new file mode 100644 index 000000000..b9c832c21 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/lowercase/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.lowercase.factory` + +::: edsnlp.pipelines.core.normalizer.lowercase.factory diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/lowercase/index.md b/edsnlp/docs/reference/pipelines/core/normalizer/lowercase/index.md new file mode 100644 index 000000000..b537ad71e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/lowercase/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.lowercase` + +::: edsnlp.pipelines.core.normalizer.lowercase diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/normalizer.md b/edsnlp/docs/reference/pipelines/core/normalizer/normalizer.md new file mode 100644 index 000000000..562186b14 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/normalizer.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.normalizer` + +::: edsnlp.pipelines.core.normalizer.normalizer diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/pollution/factory.md b/edsnlp/docs/reference/pipelines/core/normalizer/pollution/factory.md new file mode 100644 index 000000000..edde5b58e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/pollution/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.pollution.factory` + +::: edsnlp.pipelines.core.normalizer.pollution.factory diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/pollution/index.md b/edsnlp/docs/reference/pipelines/core/normalizer/pollution/index.md new file mode 100644 index 000000000..5f26500c5 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/pollution/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.pollution` + +::: edsnlp.pipelines.core.normalizer.pollution diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/pollution/patterns.md b/edsnlp/docs/reference/pipelines/core/normalizer/pollution/patterns.md new file mode 100644 index 000000000..652be29b8 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/pollution/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.pollution.patterns` + +::: edsnlp.pipelines.core.normalizer.pollution.patterns diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/pollution/pollution.md b/edsnlp/docs/reference/pipelines/core/normalizer/pollution/pollution.md new file mode 100644 index 000000000..f443bbadf --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/pollution/pollution.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.pollution.pollution` + +::: edsnlp.pipelines.core.normalizer.pollution.pollution diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/quotes/factory.md b/edsnlp/docs/reference/pipelines/core/normalizer/quotes/factory.md new file mode 100644 index 000000000..e14bd2802 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/quotes/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.quotes.factory` + +::: edsnlp.pipelines.core.normalizer.quotes.factory diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/quotes/index.md b/edsnlp/docs/reference/pipelines/core/normalizer/quotes/index.md new file mode 100644 index 000000000..65fcf969d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/quotes/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.quotes` + +::: edsnlp.pipelines.core.normalizer.quotes diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/quotes/patterns.md b/edsnlp/docs/reference/pipelines/core/normalizer/quotes/patterns.md new file mode 100644 index 000000000..1129dfc86 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/quotes/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.quotes.patterns` + +::: edsnlp.pipelines.core.normalizer.quotes.patterns diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/quotes/quotes.md b/edsnlp/docs/reference/pipelines/core/normalizer/quotes/quotes.md new file mode 100644 index 000000000..775fcc1b8 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/quotes/quotes.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.quotes.quotes` + +::: edsnlp.pipelines.core.normalizer.quotes.quotes diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/spaces/factory.md b/edsnlp/docs/reference/pipelines/core/normalizer/spaces/factory.md new file mode 100644 index 000000000..206c573a3 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/spaces/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.spaces.factory` + +::: edsnlp.pipelines.core.normalizer.spaces.factory diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/spaces/index.md b/edsnlp/docs/reference/pipelines/core/normalizer/spaces/index.md new file mode 100644 index 000000000..fecfcfc29 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/spaces/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.spaces` + +::: edsnlp.pipelines.core.normalizer.spaces diff --git a/edsnlp/docs/reference/pipelines/core/normalizer/spaces/spaces.md b/edsnlp/docs/reference/pipelines/core/normalizer/spaces/spaces.md new file mode 100644 index 000000000..94fa6da1b --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/normalizer/spaces/spaces.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.normalizer.spaces.spaces` + +::: edsnlp.pipelines.core.normalizer.spaces.spaces diff --git a/edsnlp/docs/reference/pipelines/core/sentences/factory.md b/edsnlp/docs/reference/pipelines/core/sentences/factory.md new file mode 100644 index 000000000..346b9f476 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/sentences/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.sentences.factory` + +::: edsnlp.pipelines.core.sentences.factory diff --git a/edsnlp/docs/reference/pipelines/core/sentences/index.md b/edsnlp/docs/reference/pipelines/core/sentences/index.md new file mode 100644 index 000000000..a415052ac --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/sentences/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.sentences` + +::: edsnlp.pipelines.core.sentences diff --git a/edsnlp/docs/reference/pipelines/core/sentences/terms.md b/edsnlp/docs/reference/pipelines/core/sentences/terms.md new file mode 100644 index 000000000..c586f143e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/sentences/terms.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.sentences.terms` + +::: edsnlp.pipelines.core.sentences.terms diff --git a/edsnlp/docs/reference/pipelines/core/terminology/factory.md b/edsnlp/docs/reference/pipelines/core/terminology/factory.md new file mode 100644 index 000000000..0afca89a1 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/terminology/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.terminology.factory` + +::: edsnlp.pipelines.core.terminology.factory diff --git a/edsnlp/docs/reference/pipelines/core/terminology/index.md b/edsnlp/docs/reference/pipelines/core/terminology/index.md new file mode 100644 index 000000000..269e84b43 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/terminology/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.terminology` + +::: edsnlp.pipelines.core.terminology diff --git a/edsnlp/docs/reference/pipelines/core/terminology/terminology.md b/edsnlp/docs/reference/pipelines/core/terminology/terminology.md new file mode 100644 index 000000000..476048021 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/core/terminology/terminology.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.core.terminology.terminology` + +::: edsnlp.pipelines.core.terminology.terminology diff --git a/edsnlp/docs/reference/pipelines/factories.md b/edsnlp/docs/reference/pipelines/factories.md new file mode 100644 index 000000000..bd043e2b6 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/factories.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.factories` + +::: edsnlp.pipelines.factories diff --git a/edsnlp/docs/reference/pipelines/index.md b/edsnlp/docs/reference/pipelines/index.md new file mode 100644 index 000000000..687bcaca5 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines` + +::: edsnlp.pipelines diff --git a/edsnlp/docs/reference/pipelines/misc/consultation_dates/consultation_dates.md b/edsnlp/docs/reference/pipelines/misc/consultation_dates/consultation_dates.md new file mode 100644 index 000000000..a7dfca1ab --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/consultation_dates/consultation_dates.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.consultation_dates.consultation_dates` + +::: edsnlp.pipelines.misc.consultation_dates.consultation_dates diff --git a/edsnlp/docs/reference/pipelines/misc/consultation_dates/factory.md b/edsnlp/docs/reference/pipelines/misc/consultation_dates/factory.md new file mode 100644 index 000000000..43f45b013 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/consultation_dates/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.consultation_dates.factory` + +::: edsnlp.pipelines.misc.consultation_dates.factory diff --git a/edsnlp/docs/reference/pipelines/misc/consultation_dates/index.md b/edsnlp/docs/reference/pipelines/misc/consultation_dates/index.md new file mode 100644 index 000000000..1873b2e24 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/consultation_dates/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.consultation_dates` + +::: edsnlp.pipelines.misc.consultation_dates diff --git a/edsnlp/docs/reference/pipelines/misc/consultation_dates/patterns.md b/edsnlp/docs/reference/pipelines/misc/consultation_dates/patterns.md new file mode 100644 index 000000000..fa527fed3 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/consultation_dates/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.consultation_dates.patterns` + +::: edsnlp.pipelines.misc.consultation_dates.patterns diff --git a/edsnlp/docs/reference/pipelines/misc/dates/dates.md b/edsnlp/docs/reference/pipelines/misc/dates/dates.md new file mode 100644 index 000000000..bb05936b6 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/dates.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.dates` + +::: edsnlp.pipelines.misc.dates.dates diff --git a/edsnlp/docs/reference/pipelines/misc/dates/factory.md b/edsnlp/docs/reference/pipelines/misc/dates/factory.md new file mode 100644 index 000000000..33b431cb6 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.factory` + +::: edsnlp.pipelines.misc.dates.factory diff --git a/edsnlp/docs/reference/pipelines/misc/dates/index.md b/edsnlp/docs/reference/pipelines/misc/dates/index.md new file mode 100644 index 000000000..614ea1ee3 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates` + +::: edsnlp.pipelines.misc.dates diff --git a/edsnlp/docs/reference/pipelines/misc/dates/models.md b/edsnlp/docs/reference/pipelines/misc/dates/models.md new file mode 100644 index 000000000..d79a7eeff --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/models.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.models` + +::: edsnlp.pipelines.misc.dates.models diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/absolute.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/absolute.md new file mode 100644 index 000000000..77e04e7ed --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/absolute.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.absolute` + +::: edsnlp.pipelines.misc.dates.patterns.absolute diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/days.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/days.md new file mode 100644 index 000000000..b928b78c1 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/days.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.days` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.days diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/delimiters.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/delimiters.md new file mode 100644 index 000000000..a21f41b3e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/delimiters.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.delimiters` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.delimiters diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/directions.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/directions.md new file mode 100644 index 000000000..40304ce02 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/directions.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.directions` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.directions diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/index.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/index.md new file mode 100644 index 000000000..80c069235 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic` + +::: edsnlp.pipelines.misc.dates.patterns.atomic diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/modes.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/modes.md new file mode 100644 index 000000000..c31a86b02 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/modes.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.modes` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.modes diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/months.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/months.md new file mode 100644 index 000000000..585983903 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/months.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.months` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.months diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/numbers.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/numbers.md new file mode 100644 index 000000000..901944da6 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/numbers.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.numbers` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.numbers diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/time.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/time.md new file mode 100644 index 000000000..64ad27b4d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/time.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.time` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.time diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/units.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/units.md new file mode 100644 index 000000000..32a7029f5 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/units.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.units` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.units diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/years.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/years.md new file mode 100644 index 000000000..6120d228d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/atomic/years.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.atomic.years` + +::: edsnlp.pipelines.misc.dates.patterns.atomic.years diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/current.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/current.md new file mode 100644 index 000000000..378aad259 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/current.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.current` + +::: edsnlp.pipelines.misc.dates.patterns.current diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/duration.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/duration.md new file mode 100644 index 000000000..b9a832562 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/duration.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.duration` + +::: edsnlp.pipelines.misc.dates.patterns.duration diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/false_positive.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/false_positive.md new file mode 100644 index 000000000..1f943d60c --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/false_positive.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.false_positive` + +::: edsnlp.pipelines.misc.dates.patterns.false_positive diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/index.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/index.md new file mode 100644 index 000000000..49c355615 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns` + +::: edsnlp.pipelines.misc.dates.patterns diff --git a/edsnlp/docs/reference/pipelines/misc/dates/patterns/relative.md b/edsnlp/docs/reference/pipelines/misc/dates/patterns/relative.md new file mode 100644 index 000000000..20151b8dc --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/dates/patterns/relative.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.dates.patterns.relative` + +::: edsnlp.pipelines.misc.dates.patterns.relative diff --git a/edsnlp/docs/reference/pipelines/misc/index.md b/edsnlp/docs/reference/pipelines/misc/index.md new file mode 100644 index 000000000..6496d081d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc` + +::: edsnlp.pipelines.misc diff --git a/edsnlp/docs/reference/pipelines/misc/measurements/factory.md b/edsnlp/docs/reference/pipelines/misc/measurements/factory.md new file mode 100644 index 000000000..094289408 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/measurements/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.measurements.factory` + +::: edsnlp.pipelines.misc.measurements.factory diff --git a/edsnlp/docs/reference/pipelines/misc/measurements/index.md b/edsnlp/docs/reference/pipelines/misc/measurements/index.md new file mode 100644 index 000000000..b2faab473 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/measurements/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.measurements` + +::: edsnlp.pipelines.misc.measurements diff --git a/edsnlp/docs/reference/pipelines/misc/measurements/measurements.md b/edsnlp/docs/reference/pipelines/misc/measurements/measurements.md new file mode 100644 index 000000000..3f342691f --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/measurements/measurements.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.measurements.measurements` + +::: edsnlp.pipelines.misc.measurements.measurements diff --git a/edsnlp/docs/reference/pipelines/misc/measurements/patterns.md b/edsnlp/docs/reference/pipelines/misc/measurements/patterns.md new file mode 100644 index 000000000..f578fc732 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/measurements/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.measurements.patterns` + +::: edsnlp.pipelines.misc.measurements.patterns diff --git a/edsnlp/docs/reference/pipelines/misc/reason/factory.md b/edsnlp/docs/reference/pipelines/misc/reason/factory.md new file mode 100644 index 000000000..20ab1f43a --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/reason/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.reason.factory` + +::: edsnlp.pipelines.misc.reason.factory diff --git a/edsnlp/docs/reference/pipelines/misc/reason/index.md b/edsnlp/docs/reference/pipelines/misc/reason/index.md new file mode 100644 index 000000000..08f734858 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/reason/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.reason` + +::: edsnlp.pipelines.misc.reason diff --git a/edsnlp/docs/reference/pipelines/misc/reason/patterns.md b/edsnlp/docs/reference/pipelines/misc/reason/patterns.md new file mode 100644 index 000000000..3c152f437 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/reason/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.reason.patterns` + +::: edsnlp.pipelines.misc.reason.patterns diff --git a/edsnlp/docs/reference/pipelines/misc/reason/reason.md b/edsnlp/docs/reference/pipelines/misc/reason/reason.md new file mode 100644 index 000000000..2c5ed9f55 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/reason/reason.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.reason.reason` + +::: edsnlp.pipelines.misc.reason.reason diff --git a/edsnlp/docs/reference/pipelines/misc/sections/factory.md b/edsnlp/docs/reference/pipelines/misc/sections/factory.md new file mode 100644 index 000000000..4f571c56d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/sections/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.sections.factory` + +::: edsnlp.pipelines.misc.sections.factory diff --git a/edsnlp/docs/reference/pipelines/misc/sections/index.md b/edsnlp/docs/reference/pipelines/misc/sections/index.md new file mode 100644 index 000000000..e379c60c3 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/sections/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.sections` + +::: edsnlp.pipelines.misc.sections diff --git a/edsnlp/docs/reference/pipelines/misc/sections/patterns.md b/edsnlp/docs/reference/pipelines/misc/sections/patterns.md new file mode 100644 index 000000000..0465ddbf5 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/sections/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.sections.patterns` + +::: edsnlp.pipelines.misc.sections.patterns diff --git a/edsnlp/docs/reference/pipelines/misc/sections/sections.md b/edsnlp/docs/reference/pipelines/misc/sections/sections.md new file mode 100644 index 000000000..72e3fff03 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/misc/sections/sections.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.misc.sections.sections` + +::: edsnlp.pipelines.misc.sections.sections diff --git a/edsnlp/docs/reference/pipelines/ner/adicap/adicap.md b/edsnlp/docs/reference/pipelines/ner/adicap/adicap.md new file mode 100644 index 000000000..6ff11b0e1 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/adicap/adicap.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.adicap.adicap` + +::: edsnlp.pipelines.ner.adicap.adicap diff --git a/edsnlp/docs/reference/pipelines/ner/adicap/factory.md b/edsnlp/docs/reference/pipelines/ner/adicap/factory.md new file mode 100644 index 000000000..e0e4d89e3 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/adicap/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.adicap.factory` + +::: edsnlp.pipelines.ner.adicap.factory diff --git a/edsnlp/docs/reference/pipelines/ner/adicap/index.md b/edsnlp/docs/reference/pipelines/ner/adicap/index.md new file mode 100644 index 000000000..db502968c --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/adicap/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.adicap` + +::: edsnlp.pipelines.ner.adicap diff --git a/edsnlp/docs/reference/pipelines/ner/adicap/models.md b/edsnlp/docs/reference/pipelines/ner/adicap/models.md new file mode 100644 index 000000000..74b431fc4 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/adicap/models.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.adicap.models` + +::: edsnlp.pipelines.ner.adicap.models diff --git a/edsnlp/docs/reference/pipelines/ner/adicap/patterns.md b/edsnlp/docs/reference/pipelines/ner/adicap/patterns.md new file mode 100644 index 000000000..fcc047f46 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/adicap/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.adicap.patterns` + +::: edsnlp.pipelines.ner.adicap.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/cim10/factory.md b/edsnlp/docs/reference/pipelines/ner/cim10/factory.md new file mode 100644 index 000000000..46a66d27e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/cim10/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.cim10.factory` + +::: edsnlp.pipelines.ner.cim10.factory diff --git a/edsnlp/docs/reference/pipelines/ner/cim10/index.md b/edsnlp/docs/reference/pipelines/ner/cim10/index.md new file mode 100644 index 000000000..96a3edec8 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/cim10/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.cim10` + +::: edsnlp.pipelines.ner.cim10 diff --git a/edsnlp/docs/reference/pipelines/ner/cim10/patterns.md b/edsnlp/docs/reference/pipelines/ner/cim10/patterns.md new file mode 100644 index 000000000..0e592c4b7 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/cim10/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.cim10.patterns` + +::: edsnlp.pipelines.ner.cim10.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/covid/factory.md b/edsnlp/docs/reference/pipelines/ner/covid/factory.md new file mode 100644 index 000000000..0ac87f5a0 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/covid/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.covid.factory` + +::: edsnlp.pipelines.ner.covid.factory diff --git a/edsnlp/docs/reference/pipelines/ner/covid/index.md b/edsnlp/docs/reference/pipelines/ner/covid/index.md new file mode 100644 index 000000000..ea3539e02 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/covid/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.covid` + +::: edsnlp.pipelines.ner.covid diff --git a/edsnlp/docs/reference/pipelines/ner/covid/patterns.md b/edsnlp/docs/reference/pipelines/ner/covid/patterns.md new file mode 100644 index 000000000..0298a6dab --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/covid/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.covid.patterns` + +::: edsnlp.pipelines.ner.covid.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/drugs/factory.md b/edsnlp/docs/reference/pipelines/ner/drugs/factory.md new file mode 100644 index 000000000..8555428d5 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/drugs/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.drugs.factory` + +::: edsnlp.pipelines.ner.drugs.factory diff --git a/edsnlp/docs/reference/pipelines/ner/drugs/index.md b/edsnlp/docs/reference/pipelines/ner/drugs/index.md new file mode 100644 index 000000000..562836792 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/drugs/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.drugs` + +::: edsnlp.pipelines.ner.drugs diff --git a/edsnlp/docs/reference/pipelines/ner/drugs/patterns.md b/edsnlp/docs/reference/pipelines/ner/drugs/patterns.md new file mode 100644 index 000000000..8ef57b99d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/drugs/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.drugs.patterns` + +::: edsnlp.pipelines.ner.drugs.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/index.md b/edsnlp/docs/reference/pipelines/ner/index.md new file mode 100644 index 000000000..2bca642d3 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner` + +::: edsnlp.pipelines.ner diff --git a/edsnlp/docs/reference/pipelines/ner/scores/base_score.md b/edsnlp/docs/reference/pipelines/ner/scores/base_score.md new file mode 100644 index 000000000..845a40294 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/base_score.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.base_score` + +::: edsnlp.pipelines.ner.scores.base_score diff --git a/edsnlp/docs/reference/pipelines/ner/scores/charlson/factory.md b/edsnlp/docs/reference/pipelines/ner/scores/charlson/factory.md new file mode 100644 index 000000000..e50b1d768 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/charlson/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.charlson.factory` + +::: edsnlp.pipelines.ner.scores.charlson.factory diff --git a/edsnlp/docs/reference/pipelines/ner/scores/charlson/index.md b/edsnlp/docs/reference/pipelines/ner/scores/charlson/index.md new file mode 100644 index 000000000..cb0f203ee --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/charlson/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.charlson` + +::: edsnlp.pipelines.ner.scores.charlson diff --git a/edsnlp/docs/reference/pipelines/ner/scores/charlson/patterns.md b/edsnlp/docs/reference/pipelines/ner/scores/charlson/patterns.md new file mode 100644 index 000000000..bbabb6137 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/charlson/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.charlson.patterns` + +::: edsnlp.pipelines.ner.scores.charlson.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/factory.md b/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/factory.md new file mode 100644 index 000000000..434be1c5e --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.elstonellis.factory` + +::: edsnlp.pipelines.ner.scores.elstonellis.factory diff --git a/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/index.md b/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/index.md new file mode 100644 index 000000000..4e02a6d94 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.elstonellis` + +::: edsnlp.pipelines.ner.scores.elstonellis diff --git a/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/patterns.md b/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/patterns.md new file mode 100644 index 000000000..53d717bf4 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/elstonellis/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.elstonellis.patterns` + +::: edsnlp.pipelines.ner.scores.elstonellis.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/factory.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/factory.md new file mode 100644 index 000000000..a93cc1455 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.ccmu.factory` + +::: edsnlp.pipelines.ner.scores.emergency.ccmu.factory diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/index.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/index.md new file mode 100644 index 000000000..3bb9ee9d0 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.ccmu` + +::: edsnlp.pipelines.ner.scores.emergency.ccmu diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/patterns.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/patterns.md new file mode 100644 index 000000000..8a5b93a89 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/ccmu/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.ccmu.patterns` + +::: edsnlp.pipelines.ner.scores.emergency.ccmu.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/factory.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/factory.md new file mode 100644 index 000000000..35aed7169 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.gemsa.factory` + +::: edsnlp.pipelines.ner.scores.emergency.gemsa.factory diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/index.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/index.md new file mode 100644 index 000000000..3d79bc9b0 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.gemsa` + +::: edsnlp.pipelines.ner.scores.emergency.gemsa diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/patterns.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/patterns.md new file mode 100644 index 000000000..e4f8e331d --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/gemsa/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.gemsa.patterns` + +::: edsnlp.pipelines.ner.scores.emergency.gemsa.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/index.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/index.md new file mode 100644 index 000000000..58493d38a --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency` + +::: edsnlp.pipelines.ner.scores.emergency diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/factory.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/factory.md new file mode 100644 index 000000000..b47b10288 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.priority.factory` + +::: edsnlp.pipelines.ner.scores.emergency.priority.factory diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/index.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/index.md new file mode 100644 index 000000000..fea97464c --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.priority` + +::: edsnlp.pipelines.ner.scores.emergency.priority diff --git a/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/patterns.md b/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/patterns.md new file mode 100644 index 000000000..df5d28842 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/emergency/priority/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.emergency.priority.patterns` + +::: edsnlp.pipelines.ner.scores.emergency.priority.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/scores/factory.md b/edsnlp/docs/reference/pipelines/ner/scores/factory.md new file mode 100644 index 000000000..0671ca1b6 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.factory` + +::: edsnlp.pipelines.ner.scores.factory diff --git a/edsnlp/docs/reference/pipelines/ner/scores/index.md b/edsnlp/docs/reference/pipelines/ner/scores/index.md new file mode 100644 index 000000000..300d26fd0 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores` + +::: edsnlp.pipelines.ner.scores diff --git a/edsnlp/docs/reference/pipelines/ner/scores/sofa/factory.md b/edsnlp/docs/reference/pipelines/ner/scores/sofa/factory.md new file mode 100644 index 000000000..216c643bd --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/sofa/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.sofa.factory` + +::: edsnlp.pipelines.ner.scores.sofa.factory diff --git a/edsnlp/docs/reference/pipelines/ner/scores/sofa/index.md b/edsnlp/docs/reference/pipelines/ner/scores/sofa/index.md new file mode 100644 index 000000000..ff8780440 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/sofa/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.sofa` + +::: edsnlp.pipelines.ner.scores.sofa diff --git a/edsnlp/docs/reference/pipelines/ner/scores/sofa/patterns.md b/edsnlp/docs/reference/pipelines/ner/scores/sofa/patterns.md new file mode 100644 index 000000000..a3664dd5b --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/sofa/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.sofa.patterns` + +::: edsnlp.pipelines.ner.scores.sofa.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/scores/sofa/sofa.md b/edsnlp/docs/reference/pipelines/ner/scores/sofa/sofa.md new file mode 100644 index 000000000..ef88dce94 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/sofa/sofa.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.sofa.sofa` + +::: edsnlp.pipelines.ner.scores.sofa.sofa diff --git a/edsnlp/docs/reference/pipelines/ner/scores/tnm/factory.md b/edsnlp/docs/reference/pipelines/ner/scores/tnm/factory.md new file mode 100644 index 000000000..e90e031e1 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/tnm/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.tnm.factory` + +::: edsnlp.pipelines.ner.scores.tnm.factory diff --git a/edsnlp/docs/reference/pipelines/ner/scores/tnm/index.md b/edsnlp/docs/reference/pipelines/ner/scores/tnm/index.md new file mode 100644 index 000000000..65134b9ca --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/tnm/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.tnm` + +::: edsnlp.pipelines.ner.scores.tnm diff --git a/edsnlp/docs/reference/pipelines/ner/scores/tnm/models.md b/edsnlp/docs/reference/pipelines/ner/scores/tnm/models.md new file mode 100644 index 000000000..dc6c97b64 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/tnm/models.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.tnm.models` + +::: edsnlp.pipelines.ner.scores.tnm.models diff --git a/edsnlp/docs/reference/pipelines/ner/scores/tnm/patterns.md b/edsnlp/docs/reference/pipelines/ner/scores/tnm/patterns.md new file mode 100644 index 000000000..82d0fb890 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/tnm/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.tnm.patterns` + +::: edsnlp.pipelines.ner.scores.tnm.patterns diff --git a/edsnlp/docs/reference/pipelines/ner/scores/tnm/tnm.md b/edsnlp/docs/reference/pipelines/ner/scores/tnm/tnm.md new file mode 100644 index 000000000..38ce86f1c --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/scores/tnm/tnm.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.scores.tnm.tnm` + +::: edsnlp.pipelines.ner.scores.tnm.tnm diff --git a/edsnlp/docs/reference/pipelines/ner/umls/factory.md b/edsnlp/docs/reference/pipelines/ner/umls/factory.md new file mode 100644 index 000000000..73f8c3a44 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/umls/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.umls.factory` + +::: edsnlp.pipelines.ner.umls.factory diff --git a/edsnlp/docs/reference/pipelines/ner/umls/index.md b/edsnlp/docs/reference/pipelines/ner/umls/index.md new file mode 100644 index 000000000..b6a0a4593 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/umls/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.umls` + +::: edsnlp.pipelines.ner.umls diff --git a/edsnlp/docs/reference/pipelines/ner/umls/patterns.md b/edsnlp/docs/reference/pipelines/ner/umls/patterns.md new file mode 100644 index 000000000..72f781ac1 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/ner/umls/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.ner.umls.patterns` + +::: edsnlp.pipelines.ner.umls.patterns diff --git a/edsnlp/docs/reference/pipelines/qualifiers/base.md b/edsnlp/docs/reference/pipelines/qualifiers/base.md new file mode 100644 index 000000000..89b6aff03 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/base.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.base` + +::: edsnlp.pipelines.qualifiers.base diff --git a/edsnlp/docs/reference/pipelines/qualifiers/factories.md b/edsnlp/docs/reference/pipelines/qualifiers/factories.md new file mode 100644 index 000000000..451b0d57b --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/factories.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.factories` + +::: edsnlp.pipelines.qualifiers.factories diff --git a/edsnlp/docs/reference/pipelines/qualifiers/family/factory.md b/edsnlp/docs/reference/pipelines/qualifiers/family/factory.md new file mode 100644 index 000000000..1c1579e87 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/family/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.family.factory` + +::: edsnlp.pipelines.qualifiers.family.factory diff --git a/edsnlp/docs/reference/pipelines/qualifiers/family/family.md b/edsnlp/docs/reference/pipelines/qualifiers/family/family.md new file mode 100644 index 000000000..8c88ab215 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/family/family.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.family.family` + +::: edsnlp.pipelines.qualifiers.family.family diff --git a/edsnlp/docs/reference/pipelines/qualifiers/family/index.md b/edsnlp/docs/reference/pipelines/qualifiers/family/index.md new file mode 100644 index 000000000..ffd044399 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/family/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.family` + +::: edsnlp.pipelines.qualifiers.family diff --git a/edsnlp/docs/reference/pipelines/qualifiers/family/patterns.md b/edsnlp/docs/reference/pipelines/qualifiers/family/patterns.md new file mode 100644 index 000000000..5310ceb08 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/family/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.family.patterns` + +::: edsnlp.pipelines.qualifiers.family.patterns diff --git a/edsnlp/docs/reference/pipelines/qualifiers/history/factory.md b/edsnlp/docs/reference/pipelines/qualifiers/history/factory.md new file mode 100644 index 000000000..8a5c7cdc7 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/history/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.history.factory` + +::: edsnlp.pipelines.qualifiers.history.factory diff --git a/edsnlp/docs/reference/pipelines/qualifiers/history/history.md b/edsnlp/docs/reference/pipelines/qualifiers/history/history.md new file mode 100644 index 000000000..87020b5e7 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/history/history.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.history.history` + +::: edsnlp.pipelines.qualifiers.history.history diff --git a/edsnlp/docs/reference/pipelines/qualifiers/history/index.md b/edsnlp/docs/reference/pipelines/qualifiers/history/index.md new file mode 100644 index 000000000..42d8de8e6 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/history/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.history` + +::: edsnlp.pipelines.qualifiers.history diff --git a/edsnlp/docs/reference/pipelines/qualifiers/history/patterns.md b/edsnlp/docs/reference/pipelines/qualifiers/history/patterns.md new file mode 100644 index 000000000..6369de836 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/history/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.history.patterns` + +::: edsnlp.pipelines.qualifiers.history.patterns diff --git a/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/factory.md b/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/factory.md new file mode 100644 index 000000000..f51bcf705 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.hypothesis.factory` + +::: edsnlp.pipelines.qualifiers.hypothesis.factory diff --git a/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/hypothesis.md b/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/hypothesis.md new file mode 100644 index 000000000..aab79687a --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/hypothesis.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.hypothesis.hypothesis` + +::: edsnlp.pipelines.qualifiers.hypothesis.hypothesis diff --git a/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/index.md b/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/index.md new file mode 100644 index 000000000..0c9b50508 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.hypothesis` + +::: edsnlp.pipelines.qualifiers.hypothesis diff --git a/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/patterns.md b/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/patterns.md new file mode 100644 index 000000000..41ac3e0d0 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/hypothesis/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.hypothesis.patterns` + +::: edsnlp.pipelines.qualifiers.hypothesis.patterns diff --git a/edsnlp/docs/reference/pipelines/qualifiers/index.md b/edsnlp/docs/reference/pipelines/qualifiers/index.md new file mode 100644 index 000000000..4c3cf698a --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers` + +::: edsnlp.pipelines.qualifiers diff --git a/edsnlp/docs/reference/pipelines/qualifiers/negation/factory.md b/edsnlp/docs/reference/pipelines/qualifiers/negation/factory.md new file mode 100644 index 000000000..867b4462b --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/negation/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.negation.factory` + +::: edsnlp.pipelines.qualifiers.negation.factory diff --git a/edsnlp/docs/reference/pipelines/qualifiers/negation/index.md b/edsnlp/docs/reference/pipelines/qualifiers/negation/index.md new file mode 100644 index 000000000..4e20cb2e3 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/negation/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.negation` + +::: edsnlp.pipelines.qualifiers.negation diff --git a/edsnlp/docs/reference/pipelines/qualifiers/negation/negation.md b/edsnlp/docs/reference/pipelines/qualifiers/negation/negation.md new file mode 100644 index 000000000..d16e56171 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/negation/negation.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.negation.negation` + +::: edsnlp.pipelines.qualifiers.negation.negation diff --git a/edsnlp/docs/reference/pipelines/qualifiers/negation/patterns.md b/edsnlp/docs/reference/pipelines/qualifiers/negation/patterns.md new file mode 100644 index 000000000..fbc504d97 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/negation/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.negation.patterns` + +::: edsnlp.pipelines.qualifiers.negation.patterns diff --git a/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/factory.md b/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/factory.md new file mode 100644 index 000000000..07a124716 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/factory.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.reported_speech.factory` + +::: edsnlp.pipelines.qualifiers.reported_speech.factory diff --git a/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/index.md b/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/index.md new file mode 100644 index 000000000..9e5200189 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.reported_speech` + +::: edsnlp.pipelines.qualifiers.reported_speech diff --git a/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/patterns.md b/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/patterns.md new file mode 100644 index 000000000..cd50eecd6 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/patterns.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.reported_speech.patterns` + +::: edsnlp.pipelines.qualifiers.reported_speech.patterns diff --git a/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/reported_speech.md b/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/reported_speech.md new file mode 100644 index 000000000..e75ae1495 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/qualifiers/reported_speech/reported_speech.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.qualifiers.reported_speech.reported_speech` + +::: edsnlp.pipelines.qualifiers.reported_speech.reported_speech diff --git a/edsnlp/docs/reference/pipelines/terminations.md b/edsnlp/docs/reference/pipelines/terminations.md new file mode 100644 index 000000000..1cb07f9e6 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/terminations.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.terminations` + +::: edsnlp.pipelines.terminations diff --git a/edsnlp/docs/reference/pipelines/trainable/index.md b/edsnlp/docs/reference/pipelines/trainable/index.md new file mode 100644 index 000000000..dce41bd34 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/trainable/index.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.trainable` + +::: edsnlp.pipelines.trainable diff --git a/edsnlp/docs/reference/pipelines/trainable/nested_ner.md b/edsnlp/docs/reference/pipelines/trainable/nested_ner.md new file mode 100644 index 000000000..79a3349d8 --- /dev/null +++ b/edsnlp/docs/reference/pipelines/trainable/nested_ner.md @@ -0,0 +1,3 @@ +# `edsnlp.pipelines.trainable.nested_ner` + +::: edsnlp.pipelines.trainable.nested_ner diff --git a/edsnlp/docs/reference/processing/distributed.md b/edsnlp/docs/reference/processing/distributed.md new file mode 100644 index 000000000..b209fbe98 --- /dev/null +++ b/edsnlp/docs/reference/processing/distributed.md @@ -0,0 +1,3 @@ +# `edsnlp.processing.distributed` + +::: edsnlp.processing.distributed diff --git a/edsnlp/docs/reference/processing/helpers.md b/edsnlp/docs/reference/processing/helpers.md new file mode 100644 index 000000000..48657b2bd --- /dev/null +++ b/edsnlp/docs/reference/processing/helpers.md @@ -0,0 +1,3 @@ +# `edsnlp.processing.helpers` + +::: edsnlp.processing.helpers diff --git a/edsnlp/docs/reference/processing/index.md b/edsnlp/docs/reference/processing/index.md new file mode 100644 index 000000000..626e52c2c --- /dev/null +++ b/edsnlp/docs/reference/processing/index.md @@ -0,0 +1,3 @@ +# `edsnlp.processing` + +::: edsnlp.processing diff --git a/edsnlp/docs/reference/processing/parallel.md b/edsnlp/docs/reference/processing/parallel.md new file mode 100644 index 000000000..52c0349a4 --- /dev/null +++ b/edsnlp/docs/reference/processing/parallel.md @@ -0,0 +1,3 @@ +# `edsnlp.processing.parallel` + +::: edsnlp.processing.parallel diff --git a/edsnlp/docs/reference/processing/simple.md b/edsnlp/docs/reference/processing/simple.md new file mode 100644 index 000000000..cbc825c01 --- /dev/null +++ b/edsnlp/docs/reference/processing/simple.md @@ -0,0 +1,3 @@ +# `edsnlp.processing.simple` + +::: edsnlp.processing.simple diff --git a/edsnlp/docs/reference/processing/utils.md b/edsnlp/docs/reference/processing/utils.md new file mode 100644 index 000000000..ad415738c --- /dev/null +++ b/edsnlp/docs/reference/processing/utils.md @@ -0,0 +1,3 @@ +# `edsnlp.processing.utils` + +::: edsnlp.processing.utils diff --git a/edsnlp/docs/reference/processing/wrapper.md b/edsnlp/docs/reference/processing/wrapper.md new file mode 100644 index 000000000..caefa8d2d --- /dev/null +++ b/edsnlp/docs/reference/processing/wrapper.md @@ -0,0 +1,3 @@ +# `edsnlp.processing.wrapper` + +::: edsnlp.processing.wrapper diff --git a/edsnlp/docs/reference/utils/blocs.md b/edsnlp/docs/reference/utils/blocs.md new file mode 100644 index 000000000..559d9e814 --- /dev/null +++ b/edsnlp/docs/reference/utils/blocs.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.blocs` + +::: edsnlp.utils.blocs diff --git a/edsnlp/docs/reference/utils/colors.md b/edsnlp/docs/reference/utils/colors.md new file mode 100644 index 000000000..822c8fce0 --- /dev/null +++ b/edsnlp/docs/reference/utils/colors.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.colors` + +::: edsnlp.utils.colors diff --git a/edsnlp/docs/reference/utils/deprecation.md b/edsnlp/docs/reference/utils/deprecation.md new file mode 100644 index 000000000..116dc9d5c --- /dev/null +++ b/edsnlp/docs/reference/utils/deprecation.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.deprecation` + +::: edsnlp.utils.deprecation diff --git a/edsnlp/docs/reference/utils/examples.md b/edsnlp/docs/reference/utils/examples.md new file mode 100644 index 000000000..a07b55697 --- /dev/null +++ b/edsnlp/docs/reference/utils/examples.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.examples` + +::: edsnlp.utils.examples diff --git a/edsnlp/docs/reference/utils/extensions.md b/edsnlp/docs/reference/utils/extensions.md new file mode 100644 index 000000000..d0575eebd --- /dev/null +++ b/edsnlp/docs/reference/utils/extensions.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.extensions` + +::: edsnlp.utils.extensions diff --git a/edsnlp/docs/reference/utils/filter.md b/edsnlp/docs/reference/utils/filter.md new file mode 100644 index 000000000..8e6ce836f --- /dev/null +++ b/edsnlp/docs/reference/utils/filter.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.filter` + +::: edsnlp.utils.filter diff --git a/edsnlp/docs/reference/utils/inclusion.md b/edsnlp/docs/reference/utils/inclusion.md new file mode 100644 index 000000000..81bd9f33c --- /dev/null +++ b/edsnlp/docs/reference/utils/inclusion.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.inclusion` + +::: edsnlp.utils.inclusion diff --git a/edsnlp/docs/reference/utils/index.md b/edsnlp/docs/reference/utils/index.md new file mode 100644 index 000000000..042f9f2cc --- /dev/null +++ b/edsnlp/docs/reference/utils/index.md @@ -0,0 +1,3 @@ +# `edsnlp.utils` + +::: edsnlp.utils diff --git a/edsnlp/docs/reference/utils/lists.md b/edsnlp/docs/reference/utils/lists.md new file mode 100644 index 000000000..a323c30eb --- /dev/null +++ b/edsnlp/docs/reference/utils/lists.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.lists` + +::: edsnlp.utils.lists diff --git a/edsnlp/docs/reference/utils/merge_configs.md b/edsnlp/docs/reference/utils/merge_configs.md new file mode 100644 index 000000000..930f356b4 --- /dev/null +++ b/edsnlp/docs/reference/utils/merge_configs.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.merge_configs` + +::: edsnlp.utils.merge_configs diff --git a/edsnlp/docs/reference/utils/regex.md b/edsnlp/docs/reference/utils/regex.md new file mode 100644 index 000000000..1eb13aca0 --- /dev/null +++ b/edsnlp/docs/reference/utils/regex.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.regex` + +::: edsnlp.utils.regex diff --git a/edsnlp/docs/reference/utils/resources.md b/edsnlp/docs/reference/utils/resources.md new file mode 100644 index 000000000..5f5b4873b --- /dev/null +++ b/edsnlp/docs/reference/utils/resources.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.resources` + +::: edsnlp.utils.resources diff --git a/edsnlp/docs/reference/utils/training.md b/edsnlp/docs/reference/utils/training.md new file mode 100644 index 000000000..c3df27135 --- /dev/null +++ b/edsnlp/docs/reference/utils/training.md @@ -0,0 +1,3 @@ +# `edsnlp.utils.training` + +::: edsnlp.utils.training diff --git a/edsnlp/docs/reference/viz/index.md b/edsnlp/docs/reference/viz/index.md new file mode 100644 index 000000000..6355f6f36 --- /dev/null +++ b/edsnlp/docs/reference/viz/index.md @@ -0,0 +1,3 @@ +# `edsnlp.viz` + +::: edsnlp.viz diff --git a/edsnlp/docs/reference/viz/quick_examples.md b/edsnlp/docs/reference/viz/quick_examples.md new file mode 100644 index 000000000..88311e26a --- /dev/null +++ b/edsnlp/docs/reference/viz/quick_examples.md @@ -0,0 +1,3 @@ +# `edsnlp.viz.quick_examples` + +::: edsnlp.viz.quick_examples diff --git a/docs/references.bib b/edsnlp/docs/references.bib similarity index 100% rename from docs/references.bib rename to edsnlp/docs/references.bib diff --git a/docs/resources/sections.svg b/edsnlp/docs/resources/sections.svg similarity index 100% rename from docs/resources/sections.svg rename to edsnlp/docs/resources/sections.svg diff --git a/docs/scripts/plugin.py b/edsnlp/docs/scripts/plugin.py similarity index 100% rename from docs/scripts/plugin.py rename to edsnlp/docs/scripts/plugin.py diff --git a/docs/tokenizers.md b/edsnlp/docs/tokenizers.md similarity index 100% rename from docs/tokenizers.md rename to edsnlp/docs/tokenizers.md diff --git a/docs/tutorials/detecting-dates.md b/edsnlp/docs/tutorials/detecting-dates.md similarity index 100% rename from docs/tutorials/detecting-dates.md rename to edsnlp/docs/tutorials/detecting-dates.md diff --git a/docs/tutorials/endlines.md b/edsnlp/docs/tutorials/endlines.md similarity index 100% rename from docs/tutorials/endlines.md rename to edsnlp/docs/tutorials/endlines.md diff --git a/docs/tutorials/index.md b/edsnlp/docs/tutorials/index.md similarity index 100% rename from docs/tutorials/index.md rename to edsnlp/docs/tutorials/index.md diff --git a/docs/tutorials/matching-a-terminology.md b/edsnlp/docs/tutorials/matching-a-terminology.md similarity index 100% rename from docs/tutorials/matching-a-terminology.md rename to edsnlp/docs/tutorials/matching-a-terminology.md diff --git a/docs/tutorials/multiple-texts.md b/edsnlp/docs/tutorials/multiple-texts.md similarity index 100% rename from docs/tutorials/multiple-texts.md rename to edsnlp/docs/tutorials/multiple-texts.md diff --git a/docs/tutorials/qualifying-entities.md b/edsnlp/docs/tutorials/qualifying-entities.md similarity index 100% rename from docs/tutorials/qualifying-entities.md rename to edsnlp/docs/tutorials/qualifying-entities.md diff --git a/docs/tutorials/quick-examples.md b/edsnlp/docs/tutorials/quick-examples.md similarity index 100% rename from docs/tutorials/quick-examples.md rename to edsnlp/docs/tutorials/quick-examples.md diff --git a/docs/tutorials/reason.md b/edsnlp/docs/tutorials/reason.md similarity index 100% rename from docs/tutorials/reason.md rename to edsnlp/docs/tutorials/reason.md diff --git a/docs/tutorials/spacy101.md b/edsnlp/docs/tutorials/spacy101.md similarity index 100% rename from docs/tutorials/spacy101.md rename to edsnlp/docs/tutorials/spacy101.md diff --git a/docs/utilities/connectors/brat.md b/edsnlp/docs/utilities/connectors/brat.md similarity index 100% rename from docs/utilities/connectors/brat.md rename to edsnlp/docs/utilities/connectors/brat.md diff --git a/docs/utilities/connectors/index.md b/edsnlp/docs/utilities/connectors/index.md similarity index 100% rename from docs/utilities/connectors/index.md rename to edsnlp/docs/utilities/connectors/index.md diff --git a/docs/utilities/connectors/labeltool.md b/edsnlp/docs/utilities/connectors/labeltool.md similarity index 100% rename from docs/utilities/connectors/labeltool.md rename to edsnlp/docs/utilities/connectors/labeltool.md diff --git a/docs/utilities/connectors/omop.md b/edsnlp/docs/utilities/connectors/omop.md similarity index 100% rename from docs/utilities/connectors/omop.md rename to edsnlp/docs/utilities/connectors/omop.md diff --git a/docs/utilities/evaluation.md b/edsnlp/docs/utilities/evaluation.md similarity index 100% rename from docs/utilities/evaluation.md rename to edsnlp/docs/utilities/evaluation.md diff --git a/docs/utilities/index.md b/edsnlp/docs/utilities/index.md similarity index 100% rename from docs/utilities/index.md rename to edsnlp/docs/utilities/index.md diff --git a/docs/utilities/matchers.md b/edsnlp/docs/utilities/matchers.md similarity index 100% rename from docs/utilities/matchers.md rename to edsnlp/docs/utilities/matchers.md diff --git a/docs/utilities/processing/index.md b/edsnlp/docs/utilities/processing/index.md similarity index 100% rename from docs/utilities/processing/index.md rename to edsnlp/docs/utilities/processing/index.md diff --git a/docs/utilities/processing/multi.md b/edsnlp/docs/utilities/processing/multi.md similarity index 100% rename from docs/utilities/processing/multi.md rename to edsnlp/docs/utilities/processing/multi.md diff --git a/docs/utilities/processing/single.md b/edsnlp/docs/utilities/processing/single.md similarity index 100% rename from docs/utilities/processing/single.md rename to edsnlp/docs/utilities/processing/single.md diff --git a/docs/utilities/processing/spark.md b/edsnlp/docs/utilities/processing/spark.md similarity index 100% rename from docs/utilities/processing/spark.md rename to edsnlp/docs/utilities/processing/spark.md diff --git a/docs/utilities/regex.md b/edsnlp/docs/utilities/regex.md similarity index 100% rename from docs/utilities/regex.md rename to edsnlp/docs/utilities/regex.md diff --git a/docs/utilities/tests/blocs.md b/edsnlp/docs/utilities/tests/blocs.md similarity index 100% rename from docs/utilities/tests/blocs.md rename to edsnlp/docs/utilities/tests/blocs.md diff --git a/docs/utilities/tests/examples.md b/edsnlp/docs/utilities/tests/examples.md similarity index 100% rename from docs/utilities/tests/examples.md rename to edsnlp/docs/utilities/tests/examples.md diff --git a/docs/utilities/tests/index.md b/edsnlp/docs/utilities/tests/index.md similarity index 100% rename from docs/utilities/tests/index.md rename to edsnlp/docs/utilities/tests/index.md diff --git a/edsnlp/edsnlp/__init__.py b/edsnlp/edsnlp/__init__.py new file mode 100644 index 000000000..98edc125a --- /dev/null +++ b/edsnlp/edsnlp/__init__.py @@ -0,0 +1,23 @@ +""" +EDS-NLP +""" + +import spacy +from . import patch_spacy_dot_components # isort: skip +from pathlib import Path + +from .evaluate import evaluate +from . import extensions +from .language import * + +__version__ = "0.8.0" + +BASE_DIR = Path(__file__).parent + + +for ext in ["Allergie","Action","Certainty","Temporality","Negation","Family"]: + if not spacy.tokens.Span.has_extension(ext): + spacy.tokens.Span.set_extension(ext, default=None) + +print("Monkey patching spacy.Language.evaluate") +spacy.Language.evaluate = evaluate diff --git a/edsnlp/components.py b/edsnlp/edsnlp/components.py similarity index 100% rename from edsnlp/components.py rename to edsnlp/edsnlp/components.py diff --git a/edsnlp/conjugator.py b/edsnlp/edsnlp/conjugator.py similarity index 100% rename from edsnlp/conjugator.py rename to edsnlp/edsnlp/conjugator.py diff --git a/edsnlp/connectors/__init__.py b/edsnlp/edsnlp/connectors/__init__.py similarity index 100% rename from edsnlp/connectors/__init__.py rename to edsnlp/edsnlp/connectors/__init__.py diff --git a/edsnlp/connectors/brat.py b/edsnlp/edsnlp/connectors/brat.py similarity index 95% rename from edsnlp/connectors/brat.py rename to edsnlp/edsnlp/connectors/brat.py index b69ac37ff..bf0218552 100644 --- a/edsnlp/connectors/brat.py +++ b/edsnlp/edsnlp/connectors/brat.py @@ -226,18 +226,17 @@ def export_to_brat(doc, txt_filename, overwrite_txt=False, overwrite_ann=False): ): idx = fragment["begin"] entity_text = doc["text"][fragment["begin"] : fragment["end"]] - for part in entity_text.split("\n"): - begin = idx - end = idx + len(part) - idx = end + 1 - if begin != end: - spans.append((begin, end)) + # eg: "mon entité \n est problématique" + for match in re.finditer( + r"\s*(.+?)(?:( *\n+)+ *|$)", entity_text, flags=re.DOTALL + ): + spans.append((idx + match.start(1), idx + match.end(1))) print( "{}\t{} {}\t{}".format( brat_entity_id, str(entity["label"]), ";".join(" ".join(map(str, span)) for span in spans), - entity_text.replace("\n", " "), + " ".join(doc["text"][begin:end] for begin, end in spans), ), file=f, ) @@ -313,20 +312,16 @@ def load_brat(self) -> List[Dict]: """ Transforms a BRAT folder to a list of spaCy documents. - Parameters - ---------- - nlp: - A spaCy pipeline. - Returns ------- - docs: + List[Dict] List of spaCy documents, with annotations in the `ents` attribute. """ filenames = [ - path.relative_to(self.directory) for path in self.directory.rglob("*.txt") + path.relative_to(self.directory) + for path in self.directory.rglob("*.txt") + if "checkpoint" not in path.stem ] - assert len(filenames), f"BRAT directory {self.directory} is empty!" logger.info( @@ -371,7 +366,6 @@ def brat2docs(self, nlp: Language, run_pipe=False) -> List[Doc]: """ annotations = self.load_brat() - texts = [doc["text"] for doc in annotations] docs = [] @@ -390,7 +384,6 @@ def brat2docs(self, nlp: Language, run_pipe=False) -> List[Doc]: ): doc._.note_id = doc_annotations["note_id"] - spans = [] span_groups = defaultdict(lambda: []) @@ -400,6 +393,7 @@ def brat2docs(self, nlp: Language, run_pipe=False) -> List[Doc]: Span.set_extension(dst, default=None) encountered_attributes = set() + encountered_span_groups = set() for ent in doc_annotations["entities"]: if self.attr_map is None: for a in ent["attributes"]: @@ -423,22 +417,19 @@ def brat2docs(self, nlp: Language, run_pipe=False) -> List[Doc]: ) span._.set(new_name, a["value"] if a is not None else True) spans.append(span) - - if self.span_groups is None or ent["label"] in self.span_groups: - span_groups[ent["label"]].append(span) - - if self.attr_map is None: - self.attr_map = {k: k for k in encountered_attributes} - - if self.span_groups is None: - self.span_groups = sorted(span_groups.keys()) + span_groups[ent["label"]].append(span) + encountered_span_groups.add(ent["label"]) doc.ents = filter_spans(spans) for group_name, group in span_groups.items(): doc.spans[group_name] = group docs.append(doc) - + + if self.span_groups is None: + self.span_groups = sorted(list(encountered_span_groups)) + if self.attr_map is None: + self.attr_map = {k: k for k in encountered_attributes} return docs def doc2brat(self, doc: Doc) -> None: @@ -551,3 +542,4 @@ def get_brat(self) -> Tuple[pd.DataFrame, pd.DataFrame]: ) return texts, annotations + \ No newline at end of file diff --git a/edsnlp/connectors/labeltool.py b/edsnlp/edsnlp/connectors/labeltool.py similarity index 100% rename from edsnlp/connectors/labeltool.py rename to edsnlp/edsnlp/connectors/labeltool.py diff --git a/edsnlp/connectors/omop.py b/edsnlp/edsnlp/connectors/omop.py similarity index 100% rename from edsnlp/connectors/omop.py rename to edsnlp/edsnlp/connectors/omop.py diff --git a/edsnlp/edsnlp/corpus_reader.py b/edsnlp/edsnlp/corpus_reader.py new file mode 100644 index 000000000..74098e622 --- /dev/null +++ b/edsnlp/edsnlp/corpus_reader.py @@ -0,0 +1,175 @@ +import random +from pathlib import Path +from typing import Callable, Iterable, Iterator, Optional, Union + +import spacy +from spacy import Errors, Vocab +from spacy.language import Language +from spacy.tokens import Doc, DocBin, Span +from spacy.training import Corpus, Example, dont_augment +from spacy.training.corpus import FILE_TYPE, walk_corpus + +if not Doc.has_extension("context"): + Doc.set_extension("context", default=dict()) +if not Doc.has_extension("note_id"): + Doc.set_extension("note_id", default=None) +if not Doc.has_extension("note_datetime"): + Doc.set_extension("note_datetime", default=None) +if not Doc.has_extension("note_class_source_value"): + Doc.set_extension("note_class_source_value", default=None) +if not Doc.has_extension("split"): + Doc.set_extension("split", default=None) + + +@spacy.registry.readers("eds.Corpus.v1") +class Corpus(Corpus): + def __init__( + self, + path: Union[str, Path], + *, + limit: int = 0, + gold_preproc: bool = False, + max_length: int = 0, + augmenter: Optional[Callable] = None, + shuffle: bool = False, + filter_expr: Optional[str] = None, + seed: Optional[int] = None, + ) -> None: + if path is None: + raise ValueError(Errors.E913) + spacy.util.logger.debug(f"Loading corpus from path: {path}") + self.path = spacy.util.ensure_path(path) + self.gold_preproc = gold_preproc + self.max_length = max_length + self.limit = limit + self.augmenter = augmenter if augmenter is not None else dont_augment + self.shuffle = shuffle + self.filter_fn = eval(f"lambda doc: {filter_expr}") if filter_expr else None + if filter_expr is not None: + spacy.util.logger.info(f"Filtering corpus with expression: {filter_expr}") + self.seed = seed + + def __call__(self, nlp: "Language") -> Iterator[Example]: + """Yield examples from the data. + + A difference with the standard spacy.Corpus object is that we + - first shuffle the data + - then subset it + + nlp (Language): The current nlp object. + YIELDS (Example): The examples. + DOCS: https://spacy.io/api/corpus#call + """ + ref_docs = self.read_docbin(nlp.vocab, walk_corpus(self.path, FILE_TYPE)) + if self.shuffle: + ref_docs = list(ref_docs) # type: ignore + random.Random(self.seed).shuffle(ref_docs) # type: ignore + + if self.limit >= 1: + ref_docs = ref_docs[: self.limit] + + if self.gold_preproc: + examples = self.make_examples_gold_preproc(nlp, ref_docs) + else: + examples = self.make_examples(nlp, ref_docs) + for real_eg in examples: + if len(real_eg) and len(real_eg.reference.ents): + for augmented_eg in self.augmenter( + nlp, real_eg + ): # type: ignore[operator] + yield augmented_eg + + def subset_doc(self, doc, start, end): + new_doc = doc[start:end].as_doc(copy_user_data=True) + for name, group in doc.spans.items(): + new_doc.spans[name] = [ + Span( + new_doc, + max(0, span.start - start), + min(end, span.end) - start, + span.label, + ) + for span in group + if span.end > start and span.start < end + ] + + return new_doc + + def make_examples( + self, nlp: "Language", reference_docs: Iterable[Doc] + ) -> Iterator[Example]: + for reference in reference_docs: + if len(reference) == 0: + continue + elif self.max_length == 0 or len(reference) < self.max_length: + yield self._make_example(nlp, reference, False) + else: + start = 0 + end = 0 + for sent in ( + reference.sents + if reference.has_annotation("SENT_START") + else (reference[:],) + ): + if len(sent) == 0: + continue + # If the sentence adds too many tokens + if sent.end - start > self.max_length: + # But the current buffer too large + while end - start > self.max_length: + yield self._make_example( + nlp, + self.subset_doc( + reference, start, start + self.max_length + ), + False, + ) + start = start + self.max_length + yield self._make_example( + nlp, self.subset_doc(reference, start, end), False + ) + start = end + + # Otherwise, extend the current buffer + end = sent.end + + while end - start > self.max_length: + yield self._make_example( + nlp, + self.subset_doc(reference, start, start + self.max_length), + False, + ) + start = start + self.max_length + yield self._make_example( + nlp, self.subset_doc(reference, start, end), False + ) + + def _make_example( + self, nlp: "Language", reference: Doc, gold_preproc: bool + ) -> Example: + eg = super()._make_example(nlp, reference, gold_preproc) + eg.predicted._.note_id = eg.reference._.note_id + eg.predicted._.note_datetime = eg.reference._.note_datetime + eg.predicted._.note_class_source_value = eg.reference._.note_class_source_value + eg.predicted._.context = eg.reference._.context + eg.predicted._.split = eg.reference._.split + + eg.predicted.ents = eg.reference.ents + for name in eg.predicted.spans: + eg.predicted.spans[name] = eg.reference.spans[name] + + return eg + + def read_docbin( + self, vocab: Vocab, locs: Iterable[Union[str, Path]] + ) -> Iterator[Doc]: + """Yield training examples as example dicts""" + self.not_called_twice = False + for loc in locs: + loc = spacy.util.ensure_path(loc) + if loc.parts[-1].endswith(FILE_TYPE): # type: ignore[union-attr] + doc_bin = DocBin().from_disk(loc) + docs = doc_bin.get_docs(vocab) + for doc in docs: + if len(doc) and (self.filter_fn is None or self.filter_fn(doc)): + yield doc diff --git a/edsnlp/edsnlp/evaluate.py b/edsnlp/edsnlp/evaluate.py new file mode 100644 index 000000000..c980cc02c --- /dev/null +++ b/edsnlp/edsnlp/evaluate.py @@ -0,0 +1,431 @@ +from typing import Optional, Any, Dict, List, Iterable +import time +from spacy.language import _copy_examples +from spacy.tokens import Doc +from spacy.training import validate_examples, Example +from copy import deepcopy +from tqdm import tqdm +import numpy as np +from timeit import default_timer as timer + +def get_annotation(docs): + full_annot = [] + for doc in docs: + annotation = [doc._.note_id] + for label, ents in doc.spans.items(): + for ent in ents: + annotation.append([ent.text, label, ent.start_char, ent.end_char]) + full_annot.append(annotation) + return full_annot + + +def overlap(start_g, end_g, start_p, end_p, exact): + if exact == False: + if start_p <= start_g and end_p >= end_g: + return 1 + else: + return 0 + + if exact == True: + if start_g == start_p and end_g == end_p: + return 1 + else: + return 0 +def compute_scores( + ents_gold, ents_pred, boostrap_level="entity", exact=True, n_draw=500, alpha=0.05, digits=2 +): + docs = [doc[0] for doc in ents_gold] + gold_labels = [ + [ent[1] for ent in doc[1:]] for doc in ents_gold + ] # get all the entities from the various documents of ents_gold + gold_labels = set( + [item for sublist in gold_labels for item in sublist] + ) # flatten and transform it to a set to get unique values + pred_labels = [ + [ent[1] for ent in doc[1:]] for doc in ents_pred + ] # get all the entities from the various documents of ents_gold + pred_labels = set( + [item for sublist in pred_labels for item in sublist] + ) # flatten and transform it to a set to get unique values + results = { # we create a dic with the labels of the dataset (CHEM, BIO...) + label: {} for label in pred_labels.union(gold_labels) + } + # COMPUTATION OF TRUE POSITIVE / FALSE POSITIVE / FALSE NEGATIVE + for label in results.keys(): + results[label]["TP"] = 0 + results[label]["FP"] = 0 + results[label]["FN"] = 0 + results_by_doc = {doc : deepcopy(results) for doc in docs} + + for i in range(len(ents_gold)): # iterate through doc + # list of doc, inside each of them is a quadrupet ['text','label','start_char','stop_char'] + doc_id = ents_gold[i][0] + ents_gold_doc = [(ent[1], ent[2], ent[3]) for ent in ents_gold[i][1:]] + ents_pred_doc = [(ent[1], ent[2], ent[3]) for ent in ents_pred[i][1:]] + + for ent in ents_gold_doc: + label_g = ent[0] + start_g = ent[1] + stop_g = ent[2] + r = False + + for ent in ents_pred_doc: + label_p = ent[0] + start_p = ent[1] + stop_p = ent[2] + + # exact is given as parameter because the overlap function take into account if we want an exact match or an inclusive match + if ( + label_g == label_p + and overlap(start_g, stop_g, start_p, stop_p, exact) > 0 + ): + r = True + + if r: + results_by_doc[doc_id][label_g]["TP"] += 1 + results[label_g]["TP"] += 1 + else: + results_by_doc[doc_id][label_g]["FN"] += 1 + results[label_g]["FN"] += 1 + + for ent in ents_pred_doc: + label_p = ent[0] + start_p = ent[1] + stop_p = ent[2] + r = True + + for ent in ents_gold_doc: + label_g = ent[0] + start_g = ent[1] + stop_g = ent[2] + + if ( + label_g == label_p + and overlap(start_g, stop_g, start_p, stop_p, exact) > 0 + ): + r = False + if r == True: + results_by_doc[doc_id][label_p]["FP"] += 1 + results[label_p]["FP"] += 1 + + if exact == True: + print("Exact match") + else: + print("Inclusive match") + + # we will use this copy of the results dataframe + results_list = deepcopy(results) + + # We transform the result dictionnary value from int to list to be able to append the new ones + for key, value in results_list.items(): + for k, v in value.items(): + results_list[key][k] = [v] + + total_words = 0 + for entity in results_list.keys(): + total_words += results[entity]["TP"] + total_words += results[entity]["FN"] + total_words += results[entity]["FP"] + label_to_draw = [] + proba = [] + for entity in results_list.keys(): + for test in ["TP", "FN", "FP"]: + label_to_draw.append(entity + "-" + test) + proba.append(results[entity][test] / total_words) + + micro_avg = { + "TP": [sum(results[entity]["TP"] for entity in results.keys())], + "FN": [sum(results[entity]["FN"] for entity in results.keys())], + "FP": [sum(results[entity]["FP"] for entity in results.keys())], + } + + # Bootstrap per doc + if boostrap_level == 'doc': + for i in tqdm(range(1, n_draw)): + draw = np.random.choice( + docs, + size=len(docs), + replace=True, + ) + micro_avg_draw = {"TP":0, "FN": 0, "FP": 0} + results_draw = { # we create a dic with the labels of the dataset (CHEM, BIO...) + label: {} for label in pred_labels.union(gold_labels) + } + for label in results_draw.keys(): + results_draw[label]["Precision"] = 0 + results_draw[label]["TP"] = 0 + results_draw[label]["FP"] = 0 + results_draw[label]["FN"] = 0 + for doc in draw: + for label in results_by_doc[doc].keys(): + micro_avg_draw["TP"]+=results_by_doc[doc][label]["TP"] + results_draw[label]["TP"]+=results_by_doc[doc][label]["TP"] + micro_avg_draw["FN"]+=results_by_doc[doc][label]["FN"] + results_draw[label]["FN"]+=results_by_doc[doc][label]["FN"] + micro_avg_draw["FP"]+=results_by_doc[doc][label]["FP"] + results_draw[label]["FP"]+=results_by_doc[doc][label]["FP"] + for entity in results_list.keys(): + results_list[entity]["TP"].append( + results_draw[entity]["TP"] + ) + results_list[entity]["FN"].append( + results_draw[entity]["FN"] + ) + results_list[entity]["FP"].append( + results_draw[entity]["FP"] + ) + micro_avg["TP"].append(micro_avg_draw["TP"]) + micro_avg["FN"].append(micro_avg_draw["FN"]) + micro_avg["FP"].append(micro_avg_draw["FP"]) + + # Bootstrap per entities + if boostrap_level == 'entity': + for i in tqdm(range(1, n_draw)): + draw = np.random.choice( + label_to_draw, + size=total_words, + p=proba, + replace=True, + ) + draw = np.stack( + np.char.split(draw, "-"), + axis=0, + ) + micro_avg["TP"].append(len(draw[(draw[:, 1] == "TP")])) + micro_avg["FN"].append(len(draw[(draw[:, 1] == "FN")])) + micro_avg["FP"].append(len(draw[(draw[:, 1] == "FP")])) + for entity in results_list.keys(): + results_list[entity]["TP"].append( + len(draw[(draw[:, 0] == entity) & (draw[:, 1] == "TP")]) + ) + results_list[entity]["FN"].append( + len(draw[(draw[:, 0] == entity) & (draw[:, 1] == "FN")]) + ) + results_list[entity]["FP"].append( + len(draw[(draw[:, 0] == entity) & (draw[:, 1] == "FP")]) + ) + + results_list["Overall"] = micro_avg + for entity in results_list.keys(): + results_list[entity]["N_entity"] = [] + results_list[entity]["Precision"] = [] + results_list[entity]["Recall"] = [] + results_list[entity]["F1"] = [] + for i in range(n_draw): + results_list[entity]["N_entity"].append(results_list[entity]["TP"][i] + results_list[entity]["FP"][i] + results_list[entity]["FN"][i]) + if results_list[entity]["TP"][i] + results_list[entity]["FP"][i] != 0: + results_list[entity]["Precision"].append( + results_list[entity]["TP"][i] + / ( + results_list[entity]["TP"][i] + + results_list[entity]["FP"][i] + ) * 100 + ) + else: + results_list[entity]["Precision"].append(int(results_list[entity]["TP"][i] == 0) * 100) + if (results_list[entity]["TP"][i] + results_list[entity]["FN"][i]) != 0: + results_list[entity]["Recall"].append( + results_list[entity]["TP"][i] + / ( + results_list[entity]["TP"][i] + + results_list[entity]["FN"][i] + ) * 100 + ) + else: + results_list[entity]["Recall"].append(int(results_list[entity]["TP"][i] == 0) * 100) + if ( + results_list[entity]["Precision"][i] + + results_list[entity]["Recall"][i] + ) != 0: + results_list[entity]["F1"].append( + 2 + * ( + results_list[entity]["Precision"][i] + * results_list[entity]["Recall"][i] + ) + / ( + results_list[entity]["Precision"][i] + + results_list[entity]["Recall"][i] + ) + ) + else: + results_list[entity]["F1"].append(0) + # we aim at displaying the "true" observe value with confidence interval corresponding to the top 5 and 95% of the bootstrapped data + lower_confidence_interval = { + label: { + k: round(np.quantile(v, alpha / 2), digits) + for k, v in results_list[label].items() + if k in ["Precision", "Recall", "F1", "N_entity"] + } + for label in results_list.keys() + } + upper_confidence_interval = { + label: { + k: round(np.quantile(v, (1 - alpha / 2)), digits) + for k, v in results_list[label].items() + if k in ["Precision", "Recall", "F1", "N_entity"] + } + for label in results_list.keys() + } + + # we create a dict result_panel with the same keys as results_list but with the values of the nested dict being empty + result_panel = { + label: { + k: "" + for k, v in results_list[label].items() + if k in ["Precision", "Recall", "F1", "N_entity"] + } + for label in results_list.keys() + } + + # we take the value to build the result panel and the confidence interval + # we take value['Precision'][0] because it is the original draw + for key, value in results_list.items(): + precision = value["Precision"][0] + precision_up = upper_confidence_interval[key]["Precision"] + precision_down = lower_confidence_interval[key]["Precision"] + recall = value["Recall"][0] + recall_up = upper_confidence_interval[key]["Recall"] + recall_down = lower_confidence_interval[key]["Recall"] + f1 = value["F1"][0] + f1_up = upper_confidence_interval[key]["F1"] + f1_down = lower_confidence_interval[key]["F1"] + n_entity = value["N_entity"][0] + n_entity_up = upper_confidence_interval[key]["N_entity"] + n_entity_down = lower_confidence_interval[key]["N_entity"] + + result_panel[key]["Precision"] = ( + str(round(precision, digits)) + + " (" + + str(precision_down) + + "-" + + str(precision_up) + + ")" + ) + result_panel[key]["Recall"] = ( + str(round(recall, digits)) + + " (" + + str(recall_down) + + "-" + + str(recall_up) + + ")" + ) + result_panel[key]["F1"] = ( + str(round(f1, digits)) + " (" + str(f1_down) + "-" + str(f1_up) + ")" + ) + result_panel[key]["N_entity"] = ( + str(n_entity) + " (" + str(int(n_entity_up)) + "-" + str(int(n_entity_down)) + ")" + ) + print(f"With alpha = {alpha} and {n_draw} draws") + output = f"With alpha = {alpha} and {n_draw} draws\n" + for key, value in result_panel.items(): + if "SECTION" not in key: + output += f"\nLabel: {key}\n" + for metric, metric_value in value.items(): + output += f"{metric}: {metric_value}\n" + output += "-" * 30 + + # print(output) + result_panel["ents_per_type"] = {label: {"p": value["Precision"], "r": value["Recall"], "f": value["F1"], "n_entity": value["N_entity"]} for label, value in result_panel.items()} + return result_panel + +def evaluate_test( + gold_docs: List[Doc], + pred_docs: List[Doc], + boostrap_level: str = "entity", + exact: bool = True, + n_draw: int = 500, + alpha: float = 0.05, + digits: int = 2, +) -> Dict[str, Any]: + """ + Evaluate a model's pipeline components. + + Parameters + ---------- + gold_docs : List[Doc] + `Doc` objects. + pred_docs : List[Doc] + `Doc` objects. + + Returns + ------- + Dict[str, Any] + The evaluation results. + """ + ents_pred, ents_gold = get_annotation(pred_docs), get_annotation(gold_docs) + ents_pred.sort(key=lambda l: l[0]) + ents_gold.sort(key=lambda l: l[0]) + + scores = compute_scores( + ents_gold, + ents_pred, + boostrap_level=boostrap_level, + exact=exact, + n_draw=n_draw, + alpha=alpha, + digits=digits, + ) + + return scores + + +def evaluate( + self, + examples: Iterable[Example], + *, + batch_size: Optional[int] = None, + **kwargs: Any, +) -> Dict[str, Any]: + """ + Evaluate a model's pipeline components. + + Parameters + ---------- + examples : Iterable[Example] + `Example` objects. + batch_size : Optional[int] + Batch size to use. + + Returns + ------- + Dict[str, Any] + The evaluation results. + """ + examples = list(examples) + validate_examples(examples, "Language.evaluate") + examples = _copy_examples(examples) + if batch_size is None: + batch_size = self.batch_size + + scores = {} + + total_time = 0 + + begin_time = timer() + # this is purely for timing + for eg in examples: + self.make_doc(eg.reference.text) + total_time += timer() - begin_time + + n_words = sum(len(eg.predicted) for eg in examples) + + predictions = [eg.predicted for eg in examples] + + for name, component in self.pipeline: + begin_time = timer() + docs = [doc.copy() for doc in predictions] + docs = list(component.pipe(docs, batch_size=batch_size)) + total_time += timer() - begin_time + + if name == "tok2vec": + predictions = docs + if hasattr(component, "score"): + scores.update( + component.score( + [Example(doc, eg.reference) for doc, eg in zip(docs, examples)] + ) + ) + + scores["speed"] = n_words / total_time + return scores diff --git a/edsnlp/extensions.py b/edsnlp/edsnlp/extensions.py similarity index 100% rename from edsnlp/extensions.py rename to edsnlp/edsnlp/extensions.py diff --git a/edsnlp/language.py b/edsnlp/edsnlp/language.py similarity index 100% rename from edsnlp/language.py rename to edsnlp/edsnlp/language.py diff --git a/edsnlp/models/__init__.py b/edsnlp/edsnlp/matchers/__init__.py similarity index 100% rename from edsnlp/models/__init__.py rename to edsnlp/edsnlp/matchers/__init__.py diff --git a/edsnlp/matchers/phrase.pxd b/edsnlp/edsnlp/matchers/phrase.pxd similarity index 100% rename from edsnlp/matchers/phrase.pxd rename to edsnlp/edsnlp/matchers/phrase.pxd diff --git a/edsnlp/matchers/phrase.pyx b/edsnlp/edsnlp/matchers/phrase.pyx similarity index 100% rename from edsnlp/matchers/phrase.pyx rename to edsnlp/edsnlp/matchers/phrase.pyx diff --git a/edsnlp/matchers/regex.py b/edsnlp/edsnlp/matchers/regex.py similarity index 100% rename from edsnlp/matchers/regex.py rename to edsnlp/edsnlp/matchers/regex.py diff --git a/edsnlp/matchers/simstring.py b/edsnlp/edsnlp/matchers/simstring.py similarity index 100% rename from edsnlp/matchers/simstring.py rename to edsnlp/edsnlp/matchers/simstring.py diff --git a/edsnlp/matchers/utils/__init__.py b/edsnlp/edsnlp/matchers/utils/__init__.py similarity index 100% rename from edsnlp/matchers/utils/__init__.py rename to edsnlp/edsnlp/matchers/utils/__init__.py diff --git a/edsnlp/matchers/utils/offset.py b/edsnlp/edsnlp/matchers/utils/offset.py similarity index 100% rename from edsnlp/matchers/utils/offset.py rename to edsnlp/edsnlp/matchers/utils/offset.py diff --git a/edsnlp/matchers/utils/text.py b/edsnlp/edsnlp/matchers/utils/text.py similarity index 100% rename from edsnlp/matchers/utils/text.py rename to edsnlp/edsnlp/matchers/utils/text.py diff --git a/edsnlp/patch_spacy_dot_components.py b/edsnlp/edsnlp/patch_spacy_dot_components.py similarity index 100% rename from edsnlp/patch_spacy_dot_components.py rename to edsnlp/edsnlp/patch_spacy_dot_components.py diff --git a/edsnlp/models/torch/__init__.py b/edsnlp/edsnlp/pipelines/__init__.py similarity index 100% rename from edsnlp/models/torch/__init__.py rename to edsnlp/edsnlp/pipelines/__init__.py diff --git a/edsnlp/edsnlp/pipelines/base.py b/edsnlp/edsnlp/pipelines/base.py new file mode 100644 index 000000000..685406307 --- /dev/null +++ b/edsnlp/edsnlp/pipelines/base.py @@ -0,0 +1,318 @@ +from collections import defaultdict +from operator import attrgetter +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, + Union, +) + +from spacy import Language +from spacy.tokens import Doc, Span + +from edsnlp.utils.filter import filter_spans + + +class BaseComponent: + """ + The `BaseComponent` adds a `set_extensions` method, + called at the creation of the object. + + It helps decouple the initialisation of the pipeline from + the creation of extensions, and is particularly usefull when + distributing EDSNLP on a cluster, since the serialisation mechanism + imposes that the extensions be reset. + """ + + def __init__(self, nlp: Language = None, name: str = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.nlp = nlp + self.name = name + self.set_extensions() + + def set_extensions(self): + """ + Set `Doc`, `Span` and `Token` extensions. + """ + Span.set_extension( + "value", + getter=lambda span: span._.get(span.label_) + if span._.has(span.label_) + else None, + force=True, + ) + + def get_spans(self, doc: Doc): + """ + Returns sorted spans of interest according to the + possible value of `on_ents_only`. + Includes `doc.ents` by default, and adds eventual SpanGroups. + """ + ents = list(doc.ents) + list(doc.spans.get("discarded", [])) + + on_ents_only = getattr(self, "on_ents_only", None) + + if isinstance(on_ents_only, str): + on_ents_only = [on_ents_only] + if isinstance(on_ents_only, (set, list)): + for spankey in set(on_ents_only) & set(doc.spans.keys()): + ents.extend(doc.spans.get(spankey, [])) + + return sorted(list(set(ents)), key=(attrgetter("start", "end"))) + + def _boundaries( + self, doc: Doc, terminations: Optional[List[Span]] = None + ) -> List[Tuple[int, int]]: + """ + Create sub sentences based sentences and terminations found in text. + + Parameters + ---------- + doc: + spaCy Doc object + terminations: + List of tuples with (match_id, start, end) + + Returns + ------- + boundaries: + List of tuples with (start, end) of spans + """ + + if terminations is None: + terminations = [] + + sent_starts = [sent.start for sent in doc.sents] + termination_starts = [t.start for t in terminations] + + starts = sent_starts + termination_starts + [len(doc)] + + # Remove duplicates + starts = list(set(starts)) + + # Sort starts + starts.sort() + + boundaries = [(start, end) for start, end in zip(starts[:-1], starts[1:])] + + return boundaries + + +SeqStr = Union[str, Sequence[str]] +SpanFilter = Union[bool, SeqStr] + +SpanSetterMapping = Dict[str, SpanFilter] +SpanGetterMapping = Dict[str, SpanFilter] + +SpanGetter = Union[ + SpanGetterMapping, + Callable[[Doc], Iterable[Span]], +] +SpanSetter = Union[ + SpanSetterMapping, + Callable[[Doc, Iterable[Span]], Any], +] + + +def get_spans(doc, span_getter): + if callable(span_getter): + yield from span_getter(doc) + return + for key, span_filter in span_getter.items(): + candidates = doc.spans.get(key, ()) if key != "ents" else doc.ents + if span_filter is True: + yield from candidates + else: + for span in candidates: + if span.label_ in span_filter: + yield span + + +def validate_span_setter(value: Union[SeqStr, Dict[str, SpanFilter]]) -> SpanSetter: + if callable(value): + return value + if isinstance(value, str): + return {value: True} + if isinstance(value, list): + return {group: True for group in value} + elif isinstance(value, dict): + new_value = {} + for k, v in value.items(): + if isinstance(v, bool): + new_value[k] = v + elif isinstance(v, str): + new_value[k] = [v] + elif isinstance(v, list) and all(isinstance(i, str) for i in v): + new_value[k] = v + else: + raise TypeError( + f"Invalid entry {value} ({type(value)}) for SpanSetterArg, " + f"expected bool/string(s), dict of bool/string(s) or callable" + ) + return new_value + else: + raise TypeError( + f"Invalid entry {value} ({type(value)}) for SpanSetterArg, " + f"expected bool/string(s), dict of bool/string(s) or callable" + ) + + +def validate_span_getter( + value: Union[SeqStr, Dict[str, SpanFilter]], optional: bool = False +) -> SpanSetter: + if value is None: + if optional: + return None + raise ValueError( + "Mising entry for SpanGetterArg, " + "expected bool/string(s), dict of bool/string(s) or callable" + ) + if callable(value): + return value + if isinstance(value, str): + return {value: True} + if isinstance(value, list): + return {group: True for group in value} + elif isinstance(value, dict): + new_value = {} + for k, v in value.items(): + if isinstance(v, bool): + new_value[k] = v + elif isinstance(v, str): + new_value[k] = [v] + elif isinstance(v, list) and all(isinstance(i, str) for i in v): + new_value[k] = v + else: + raise TypeError( + f"Invalid entry {value} ({type(value)}) for SpanGetterArg, " + f"expected bool/string(s), dict of bool/string(s) or callable" + ) + return new_value + else: + raise TypeError( + f"Invalid entry {value} ({type(value)}) for SpanGetterArg, " + f"expected bool/string(s), dict of bool/string(s) or callable" + ) + + +class SpanSetterArg: + """ + Valid values for the `span_setter` argument of a component can be : + + - a (doc, matches) -> None callable + - a span group name + - a list of span group names + - a dict of group name to True or list of labels + + The group name `"ents"` is a special case, and will add the matches to `doc.ents` + + Examples + -------- + - `span_setter=["ents", "ckd"]` will add the matches to both `doc.ents` and + `doc.spans["ckd"]`. It is equivalent to `{"ents": True, "ckd": True}`. + - `span_setter={"ents": ["foo", "bar"]}` will add the matches with label + "foo" and "bar" to `doc.ents`. + - `span_setter="ents"` will add all matches only to `doc.ents`. + - `span_setter="ckd"` will add all matches only to `doc.spans["ckd"]`. + """ + + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, value: Union[SeqStr, Dict[str, SpanFilter]]) -> SpanSetter: + return validate_span_setter(value) + + +class SpanGetterArg: + """ + Valid values for the `span_getter` argument of a component can be : + + - a (doc) -> spans callable + - a span group name + - a list of span group names + - a dict of group name to True or list of labels + + The group name `"ents"` is a special case, and will get the matches from `doc.ents` + + Examples + -------- + - `span_getter=["ents", "ckd"]` will get the matches from both `doc.ents` and + `doc.spans["ckd"]`. It is equivalent to `{"ents": True, "ckd": True}`. + - `span_getter={"ents": ["foo", "bar"]}` will get the matches with label + "foo" and "bar" from `doc.ents`. + - `span_getter="ents"` will get all matches from `doc.ents`. + - `span_getter="ckd"` will get all matches from `doc.spans["ckd"]`. + """ + + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, value: Union[SeqStr, Dict[str, SpanFilter]]) -> SpanSetter: + return validate_span_setter(value) + + +class BaseNERComponent(BaseComponent): + def __init__( + self, + nlp: Language = None, + name: str = None, + *args, + span_setter: SpanSetterArg, + **kwargs, + ): + super().__init__(nlp, name, *args, **kwargs) + self.span_setter: SpanSetter = validate_span_setter(span_setter) # type: ignore + + def set_spans(self, doc, matches): + if callable(self.span_setter): + self.span_setter(doc, matches) + else: + + match_all = [] + label_to_group = defaultdict(list) + for name, spans_filter in self.span_setter.items(): + if name != "ents": + doc.spans.setdefault(name, []) + if spans_filter: + if spans_filter is True: + match_all.append(name) + else: + for label in spans_filter: + label_to_group[label].append(name) + + new_ents = [] if "ents" in self.span_setter else None + + for span in matches: + for group in match_all + label_to_group[span.label_]: + if group == "ents": + new_ents.append(span) + else: + doc.spans[group].append(span) + if new_ents is not None: + doc.ents = filter_spans((*new_ents, *doc.ents)) + return doc + + +if TYPE_CHECKING: + SpanGetterArg = Union[ # noqa: F811 + str, + Sequence[str], + SpanGetterMapping, + Callable[[Doc], Iterable[Span]], + ] + SpanSetterArg = Union[ # noqa: F811 + str, + Sequence[str], + SpanSetterMapping, + Callable[[Doc, Iterable[Span]], Any], + ] diff --git a/edsnlp/edsnlp/pipelines/clean_entities.py b/edsnlp/edsnlp/pipelines/clean_entities.py new file mode 100644 index 000000000..dba742694 --- /dev/null +++ b/edsnlp/edsnlp/pipelines/clean_entities.py @@ -0,0 +1,47 @@ +import re +import string +from typing import Callable, Optional + +from spacy.language import Language +from spacy.tokens import Doc + +DEFAULT_CONFIG = dict( + scorer={"@scorers": "eds.nested_ner_scorer.v1"}, +) + + +@Language.factory("clean-entities", default_config=DEFAULT_CONFIG) +class CleanEntities: + def __init__( + self, + nlp: Language, + name: str, + scorer: Optional[Callable], + ): + """ + Removes empty entities from the document and clean entity boundaries + """ + self.scorer = scorer + + def score(self, examples, **kwargs): + return self.scorer(examples, **kwargs) + + def __call__(self, doc: Doc) -> Doc: + new_ents = [] + for ent in doc.ents: + if len(ent.text.strip(string.punctuation)) == 0: + continue + m = re.match(r"^\s*(.*?)\s*$", ent.text, flags=re.DOTALL) + new_begin = m.start(1) + new_end = m.end(1) + new_ent = doc.char_span( + ent[0].idx + new_begin, + ent[0].idx + new_end, + label=ent.label_, + alignment_mode="expand", + ) + if new_ent is not None: + new_ents.append(new_ent) + + doc.ents = new_ents + return doc diff --git a/edsnlp/pipelines/__init__.py b/edsnlp/edsnlp/pipelines/core/__init__.py similarity index 100% rename from edsnlp/pipelines/__init__.py rename to edsnlp/edsnlp/pipelines/core/__init__.py diff --git a/edsnlp/pipelines/core/context/__init__.py b/edsnlp/edsnlp/pipelines/core/context/__init__.py similarity index 100% rename from edsnlp/pipelines/core/context/__init__.py rename to edsnlp/edsnlp/pipelines/core/context/__init__.py diff --git a/edsnlp/pipelines/core/context/context.py b/edsnlp/edsnlp/pipelines/core/context/context.py similarity index 100% rename from edsnlp/pipelines/core/context/context.py rename to edsnlp/edsnlp/pipelines/core/context/context.py diff --git a/edsnlp/pipelines/core/context/factory.py b/edsnlp/edsnlp/pipelines/core/context/factory.py similarity index 100% rename from edsnlp/pipelines/core/context/factory.py rename to edsnlp/edsnlp/pipelines/core/context/factory.py diff --git a/edsnlp/pipelines/core/contextual_matcher/__init__.py b/edsnlp/edsnlp/pipelines/core/contextual_matcher/__init__.py similarity index 100% rename from edsnlp/pipelines/core/contextual_matcher/__init__.py rename to edsnlp/edsnlp/pipelines/core/contextual_matcher/__init__.py diff --git a/edsnlp/pipelines/core/contextual_matcher/contextual_matcher.py b/edsnlp/edsnlp/pipelines/core/contextual_matcher/contextual_matcher.py similarity index 100% rename from edsnlp/pipelines/core/contextual_matcher/contextual_matcher.py rename to edsnlp/edsnlp/pipelines/core/contextual_matcher/contextual_matcher.py diff --git a/edsnlp/pipelines/core/contextual_matcher/factory.py b/edsnlp/edsnlp/pipelines/core/contextual_matcher/factory.py similarity index 100% rename from edsnlp/pipelines/core/contextual_matcher/factory.py rename to edsnlp/edsnlp/pipelines/core/contextual_matcher/factory.py diff --git a/edsnlp/pipelines/core/contextual_matcher/models.py b/edsnlp/edsnlp/pipelines/core/contextual_matcher/models.py similarity index 100% rename from edsnlp/pipelines/core/contextual_matcher/models.py rename to edsnlp/edsnlp/pipelines/core/contextual_matcher/models.py diff --git a/edsnlp/pipelines/core/endlines/__init__.py b/edsnlp/edsnlp/pipelines/core/endlines/__init__.py similarity index 100% rename from edsnlp/pipelines/core/endlines/__init__.py rename to edsnlp/edsnlp/pipelines/core/endlines/__init__.py diff --git a/edsnlp/pipelines/core/endlines/endlines.py b/edsnlp/edsnlp/pipelines/core/endlines/endlines.py similarity index 100% rename from edsnlp/pipelines/core/endlines/endlines.py rename to edsnlp/edsnlp/pipelines/core/endlines/endlines.py diff --git a/edsnlp/pipelines/core/endlines/endlinesmodel.py b/edsnlp/edsnlp/pipelines/core/endlines/endlinesmodel.py similarity index 100% rename from edsnlp/pipelines/core/endlines/endlinesmodel.py rename to edsnlp/edsnlp/pipelines/core/endlines/endlinesmodel.py diff --git a/edsnlp/pipelines/core/endlines/factory.py b/edsnlp/edsnlp/pipelines/core/endlines/factory.py similarity index 100% rename from edsnlp/pipelines/core/endlines/factory.py rename to edsnlp/edsnlp/pipelines/core/endlines/factory.py diff --git a/edsnlp/pipelines/core/endlines/functional.py b/edsnlp/edsnlp/pipelines/core/endlines/functional.py similarity index 100% rename from edsnlp/pipelines/core/endlines/functional.py rename to edsnlp/edsnlp/pipelines/core/endlines/functional.py diff --git a/edsnlp/pipelines/core/matcher/__init__.py b/edsnlp/edsnlp/pipelines/core/matcher/__init__.py similarity index 100% rename from edsnlp/pipelines/core/matcher/__init__.py rename to edsnlp/edsnlp/pipelines/core/matcher/__init__.py diff --git a/edsnlp/pipelines/core/matcher/factory.py b/edsnlp/edsnlp/pipelines/core/matcher/factory.py similarity index 100% rename from edsnlp/pipelines/core/matcher/factory.py rename to edsnlp/edsnlp/pipelines/core/matcher/factory.py diff --git a/edsnlp/pipelines/core/matcher/matcher.py b/edsnlp/edsnlp/pipelines/core/matcher/matcher.py similarity index 100% rename from edsnlp/pipelines/core/matcher/matcher.py rename to edsnlp/edsnlp/pipelines/core/matcher/matcher.py diff --git a/edsnlp/pipelines/core/normalizer/__init__.py b/edsnlp/edsnlp/pipelines/core/normalizer/__init__.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/__init__.py rename to edsnlp/edsnlp/pipelines/core/normalizer/__init__.py diff --git a/edsnlp/pipelines/core/normalizer/accents/__init__.py b/edsnlp/edsnlp/pipelines/core/normalizer/accents/__init__.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/accents/__init__.py rename to edsnlp/edsnlp/pipelines/core/normalizer/accents/__init__.py diff --git a/edsnlp/pipelines/core/normalizer/accents/accents.py b/edsnlp/edsnlp/pipelines/core/normalizer/accents/accents.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/accents/accents.py rename to edsnlp/edsnlp/pipelines/core/normalizer/accents/accents.py diff --git a/edsnlp/pipelines/core/normalizer/accents/factory.py b/edsnlp/edsnlp/pipelines/core/normalizer/accents/factory.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/accents/factory.py rename to edsnlp/edsnlp/pipelines/core/normalizer/accents/factory.py diff --git a/edsnlp/pipelines/core/normalizer/accents/patterns.py b/edsnlp/edsnlp/pipelines/core/normalizer/accents/patterns.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/accents/patterns.py rename to edsnlp/edsnlp/pipelines/core/normalizer/accents/patterns.py diff --git a/edsnlp/pipelines/core/normalizer/factory.py b/edsnlp/edsnlp/pipelines/core/normalizer/factory.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/factory.py rename to edsnlp/edsnlp/pipelines/core/normalizer/factory.py diff --git a/edsnlp/pipelines/core/normalizer/lowercase/__init__.py b/edsnlp/edsnlp/pipelines/core/normalizer/lowercase/__init__.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/lowercase/__init__.py rename to edsnlp/edsnlp/pipelines/core/normalizer/lowercase/__init__.py diff --git a/edsnlp/pipelines/core/normalizer/lowercase/factory.py b/edsnlp/edsnlp/pipelines/core/normalizer/lowercase/factory.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/lowercase/factory.py rename to edsnlp/edsnlp/pipelines/core/normalizer/lowercase/factory.py diff --git a/edsnlp/pipelines/core/normalizer/normalizer.py b/edsnlp/edsnlp/pipelines/core/normalizer/normalizer.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/normalizer.py rename to edsnlp/edsnlp/pipelines/core/normalizer/normalizer.py diff --git a/edsnlp/pipelines/core/normalizer/pollution/__init__.py b/edsnlp/edsnlp/pipelines/core/normalizer/pollution/__init__.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/pollution/__init__.py rename to edsnlp/edsnlp/pipelines/core/normalizer/pollution/__init__.py diff --git a/edsnlp/pipelines/core/normalizer/pollution/factory.py b/edsnlp/edsnlp/pipelines/core/normalizer/pollution/factory.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/pollution/factory.py rename to edsnlp/edsnlp/pipelines/core/normalizer/pollution/factory.py diff --git a/edsnlp/pipelines/core/normalizer/pollution/patterns.py b/edsnlp/edsnlp/pipelines/core/normalizer/pollution/patterns.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/pollution/patterns.py rename to edsnlp/edsnlp/pipelines/core/normalizer/pollution/patterns.py diff --git a/edsnlp/pipelines/core/normalizer/pollution/pollution.py b/edsnlp/edsnlp/pipelines/core/normalizer/pollution/pollution.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/pollution/pollution.py rename to edsnlp/edsnlp/pipelines/core/normalizer/pollution/pollution.py diff --git a/edsnlp/pipelines/core/normalizer/quotes/__init__.py b/edsnlp/edsnlp/pipelines/core/normalizer/quotes/__init__.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/quotes/__init__.py rename to edsnlp/edsnlp/pipelines/core/normalizer/quotes/__init__.py diff --git a/edsnlp/pipelines/core/normalizer/quotes/factory.py b/edsnlp/edsnlp/pipelines/core/normalizer/quotes/factory.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/quotes/factory.py rename to edsnlp/edsnlp/pipelines/core/normalizer/quotes/factory.py diff --git a/edsnlp/pipelines/core/normalizer/quotes/patterns.py b/edsnlp/edsnlp/pipelines/core/normalizer/quotes/patterns.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/quotes/patterns.py rename to edsnlp/edsnlp/pipelines/core/normalizer/quotes/patterns.py diff --git a/edsnlp/pipelines/core/normalizer/quotes/quotes.py b/edsnlp/edsnlp/pipelines/core/normalizer/quotes/quotes.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/quotes/quotes.py rename to edsnlp/edsnlp/pipelines/core/normalizer/quotes/quotes.py diff --git a/edsnlp/pipelines/core/normalizer/spaces/__init__.py b/edsnlp/edsnlp/pipelines/core/normalizer/spaces/__init__.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/spaces/__init__.py rename to edsnlp/edsnlp/pipelines/core/normalizer/spaces/__init__.py diff --git a/edsnlp/pipelines/core/normalizer/spaces/factory.py b/edsnlp/edsnlp/pipelines/core/normalizer/spaces/factory.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/spaces/factory.py rename to edsnlp/edsnlp/pipelines/core/normalizer/spaces/factory.py diff --git a/edsnlp/pipelines/core/normalizer/spaces/spaces.py b/edsnlp/edsnlp/pipelines/core/normalizer/spaces/spaces.py similarity index 100% rename from edsnlp/pipelines/core/normalizer/spaces/spaces.py rename to edsnlp/edsnlp/pipelines/core/normalizer/spaces/spaces.py diff --git a/edsnlp/pipelines/core/sentences/__init__.py b/edsnlp/edsnlp/pipelines/core/sentences/__init__.py similarity index 100% rename from edsnlp/pipelines/core/sentences/__init__.py rename to edsnlp/edsnlp/pipelines/core/sentences/__init__.py diff --git a/edsnlp/pipelines/core/sentences/factory.py b/edsnlp/edsnlp/pipelines/core/sentences/factory.py similarity index 100% rename from edsnlp/pipelines/core/sentences/factory.py rename to edsnlp/edsnlp/pipelines/core/sentences/factory.py diff --git a/edsnlp/pipelines/core/sentences/sentences.pxd b/edsnlp/edsnlp/pipelines/core/sentences/sentences.pxd similarity index 100% rename from edsnlp/pipelines/core/sentences/sentences.pxd rename to edsnlp/edsnlp/pipelines/core/sentences/sentences.pxd diff --git a/edsnlp/pipelines/core/sentences/sentences.pyx b/edsnlp/edsnlp/pipelines/core/sentences/sentences.pyx similarity index 100% rename from edsnlp/pipelines/core/sentences/sentences.pyx rename to edsnlp/edsnlp/pipelines/core/sentences/sentences.pyx diff --git a/edsnlp/pipelines/core/sentences/terms.py b/edsnlp/edsnlp/pipelines/core/sentences/terms.py similarity index 100% rename from edsnlp/pipelines/core/sentences/terms.py rename to edsnlp/edsnlp/pipelines/core/sentences/terms.py diff --git a/edsnlp/pipelines/core/terminology/__init__.py b/edsnlp/edsnlp/pipelines/core/terminology/__init__.py similarity index 100% rename from edsnlp/pipelines/core/terminology/__init__.py rename to edsnlp/edsnlp/pipelines/core/terminology/__init__.py diff --git a/edsnlp/pipelines/core/terminology/factory.py b/edsnlp/edsnlp/pipelines/core/terminology/factory.py similarity index 100% rename from edsnlp/pipelines/core/terminology/factory.py rename to edsnlp/edsnlp/pipelines/core/terminology/factory.py diff --git a/edsnlp/pipelines/core/terminology/terminology.py b/edsnlp/edsnlp/pipelines/core/terminology/terminology.py similarity index 100% rename from edsnlp/pipelines/core/terminology/terminology.py rename to edsnlp/edsnlp/pipelines/core/terminology/terminology.py diff --git a/edsnlp/pipelines/factories.py b/edsnlp/edsnlp/pipelines/factories.py similarity index 93% rename from edsnlp/pipelines/factories.py rename to edsnlp/edsnlp/pipelines/factories.py index 113716331..76a52ddf7 100644 --- a/edsnlp/pipelines/factories.py +++ b/edsnlp/edsnlp/pipelines/factories.py @@ -33,4 +33,5 @@ from .qualifiers.hypothesis.factory import create_component as hypothesis from .qualifiers.negation.factory import create_component as negation from .qualifiers.reported_speech.factory import create_component as rspeech -from .trainable.nested_ner import create_component as nested_ner +from .trainable.nested_ner.factory import create_component as nested_ner +from .trainable.span_qualifier.factory import create_component as span_qualifier diff --git a/edsnlp/pipelines/core/__init__.py b/edsnlp/edsnlp/pipelines/misc/__init__.py similarity index 100% rename from edsnlp/pipelines/core/__init__.py rename to edsnlp/edsnlp/pipelines/misc/__init__.py diff --git a/edsnlp/pipelines/misc/consultation_dates/__init__.py b/edsnlp/edsnlp/pipelines/misc/consultation_dates/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/consultation_dates/__init__.py rename to edsnlp/edsnlp/pipelines/misc/consultation_dates/__init__.py diff --git a/edsnlp/pipelines/misc/consultation_dates/consultation_dates.py b/edsnlp/edsnlp/pipelines/misc/consultation_dates/consultation_dates.py similarity index 100% rename from edsnlp/pipelines/misc/consultation_dates/consultation_dates.py rename to edsnlp/edsnlp/pipelines/misc/consultation_dates/consultation_dates.py diff --git a/edsnlp/pipelines/misc/consultation_dates/factory.py b/edsnlp/edsnlp/pipelines/misc/consultation_dates/factory.py similarity index 100% rename from edsnlp/pipelines/misc/consultation_dates/factory.py rename to edsnlp/edsnlp/pipelines/misc/consultation_dates/factory.py diff --git a/edsnlp/pipelines/misc/consultation_dates/patterns.py b/edsnlp/edsnlp/pipelines/misc/consultation_dates/patterns.py similarity index 100% rename from edsnlp/pipelines/misc/consultation_dates/patterns.py rename to edsnlp/edsnlp/pipelines/misc/consultation_dates/patterns.py diff --git a/edsnlp/pipelines/misc/dates/__init__.py b/edsnlp/edsnlp/pipelines/misc/dates/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/dates/__init__.py rename to edsnlp/edsnlp/pipelines/misc/dates/__init__.py diff --git a/edsnlp/pipelines/misc/dates/dates.py b/edsnlp/edsnlp/pipelines/misc/dates/dates.py similarity index 100% rename from edsnlp/pipelines/misc/dates/dates.py rename to edsnlp/edsnlp/pipelines/misc/dates/dates.py diff --git a/edsnlp/pipelines/misc/dates/factory.py b/edsnlp/edsnlp/pipelines/misc/dates/factory.py similarity index 100% rename from edsnlp/pipelines/misc/dates/factory.py rename to edsnlp/edsnlp/pipelines/misc/dates/factory.py diff --git a/edsnlp/pipelines/misc/dates/models.py b/edsnlp/edsnlp/pipelines/misc/dates/models.py similarity index 100% rename from edsnlp/pipelines/misc/dates/models.py rename to edsnlp/edsnlp/pipelines/misc/dates/models.py diff --git a/edsnlp/pipelines/misc/dates/patterns/__init__.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/__init__.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/__init__.py diff --git a/edsnlp/pipelines/misc/dates/patterns/absolute.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/absolute.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/absolute.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/absolute.py diff --git a/edsnlp/pipelines/misc/__init__.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/__init__.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/__init__.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/days.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/days.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/days.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/days.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/delimiters.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/delimiters.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/delimiters.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/delimiters.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/directions.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/directions.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/directions.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/directions.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/modes.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/modes.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/modes.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/modes.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/months.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/months.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/months.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/months.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/numbers.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/numbers.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/numbers.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/numbers.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/time.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/time.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/time.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/time.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/units.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/units.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/units.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/units.py diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/years.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/years.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/years.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/atomic/years.py diff --git a/edsnlp/pipelines/misc/dates/patterns/current.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/current.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/current.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/current.py diff --git a/edsnlp/pipelines/misc/dates/patterns/duration.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/duration.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/duration.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/duration.py diff --git a/edsnlp/pipelines/misc/dates/patterns/false_positive.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/false_positive.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/false_positive.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/false_positive.py diff --git a/edsnlp/pipelines/misc/dates/patterns/relative.py b/edsnlp/edsnlp/pipelines/misc/dates/patterns/relative.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/relative.py rename to edsnlp/edsnlp/pipelines/misc/dates/patterns/relative.py diff --git a/edsnlp/pipelines/misc/measurements/__init__.py b/edsnlp/edsnlp/pipelines/misc/measurements/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/measurements/__init__.py rename to edsnlp/edsnlp/pipelines/misc/measurements/__init__.py diff --git a/edsnlp/pipelines/misc/measurements/factory.py b/edsnlp/edsnlp/pipelines/misc/measurements/factory.py similarity index 71% rename from edsnlp/pipelines/misc/measurements/factory.py rename to edsnlp/edsnlp/pipelines/misc/measurements/factory.py index db63bf7de..9dc0f89c9 100644 --- a/edsnlp/pipelines/misc/measurements/factory.py +++ b/edsnlp/edsnlp/pipelines/misc/measurements/factory.py @@ -1,7 +1,10 @@ -from typing import Dict, List, Optional, Union - +from typing import Dict, List, Optional, Union, Tuple +from typing_extensions import Literal from spacy.language import Language - +from edsnlp.pipelines.base import ( + SpanGetterArg, + SpanSetterArg, +) import edsnlp.pipelines.misc.measurements.patterns as patterns from edsnlp.pipelines.misc.measurements.measurements import ( MeasureConfig, @@ -24,6 +27,12 @@ parse_doc=True, parse_tables=True, all_measurements=True, + extract_ranges=False, + range_patterns=patterns.range_patterns, + span_setter=None, + span_getter=None, + merge_mode="intersect", + as_ents=False, ) @@ -45,6 +54,12 @@ def create_component( unit_divisors: List[str], ignore_excluded: bool, attr: str, + span_setter: Optional[SpanSetterArg], + span_getter: Optional[SpanGetterArg], + merge_mode: Literal["intersect", "align", "union"], + extract_ranges: bool, + range_patterns: List[Tuple[Optional[str], Optional[str]]], + as_ents: bool, ): return MeasurementsMatcher( nlp, @@ -61,4 +76,10 @@ def create_component( measure_before_unit=measure_before_unit, attr=attr, ignore_excluded=ignore_excluded, + extract_ranges=extract_ranges, + range_patterns=range_patterns, + span_setter=span_setter, + span_getter=span_getter, + merge_mode=merge_mode, + as_ents=as_ents, ) diff --git a/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/edsnlp/pipelines/misc/measurements/measurements.py similarity index 86% rename from edsnlp/pipelines/misc/measurements/measurements.py rename to edsnlp/edsnlp/pipelines/misc/measurements/measurements.py index 3686120fd..9a9265252 100644 --- a/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/edsnlp/pipelines/misc/measurements/measurements.py @@ -9,12 +9,20 @@ import regex import spacy from spacy.tokens import Doc, Span -from typing_extensions import NotRequired, TypedDict +from typing_extensions import Literal, NotRequired, TypedDict from edsnlp.matchers.phrase import EDSPhraseMatcher from edsnlp.matchers.regex import RegexMatcher from edsnlp.pipelines.misc.measurements.patterns import common_measurements -from edsnlp.utils.filter import filter_spans +from edsnlp.pipelines.base import ( + BaseNERComponent, + SpanGetterArg, + SpanSetterArg, + get_spans, + validate_span_getter, +) +from edsnlp.pipelines.misc.measurements import patterns +from edsnlp.utils.filter import align_spans, filter_spans __all__ = ["MeasurementsMatcher"] @@ -187,11 +195,11 @@ def __sub__(self, other: "SimpleMeasurement"): self.registry, ) - def __lt__(self, other: "SimpleMeasurement"): - return self.convert_to(other.unit) < other.value + def __lt__(self, other: Union["SimpleMeasurement", "RangeMeasurement"]): + return self.convert_to(other.unit) < min((part.value for part in other)) - def __le__(self, other: "SimpleMeasurement"): - return self.convert_to(other.unit) <= other.value + def __le__(self, other: Union["SimpleMeasurement", "RangeMeasurement"]): + return self.convert_to(other.unit) <= min((part.value for part in other)) def convert_to(self, other_unit): self_degrees, self_scale = self.registry.parse_unit(self.unit) @@ -220,9 +228,72 @@ def __getattr__(self, other_unit): @classmethod def verify(cls, ent): return True + + +class RangeMeasurement(Measurement): + def __init__(self, from_value, to_value, unit, registry): + super().__init__() + self.value = (from_value, to_value) + self.unit = unit + self.registry = registry + + @classmethod + def from_measurements(cls, a, b): + a_value = a.value + b_value = b.convert_to(a.unit) + return RangeMeasurement(a_value, b_value, a.unit, a.registry) + + def convert_to(self, other_unit): + self_degrees, self_scale = self.registry.parse_unit(self.unit) + other_degrees, other_scale = self.registry.parse_unit(other_unit) + + if self_degrees != other_degrees: + raise AttributeError( + f"Units {self.unit} and {other_unit} are not homogenous" + ) + ratio = self_scale / other_scale + if ratio == 1: + return self.value + from_value = self.value[0] * ratio + to_value = self.value[1] * ratio + return (from_value, to_value) + + def __iter__(self): + yield self[0] + yield self[1] + + def __len__(self): + return 2 + + def __lt__(self, other: Union[SimpleMeasurement, "RangeMeasurement"]): + return max(self.convert_to(other.unit)) < min((part.value for part in other)) + + def __le__(self, other: Union[SimpleMeasurement, "RangeMeasurement"]): + return max(self.convert_to(other.unit)) <= max((part.value for part in other)) + def __getattr__(self, other_unit): + return self.convert_to(other_unit) + + def __eq__(self, other: Any): + if isinstance(other, RangeMeasurement): + return self.convert_to(other.unit) == other.value + return False + + def __str__(self): + return f"{self.value[0]}-{self.value[1]} {self.unit}" + + def __repr__(self): + return f"RangeMeasurement({self.value}, {repr(self.unit)})" -class MeasurementsMatcher: + def __getitem__(self, item: int): + assert isinstance(item, int) + return SimpleMeasurement(None, self.value[item], self.unit, self.registry) + + @classmethod + def verify(cls, ent): + return True + +class MeasurementsMatcher(BaseNERComponent): def __init__( self, nlp: spacy.Language, @@ -240,6 +311,12 @@ def __init__( name: str = "measurements", ignore_excluded: bool = False, attr: str = "NORM", + span_setter: Optional[SpanSetterArg] = None, + span_getter: Optional[SpanGetterArg] = None, + merge_mode: Literal["intersect", "align", "union"] = "intersect", + extract_ranges: bool = False, + range_patterns: List[Tuple[Optional[str], Optional[str]]] = patterns.range_patterns, # noqa: E501 + as_ents: bool = False, ): """ Matcher component to extract measurements. @@ -332,6 +409,26 @@ def __init__( Whether to match on the text ('TEXT') or on the normalized text ('NORM') ignore_excluded : bool Whether to exclude pollution patterns when matching in the text + span_setter: Optional[SpanSetterArg] + How to set the spans in the document. By default, each measurement will + be assigned to its own span group (using either the "name" field of the + config, or the key if you passed a dict), and to the "measurements" group. + span_getter : SpanGetterArg + Where to look for measurements in the doc. By default, look in the whole doc. + You can combine this with the `merge_mode` argument for interesting results. + merge_mode : Literal["intersect", "align"] + How to merge matches with the spans from `span_getter`, if given: + + - `intersect`: return only the matches that fall in the `span_getter` spans + - `align`: if a match overlaps a span from `span_getter` (e.g. a match + extracted by a machine learning model), return the `span_getter` span + instead, and assign all the parsed information (`._.date` / `._.duration`) + to it. Otherwise, don't return the date. + - `union`: extract measurements regardless of whether they fall within an existing span + extract_ranges: bool + Whether to extract ranges (like "entre 1 et 2 cm") + range_patterns: List[Tuple[str, str]] + A list of "{FROM} xx {TO} yy" patterns to match range measurements """ if measurements is None: @@ -356,7 +453,28 @@ def __init__( self.all_measurements = all_measurements self.parse_tables = parse_tables self.parse_doc = parse_doc + self.span_getter = ( + validate_span_getter(span_getter) + if span_getter is not None + else None + ) + self.merge_mode = merge_mode + self.extract_ranges = extract_ranges + self.range_patterns = range_patterns + + if span_setter is None: + span_setter = { + "ents": as_ents, + "measurements": True, + **{ + name: [name] + for name in self.measure_names.values() + } + } + + super().__init__(nlp=nlp, name=name, span_setter=span_setter) + # INTERVALS self.regex_matcher.add( "interval", @@ -1350,7 +1468,96 @@ def merge_adjacent_measurements(cls, measurements: List[Span]) -> List[Span]: merged.append(ent) return merged + + def merge_measurements_in_ranges(self, measurements: List[Span]) -> List[Span]: + """ + Aggregates extracted measurements together when they are adjacent to handle + cases like + - 1 meter 50 cm + - 30° 4' 54" + + Parameters + ---------- + measurements: List[Span] + + Returns + ------- + List[Span] + """ + if not self.extract_ranges or not self.range_patterns: + return measurements + + merged = measurements[:1] + for ent in measurements[1:]: + last = merged[-1] + + from_text = last.doc[last.start - 1].norm_ if last.start > 0 else None + to_text = get_text(last.doc[last.end: ent.start], "NORM", True) + matching_patterns = [ + (a, b) + for a, b in self.range_patterns + if b == to_text and (a is None or a == from_text) + ] + if len(matching_patterns): + try: + new_value = RangeMeasurement.from_measurements( + last._.value, ent._.value + ) + merged[-1] = last = last.doc[ + last.start + if matching_patterns[0][0] is None + else last.start - 1: ent.end + ] + last.label_ = ent.label_ + last._.set(last.label_, new_value) + except (AttributeError, TypeError): + merged.append(ent) + else: + merged.append(ent) + return merged + + def merge_with_existing( + self, + extracted: List[Span], + existing: List[Span], + ) -> List[Span]: + """ + Merges the extracted measurements with the existing measurements in the + document. + + Parameters + ---------- + extracted: List[Span] + The extracted measurements + existing: List[Span] + The existing measurements in the document + + Returns + ------- + List[Span] + """ + if self.merge_mode == "align": + spans_measurements = align_spans(extracted, existing, sort_by_overlap=True) + + extracted = [] + for span, span_measurements in zip(existing, spans_measurements): + if len(span_measurements): + span._.set(span.label_, span_measurements[0]._.get(span.label_)) + extracted.append(span) + + elif self.merge_mode == "intersect": + spans_measurements = align_spans(extracted, existing) + extracted = [] + for span, span_measurements in zip(existing, spans_measurements): + extracted.extend(span_measurements) + extracted = list(dict.fromkeys(extracted)) + + elif self.merge_mode == "union": + extracted = [*extracted, *existing] + + return extracted + def __call__(self, doc): """ Adds measurements to document's "measurements" SpanGroup. @@ -1367,7 +1574,12 @@ def __call__(self, doc): """ measurements = self.extract_measurements(doc) measurements = self.merge_adjacent_measurements(measurements) - + measurements = self.merge_measurements_in_ranges(measurements) + + if self.span_getter is not None: + existing = list(get_spans(doc, self.span_getter)) + measurements = self.merge_with_existing(measurements, existing) + doc.spans["measurements"] = measurements # for backward compatibility diff --git a/edsnlp/pipelines/misc/measurements/patterns.py b/edsnlp/edsnlp/pipelines/misc/measurements/patterns.py similarity index 99% rename from edsnlp/pipelines/misc/measurements/patterns.py rename to edsnlp/edsnlp/pipelines/misc/measurements/patterns.py index f989df2fe..352acd521 100644 --- a/edsnlp/pipelines/misc/measurements/patterns.py +++ b/edsnlp/edsnlp/pipelines/misc/measurements/patterns.py @@ -2381,3 +2381,13 @@ "ui_decomposition": {"bool": 1}, }, } + +range_patterns = [ + ("De", "à"), + ("De", "a"), + ("de", "à"), + ("de", "a"), + ("Entre", "et"), + ("entre", "et"), + (None, "à"), +] \ No newline at end of file diff --git a/edsnlp/pipelines/misc/reason/__init__.py b/edsnlp/edsnlp/pipelines/misc/reason/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/reason/__init__.py rename to edsnlp/edsnlp/pipelines/misc/reason/__init__.py diff --git a/edsnlp/pipelines/misc/reason/factory.py b/edsnlp/edsnlp/pipelines/misc/reason/factory.py similarity index 100% rename from edsnlp/pipelines/misc/reason/factory.py rename to edsnlp/edsnlp/pipelines/misc/reason/factory.py diff --git a/edsnlp/pipelines/misc/reason/patterns.py b/edsnlp/edsnlp/pipelines/misc/reason/patterns.py similarity index 100% rename from edsnlp/pipelines/misc/reason/patterns.py rename to edsnlp/edsnlp/pipelines/misc/reason/patterns.py diff --git a/edsnlp/pipelines/misc/reason/reason.py b/edsnlp/edsnlp/pipelines/misc/reason/reason.py similarity index 100% rename from edsnlp/pipelines/misc/reason/reason.py rename to edsnlp/edsnlp/pipelines/misc/reason/reason.py diff --git a/edsnlp/pipelines/misc/sections/__init__.py b/edsnlp/edsnlp/pipelines/misc/sections/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/sections/__init__.py rename to edsnlp/edsnlp/pipelines/misc/sections/__init__.py diff --git a/edsnlp/pipelines/misc/sections/factory.py b/edsnlp/edsnlp/pipelines/misc/sections/factory.py similarity index 100% rename from edsnlp/pipelines/misc/sections/factory.py rename to edsnlp/edsnlp/pipelines/misc/sections/factory.py diff --git a/edsnlp/pipelines/misc/sections/patterns.py b/edsnlp/edsnlp/pipelines/misc/sections/patterns.py similarity index 100% rename from edsnlp/pipelines/misc/sections/patterns.py rename to edsnlp/edsnlp/pipelines/misc/sections/patterns.py diff --git a/edsnlp/pipelines/misc/sections/sections.py b/edsnlp/edsnlp/pipelines/misc/sections/sections.py similarity index 100% rename from edsnlp/pipelines/misc/sections/sections.py rename to edsnlp/edsnlp/pipelines/misc/sections/sections.py diff --git a/edsnlp/pipelines/misc/tables/__init__.py b/edsnlp/edsnlp/pipelines/misc/tables/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/tables/__init__.py rename to edsnlp/edsnlp/pipelines/misc/tables/__init__.py diff --git a/edsnlp/pipelines/misc/tables/factory.py b/edsnlp/edsnlp/pipelines/misc/tables/factory.py similarity index 100% rename from edsnlp/pipelines/misc/tables/factory.py rename to edsnlp/edsnlp/pipelines/misc/tables/factory.py diff --git a/edsnlp/pipelines/misc/tables/patterns.py b/edsnlp/edsnlp/pipelines/misc/tables/patterns.py similarity index 100% rename from edsnlp/pipelines/misc/tables/patterns.py rename to edsnlp/edsnlp/pipelines/misc/tables/patterns.py diff --git a/edsnlp/pipelines/misc/tables/tables.py b/edsnlp/edsnlp/pipelines/misc/tables/tables.py similarity index 90% rename from edsnlp/pipelines/misc/tables/tables.py rename to edsnlp/edsnlp/pipelines/misc/tables/tables.py index a6d29f9b6..a9f82b562 100644 --- a/edsnlp/pipelines/misc/tables/tables.py +++ b/edsnlp/edsnlp/pipelines/misc/tables/tables.py @@ -46,7 +46,6 @@ def __init__( col_names: Optional[bool] = False, row_names: Optional[bool] = False, ): - if tables_pattern is None: tables_pattern = patterns.regex @@ -143,7 +142,6 @@ def get_tables(self, matches): # to find a new table row_len = len(processed_table[0]) if not all(len(row) == row_len for row in processed_table): - # Method to find all possible lengths of the rows def divisors(n): result = set() @@ -175,20 +173,22 @@ def divisors(n): for i in range(n_rows // n_rows_to_merge): # Init new_row with the first subrow new_row = processed_table[i * n_rows_to_merge] - for subrow in processed_table[ - i * n_rows_to_merge + 1 : (i + 1) * n_rows_to_merge - ]: - new_row = ( - new_row[:-1] - + [ - table[ - new_row[-1].start - - table.start : subrow[0].end - - table.start - ] - ] - + subrow[1:] - ) + if new_row: + for subrow in processed_table[ + i * n_rows_to_merge + 1 : (i + 1) * n_rows_to_merge + ]: + if subrow: + new_row = ( + new_row[:-1] + + [ + table[ + new_row[-1].start + - table.start : subrow[0].end + - table.start + ] + ] + + subrow[1:] + ) new_table.append(new_row) tables_list.append(new_table) break diff --git a/edsnlp/pipelines/misc/dates/patterns/atomic/__init__.py b/edsnlp/edsnlp/pipelines/ner/__init__.py similarity index 100% rename from edsnlp/pipelines/misc/dates/patterns/atomic/__init__.py rename to edsnlp/edsnlp/pipelines/ner/__init__.py diff --git a/edsnlp/pipelines/ner/__init__.py b/edsnlp/edsnlp/pipelines/ner/adicap/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/__init__.py rename to edsnlp/edsnlp/pipelines/ner/adicap/__init__.py diff --git a/edsnlp/pipelines/ner/adicap/adicap.py b/edsnlp/edsnlp/pipelines/ner/adicap/adicap.py similarity index 100% rename from edsnlp/pipelines/ner/adicap/adicap.py rename to edsnlp/edsnlp/pipelines/ner/adicap/adicap.py diff --git a/edsnlp/pipelines/ner/adicap/factory.py b/edsnlp/edsnlp/pipelines/ner/adicap/factory.py similarity index 100% rename from edsnlp/pipelines/ner/adicap/factory.py rename to edsnlp/edsnlp/pipelines/ner/adicap/factory.py diff --git a/edsnlp/pipelines/ner/adicap/models.py b/edsnlp/edsnlp/pipelines/ner/adicap/models.py similarity index 100% rename from edsnlp/pipelines/ner/adicap/models.py rename to edsnlp/edsnlp/pipelines/ner/adicap/models.py diff --git a/edsnlp/pipelines/ner/adicap/patterns.py b/edsnlp/edsnlp/pipelines/ner/adicap/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/adicap/patterns.py rename to edsnlp/edsnlp/pipelines/ner/adicap/patterns.py diff --git a/edsnlp/pipelines/ner/adicap/__init__.py b/edsnlp/edsnlp/pipelines/ner/cim10/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/adicap/__init__.py rename to edsnlp/edsnlp/pipelines/ner/cim10/__init__.py diff --git a/edsnlp/pipelines/ner/cim10/factory.py b/edsnlp/edsnlp/pipelines/ner/cim10/factory.py similarity index 100% rename from edsnlp/pipelines/ner/cim10/factory.py rename to edsnlp/edsnlp/pipelines/ner/cim10/factory.py diff --git a/edsnlp/pipelines/ner/cim10/patterns.py b/edsnlp/edsnlp/pipelines/ner/cim10/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/cim10/patterns.py rename to edsnlp/edsnlp/pipelines/ner/cim10/patterns.py diff --git a/edsnlp/pipelines/ner/cim10/__init__.py b/edsnlp/edsnlp/pipelines/ner/covid/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/cim10/__init__.py rename to edsnlp/edsnlp/pipelines/ner/covid/__init__.py diff --git a/edsnlp/pipelines/ner/covid/factory.py b/edsnlp/edsnlp/pipelines/ner/covid/factory.py similarity index 100% rename from edsnlp/pipelines/ner/covid/factory.py rename to edsnlp/edsnlp/pipelines/ner/covid/factory.py diff --git a/edsnlp/pipelines/ner/covid/patterns.py b/edsnlp/edsnlp/pipelines/ner/covid/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/covid/patterns.py rename to edsnlp/edsnlp/pipelines/ner/covid/patterns.py diff --git a/edsnlp/pipelines/ner/covid/__init__.py b/edsnlp/edsnlp/pipelines/ner/drugs/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/covid/__init__.py rename to edsnlp/edsnlp/pipelines/ner/drugs/__init__.py diff --git a/edsnlp/pipelines/ner/drugs/factory.py b/edsnlp/edsnlp/pipelines/ner/drugs/factory.py similarity index 100% rename from edsnlp/pipelines/ner/drugs/factory.py rename to edsnlp/edsnlp/pipelines/ner/drugs/factory.py diff --git a/edsnlp/pipelines/ner/drugs/patterns.py b/edsnlp/edsnlp/pipelines/ner/drugs/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/drugs/patterns.py rename to edsnlp/edsnlp/pipelines/ner/drugs/patterns.py diff --git a/edsnlp/pipelines/ner/scores/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/__init__.py diff --git a/edsnlp/pipelines/ner/scores/base_score.py b/edsnlp/edsnlp/pipelines/ner/scores/base_score.py similarity index 100% rename from edsnlp/pipelines/ner/scores/base_score.py rename to edsnlp/edsnlp/pipelines/ner/scores/base_score.py diff --git a/edsnlp/pipelines/ner/drugs/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/charlson/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/drugs/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/charlson/__init__.py diff --git a/edsnlp/pipelines/ner/scores/charlson/factory.py b/edsnlp/edsnlp/pipelines/ner/scores/charlson/factory.py similarity index 100% rename from edsnlp/pipelines/ner/scores/charlson/factory.py rename to edsnlp/edsnlp/pipelines/ner/scores/charlson/factory.py diff --git a/edsnlp/pipelines/ner/scores/charlson/patterns.py b/edsnlp/edsnlp/pipelines/ner/scores/charlson/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/scores/charlson/patterns.py rename to edsnlp/edsnlp/pipelines/ner/scores/charlson/patterns.py diff --git a/edsnlp/pipelines/ner/scores/charlson/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/elstonellis/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/charlson/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/elstonellis/__init__.py diff --git a/edsnlp/pipelines/ner/scores/elstonellis/factory.py b/edsnlp/edsnlp/pipelines/ner/scores/elstonellis/factory.py similarity index 100% rename from edsnlp/pipelines/ner/scores/elstonellis/factory.py rename to edsnlp/edsnlp/pipelines/ner/scores/elstonellis/factory.py diff --git a/edsnlp/pipelines/ner/scores/elstonellis/patterns.py b/edsnlp/edsnlp/pipelines/ner/scores/elstonellis/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/scores/elstonellis/patterns.py rename to edsnlp/edsnlp/pipelines/ner/scores/elstonellis/patterns.py diff --git a/edsnlp/pipelines/ner/scores/elstonellis/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/elstonellis/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/__init__.py diff --git a/edsnlp/pipelines/ner/scores/emergency/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/ccmu/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/ccmu/__init__.py diff --git a/edsnlp/pipelines/ner/scores/emergency/ccmu/factory.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/ccmu/factory.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/ccmu/factory.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/ccmu/factory.py diff --git a/edsnlp/pipelines/ner/scores/emergency/ccmu/patterns.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/ccmu/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/ccmu/patterns.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/ccmu/patterns.py diff --git a/edsnlp/pipelines/ner/scores/emergency/ccmu/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/gemsa/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/ccmu/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/gemsa/__init__.py diff --git a/edsnlp/pipelines/ner/scores/emergency/gemsa/factory.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/gemsa/factory.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/gemsa/factory.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/gemsa/factory.py diff --git a/edsnlp/pipelines/ner/scores/emergency/gemsa/patterns.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/gemsa/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/gemsa/patterns.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/gemsa/patterns.py diff --git a/edsnlp/pipelines/ner/scores/emergency/gemsa/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/priority/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/gemsa/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/priority/__init__.py diff --git a/edsnlp/pipelines/ner/scores/emergency/priority/factory.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/priority/factory.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/priority/factory.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/priority/factory.py diff --git a/edsnlp/pipelines/ner/scores/emergency/priority/patterns.py b/edsnlp/edsnlp/pipelines/ner/scores/emergency/priority/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/priority/patterns.py rename to edsnlp/edsnlp/pipelines/ner/scores/emergency/priority/patterns.py diff --git a/edsnlp/pipelines/ner/scores/factory.py b/edsnlp/edsnlp/pipelines/ner/scores/factory.py similarity index 100% rename from edsnlp/pipelines/ner/scores/factory.py rename to edsnlp/edsnlp/pipelines/ner/scores/factory.py diff --git a/edsnlp/pipelines/ner/scores/sofa/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/sofa/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/sofa/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/sofa/__init__.py diff --git a/edsnlp/pipelines/ner/scores/sofa/factory.py b/edsnlp/edsnlp/pipelines/ner/scores/sofa/factory.py similarity index 100% rename from edsnlp/pipelines/ner/scores/sofa/factory.py rename to edsnlp/edsnlp/pipelines/ner/scores/sofa/factory.py diff --git a/edsnlp/pipelines/ner/scores/sofa/patterns.py b/edsnlp/edsnlp/pipelines/ner/scores/sofa/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/scores/sofa/patterns.py rename to edsnlp/edsnlp/pipelines/ner/scores/sofa/patterns.py diff --git a/edsnlp/pipelines/ner/scores/sofa/sofa.py b/edsnlp/edsnlp/pipelines/ner/scores/sofa/sofa.py similarity index 100% rename from edsnlp/pipelines/ner/scores/sofa/sofa.py rename to edsnlp/edsnlp/pipelines/ner/scores/sofa/sofa.py diff --git a/edsnlp/pipelines/ner/scores/tnm/__init__.py b/edsnlp/edsnlp/pipelines/ner/scores/tnm/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/tnm/__init__.py rename to edsnlp/edsnlp/pipelines/ner/scores/tnm/__init__.py diff --git a/edsnlp/pipelines/ner/scores/tnm/factory.py b/edsnlp/edsnlp/pipelines/ner/scores/tnm/factory.py similarity index 100% rename from edsnlp/pipelines/ner/scores/tnm/factory.py rename to edsnlp/edsnlp/pipelines/ner/scores/tnm/factory.py diff --git a/edsnlp/pipelines/ner/scores/tnm/models.py b/edsnlp/edsnlp/pipelines/ner/scores/tnm/models.py similarity index 100% rename from edsnlp/pipelines/ner/scores/tnm/models.py rename to edsnlp/edsnlp/pipelines/ner/scores/tnm/models.py diff --git a/edsnlp/pipelines/ner/scores/tnm/patterns.py b/edsnlp/edsnlp/pipelines/ner/scores/tnm/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/scores/tnm/patterns.py rename to edsnlp/edsnlp/pipelines/ner/scores/tnm/patterns.py diff --git a/edsnlp/pipelines/ner/scores/tnm/tnm.py b/edsnlp/edsnlp/pipelines/ner/scores/tnm/tnm.py similarity index 100% rename from edsnlp/pipelines/ner/scores/tnm/tnm.py rename to edsnlp/edsnlp/pipelines/ner/scores/tnm/tnm.py diff --git a/edsnlp/pipelines/ner/scores/emergency/priority/__init__.py b/edsnlp/edsnlp/pipelines/ner/umls/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/scores/emergency/priority/__init__.py rename to edsnlp/edsnlp/pipelines/ner/umls/__init__.py diff --git a/edsnlp/pipelines/ner/umls/factory.py b/edsnlp/edsnlp/pipelines/ner/umls/factory.py similarity index 100% rename from edsnlp/pipelines/ner/umls/factory.py rename to edsnlp/edsnlp/pipelines/ner/umls/factory.py diff --git a/edsnlp/pipelines/ner/umls/patterns.py b/edsnlp/edsnlp/pipelines/ner/umls/patterns.py similarity index 100% rename from edsnlp/pipelines/ner/umls/patterns.py rename to edsnlp/edsnlp/pipelines/ner/umls/patterns.py diff --git a/edsnlp/pipelines/ner/umls/__init__.py b/edsnlp/edsnlp/pipelines/qualifiers/__init__.py similarity index 100% rename from edsnlp/pipelines/ner/umls/__init__.py rename to edsnlp/edsnlp/pipelines/qualifiers/__init__.py diff --git a/edsnlp/pipelines/qualifiers/base.py b/edsnlp/edsnlp/pipelines/qualifiers/base.py similarity index 100% rename from edsnlp/pipelines/qualifiers/base.py rename to edsnlp/edsnlp/pipelines/qualifiers/base.py diff --git a/edsnlp/pipelines/qualifiers/factories.py b/edsnlp/edsnlp/pipelines/qualifiers/factories.py similarity index 100% rename from edsnlp/pipelines/qualifiers/factories.py rename to edsnlp/edsnlp/pipelines/qualifiers/factories.py diff --git a/edsnlp/pipelines/qualifiers/family/__init__.py b/edsnlp/edsnlp/pipelines/qualifiers/family/__init__.py similarity index 100% rename from edsnlp/pipelines/qualifiers/family/__init__.py rename to edsnlp/edsnlp/pipelines/qualifiers/family/__init__.py diff --git a/edsnlp/pipelines/qualifiers/family/factory.py b/edsnlp/edsnlp/pipelines/qualifiers/family/factory.py similarity index 100% rename from edsnlp/pipelines/qualifiers/family/factory.py rename to edsnlp/edsnlp/pipelines/qualifiers/family/factory.py diff --git a/edsnlp/pipelines/qualifiers/family/family.py b/edsnlp/edsnlp/pipelines/qualifiers/family/family.py similarity index 100% rename from edsnlp/pipelines/qualifiers/family/family.py rename to edsnlp/edsnlp/pipelines/qualifiers/family/family.py diff --git a/edsnlp/pipelines/qualifiers/family/patterns.py b/edsnlp/edsnlp/pipelines/qualifiers/family/patterns.py similarity index 100% rename from edsnlp/pipelines/qualifiers/family/patterns.py rename to edsnlp/edsnlp/pipelines/qualifiers/family/patterns.py diff --git a/edsnlp/pipelines/qualifiers/history/__init__.py b/edsnlp/edsnlp/pipelines/qualifiers/history/__init__.py similarity index 100% rename from edsnlp/pipelines/qualifiers/history/__init__.py rename to edsnlp/edsnlp/pipelines/qualifiers/history/__init__.py diff --git a/edsnlp/pipelines/qualifiers/history/factory.py b/edsnlp/edsnlp/pipelines/qualifiers/history/factory.py similarity index 100% rename from edsnlp/pipelines/qualifiers/history/factory.py rename to edsnlp/edsnlp/pipelines/qualifiers/history/factory.py diff --git a/edsnlp/pipelines/qualifiers/history/history.py b/edsnlp/edsnlp/pipelines/qualifiers/history/history.py similarity index 100% rename from edsnlp/pipelines/qualifiers/history/history.py rename to edsnlp/edsnlp/pipelines/qualifiers/history/history.py diff --git a/edsnlp/pipelines/qualifiers/history/patterns.py b/edsnlp/edsnlp/pipelines/qualifiers/history/patterns.py similarity index 100% rename from edsnlp/pipelines/qualifiers/history/patterns.py rename to edsnlp/edsnlp/pipelines/qualifiers/history/patterns.py diff --git a/edsnlp/pipelines/qualifiers/hypothesis/__init__.py b/edsnlp/edsnlp/pipelines/qualifiers/hypothesis/__init__.py similarity index 100% rename from edsnlp/pipelines/qualifiers/hypothesis/__init__.py rename to edsnlp/edsnlp/pipelines/qualifiers/hypothesis/__init__.py diff --git a/edsnlp/pipelines/qualifiers/hypothesis/factory.py b/edsnlp/edsnlp/pipelines/qualifiers/hypothesis/factory.py similarity index 100% rename from edsnlp/pipelines/qualifiers/hypothesis/factory.py rename to edsnlp/edsnlp/pipelines/qualifiers/hypothesis/factory.py diff --git a/edsnlp/pipelines/qualifiers/hypothesis/hypothesis.py b/edsnlp/edsnlp/pipelines/qualifiers/hypothesis/hypothesis.py similarity index 100% rename from edsnlp/pipelines/qualifiers/hypothesis/hypothesis.py rename to edsnlp/edsnlp/pipelines/qualifiers/hypothesis/hypothesis.py diff --git a/edsnlp/pipelines/qualifiers/hypothesis/patterns.py b/edsnlp/edsnlp/pipelines/qualifiers/hypothesis/patterns.py similarity index 100% rename from edsnlp/pipelines/qualifiers/hypothesis/patterns.py rename to edsnlp/edsnlp/pipelines/qualifiers/hypothesis/patterns.py diff --git a/edsnlp/pipelines/qualifiers/negation/__init__.py b/edsnlp/edsnlp/pipelines/qualifiers/negation/__init__.py similarity index 100% rename from edsnlp/pipelines/qualifiers/negation/__init__.py rename to edsnlp/edsnlp/pipelines/qualifiers/negation/__init__.py diff --git a/edsnlp/pipelines/qualifiers/negation/factory.py b/edsnlp/edsnlp/pipelines/qualifiers/negation/factory.py similarity index 100% rename from edsnlp/pipelines/qualifiers/negation/factory.py rename to edsnlp/edsnlp/pipelines/qualifiers/negation/factory.py diff --git a/edsnlp/pipelines/qualifiers/negation/negation.py b/edsnlp/edsnlp/pipelines/qualifiers/negation/negation.py similarity index 100% rename from edsnlp/pipelines/qualifiers/negation/negation.py rename to edsnlp/edsnlp/pipelines/qualifiers/negation/negation.py diff --git a/edsnlp/pipelines/qualifiers/negation/patterns.py b/edsnlp/edsnlp/pipelines/qualifiers/negation/patterns.py similarity index 100% rename from edsnlp/pipelines/qualifiers/negation/patterns.py rename to edsnlp/edsnlp/pipelines/qualifiers/negation/patterns.py diff --git a/edsnlp/pipelines/qualifiers/reported_speech/__init__.py b/edsnlp/edsnlp/pipelines/qualifiers/reported_speech/__init__.py similarity index 100% rename from edsnlp/pipelines/qualifiers/reported_speech/__init__.py rename to edsnlp/edsnlp/pipelines/qualifiers/reported_speech/__init__.py diff --git a/edsnlp/pipelines/qualifiers/reported_speech/factory.py b/edsnlp/edsnlp/pipelines/qualifiers/reported_speech/factory.py similarity index 100% rename from edsnlp/pipelines/qualifiers/reported_speech/factory.py rename to edsnlp/edsnlp/pipelines/qualifiers/reported_speech/factory.py diff --git a/edsnlp/pipelines/qualifiers/reported_speech/patterns.py b/edsnlp/edsnlp/pipelines/qualifiers/reported_speech/patterns.py similarity index 100% rename from edsnlp/pipelines/qualifiers/reported_speech/patterns.py rename to edsnlp/edsnlp/pipelines/qualifiers/reported_speech/patterns.py diff --git a/edsnlp/pipelines/qualifiers/reported_speech/reported_speech.py b/edsnlp/edsnlp/pipelines/qualifiers/reported_speech/reported_speech.py similarity index 100% rename from edsnlp/pipelines/qualifiers/reported_speech/reported_speech.py rename to edsnlp/edsnlp/pipelines/qualifiers/reported_speech/reported_speech.py diff --git a/edsnlp/pipelines/terminations.py b/edsnlp/edsnlp/pipelines/terminations.py similarity index 100% rename from edsnlp/pipelines/terminations.py rename to edsnlp/edsnlp/pipelines/terminations.py diff --git a/edsnlp/pipelines/qualifiers/__init__.py b/edsnlp/edsnlp/pipelines/trainable/__init__.py similarity index 100% rename from edsnlp/pipelines/qualifiers/__init__.py rename to edsnlp/edsnlp/pipelines/trainable/__init__.py diff --git a/edsnlp/pipelines/trainable/__init__.py b/edsnlp/edsnlp/pipelines/trainable/layers/__init__.py similarity index 100% rename from edsnlp/pipelines/trainable/__init__.py rename to edsnlp/edsnlp/pipelines/trainable/layers/__init__.py diff --git a/edsnlp/models/torch/crf.py b/edsnlp/edsnlp/pipelines/trainable/layers/crf.py similarity index 99% rename from edsnlp/models/torch/crf.py rename to edsnlp/edsnlp/pipelines/trainable/layers/crf.py index 7e7bf0d49..800b5dd90 100644 --- a/edsnlp/models/torch/crf.py +++ b/edsnlp/edsnlp/pipelines/trainable/layers/crf.py @@ -47,7 +47,7 @@ def __init__( Shape: n_tags Impossible transitions at the start of a sequence end_forbidden_transitions Optional[torch.BoolTensor] - Shape: n_tags + Shape is (n_tags,) Impossible transitions at the end of a sequence learnable_transitions: bool Should we learn transition scores to complete the @@ -410,8 +410,6 @@ def tags_to_spans(tags): ---------- tags: torch.LongTensor Shape: n_samples * n_labels * n_tokens - mask: torch.BoolTensor - Shape: n_samples * n_labels * n_tokens Returns ------- diff --git a/edsnlp/edsnlp/pipelines/trainable/nested_ner/__init__.py b/edsnlp/edsnlp/pipelines/trainable/nested_ner/__init__.py new file mode 100644 index 000000000..5cd383edd --- /dev/null +++ b/edsnlp/edsnlp/pipelines/trainable/nested_ner/__init__.py @@ -0,0 +1 @@ +from .factory import create_component, create_scorer diff --git a/edsnlp/edsnlp/pipelines/trainable/nested_ner/factory.py b/edsnlp/edsnlp/pipelines/trainable/nested_ner/factory.py new file mode 100644 index 000000000..7be0d1db2 --- /dev/null +++ b/edsnlp/edsnlp/pipelines/trainable/nested_ner/factory.py @@ -0,0 +1,83 @@ +from spacy import Language +from thinc.api import Model +from thinc.config import Config + +from .nested_ner import TrainableNer +from .nested_ner import make_nested_ner_scorer as create_scorer # noqa: F401 + +nested_ner_default_config = """ +[model] + @architectures = "eds.stack_crf_ner_model.v1" + mode = "joint" + + [model.tok2vec] + @architectures = "spacy.Tok2Vec.v1" + + [model.tok2vec.embed] + @architectures = "spacy.MultiHashEmbed.v1" + width = 96 + rows = [5000, 2000, 1000, 1000] + attrs = ["ORTH", "PREFIX", "SUFFIX", "SHAPE"] + include_static_vectors = false + + [model.tok2vec.encode] + @architectures = "spacy.MaxoutWindowEncoder.v1" + width = ${model.tok2vec.embed.width} + window_size = 1 + maxout_pieces = 3 + depth = 4 + +[scorer] + @scorers = "eds.nested_ner_scorer.v1" +""" + +NESTED_NER_DEFAULTS = Config().from_str(nested_ner_default_config) + + +@Language.factory( + "nested_ner", + default_config=NESTED_NER_DEFAULTS, + requires=["doc.ents", "doc.spans"], + assigns=["doc.ents", "doc.spans"], + default_score_weights={ + "ents_f": 1.0, + "ents_p": 0.0, + "ents_r": 0.0, + }, +) +def create_component( + nlp: Language, + name: str, + model: Model, + ent_labels=None, + spans_labels=None, + scorer=None, +): + """ + Initialize a general named entity recognizer (with or without nested or + overlapping entities). + + Parameters + ---------- + nlp: Language + The current nlp object + name: str + Name of the component + model: Model + The model to extract the spans + ent_labels: Iterable[str] + list of labels to filter entities for in `doc.ents` + spans_labels: Mapping[str, Iterable[str]] + Mapping from span group names to list of labels to look for entities + and assign the predicted entities + scorer: Optional[Callable] + Method to call to score predictions + """ + return TrainableNer( + vocab=nlp.vocab, + model=model, + name=name, + ent_labels=ent_labels, + spans_labels=spans_labels, + scorer=scorer, + ) diff --git a/edsnlp/pipelines/trainable/nested_ner.py b/edsnlp/edsnlp/pipelines/trainable/nested_ner/nested_ner.py similarity index 82% rename from edsnlp/pipelines/trainable/nested_ner.py rename to edsnlp/edsnlp/pipelines/trainable/nested_ner/nested_ner.py index b2359d027..d3900a939 100644 --- a/edsnlp/pipelines/trainable/nested_ner.py +++ b/edsnlp/edsnlp/pipelines/trainable/nested_ner/nested_ner.py @@ -10,7 +10,6 @@ from spacy.vocab import Vocab from thinc.api import Model, Optimizer from thinc.backends import NumpyOps -from thinc.config import Config from thinc.model import set_dropout_rate from thinc.types import Ints2d from wasabi import Printer @@ -19,70 +18,11 @@ msg = Printer() - NUM_INITIALIZATION_EXAMPLES = 1000 - -nested_ner_default_config = """ -[model] - @architectures = "eds.stack_crf_ner_model.v1" - mode = "joint" - - [model.tok2vec] - @architectures = "spacy.Tok2Vec.v1" - - [model.tok2vec.embed] - @architectures = "spacy.MultiHashEmbed.v1" - width = 96 - rows = [5000, 2000, 1000, 1000] - attrs = ["ORTH", "PREFIX", "SUFFIX", "SHAPE"] - include_static_vectors = false - - [model.tok2vec.encode] - @architectures = "spacy.MaxoutWindowEncoder.v1" - width = ${model.tok2vec.embed.width} - window_size = 1 - maxout_pieces = 3 - depth = 4 - -[scorer] - @scorers = "eds.nested_ner_scorer.v1" -""" - -NESTED_NER_DEFAULTS = Config().from_str(nested_ner_default_config) np_ops = NumpyOps() -@Language.factory( - "nested_ner", - default_config=NESTED_NER_DEFAULTS, - requires=["doc.ents", "doc.spans"], - assigns=["doc.ents", "doc.spans"], - default_score_weights={ - "ents_f": 1.0, - "ents_p": 0.0, - "ents_r": 0.0, - }, -) -def create_component( - nlp: Language, - name: str, - model: Model, - ent_labels=None, - spans_labels=None, - scorer=None, -): - """Construct a TrainableQualifier component.""" - return TrainableNer( - vocab=nlp.vocab, - model=model, - name=name, - ent_labels=ent_labels, - spans_labels=spans_labels, - scorer=scorer, - ) - - def nested_ner_scorer(examples: Iterable[Example], **cfg): """ Scores the extracted entities that may be overlapping or nested @@ -101,22 +41,32 @@ def nested_ner_scorer(examples: Iterable[Example], **cfg): """ labels = set(cfg["labels"]) if "labels" in cfg is not None else None spans_labels = cfg["spans_labels"] - - pred_spans = set() - gold_spans = set() + + total_pred_spans = set() + total_gold_spans = set() + if labels is not None: + pred_spans = {} + gold_spans = {} for eg_idx, eg in enumerate(examples): for span in ( *eg.predicted.ents, *( span for name in ( - spans_labels if spans_labels is not None else eg.reference.spans + spans_labels if spans_labels is not None else eg.predicted.spans ) for span in eg.predicted.spans.get(name, ()) ), ): - if labels is None or span.label_ in labels: - pred_spans.add((eg_idx, span.start, span.end, span.label_)) + if labels is None: + total_pred_spans.add((eg_idx, span.start, span.end, span.label_)) + elif span.label_ in labels: + if span.label_ not in pred_spans.keys(): + pred_spans[span.label_] = set() + if span.label_ not in gold_spans.keys(): + gold_spans[span.label_] = set() + pred_spans[span.label_].add((eg_idx, span.start, span.end, span.label_)) + total_pred_spans.add((eg_idx, span.start, span.end, span.label_)) for span in ( *eg.reference.ents, @@ -128,18 +78,50 @@ def nested_ner_scorer(examples: Iterable[Example], **cfg): for span in eg.reference.spans.get(name, ()) ), ): - if labels is None or span.label_ in labels: - gold_spans.add((eg_idx, span.start, span.end, span.label_)) - - tp = len(pred_spans & gold_spans) + if labels is None: + total_gold_spans.add((eg_idx, span.start, span.end, span.label_)) + elif span.label_ in labels: + if span.label_ not in pred_spans.keys(): + pred_spans[span.label_] = set() + if span.label_ not in gold_spans.keys(): + gold_spans[span.label_] = set() + gold_spans[span.label_].add((eg_idx, span.start, span.end, span.label_)) + total_gold_spans.add((eg_idx, span.start, span.end, span.label_)) + + results = {} + tp = len(total_pred_spans & total_gold_spans) + results["ents_p"] = ( + tp / len(total_pred_spans) + if total_pred_spans + else float(tp == len(total_pred_spans)) + ) + results["ents_r"] = ( + tp / len(total_gold_spans) + if total_gold_spans + else float(tp == len(total_gold_spans)) + ) + results["ents_f"] = ( + 2 * tp / (len(total_pred_spans) + len(total_gold_spans)) + if total_pred_spans or gold_spans + else float(len(total_pred_spans) == len(total_gold_spans)) + ) - return { - "ents_p": tp / len(pred_spans) if pred_spans else float(tp == len(pred_spans)), - "ents_r": tp / len(gold_spans) if gold_spans else float(tp == len(gold_spans)), - "ents_f": 2 * tp / (len(pred_spans) + len(gold_spans)) - if pred_spans or gold_spans - else float(len(pred_spans) == len(gold_spans)), - } + if labels is not None: + results["ents_per_type"] = {} + for label in gold_spans.keys(): + tp = len(pred_spans[label] & gold_spans[label]) + results["ents_per_type"][label] = { + "p": tp / len(pred_spans[label]) + if pred_spans[label] + else float(tp == len(pred_spans[label])), + "r": tp / len(gold_spans[label]) + if gold_spans[label] + else float(tp == len(gold_spans[label])), + "f": 2 * tp / (len(pred_spans[label]) + len(gold_spans[label])) + if pred_spans[label] or gold_spans[label] + else float(len(pred_spans[label]) == len(gold_spans[label])), + } + return results @spacy.registry.scorers("eds.nested_ner_scorer.v1") @@ -444,4 +426,4 @@ def examples_to_truth(self, examples: List[Example]) -> Ints2d: continue spans.add((eg_idx, label_idx, span.start, span.end)) truths = self.model.ops.asarray(list(spans)) - return truths + return truths \ No newline at end of file diff --git a/edsnlp/models/stack_crf_ner.py b/edsnlp/edsnlp/pipelines/trainable/nested_ner/stack_crf_ner.py similarity index 87% rename from edsnlp/models/stack_crf_ner.py rename to edsnlp/edsnlp/pipelines/trainable/nested_ner/stack_crf_ner.py index 62f8ae4f2..967c1b056 100644 --- a/edsnlp/models/stack_crf_ner.py +++ b/edsnlp/edsnlp/pipelines/trainable/nested_ner/stack_crf_ner.py @@ -8,8 +8,11 @@ from thinc.model import Model from thinc.types import Floats1d, Floats2d, Ints2d -from edsnlp.models.pytorch_wrapper import PytorchWrapperModule, wrap_pytorch_model -from edsnlp.models.torch.crf import IMPOSSIBLE, MultiLabelBIOULDecoder +from edsnlp.pipelines.trainable.layers.crf import IMPOSSIBLE, MultiLabelBIOULDecoder +from edsnlp.pipelines.trainable.pytorch_wrapper import ( + PytorchWrapperModule, + wrap_pytorch_model, +) class CRFMode(str, Enum): @@ -176,11 +179,29 @@ def forward( def create_model( tok2vec: Model[List[Doc], List[Floats2d]], mode: CRFMode, - n_labels: int = None, + n_labels: Optional[int] = None, ) -> Model[ Tuple[Iterable[Doc], Optional[Ints2d], Optional[bool]], Tuple[Floats1d, Ints2d], ]: + """ + Parameters + ---------- + tok2vec: Model[List[Doc], List[Floats2d]] + The tok2vec embedding model used to generate the word embeddings + mode: CRFMode + How the CRF loss is computed + + - `joint`: Loss accounts for CRF transitions + - `independent`: Loss does not account for CRF transitions (softmax loss) + - `marginal`: Tag scores are smoothly updated with CRF transitions, and softmax loss is applied + n_labels: Optional[int] + Number of labels. This will be automatically set later during initialization + + Returns + ------- + Model + """ # noqa: E501 return wrap_pytorch_model( # noqa encoder=tok2vec, pt_model=StackedCRFNERModule( diff --git a/edsnlp/models/pytorch_wrapper.py b/edsnlp/edsnlp/pipelines/trainable/pytorch_wrapper.py similarity index 95% rename from edsnlp/models/pytorch_wrapper.py rename to edsnlp/edsnlp/pipelines/trainable/pytorch_wrapper.py index de7d7f8f3..e69b6ee0a 100644 --- a/edsnlp/models/pytorch_wrapper.py +++ b/edsnlp/edsnlp/pipelines/trainable/pytorch_wrapper.py @@ -1,5 +1,5 @@ import typing -from typing import Any, Callable, Iterable, List, Optional, OrderedDict, Tuple +from typing import Any, Callable, Iterable, List, Optional, OrderedDict, Sequence, Tuple import torch from spacy.tokens import Doc @@ -27,7 +27,7 @@ def __init__( """ Pytorch wrapping module for Spacy. Models that expect to be wrapped with - [wrap_pytorch_model][edsnlp.models.pytorch_wrapper.wrap_pytorch_model] + [wrap_pytorch_model][edsnlp.pipelines.trainable.pytorch_wrapper.wrap_pytorch_model] should inherit from this module. Parameters @@ -62,7 +62,7 @@ def load_state_dict( """ self.cfg = state_dict.pop("cfg") self.initialize() - super().load_state_dict(state_dict, strict) + super().load_state_dict(state_dict, False) def state_dict(self, destination=None, prefix="", keep_vars=False): """ @@ -245,7 +245,7 @@ def instance_init(model: Model, X: List[Doc] = None, Y: Ints2d = None) -> Model: Returns ------- - + Model """ encoder = model.get_ref("encoder") if X is not None: @@ -296,6 +296,7 @@ def wrap_pytorch_model( def wrap_pytorch_model( encoder: Model[List[Doc], List[Floats2d]], pt_model: PytorchWrapperModule, + attrs: Sequence[str] = ("set_n_labels",), ) -> Model[ Tuple[Iterable[Doc], Optional[PredT], Optional[bool]], Tuple[Floats1d, PredT], @@ -311,6 +312,8 @@ def wrap_pytorch_model( The Thinc document token embedding layer pt_model: PytorchWrapperModule The Pytorch model + attrs: Sequence[str] + The attributes of the Pytorch model that should be copied to the Thinc model Returns ------- @@ -323,8 +326,8 @@ def wrap_pytorch_model( "pytorch", pytorch_forward, attrs={ - "set_n_labels": pt_model.set_n_labels, "pt_model": pt_model, + **{attr: getattr(pt_model, attr) for attr in attrs}, }, layers=[encoder], shims=[PyTorchShim(pt_model)], diff --git a/edsnlp/edsnlp/pipelines/trainable/span_qualifier/__init__.py b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/__init__.py new file mode 100644 index 000000000..a7a3a99ee --- /dev/null +++ b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/__init__.py @@ -0,0 +1 @@ +from .factory import create_candidate_getter, create_component, create_scorer diff --git a/edsnlp/edsnlp/pipelines/trainable/span_qualifier/factory.py b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/factory.py new file mode 100644 index 000000000..2fa85c5b6 --- /dev/null +++ b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/factory.py @@ -0,0 +1,124 @@ +from typing import Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union + +from spacy import Language +from spacy.tokens import Doc +from thinc.api import Model +from thinc.config import Config + +from .span_qualifier import TrainableSpanQualifier +from .span_qualifier import make_span_qualifier_scorer as create_scorer # noqa: F401 +from .utils import SpanGroups, Spans +from .utils import make_candidate_getter as create_candidate_getter + +span_qualifier_default_config = """ +[model] + @architectures = "eds.span_multi_classifier.v1" + projection_mode = "dot" + pooler_mode = "max" + [model.tok2vec] + @architectures = "spacy.Tok2Vec.v1" + + [model.tok2vec.embed] + @architectures = "spacy.MultiHashEmbed.v1" + width = 96 + rows = [5000, 2000, 1000, 1000] + attrs = ["ORTH", "PREFIX", "SUFFIX", "SHAPE"] + include_static_vectors = false + + [model.tok2vec.encode] + @architectures = "spacy.MaxoutWindowEncoder.v1" + width = ${model.tok2vec.embed.width} + window_size = 1 + maxout_pieces = 3 + depth = 4 +""" + +SPAN_QUALIFIER_DEFAULTS = Config().from_str(span_qualifier_default_config) + + +@Language.factory( + "eds.span_qualifier", + default_config=SPAN_QUALIFIER_DEFAULTS, + requires=["doc.ents", "doc.spans"], + assigns=["doc.ents", "doc.spans"], + default_score_weights={ + "qual_f": 1.0, + }, +) +def create_component( + nlp, + model: Model, + on_ents: Optional[Union[bool, Sequence[str]]] = None, + on_span_groups: Union[ + bool, Sequence[str], Mapping[str, Union[bool, Sequence[str]]] + ] = False, + qualifiers: Optional[Sequence[str]] = None, + label_constraints: Optional[Dict[str, List[str]]] = None, + candidate_getter: Optional[ + Callable[[Doc], Tuple[Spans, Optional[Spans], SpanGroups, List[List[str]]]] + ] = None, + name: str = "span_qualifier", + scorer: Optional[Callable] = None, +) -> TrainableSpanQualifier: + """ + Create a generic span classification component + + Parameters + ---------- + nlp: Language + Spacy vocabulary + model: Model + The model to extract the spans + name: str + Name of the component + on_ents: Union[bool, Sequence[str]] + Whether to look into `doc.ents` for spans to classify. If a list of strings + is provided, only the span of the given labels will be considered. If None + and `on_span_groups` is False, labels mentioned in `label_constraints` + will be used, and all ents will be used if `label_constraints` is None. + on_span_groups: Union[bool, Sequence[str], Mapping[str, Sequence[str]]] + Whether to look into `doc.spans` for spans to classify: + + - If True, all span groups will be considered + - If False, no span group will be considered + - If a list of str is provided, only these span groups will be kept + - If a mapping is provided, the keys are the span group names and the values + are either a list of allowed labels in the group or True to keep them all + qualifiers: Optional[Sequence[str]] + The qualifiers to predict or train on. If None, keys from the + `label_constraints` will be used + label_constraints: Optional[Dict[str, List[str]]] + Constraints to select qualifiers for each span depending on their labels. + Keys of the dict are the qualifiers and values are the labels for which + the qualifier is allowed. If None, all qualifiers will be used for all spans + candidate_getter: Optional[Callable[[Doc], Tuple[Spans, Optional[Spans], SpanGroups, List[List[str]]]]] + Optional method to call to extract the candidate spans and the qualifiers + to predict or train on. If None, a candidate getter will be created from + the other parameters: `on_ents`, `on_span_groups`, `qualifiers` and + `label_constraints`. + scorer: Optional[Callable] + Optional method to call to score predictions + """ # noqa: E501 + do_make_candidate_getter = ( + on_ents or on_span_groups or qualifiers or label_constraints + ) + if (candidate_getter is not None) == do_make_candidate_getter: + raise ValueError( + "You must either provide a candidate getter or the parameters to " + "make one, but not both." + ) + if do_make_candidate_getter: + candidate_getter = create_candidate_getter( + on_ents=on_ents, + on_span_groups=on_span_groups, + qualifiers=qualifiers, + label_constraints=label_constraints, + ) + + return TrainableSpanQualifier( + vocab=nlp.vocab, + model=model, + candidate_getter=candidate_getter, + name=name, + scorer=scorer, + ) diff --git a/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_multi_classifier.py b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_multi_classifier.py new file mode 100644 index 000000000..ca4fc459d --- /dev/null +++ b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_multi_classifier.py @@ -0,0 +1,240 @@ +from enum import Enum +from typing import Any, Dict, Iterable, List, Optional, OrderedDict, Tuple + +import torch +from spacy import registry +from spacy.tokens import Doc +from thinc.model import Model +from thinc.types import Floats1d, Floats2d, Ints2d + +from edsnlp.pipelines.trainable.pytorch_wrapper import ( + PytorchWrapperModule, + wrap_pytorch_model, +) + + +class ProjectionMode(str, Enum): + dot = "dot" + + +class PoolerMode(str, Enum): + max = "max" + sum = "sum" + mean = "mean" + + +class SpanMultiClassifier(PytorchWrapperModule): + def __init__( + self, + input_size: Optional[int] = None, + n_labels: Optional[int] = None, + pooler_mode: PoolerMode = "max", + projection_mode: ProjectionMode = "dot", + ): + """ + Pytorch module for constrained multi-label & multi-class span classification + + Parameters + ---------- + input_size: int + Size of the input embeddings + n_labels: int + Number of labels predicted by the module + pooler_mode: PoolerMode + How embeddings are aggregated + projection_mode: ProjectionMode + How embeddings converted into logits + """ + super().__init__(input_size, n_labels) + + self.cfg["projection_mode"] = projection_mode + self.cfg["pooler_mode"] = pooler_mode + + if projection_mode != "dot": + raise Exception( + "Only scalar product is supported " "for label classification." + ) + + self.groups_indices = None + self.classifier = None + + def initialize(self): + """ + Once the number of labels n_labels are known, this method + initializes the torch linear layer. + """ + if self.cfg["projection_mode"] == "dot": + self.classifier = torch.nn.Linear(self.input_size, self.n_labels) + + def state_dict(self, destination=None, prefix="", keep_vars=False): + sd = super().state_dict() + + sd["groups_indices"] = self.groups_indices + sd["combinations"] = list(self.combinations) + return sd + + def load_state_dict( + self, state_dict: OrderedDict[str, torch.Tensor], strict: bool = True + ): + if state_dict.get("combinations", None) is not None: + self.set_label_groups( + groups_combinations=state_dict.pop("combinations"), + groups_indices=state_dict.pop("groups_indices"), + ) + + super().load_state_dict(state_dict, strict) + + def set_label_groups( + self, + groups_combinations, + groups_indices, + ): + """ + Set the label groups matrices. + """ + + # To make the buffers discoverable by pytorch (for device moving operations), + # we need to register them as buffer, and then we can group them in a + # single list of tensors + self.groups_indices = groups_indices + for i, group_combinations in enumerate(groups_combinations): + # n_combinations_in_group * n_labels_in_group + self.register_buffer( + f"combinations_{i}", + torch.as_tensor(group_combinations, dtype=torch.bool), + ) + + @property + def combinations(self): + for i in range(len(self.groups_indices)): + yield getattr(self, f"combinations_{i}") + + def forward( + self, + embeds: torch.FloatTensor, + mask: torch.BoolTensor, + spans: Optional[torch.LongTensor], + targets: Optional[torch.LongTensor], + additional_outputs: Dict[str, Any] = None, + is_train: bool = False, + is_predict: bool = False, + ) -> Optional[torch.FloatTensor]: + """ + Apply the span classifier module to the document embeddings and given spans to: + - compute the loss + - and/or predict the labels of spans + If labels are predicted, they are assigned to the `additional_outputs` + dictionary. + + Parameters + ---------- + embeds: torch.FloatTensor + Token embeddings to predict the tags from + mask: torch.BoolTensor + Mask of the sequences + spans: Optional[torch.LongTensor] + 2d tensor of n_spans * (doc_idx, ner_label_idx, begin, end) + targets: Optional[List[torch.LongTensor]] + list of 2d tensor of n_spans * n_combinations (1 hot) + additional_outputs: Dict[str, Any] + Additional outputs that should not / cannot be back-propped through + This dict will contain the predicted 2d tensor of labels + is_train: bool=False + Are we training the model (defaults to True) + is_predict: bool=False + Are we predicting the model (defaults to False) + + Returns + ------- + Optional[torch.FloatTensor] + Optional 0d loss (shape = [1]) to train the model + """ + n_samples, n_words = embeds.shape[:2] + device = embeds.device + (sample_idx, span_begins, span_ends) = spans.unbind(1) + if len(span_begins) == 0: + loss = None + if is_train: + loss = embeds.sum().unsqueeze(0) * 0 + else: + additional_outputs["labels"] = torch.zeros( + 0, self.n_labels, device=embeds.device, dtype=torch.int + ) + return loss + + flat_begins = n_words * sample_idx + span_begins + flat_ends = n_words * sample_idx + span_ends + flat_embeds = embeds.view(-1, embeds.shape[-1]) + flat_indices = torch.cat( + [ + torch.arange(b, e, device=device) + for b, e in zip(flat_begins.cpu().tolist(), flat_ends.cpu().tolist()) + ] + ).to(embeds.device) + offsets = (flat_ends - flat_begins).cumsum(0).roll(1) + offsets[0] = 0 + span_embeds = torch.nn.functional.embedding_bag( + input=flat_indices, + weight=flat_embeds, + offsets=offsets, + mode=self.cfg["pooler_mode"], + ) + + scores = self.classifier(span_embeds) + + groups_combinations_scores = [ + # ([e]ntities * [b]indings) * ([c]ombinations * [b]indings) + torch.einsum("eb,cb->ec", scores[:, grp_ids], grp_combinations.float()) + for grp_combinations, grp_ids in zip(self.combinations, self.groups_indices) + ] # -> list of ([e]ntities * [c]ombinations) + + loss = None + if is_train: + loss = sum( + [ + -grp_combinations_scores.log_softmax(-1) + .masked_fill(~grp_gold_combinations.to(device).bool(), 0) + .sum() + for grp_combinations_scores, grp_gold_combinations in zip( + groups_combinations_scores, targets + ) + ] + ) + loss = loss.unsqueeze(0) # for the thinc-pytorch shim + if is_predict: + pred = torch.cat( + [ + group_combinations[group_scores.argmax(-1)] + for group_scores, group_combinations in zip( + groups_combinations_scores, self.combinations + ) + ], + dim=-1, + ) + additional_outputs["labels"] = pred.int() + return loss + + +@registry.layers("eds.span_multi_classifier.v1") +def create_model( + tok2vec: Model[List[Doc], List[Floats2d]], + projection_mode: ProjectionMode = ProjectionMode.dot, + pooler_mode: PoolerMode = PoolerMode.max, + n_labels: int = None, +) -> Model[ + Tuple[Iterable[Doc], Optional[Ints2d], Optional[bool]], + Tuple[Floats1d, Ints2d], +]: + return wrap_pytorch_model( # noqa + encoder=tok2vec, + pt_model=SpanMultiClassifier( + input_size=None, # will be set later during initialization + n_labels=n_labels, # will likely be set later during initialization + projection_mode=projection_mode, + pooler_mode=pooler_mode, + ), + attrs=[ + "set_n_labels", + "set_label_groups", + ], + ) diff --git a/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_qualifier.py b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_qualifier.py new file mode 100644 index 000000000..6dd11616f --- /dev/null +++ b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_qualifier.py @@ -0,0 +1,529 @@ +import pickle +from collections import defaultdict +from itertools import islice +from typing import Callable, Dict, Iterable, List, Optional, Tuple + +import numpy as np +import spacy +from spacy import Language +from spacy.pipeline import TrainablePipe +from spacy.tokens import Doc +from spacy.training import Example +from spacy.vocab import Vocab +from thinc.api import Model, Optimizer +from thinc.backends import NumpyOps +from thinc.model import set_dropout_rate +from thinc.types import Ints2d +from wasabi import Printer + +from edsnlp.pipelines.trainable.span_qualifier.utils import ( + Binding, + SpanGroups, + Spans, + keydefaultdict, + make_binding_getter, + make_binding_setter, +) + +NUM_INITIALIZATION_EXAMPLES = 10 + +msg = Printer() +np_ops = NumpyOps() + + +@spacy.registry.scorers("eds.span_qualifier_scorer.v1") +def make_span_qualifier_scorer(candidate_getter: Callable): + def span_qualifier_scorer(examples: Iterable[Example], **cfg): + """ + Scores the extracted entities that may be overlapping or nested + by looking in `doc.ents`, and `doc.spans`. + + Parameters + ---------- + examples: Iterable[Example] + The examples to score + cfg: Dict[str] + The configuration dict of the component + + Returns + ------- + Dict[str, float] + """ + labels = defaultdict(lambda: ([], [])) + labels["ALL"] = ([], []) + for eg_idx, eg in enumerate(examples): + doc_spans, *_, doc_qlf = candidate_getter(eg.predicted) + for span_idx, (span, span_qualifiers) in enumerate(zip(doc_spans, doc_qlf)): + for qualifier in span_qualifiers: + value = BINDING_GETTERS[qualifier](span) + if value: + labels["ALL"][0].append((eg_idx, span_idx, qualifier, value)) + key_str = f"{qualifier[2:]}" if value is True else f"{qualifier[2:]}-{value}" + labels[key_str][0].append((eg_idx, span_idx, value)) + + doc_spans, *_, doc_qlf = candidate_getter(eg.reference) + for span_idx, (span, span_qualifiers) in enumerate(zip(doc_spans, doc_qlf)): + for qualifier in span_qualifiers: + value = BINDING_GETTERS[qualifier](span) + if value: + labels["ALL"][1].append((eg_idx, span_idx, qualifier, value)) + key_str = f"{qualifier[2:]}" if value is True else f"{qualifier[2:]}-{value}" + labels[key_str][1].append((eg_idx, span_idx, value)) + + def prf(pred, gold): + tp = len(set(pred) & set(gold)) + np = len(pred) + ng = len(gold) + return { + "f": 2 * tp / max(1, np + ng), + "p": 1 if tp == np else (tp / np), + "r": 1 if tp == ng else (tp / ng), + } + results = {name: prf(pred, gold) for name, (pred, gold) in labels.items()} + results = dict(sorted(results.items())) + return {"qual_f": results["ALL"]["f"], "qual_per_type": results} + + return span_qualifier_scorer + + +BINDING_GETTERS = keydefaultdict(make_binding_getter) +BINDING_SETTERS = keydefaultdict(make_binding_setter) + + +# noinspection PyMethodOverriding +class TrainableSpanQualifier(TrainablePipe): + """Create a generic span classification component""" + + def __init__( + self, + vocab: Vocab, + model: Model, + candidate_getter: Callable[ + [Doc], Tuple[Spans, Optional[Spans], SpanGroups, List[List[str]]] + ], + name: str = "span_qualifier", + scorer: Optional[Callable] = None, + ) -> None: + """ + Parameters + ---------- + vocab: Vocab + Spacy vocabulary + model: Model + The model to extract the spans + name: str + Name of the component + candidate_getter: Callable[[Doc], Tuple[Spans, Optional[Spans], SpanGroups, List[List[str]]]] + Method to call to extract the candidate spans and the qualifiers + to predict or train on. + scorer: Optional[Callable] + Method to call to score predictions + """ # noqa: E501 + + super().__init__(vocab, model, name) + + self.cfg["qualifiers"]: Optional[Tuple[str]] = () + self.candidate_getter = candidate_getter + + self.bindings: List[Binding] = [] + self.ner_labels_indices: Optional[Dict[str, int]] = None + + if scorer is None: + self.scorer = make_span_qualifier_scorer(candidate_getter) + else: + self.scorer = scorer + + def to_disk(self, path, *, exclude=tuple()): + # This will receive the directory path + /my_component + super().to_disk(path, exclude=exclude) + data_path = path / "data.pkl" + with open(data_path, "wb") as f: + pickle.dump( + { + "bindings": self.bindings, + }, + f, + ) + + def from_disk(self, path, exclude=tuple()): + super().from_disk(path, exclude=exclude) + # This will receive the directory path + /my_component + data_path = path / "data.pkl" + with open(data_path, "rb") as f: + data = pickle.load(f) + self.bindings = data["bindings"] + return self + + @property + def qualifiers(self) -> Tuple[str]: + """Return the qualifiers predicted by the component""" + return self.cfg["qualifiers"] + + @property + def labels(self) -> List[str]: + return ["{}={}".format(a, b) for a, b in self.bindings] + + def add_label(self, label: str) -> int: + """Add a new label to the pipe.""" + raise Exception("Cannot add a new label to the pipe") + + def predict( + self, docs: List[Doc] + ) -> Tuple[ + Dict[str, Ints2d], + Spans, + List[Optional[Spans]], + List[SpanGroups], + List[List[str]], + ]: + """ + Apply the pipeline's model to a batch of docs, without modifying them. + + Parameters + ---------- + docs: List[Doc] + + Returns + ------- + # noqa: E501 + Tuple[Dict[str, Ints2d], Spans, List[Spans], List[SpanGroups], List[List[str]]] + The predicted list of 1-hot label sequence as a tensor + that represent the labels of spans for all the batch, + the list of all spans, and the span groups and ents in case the "label_" + qualifier is updated + """ + spans, ents, span_groups, spans_qlf, spans_array = self._get_span_data(docs) + + return ( + self.model.predict( + ( + docs, + self.model.ops.asarray(spans_array), + None, + True, + ) + )[1], + spans, + ents, + span_groups, + spans_qlf, + ) + + def set_annotations( + self, + docs: List[Doc], + predictions: Tuple[ + Dict[str, Ints2d], + Spans, + List[Optional[Spans]], + List[SpanGroups], + List[List[str]], + ], + **kwargs, + ) -> None: + """ + Modify the spans of a batch of `spacy.tokens.Span` objects, using the + predicted labels. + + # noqa: E501 + Parameters + ---------- + docs: List[Doc] + The docs to update, not used in this function + predictions: Tuple[Dict[str, Ints2d], Spans, List[SpanGroups], List[Optional[Spans]]] + Tuple returned by the `predict` method, containing: + - the label predictions. This is a 2d boolean tensor of shape + (`batch_size`, `len(self.bindings)`) + - the spans to update + - the ents to reassign if the "label_" qualifier is updated + - the span groups dicts to reassign if the "label_" qualifier is updated + - the qualifiers for each span + """ + output, spans, ents, span_groups, spans_qlf = predictions + one_hot = output["labels"] + for span, span_one_hot, span_qualifiers in zip(spans, one_hot, spans_qlf): + for binding, is_present in zip(self.bindings, span_one_hot): + if is_present and binding[0] in span_qualifiers: + BINDING_SETTERS[binding](span) + + # Because of the specific nature of the ".label_" attribute, we need to + # reassign the ents on `doc.ents` (if `span_getter.from_ents`) and the spans + # groups mentioned in `span_getter.from_spans_groups` on `doc.spans` + if "label_" in self.qualifiers or "label" in self.qualifiers: + if ents is not None: + for doc, doc_ents in zip(docs, ents): + if doc_ents is not None: + doc.ents = doc_ents + if span_groups is not None: + for doc, doc_span_groups in zip(docs, span_groups): + doc.spans.update(doc_span_groups) + + def update( + self, + examples: Iterable[Example], + *, + drop: float = 0.0, + set_annotations: bool = False, + sgd: Optional[Optimizer] = None, + losses: Optional[Dict[str, float]] = None, + ) -> Dict[str, float]: + """ + Learn from a batch of documents and gold-standard information, + updating the pipe's model. Delegates to begin_update and get_loss. + + Unlike standard TrainablePipe components, the discrete ops (best selection + of labels) is performed by the model directly (`begin_update` returns the loss + and the predictions) + + Parameters + ---------- + examples: Iterable[Example] + drop: float = 0.0 + + set_annotations: bool + Whether to update the document with predicted spans + sgd: Optional[Optimizer] + Optimizer + losses: Optional[Dict[str, float]] + Dict of loss, updated in place + + Returns + ------- + Dict[str, float] + Updated losses dict + """ + + if losses is None: + losses = {} + losses.setdefault(self.name, 0.0) + set_dropout_rate(self.model, drop) + examples = list(examples) + + # run the model + docs = [eg.predicted for eg in examples] + ( + spans, + ents, + span_groups, + spans_qlf, + spans_array, + targets, + ) = self.examples_to_truth(examples) + (loss, predictions), backprop = self.model.begin_update( + (docs, spans_array, targets, set_annotations) + ) + loss, gradient = self.get_loss(examples, loss) + backprop(gradient) + if sgd is not None: + self.model.finish_update(sgd) + if set_annotations: + self.set_annotations( + spans, + ( + predictions, + spans, + ents, + span_groups, + spans_qlf, + ), + ) + + losses[self.name] = loss + + return loss + + def get_loss(self, examples: Iterable[Example], loss) -> Tuple[float, float]: + """Find the loss and gradient of loss for the batch of documents and + their predicted scores.""" + return float(loss.item()), self.model.ops.xp.array([1]) + + def initialize( + self, + get_examples: Callable[[], Iterable[Example]], + *, + nlp: Language = None, + labels: Optional[List[str]] = None, + ): + """ + Initialize the pipe for training, using a representative set + of data examples. + + Gather the qualifier values by iterating on the spans and their qualifiers + matching the rules defined in the `candidate_getter`, and retrieving the + values of the qualifiers. + + Parameters + ---------- + get_examples: Callable[[], Iterable[Example]] + Method to sample some examples + nlp: spacy.Language + Unused spacy model + labels + Unused list of labels + """ + qualifier_values = defaultdict(set) + for eg in get_examples(): + spans, *_, spans_qualifiers = self.candidate_getter(eg.reference) + for span, span_qualifiers in zip(spans, spans_qualifiers): + for qualifier in span_qualifiers: + value = BINDING_GETTERS[qualifier](span) + qualifier_values[qualifier].add(value) + + qualifier_values = { + key: sorted(values, key=str) for key, values in qualifier_values.items() + } + + self.cfg["qualifiers"] = sorted(qualifier_values.keys()) + # groups: + # num binding_groups (e.g. ["events", "negation"]) + # * num label combinations in this group + # * positive labels in this combination + self.cfg["groups"] = [ + [((key, value),) for value in sorted(values, key=str)] + for key, values in qualifier_values.items() + ] + groups_bindings = [ + list( + dict.fromkeys( + [ + binding + for combination_bindings in group_combinations + for binding in combination_bindings + ] + ) + ) + for group_combinations in self.cfg["groups"] + ] + self.bindings = [ + binding for group_bindings in groups_bindings for binding in group_bindings + ] + self.model.attrs["set_n_labels"](len(self.bindings)) + + # combinations_one_hot: list of bool arrays of shape + # num binding_groups (e.g. ["events", "negation"]) + # * num bindings in this group (eg ["start", "stop"], [True, False]) + combinations_one_hot: List[List[List[bool]]] = [ + [ + [binding in combination_bindings for binding in group_bindings] + for combination_bindings in group_combinations + ] + for group_combinations, group_bindings in zip( + self.cfg["groups"], groups_bindings + ) + ] + # groups_indices: + # num binding_groups (e.g. ["events", "negation"]) + # * num label combinations in this group + # * presence or absence (bool) of the bindings of this groups in the combination + groups_bindings_indices = [ + [self.bindings.index(binding) for binding in group_bindings] + for group_bindings in groups_bindings + ] + + self.model.attrs["set_label_groups"]( + combinations_one_hot, + groups_bindings_indices, + ) + + # Neural network initialization + sub_batch = list(islice(get_examples(), NUM_INITIALIZATION_EXAMPLES)) + doc_sample = [eg.reference for eg in sub_batch] + spans, *_, spans_array, targets = self.examples_to_truth(sub_batch) + if len(spans) == 0: + raise ValueError( + "Call begin_training with relevant entities " + "and relations annotated in " + "at least a few reference examples!" + ) + + self.model.initialize(X=doc_sample, Y=spans_array) + + def _get_span_data( + self, docs: List[Doc] + ) -> Tuple[ + Spans, + List[Optional[Spans]], + List[SpanGroups], + List[List[str]], + np.ndarray, + ]: + spans = [] + ents, span_groups = [], [] + spans_qualifiers = [] + for doc_idx, doc in enumerate(docs): + doc_spans, doc_ents, doc_span_groups, qlf = self.candidate_getter(doc) + ents.append(doc_ents) + span_groups.append(doc_span_groups) + spans_qualifiers.extend(qlf) + spans.extend([(doc_idx, span) for span in doc_spans]) + spans = list(spans) + spans_array = np.zeros((len(spans), 3), dtype=int) + for i, (doc_idx, span) in enumerate(spans): + spans_array[i] = ( + doc_idx, + span.start, + span.end, + ) + + return ( + [span for i, span in spans], + ents, + span_groups, + spans_qualifiers, + spans_array, + ) + + def examples_to_truth( + self, examples: List[Example] + ) -> Tuple[ + Spans, + List[Spans], + List[SpanGroups], + List[List[str]], + Ints2d, + List[Ints2d], + ]: + """ + + Converts the spans of the examples into a list + of (doc_idx, label_idx, begin, end) tuple as a tensor, + and the labels of the spans into a list of 1-hot label sequence + + Parameters + ---------- + examples: List[Example] + + Returns + ------- + Tuple[Spans,List[Spans],List[SpanGroups],List[List[str]],Ints2d,List[Ints2d]] + The list of spans, the spans tensor, the qualifiers tensor, and the + list of entities and span groups to reassign them if the label_ attribute + is part of the updated qualifiers + """ # noqa E501 + spans, ents, span_groups, spans_qualifiers, spans_array = self._get_span_data( + [eg.reference for eg in examples] + ) + targets = [ + np.zeros((len(spans), len(group_combinations)), dtype=int) + for group_combinations in self.cfg["groups"] + ] + for span_idx, span in enumerate(spans): + span_bindings = [] + for j, binding in enumerate(self.bindings): + if binding[0] in spans_qualifiers[span_idx] and BINDING_GETTERS[ + binding + ](span): + span_bindings.append(binding) + for group_idx, group in enumerate(self.cfg["groups"]): + for comb_idx, group_combination in enumerate(group): + if set(group_combination).issubset(set(span_bindings)): + targets[group_idx][span_idx, comb_idx] = 1 + + return ( + spans, + ents, + span_groups, + spans_qualifiers, + self.model.ops.asarray(spans_array), + [self.model.ops.asarray(arr) for arr in targets], + ) diff --git a/edsnlp/edsnlp/pipelines/trainable/span_qualifier/utils.py b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/utils.py new file mode 100644 index 000000000..2ffc6a96f --- /dev/null +++ b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/utils.py @@ -0,0 +1,184 @@ +from typing import ( + Any, + Callable, + Dict, + List, + Mapping, + Optional, + Sequence, + Tuple, + TypeVar, + Union, +) + +from spacy import registry +from spacy.tokens import Doc, Span + +from edsnlp.utils.span_getters import make_span_getter + +Binding = Tuple[str, Any] +Spans = List[Span] +SpanGroups = Dict[str, Spans] + + +class make_candidate_getter: + def __init__( + self, + on_ents: Optional[Union[bool, Sequence[str]]] = None, + on_span_groups: Union[ + bool, Sequence[str], Mapping[str, Union[bool, Sequence[str]]] + ] = False, + qualifiers: Optional[Sequence[str]] = None, + label_constraints: Optional[Dict[str, List[str]]] = None, + ): + + """ + Make a span qualifier candidate getter function. + + Parameters + ---------- + on_ents: Union[bool, Sequence[str]] + Whether to look into `doc.ents` for spans to classify. If a list of strings + is provided, only the span of the given labels will be considered. If None + and `on_span_groups` is False, labels mentioned in `label_constraints` + will be used. + on_span_groups: Union[bool, Sequence[str], Mapping[str, Sequence[str]]] + Whether to look into `doc.spans` for spans to classify: + + - If True, all span groups will be considered + - If False, no span group will be considered + - If a list of str is provided, only these span groups will be kept + - If a mapping is provided, the keys are the span group names and the values + are either a list of allowed labels in the group or True to keep them all + qualifiers: Optional[Sequence[str]] + The qualifiers to predict or train on. If None, keys from the + `label_constraints` will be used + label_constraints: Optional[Dict[str, List[str]]] + Constraints to select qualifiers for each span depending on their labels. + Keys of the dict are the qualifiers and values are the labels for which + the qualifier is allowed. If None, all qualifiers will be used for all spans + + Returns + ------- + Callable[[Doc], Tuple[Spans, Optional[Spans], SpanGroups, List[List[str]]]] + """ + + if qualifiers is None and label_constraints is None: + raise ValueError( + "Either `qualifiers` or `label_constraints` must be given to " + "provide the qualifiers to predict / train on." + ) + elif qualifiers is None: + qualifiers = list(label_constraints.keys()) + + if not on_span_groups and on_ents is None: + if label_constraints is None: + on_ents = True + else: + on_ents = sorted( + set( + label + for qualifier in label_constraints + for label in label_constraints[qualifier] + ) + ) + + self.span_getter = make_span_getter(on_ents, on_span_groups) + self.label_constraints = label_constraints + self.qualifiers = qualifiers + + def __call__( + self, + doc: Doc, + ) -> Tuple[Spans, Optional[Spans], SpanGroups, List[List[str]]]: + flattened_spans, ents, span_groups = self.span_getter( + doc, + return_origin=True, + ) + + if self.label_constraints: + span_qualifiers = [ + [ + qualifier + for qualifier in self.qualifiers + if qualifier not in self.label_constraints + or span.label_ in self.label_constraints[qualifier] + ] + for span in flattened_spans + ] + else: + span_qualifiers = [self.qualifiers] * len(flattened_spans) + return flattened_spans, ents, span_groups, span_qualifiers + + +registry.misc("eds.candidate_span_qualifier_getter")(make_candidate_getter) + + +def _check_path(path: str): + assert [letter.isalnum() or letter == "_" or letter == "." for letter in path], ( + "The label must be a path of valid python identifier to be used as a getter" + "in the following template: span.[YOUR_LABEL], such as `label_` or `_.negated" + ) + + +def make_binding_getter(qualifier: Union[str, Binding]): + """ + Make a qualifier getter + + Parameters + ---------- + qualifier: Union[str, Binding] + Either one of the following: + - a path to a nested attributes of the span, such as "qualifier_" or "_.negated" + - a tuple of (key, value) equality, such as `("_.date.mode", "PASSED")` + + Returns + ------- + Callable[[Span], bool] + The qualifier getter + """ + if isinstance(qualifier, tuple): + path, value = qualifier + _check_path(path) + return eval(f"lambda span: span.{path} == value", {"value": value}, {}) + else: + _check_path(qualifier) + return eval(f"lambda span: span.{qualifier}") + + +def make_binding_setter(binding: Binding): + """ + Make a qualifier setter + + Parameters + ---------- + binding: Binding + A pair of + - a path to a nested attributes of the span, such as `qualifier_` or `_.negated` + - a value assignment + + Returns + ------- + Callable[[Span]] + The qualifier setter + """ + path, value = binding + _check_path(path) + fn_string = f"""def fn(span): span.{path} = value""" + loc = {"value": value} + exec(fn_string, loc, loc) + return loc["fn"] + + +K = TypeVar("K") +V = TypeVar("V") + + +class keydefaultdict(dict): + def __init__(self, default_factory: Callable[[K], V]): + super().__init__() + self.default_factory = default_factory + + def __missing__(self, key: K) -> V: + ret = self[key] = self.default_factory(key) + return ret diff --git a/edsnlp/processing/__init__.py b/edsnlp/edsnlp/processing/__init__.py similarity index 100% rename from edsnlp/processing/__init__.py rename to edsnlp/edsnlp/processing/__init__.py diff --git a/edsnlp/processing/distributed.py b/edsnlp/edsnlp/processing/distributed.py similarity index 100% rename from edsnlp/processing/distributed.py rename to edsnlp/edsnlp/processing/distributed.py diff --git a/edsnlp/processing/helpers.py b/edsnlp/edsnlp/processing/helpers.py similarity index 100% rename from edsnlp/processing/helpers.py rename to edsnlp/edsnlp/processing/helpers.py diff --git a/edsnlp/processing/parallel.py b/edsnlp/edsnlp/processing/parallel.py similarity index 100% rename from edsnlp/processing/parallel.py rename to edsnlp/edsnlp/processing/parallel.py diff --git a/edsnlp/processing/simple.py b/edsnlp/edsnlp/processing/simple.py similarity index 100% rename from edsnlp/processing/simple.py rename to edsnlp/edsnlp/processing/simple.py diff --git a/edsnlp/processing/utils.py b/edsnlp/edsnlp/processing/utils.py similarity index 100% rename from edsnlp/processing/utils.py rename to edsnlp/edsnlp/processing/utils.py diff --git a/edsnlp/processing/wrapper.py b/edsnlp/edsnlp/processing/wrapper.py similarity index 100% rename from edsnlp/processing/wrapper.py rename to edsnlp/edsnlp/processing/wrapper.py diff --git a/edsnlp/resources/adicap.json.gz b/edsnlp/edsnlp/resources/adicap.json.gz similarity index 100% rename from edsnlp/resources/adicap.json.gz rename to edsnlp/edsnlp/resources/adicap.json.gz diff --git a/edsnlp/resources/cim10.csv.gz b/edsnlp/edsnlp/resources/cim10.csv.gz similarity index 100% rename from edsnlp/resources/cim10.csv.gz rename to edsnlp/edsnlp/resources/cim10.csv.gz diff --git a/edsnlp/resources/drugs.json b/edsnlp/edsnlp/resources/drugs.json similarity index 100% rename from edsnlp/resources/drugs.json rename to edsnlp/edsnlp/resources/drugs.json diff --git a/edsnlp/resources/verbs.csv.gz b/edsnlp/edsnlp/resources/verbs.csv.gz similarity index 100% rename from edsnlp/resources/verbs.csv.gz rename to edsnlp/edsnlp/resources/verbs.csv.gz diff --git a/edsnlp/utils/__init__.py b/edsnlp/edsnlp/utils/__init__.py similarity index 100% rename from edsnlp/utils/__init__.py rename to edsnlp/edsnlp/utils/__init__.py diff --git a/edsnlp/utils/blocs.py b/edsnlp/edsnlp/utils/blocs.py similarity index 100% rename from edsnlp/utils/blocs.py rename to edsnlp/edsnlp/utils/blocs.py diff --git a/edsnlp/utils/colors.py b/edsnlp/edsnlp/utils/colors.py similarity index 100% rename from edsnlp/utils/colors.py rename to edsnlp/edsnlp/utils/colors.py diff --git a/edsnlp/utils/deprecation.py b/edsnlp/edsnlp/utils/deprecation.py similarity index 100% rename from edsnlp/utils/deprecation.py rename to edsnlp/edsnlp/utils/deprecation.py diff --git a/edsnlp/utils/examples.py b/edsnlp/edsnlp/utils/examples.py similarity index 100% rename from edsnlp/utils/examples.py rename to edsnlp/edsnlp/utils/examples.py diff --git a/edsnlp/utils/extensions.py b/edsnlp/edsnlp/utils/extensions.py similarity index 100% rename from edsnlp/utils/extensions.py rename to edsnlp/edsnlp/utils/extensions.py diff --git a/edsnlp/utils/filter.py b/edsnlp/edsnlp/utils/filter.py similarity index 69% rename from edsnlp/utils/filter.py rename to edsnlp/edsnlp/utils/filter.py index d849f7074..9d8240d97 100644 --- a/edsnlp/utils/filter.py +++ b/edsnlp/edsnlp/utils/filter.py @@ -1,6 +1,7 @@ -from typing import Any, Callable, Iterable, List, Optional, Tuple, Union +from typing import Any, Callable, Iterable, List, Optional, Sequence, Tuple, Union from spacy.tokens import Span +from spacy.tokens.doc import Doc def default_sort_key(span: Span) -> Tuple[int, int]: @@ -206,3 +207,100 @@ def get_spans(spans: List[Span], label: Union[int, str]) -> List[Span]: return [span for span in spans if span.label == label] else: return [span for span in spans if span.label_ == label] + + +def span_f1(a: Span, b: Span) -> float: + """ + Computes the F1 overlap between two spans. + + Parameters + ---------- + a: Span + First span + b: Span + Second span + + Returns + ------- + float + F1 overlap + """ + start_a, end_a = a.start, a.end + start_b, end_b = b.start, b.end + overlap = max(0, min(end_a, end_b) - max(start_a, start_b)) + return 2 * overlap / (end_a - start_a + end_b - start_b) + + +def align_spans( + source: Sequence[Span], + target: Sequence[Span], + sort_by_overlap: bool = False, +) -> List[List[Span]]: + """ + Aligns two lists of spans, by matching source spans that overlap target spans. + This function is optimized to avoid quadratic complexity. + + Parameters + ---------- + source : List[Span] + List of spans to align. + target : List[Span] + List of spans to align. + sort_by_overlap : bool + Whether to sort the aligned spans by maximum dice/f1 overlap + with the target span. + + Returns + ------- + List[List[Span]] + Subset of `source` spans for each target span + """ + source = sorted(source, key=lambda x: (x.start, x.end)) + target = sorted(target, key=lambda x: (x.start, x.end)) + + aligned = [set() for _ in target] + source_idx = 0 + for target_idx in range(len(target)): + while source[source_idx].end <= target[target_idx].start: + source_idx += 1 + i = source_idx + while i < len(source) and source[i].start <= target[target_idx].end: + aligned[target_idx].add(source[i]) + i += 1 + + aligned = [list(span_set) for span_set in aligned] + + # Sort the aligned spans by maximum dice/f1 overlap with the target span + if sort_by_overlap: + aligned = [ + sorted(span_set, key=lambda x: span_f1(x, y), reverse=True) + for span_set, y in zip(aligned, target) + ] + + return aligned + + +def get_span_group(doclike: Union[Doc, Span], group: str) -> List[Span]: + """ + Get the spans of a span group that are contained inside a doclike object. + + Parameters + ---------- + doclike : Union[Doc, Span] + Doclike object to act as a mask. + group : str + Group name from which to get the spans. + + Returns + ------- + List[Span] + List of spans. + """ + if isinstance(doclike, Doc): + return [span for span in doclike.spans.get(group, ())] + else: + return [ + span + for span in doclike.doc.spans.get(group, ()) + if span.start >= doclike.start and span.end <= doclike.end + ] \ No newline at end of file diff --git a/edsnlp/utils/inclusion.py b/edsnlp/edsnlp/utils/inclusion.py similarity index 100% rename from edsnlp/utils/inclusion.py rename to edsnlp/edsnlp/utils/inclusion.py diff --git a/edsnlp/utils/lists.py b/edsnlp/edsnlp/utils/lists.py similarity index 100% rename from edsnlp/utils/lists.py rename to edsnlp/edsnlp/utils/lists.py diff --git a/edsnlp/utils/merge_configs.py b/edsnlp/edsnlp/utils/merge_configs.py similarity index 100% rename from edsnlp/utils/merge_configs.py rename to edsnlp/edsnlp/utils/merge_configs.py diff --git a/edsnlp/utils/regex.py b/edsnlp/edsnlp/utils/regex.py similarity index 100% rename from edsnlp/utils/regex.py rename to edsnlp/edsnlp/utils/regex.py diff --git a/edsnlp/utils/resources.py b/edsnlp/edsnlp/utils/resources.py similarity index 100% rename from edsnlp/utils/resources.py rename to edsnlp/edsnlp/utils/resources.py diff --git a/edsnlp/edsnlp/utils/span_getters.py b/edsnlp/edsnlp/utils/span_getters.py new file mode 100644 index 000000000..0a6b9c431 --- /dev/null +++ b/edsnlp/edsnlp/utils/span_getters.py @@ -0,0 +1,96 @@ +from typing import Dict, List, Mapping, Optional, Sequence, Tuple, Union + +from spacy import registry +from spacy.tokens import Doc, Span + +Spans = List[Span] +SpanGroups = Dict[str, Spans] + + +class make_span_getter: + def __init__( + self, + on_ents: Optional[Union[bool, Sequence[str]]] = None, + on_spans_groups: Union[ + bool, Sequence[str], Mapping[str, Union[bool, Sequence[str]]] + ] = False, + ): + + """ + Make a span qualifier candidate getter function. + + Parameters + ---------- + on_ents: Union[bool, Sequence[str]] + Whether to look into `doc.ents` for spans to classify. If a list of strings + is provided, only the span of the given labels will be considered. If None + and `on_spans_groups` is False, labels mentioned in `label_constraints` + will be used. + on_spans_groups: Union[bool, Sequence[str], Mapping[str, Sequence[str]]] + Whether to look into `doc.spans` for spans to classify: + + - If True, all span groups will be considered + - If False, no span group will be considered + - If a list of str is provided, only these span groups will be kept + - If a mapping is provided, the keys are the span group names and the values + are either a list of allowed labels in the group or True to keep them all + """ + + if not on_spans_groups and on_ents is None: + on_ents = True + + self.on_ents = on_ents + self.on_spans_groups = on_spans_groups + + def __call__( + self, + doc: Doc, + return_origin: bool = False, + ) -> Union[Tuple[Spans], Tuple[Spans, Optional[Spans], SpanGroups]]: + flattened_spans = [] + span_groups = {} + ents = None + if self.on_ents: + # /!\ doc.ents is not a list but a Span iterator, so to ensure referential + # equality between the spans of `flattened_spans` and `ents`, + # we need to convert it to a list to "extract" the spans first + ents = list(doc.ents) + if isinstance(self.on_ents, Sequence): + flattened_spans.extend( + span for span in ents if span.label_ in self.on_ents + ) + else: + flattened_spans.extend(ents) + + if self.on_spans_groups: + if isinstance(self.on_spans_groups, Mapping): + for name, labels in self.on_spans_groups.items(): + if labels: + span_groups[name] = list(doc.spans.get(name, ())) + if isinstance(labels, Sequence): + flattened_spans.extend( + span + for span in span_groups[name] + if span.label_ in labels + ) + else: + flattened_spans.extend(span_groups[name]) + elif isinstance(self.on_spans_groups, Sequence): + for name in self.on_spans_groups: + span_groups[name] = list(doc.spans.get(name, ())) + flattened_spans.extend(span_groups[name]) + else: + for name, spans_ in doc.spans.items(): + # /!\ spans_ is not a list but a SpanGroup, so to ensure referential + # equality between the spans of `flattened_spans` and `span_groups`, + # we need to convert it to a list to "extract" the spans first + span_groups[name] = list(spans_) + flattened_spans.extend(span_groups[name]) + + if return_origin: + return flattened_spans, ents, span_groups + else: + return flattened_spans + + +registry.misc("eds.span_getter")(make_span_getter) diff --git a/edsnlp/utils/training.py b/edsnlp/edsnlp/utils/training.py similarity index 100% rename from edsnlp/utils/training.py rename to edsnlp/edsnlp/utils/training.py diff --git a/edsnlp/viz/__init__.py b/edsnlp/edsnlp/viz/__init__.py similarity index 100% rename from edsnlp/viz/__init__.py rename to edsnlp/edsnlp/viz/__init__.py diff --git a/edsnlp/viz/quick_examples.py b/edsnlp/edsnlp/viz/quick_examples.py similarity index 100% rename from edsnlp/viz/quick_examples.py rename to edsnlp/edsnlp/viz/quick_examples.py diff --git a/mkdocs.yml b/edsnlp/mkdocs.yml similarity index 100% rename from mkdocs.yml rename to edsnlp/mkdocs.yml diff --git a/notebooks/README.md b/edsnlp/notebooks/README.md similarity index 100% rename from notebooks/README.md rename to edsnlp/notebooks/README.md diff --git a/notebooks/connectors/context.py b/edsnlp/notebooks/connectors/context.py similarity index 100% rename from notebooks/connectors/context.py rename to edsnlp/notebooks/connectors/context.py diff --git a/edsnlp/notebooks/connectors/omop.md b/edsnlp/notebooks/connectors/omop.md new file mode 100644 index 000000000..beab4d523 --- /dev/null +++ b/edsnlp/notebooks/connectors/omop.md @@ -0,0 +1,99 @@ +--- +jupyter: + jupytext: + formats: md,ipynb + main_language: python + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.13.0 + kernelspec: + display_name: "Python 3.9.5 64-bit ('.env': venv)" + name: python3 +--- + +```python +import context +``` + +```python +import spacy +``` + +```python +from edsnlp.connectors.omop import OmopConnector +``` + +```python + +``` + +# Date detection + +```python +text = ( + "Le patient est arrivé le 23 août (23/08/2021). " + "Il dit avoir eu mal au ventre hier. " + "L'année dernière, on lui avait prescrit du doliprane." +) +``` + +```python +nlp = spacy.blank('fr') +``` + +```python +nlp.add_pipe('normalizer') +nlp.add_pipe('matcher', config=dict(regex=dict(word=r"(\w+)"))) +``` + +```python +doc = nlp(text) +``` + +```python +doc._.note_id = 0 +``` + +```python +docs = [] + +for i in range(10): + doc = nlp(f"Doc{i:02}" + text) + doc._.note_id = i + docs.append(doc) +``` + +```python +connector = OmopConnector(nlp) +``` + +```python +note, note_nlp = connector.docs2omop(docs) +``` + +```python +note +``` + +```python +new_docs = connector.omop2docs(note, note_nlp) +``` + +```python +new_docs[0].text == docs[0].text +``` + +```python +len(docs[0].ents) == len(new_docs[0].ents) +``` + +```python +for e, o in zip(new_docs[0].ents, docs[0].ents): + assert e.text == o.text +``` + +```python + +``` diff --git a/edsnlp/notebooks/context.py b/edsnlp/notebooks/context.py new file mode 100644 index 000000000..4b1f5cb4e --- /dev/null +++ b/edsnlp/notebooks/context.py @@ -0,0 +1,5 @@ +import os +import sys + +REPO_PATH = os.path.abspath(os.path.join(os.path.dirname("__file__"), "..")) +sys.path.insert(0, REPO_PATH) diff --git a/edsnlp/notebooks/dates/context.py b/edsnlp/notebooks/dates/context.py new file mode 100644 index 000000000..cdd4b6470 --- /dev/null +++ b/edsnlp/notebooks/dates/context.py @@ -0,0 +1,5 @@ +import os +import sys + +REPO_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, REPO_PATH) diff --git a/edsnlp/notebooks/dates/prototype.md b/edsnlp/notebooks/dates/prototype.md new file mode 100644 index 000000000..97e588ef4 --- /dev/null +++ b/edsnlp/notebooks/dates/prototype.md @@ -0,0 +1,89 @@ +--- +jupyter: + jupytext: + formats: md,ipynb + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +```python +%reload_ext autoreload +%autoreload 2 +``` + +```python +import spacy +from spacy import displacy +from spacy.tokens import Doc +``` + +```python +from edsnlp.utils.colors import create_colors +``` + +# Dates + +```python +nlp = spacy.blank('fr') +dates = nlp.add_pipe('eds.dates', config=dict(detect_periods=True)) +``` + +```python +text = "le 5 janvier à 15h32 cette année il y a trois semaines pdt 1 mois" +``` + +```python +doc = nlp(text) +``` + +```python +ds = doc.spans['dates'] +``` + +```python +colors = create_colors(['absolute', 'relative', 'duration']) + +def display_dates(doc: Doc): + doc.ents = doc.spans['dates'] + return displacy.render(doc, style='ent', options=dict(colors=colors)) +``` + +```python +display_dates(doc) +``` + +```python +for date in ds: + print(f"{str(date):<25}{repr(date._.date)}") +``` + +```python +for date in ds: + print(f"{str(date):<25}{date._.date.dict(exclude_none=True)}") +``` + +```python +for date in ds: + print(f"{str(date):<25}{date._.date.to_datetime()}") +``` + +```python +for date in ds: + print(f"{str(date):<25}{date._.date.norm()}") +``` + +```python +for p in doc.spans['periods']: + print(f"{str(p):<40}{p._.period.dict()}") +``` + +```python + +``` diff --git a/edsnlp/notebooks/dates/user-guide.md b/edsnlp/notebooks/dates/user-guide.md new file mode 100644 index 000000000..052dbecfb --- /dev/null +++ b/edsnlp/notebooks/dates/user-guide.md @@ -0,0 +1,93 @@ +--- +jupyter: + jupytext: + formats: md,ipynb + main_language: python + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: 'Python 3.9.5 64-bit (''.env'': venv)' + name: python3 +--- + +```python +import context +``` + +```python +from edsnlp.pipelines.misc.dates import Dates, terms +``` + +```python +from datetime import datetime +``` + +```python +import spacy +``` + +# Date detection + +```python +text = ( + "Le patient est arrivé le 23 août (23/08/2021). " + "Il dit avoir eu mal au ventre hier. " + "L'année dernière, on lui avait prescrit du doliprane." +) +``` + +```python +nlp = spacy.blank('fr') +``` + +```python +doc = nlp(text) +``` + +```python +dates = Dates( + nlp, + absolute=terms.absolute, + relative=terms.relative, + no_year=terms.no_year, +) +``` + +```python +dates(doc) +``` + +```python +doc.spans +``` + +```python +print(f"{'expression':<20} label") +print(f"{'----------':<20} -----") + +for span in doc.spans['dates']: + print(f"{span.text:<20} {span._.date}") +``` + +Lorsque la date du document n'est pas connue, le label des dates relatives (hier, il y a quinze jours, etc) devient `TD±` + +Si on renseigne l'extension `note_datetime` : + +```python +doc._.note_datetime = datetime(2020, 10, 10) +``` + +```python +dates(doc) +``` + +```python +print(f"{'expression':<20} label") +print(f"{'----------':<20} -----") + +for span in doc.spans['dates']: + print(f"{span.text:<20} {span._.date}") +``` diff --git a/edsnlp/notebooks/endlines/endlines-example.md b/edsnlp/notebooks/endlines/endlines-example.md new file mode 100644 index 000000000..d8a1236ad --- /dev/null +++ b/edsnlp/notebooks/endlines/endlines-example.md @@ -0,0 +1,182 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.13.0 + kernelspec: + display_name: "Python 3.7.1 64-bit ('env_debug': conda)" + name: python3 +--- + +```python +%load_ext autoreload +%autoreload 2 +``` + +```python +import spacy +``` + +```python +from edsnlp.pipelines.endlines.endlinesmodel import EndLinesModel +``` + +```python +import pandas as pd +``` + +```python +from spacy import displacy +``` + +# Train + +```python +nlp = spacy.blank("fr") +``` + +```python +text = r"""Le patient est arrivé hier soir. +Il est accompagné par son fils + +ANTECEDENTS +Il a fait une TS en 2010; +Fumeur, il est arreté il a 5 mois +Chirurgie de coeur en 2011 +CONCLUSION +Il doit prendre +le medicament indiqué 3 fois par jour. Revoir médecin +dans 1 mois. +DIAGNOSTIC : + +Antecedents Familiaux: +- 1. Père avec diabete + +""" +``` + +```python +doc = nlp(text) +``` + +```python +text2 = """J'aime le \nfromage...\n""" +doc2 = nlp(text2) +``` + +```python +text3 = '\nIntervention(s) - acte(s) réalisé(s) :\nParathyroïdectomie élective le [DATE]' +doc3 = nlp(text3) +``` + +```python +corpus = [doc,doc2, doc3] +``` + +```python +endlines = EndLinesModel(nlp = nlp) +``` + +```python +df = endlines.fit_and_predict(corpus) +df.head() +``` + +```python +pd.set_option("max_columns",None) +``` + +```python +# Save model +PATH= "/path_to_model" +endlines.save() +``` + +# Predict + +```python +df2 = pd.DataFrame({"A1":[12646014,4191891561709484510 , 1668228190683662995], + "A2":[12646065887601541794,4191891561709484510 , 1668228190683662995], + "A3": ["UPPER","DIGIT","sdf"], + "A4": ["DIGIT","ENUMERATION","STRONG_PUNCT"], + "B1": [.5,.7,10.2], + "B2": [.0,.2,-10.2], + "BLANK_LINE":[False,True,False]}) +df2 = endlines.predict(df2) +df2 +``` + +# Set spans in training data (for viz) + +```python +set_spans = endlines.set_spans +``` + +```python +set_spans(corpus, df) +``` + +```python +df.loc[df.DOC_ID==1] +``` + +```python +doc_exemple = corpus[1] +``` + +```python +doc_exemple.spans +``` + +```python +doc_exemple.ents = tuple(doc_exemple.spans['new_lines']) +``` + +```python +displacy.render(doc_exemple, style="ent", options={"colors":{"end_line":"green","space":"red"}}) +``` + +# Pipe spacy (inference) + +```python + +``` + +```python +nlp = spacy.blank("fr") +``` + +```python +nlp.add_pipe("endlines", config=dict(model_path = PATH)) +``` + +```python +docs2 = list(nlp.pipe([text,text2,text3])) +``` + +```python +doc_exemple = docs2[1] +``` + +```python +doc_exemple +``` + +```python +from edsnlp.utils.filter import filter_spans +spaces = tuple(s for s in doc_exemple.spans['new_lines'] if s.label_=="space") +ents = doc_exemple.ents + spaces +ents_f = filter_spans(ents) +doc_exemple.ents = ents_f +``` + +```python +displacy.render(doc_exemple, style="ent", options={"colors":{"space":"red"}}) +``` + +```python + +``` diff --git a/edsnlp/notebooks/normalizer/context.py b/edsnlp/notebooks/normalizer/context.py new file mode 100644 index 000000000..cdd4b6470 --- /dev/null +++ b/edsnlp/notebooks/normalizer/context.py @@ -0,0 +1,5 @@ +import os +import sys + +REPO_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, REPO_PATH) diff --git a/edsnlp/notebooks/normalizer/profiling.md b/edsnlp/notebooks/normalizer/profiling.md new file mode 100644 index 000000000..7021ec8a2 --- /dev/null +++ b/edsnlp/notebooks/normalizer/profiling.md @@ -0,0 +1,235 @@ +--- +jupyter: + jupytext: + formats: md,ipynb + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.13.4 + kernelspec: + display_name: "Python 3.9.5 64-bit ('.venv': venv)" + language: python + name: python3 +--- + +```python +%reload_ext autoreload +%autoreload 2 +``` + +```python +import context +``` + +```python + +``` + +```python +import spacy +``` + +# Date detection + +```python +text = ( + "Le patient est admis pour des douleurs dans le bras droit, mais n'a pas de problème de locomotion. " + "Historique d'AVC dans la famille. pourrait être un cas de rhume.\n" + "NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNBWbWbNbNbNBWbNbWbNbWBNbNbWbNbNBNbWbWbNbWBNbNbWbNBNbWbWbNb\n" + "Pourrait être un cas de rhume.\n" + "Motif :\n" + "Douleurs dans le bras droit.\n" + "ANTÉCÉDENTS\n" + "Le patient est déjà venu\n" + "Pas d'anomalie détectée.\n\n" +) * 10 +``` + +```python +nlp = spacy.blank('fr') +# nlp.add_pipe('lowercase') +# nlp.add_pipe('accents') +# nlp.add_pipe('pollution') +# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) +nlp.add_pipe('sentences') +nlp.add_pipe( + "matcher", + name="matcher", + config=dict( + attr='TEXT', + regex=dict(anomalie=r"anomalie"), + ), +) +nlp.add_pipe('negation', config=dict(attr='TEXT')) +``` + +```python +%%timeit +nlp(text) +``` + +```python +nlp = spacy.blank('fr') +nlp.add_pipe('lowercase') +# nlp.add_pipe('accents') +# nlp.add_pipe('pollution') +# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) +nlp.add_pipe('sentences') +nlp.add_pipe( + "matcher", + name="matcher", + config=dict( + attr='TEXT', + regex=dict(anomalie=r"anomalie"), + ), +) +nlp.add_pipe('negation', config=dict(attr='TEXT')) +``` + +```python +%%timeit +nlp(text) +``` + +```python +nlp = spacy.blank('fr') +nlp.add_pipe('lowercase') +nlp.add_pipe('accents') +# nlp.add_pipe('pollution') +# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) +nlp.add_pipe('sentences') +nlp.add_pipe( + "matcher", + name="matcher", + config=dict( + attr='TEXT', + regex=dict(anomalie=r"anomalie"), + ), +) +nlp.add_pipe('negation', config=dict(attr='TEXT')) +``` + +```python +%%timeit +nlp(text) +``` + +```python +nlp = spacy.blank('fr') +nlp.add_pipe('lowercase') +nlp.add_pipe('accents') +nlp.add_pipe('pollution') +# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) +nlp.add_pipe('sentences') +nlp.add_pipe( + "matcher", + name="matcher", + config=dict( + attr='TEXT', + regex=dict(anomalie=r"anomalie"), + ), +) +nlp.add_pipe('negation', config=dict(attr='TEXT')) +``` + +```python +%%timeit +nlp(text) +``` + +```python +nlp = spacy.blank('fr') +nlp.add_pipe('normalizer') +nlp.add_pipe('sentences') +nlp.add_pipe( + "matcher", + name="matcher", + config=dict( + attr='TEXT', + regex=dict(anomalie=r"anomalie"), + ), +) +nlp.add_pipe('negation', config=dict(attr='TEXT')) +``` + +```python +%%timeit +nlp(text) +``` + +```python +nlp = spacy.blank('fr') +# nlp.add_pipe('lowercase') +# nlp.add_pipe('accents') +# nlp.add_pipe('pollution') +# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) +nlp.add_pipe('normalizer') +nlp.add_pipe('sentences') +nlp.add_pipe( + "matcher", + name="matcher", + config=dict( + attr='CUSTOM_NORM', + regex=dict(anomalie=r"anomalie"), + ), +) +nlp.add_pipe('negation', config=dict(attr='TEXT')) +``` + +```python +%%timeit +nlp(text) +``` + +```python +nlp = spacy.blank('fr') +# nlp.add_pipe('lowercase') +# nlp.add_pipe('accents') +# nlp.add_pipe('pollution') +# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) +nlp.add_pipe('normalizer') +nlp.add_pipe('sentences') +nlp.add_pipe( + "matcher", + name="matcher", + config=dict( + attr='CUSTOM_NORM', + regex=dict(anomalie=r"anomalie"), + ), +) +nlp.add_pipe('negation', config=dict(attr='CUSTOM_NORM')) +``` + +```python +%%timeit +nlp(text) +``` + +```python +nlp = spacy.blank('fr') +# nlp.add_pipe('lowercase') +# nlp.add_pipe('accents') +# nlp.add_pipe('pollution') +# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) +nlp.add_pipe('normalizer') +nlp.add_pipe('sentences') +nlp.add_pipe( + "matcher", + name="matcher", + config=dict( + attr='CUSTOM_NORM', + regex=dict(anomalie=r"anomalie"), + ), +) +nlp.add_pipe('negation', config=dict(attr='CUSTOM_NORM')) +``` + +```python +%%timeit +nlp(text) +``` + +```python + +``` diff --git a/edsnlp/notebooks/normalizer/prototype.md b/edsnlp/notebooks/normalizer/prototype.md new file mode 100644 index 000000000..832bd1d75 --- /dev/null +++ b/edsnlp/notebooks/normalizer/prototype.md @@ -0,0 +1,109 @@ +--- +jupyter: + jupytext: + formats: md,ipynb + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.13.4 + kernelspec: + display_name: "Python 3.9.5 64-bit ('.venv': venv)" + language: python + name: python3 +--- + +```python +import context +``` + +```python +import spacy +from spacy.matcher import Matcher +``` + +```python +from spacy.tokens import Span +``` + +```python +from edsnlp.matchers.exclusion import ExclusionMatcher +``` + +```python + +``` + +```python +Span.set_extension('normalized_variant', getter=lambda s: ''.join([t.text + t.whitespace_ for t in s if not t._.excluded]).rstrip(' ')) +``` + +# Test normalisation + +```python +text = "Le patient est atteint d'une pneumopathie à NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNB coronavirus" +``` + +```python +phrase = "pneumopathie à coronavirus" +``` + +## Clean doc method + +```python +nlp = spacy.blank('fr') +nlp.add_pipe('normalizer', config=dict(lowercase=False, quotes=False, accents=False, pollution=True)) +nlp.add_pipe('matcher', config=dict(terms=dict(covid=[phrase]), attr="CUSTOM_NORM")) +``` + +```python +%timeit doc = nlp(text) +``` + +## Matcher method + +```python +nlp = spacy.blank('fr') +nlp.add_pipe('pollution') +``` + +```python +def set_ents(doc, matcher): + doc.ents = list(matcher(doc, as_spans=True)) +``` + +```python +doc_pattern = nlp(phrase) +``` + +```python +matcher = ExclusionMatcher(nlp.vocab, attr="LOWER") +``` + +```python +matcher.add('covid', [doc_pattern]) +``` + +```python +%%timeit +doc = nlp(text) +set_ents(doc, matcher) +``` + +```python +doc = nlp(text) +set_ents(doc, matcher) +``` + +```python +for token in doc: + print(token.norm_, token._.excluded) +``` + +```python +doc.ents[0]._.normalized_variant +``` + +```python + +``` diff --git a/edsnlp/notebooks/pipeline.md b/edsnlp/notebooks/pipeline.md new file mode 100644 index 000000000..570c07470 --- /dev/null +++ b/edsnlp/notebooks/pipeline.md @@ -0,0 +1,193 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.11.2 + kernelspec: + display_name: "[2.4.3] Py3" + language: python + name: pyspark-2.4.3 +--- + +```python +%reload_ext autoreload +%autoreload 2 +``` + +```python +# Importation du "contexte", ie la bibliothèque sans installation +import context +``` + +```python +import spacy +``` + +```python +# One-shot import of all declared spaCy components + +``` + +```python +a = spacy.registry.get('factories','charlson') +``` + +```python +a() +``` + +# Baselines + +```python +text = ( + "Le patient est admis pour des douleurs dans le bras droit. mais n'a pas de problème de locomotion. \n" + "Historique d'AVC dans la famille mais pas chez les voisins\n" + "mais ne semble pas en être un\n" + "Charlson 7.\n" + "Pourrait être un cas de rhume du fait d'un hiver rigoureux.\n" + "Motif :\n" + "Douleurs dans le bras droit." +) +``` + +```python +nlp = spacy.blank('fr') +nlp.add_pipe('sentencizer') +# nlp.add_pipe('sentences') +nlp.add_pipe('matcher', config=dict(terms=dict(tabac=['Tabac']))) +nlp.add_pipe('normalizer') +nlp.add_pipe('hypothesis') +nlp.add_pipe('family', config=dict(on_ents_only=False)) +#nlp.add_pipe('charlson') +``` + +```python +text = "Tabac:\n" +``` + +```python +doc = nlp(text) +``` + +```python +print([(token.text, token._.hypothesis_) for token in doc if token._.hypothesis==True]) +``` + +```python +doc.ents[0].end +``` + +```python +from edsnlp.utils.inclusion import check_inclusion +ents = [ent for ent in doc.ents if check_inclusion(ent, 0, 2)] +ents +``` + +```python +print([(ent.text, ent.start, ent.end) for ent in doc.ents]) +``` + +```python +import thinc + +registered_func = spacy.registry.get("misc", "score_norm") +``` + +```python +@spacy.registry.misc("score_normalization.charlson") +def score_normalization(extracted_score): + """ + Charlson score normalization. + If available, returns the integer value of the Charlson score. + """ + score_range = list(range(0, 30)) + if (extracted_score is not None) and (int(extracted_score) in score_range): + return int(extracted_score) + +charlson_config = dict( + score_name = 'charlson', + regex = [r'charlson'], + value_extract = r"(\d+)", + score_normalization = "score_normalization.charlson" +) + +nlp = spacy.blank('fr') +nlp.add_pipe('sentences') +nlp.add_pipe('normalizer') +nlp.add_pipe('score', config = charlson_config) +``` + +```python +# nlp.add_pipe('sentencizer') +nlp.add_pipe('sentences') +nlp.add_pipe('normalizer') +nlp.add_pipe('matcher', config=dict(terms=dict(douleurs=['probleme de locomotion', 'douleurs']), attr='NORM')) +nlp.add_pipe('sections') +nlp.add_pipe('pollution') +``` + +```python +text = ( + "Le patient est admis pour des douleurs dans le bras droit, mais n'a pas de problème de locomotion. " + "Historique d'AVC dans la famille. pourrait être un cas de rhume.\n" + "NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNBWbWbNbNbNBWbNbWbNbWBNbNbWbNbNBNbWbWbNbWBNbNbWbNBNbWbWbNb\n" + "Pourrait être un cas de rhume.\n" + "Motif :\n" + "Douleurs dans le bras droit." +) +``` + +```python +doc = nlp(text) +``` + +```python +doc.ents[0]._.after_snippet +``` + +```python +doc._.sections +``` + +```python +doc._.clean_ +``` + +```python +doc[17]._.ascii_ +``` + +```python +doc._.clean_ +``` + +On peut tester l'extraction d'entité dans le texte nettoyé : + +```python +doc._.clean_[165:181] +``` + +Les deux textes ne sont plus alignés : + +```python +doc.text[165:181] +``` + +Mais la méthode `char_clean_span` permet de réaligner les deux représentations : + +```python +span = doc._.char_clean_span(165, 181) +span +``` + +```python +doc._.sections[0] +``` + +```python + +``` diff --git a/edsnlp/notebooks/premier-pipeline.md b/edsnlp/notebooks/premier-pipeline.md new file mode 100644 index 000000000..0fdad6a5c --- /dev/null +++ b/edsnlp/notebooks/premier-pipeline.md @@ -0,0 +1,260 @@ +--- +jupyter: + jupytext: + formats: md,ipynb + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.13.5 + kernelspec: + display_name: "Python 3.9.5 64-bit ('.env': venv)" + name: python3 +--- + + + +# EDS-NLP – Présentation + + + +## Texte d'exemple + +```python +with open('example.txt', 'r') as f: + text = f.read() +``` + +```python +print(text) +``` + +## Définition d'un pipeline spaCy + +```python slideshow={"slide_type": "slide"} +# Importation de spaCy +import spacy +``` + +```python +# Chargement des composants EDS-NLP + +``` + +```python +# Création de l'instance spaCy +nlp = spacy.blank('fr') + +# Normalisation des accents, de la casse et autres caractères spéciaux +nlp.add_pipe('normalizer') +# Détection des fins de phrases +nlp.add_pipe('sentences') + +# Extraction d'entités nommées +nlp.add_pipe( + 'matcher', + config=dict( + terms=dict(respiratoire=[ + 'difficultes respiratoires', + 'asthmatique', + 'toux', + ]), + regex=dict( + covid=r'(?i)(?:infection\sau\s)?(covid[\s\-]?19|corona[\s\-]?virus)', + traitement=r'(?i)traitements?|medicaments?'), + attr='NORM', + ), +) + +nlp.add_pipe('dates') + +# Qualification des entités +nlp.add_pipe('negation') +nlp.add_pipe('hypothesis') +nlp.add_pipe('family') +nlp.add_pipe('rspeech') +``` + +## Application du pipeline + +```python +doc = nlp(text) +``` + +```python +doc +``` + +Les traitements effectués par EDS-NLP (et spaCy en général) sont non-destructifs : + +```python +# Non-destruction +doc.text == text +``` + +Pour des tâches comme la normalisation, EDS-NLP ajoute des attributs aux tokens, sans perte d'information : + +```python +# Normalisation +print(f"{'texte':<15}", 'normalisation') +print(f"{'-----':<15}", '-------------') +for token in doc[3:15]: + print(f"{token.text:<15}", f"{token.norm_}") +``` + +Le pipeline que nous avons appliqué a extrait des entités avec le `matcher`. + +Les entités détectées se retrouvent dans l'attribut `ents` : + +```python +doc.ents +``` + +EDS-NLP étant fondée sur spaCy, on peut utiliser tous les outils proposés autour de cette bibliothèque : + +```python +from spacy import displacy +``` + +```python +displacy.render( + doc, + style='ent', + options={'colors': dict(respiratoire='green', covid='orange')}, +) +``` + +Focalisons-nous sur la première entité : + +```python +entity = doc.ents[0] +``` + +```python +entity +``` + +Chaque entité a été qualifiée par les pipelines de négation, hypothèse, etc. Ces pipelines utilisent des extensions spaCy pour stocker leur résultat : + +```python +entity._.negated +``` + +Le pipeline n'a pas détecté de négation pour cette entité. + +## Application du pipleline sur une table de textes + +Les textes seront le plus souvent disponibles sous la forme d'un DataFrame pandas, qu'on peut simuler ici : + +```python +import pandas as pd +``` + +```python +note = pd.DataFrame(dict(note_text=[text] * 10)) +note['note_id'] = range(len(note)) +note = note[['note_id', 'note_text']] +``` + +```python +note +``` + +On peut appliquer la pipeline à l'ensemble des documents en utilisant la fonction `nlp.pipe`, qui permet d'accélérer les traitements en les appliquant en parallèle : + +```python +# Ici on crée une liste qui va contenir les documents traités par spaCy +docs = list(nlp.pipe(note.note_text)) +``` + +On veut récupérer les entités détectées et les information associées (empans, qualification, etc) : + +```python +def get_entities(doc): + """Extract a list of qualified entities from a spaCy Doc object""" + entities = [] + + for ent in doc.ents: + entity = dict( + start=ent.start_char, + end=ent.end_char, + label=ent.label_, + lexical_variant=ent.text, + negated=ent._.negated, + hypothesis=ent._.hypothesis, + ) + + entities.append(entity) + + return entities +``` + +```python +note['entities'] = [get_entities(doc) for doc in nlp.pipe(note.note_text)] +``` + +```python +note +``` + +On peut maintenant récupérer les entités détectées au format `NOTE_NLP` (ou similaire) : + +```python +# Sélection des colonnes +note_nlp = note[['note_id', 'entities']] + +# "Explosion" des listes d'entités, et suppression des lignes vides (documents sans entité) +note_nlp = note_nlp.explode('entities').dropna() + +# Re-création de l'index, pour des raisons internes à pandas +note_nlp = note_nlp.reset_index(drop=True) +``` + +```python +note_nlp +``` + +Il faut maintenant passer d'une colonne de dictionnaires à une table `NOTE_NLP` : + +```python +note_nlp = note_nlp[['note_id']].join(pd.json_normalize(note_nlp.entities)) +``` + +```python +note_nlp +``` + +On peut aggréger la qualification des entités en une unique colonne : + +```python +# Création d'une colonne "discard" -> si l'entité est niée ou hypothétique, on la supprime des résultats +note_nlp['discard'] = note_nlp[['negated', 'hypothesis']].max(axis=1) +``` + +```python +note_nlp +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python +%%timeit +for text in texts: + nlp(text) +``` + +```python +%%timeit +for text in nlp.pipe(texts, n_process=-1): + pass +``` diff --git a/notebooks/sentences/context.py b/edsnlp/notebooks/sections/context.py similarity index 100% rename from notebooks/sentences/context.py rename to edsnlp/notebooks/sections/context.py diff --git a/edsnlp/notebooks/sections/section-dataset.md b/edsnlp/notebooks/sections/section-dataset.md new file mode 100644 index 000000000..951523002 --- /dev/null +++ b/edsnlp/notebooks/sections/section-dataset.md @@ -0,0 +1,158 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.11.4 + kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +```python +%reload_ext autoreload +%autoreload 2 +``` + +```python +import pandas as pd +``` + +```python +import os +``` + +```python +import context +``` + +```python +from edsnlp.utils.brat import BratConnector +``` + +# Sections dataset + + +Réutilisation du [travail réalisé par Ivan Lerner à l'EDS](https://gitlab.eds.aphp.fr/IvanL/section_dataset). + +```python +data_dir = '../../data/section_dataset/' +``` + +```python +brat = BratConnector(data_dir) +``` + +```python +texts, annotations = brat.get_brat() +``` + +```python +df = annotations[['lexical_variant']].drop_duplicates() +``` + +```python +df['section'] = '' +``` + +```python +df.to_csv('sections.tsv', sep='\t', index=False) +``` + +```python +annotated = pd.read_csv('sections.tsv', sep='\t') +``` + +```python +annotated.to_csv('annotated_sections.csv', index=False) +``` + +```python +annotated = pd.read_excel('sections.xlsx', sheet_name='Annotation', engine='openpyxl') +``` + +```python +annotated.columns = ['lexical_variant', 'section', 'keep', 'comment'] +``` + +```python +annotated.keep = annotated.keep.fillna('Oui') == 'Oui' +``` + +```python +annotated = annotated.query('keep')[['lexical_variant', 'section']] +``` + +```python +annotated.merge(annotations, on='lexical_variant').section.value_counts() +``` + +```python +annotated.lexical_variant = annotated.lexical_variant.str.lower() +``` + +```python +annotated_unnaccented = annotated.copy() +``` + +```python +from unidecode import unidecode +``` + +```python +annotated_unnaccented.lexical_variant = annotated_unnaccented.lexical_variant.apply(unidecode) +``` + +```python +# annotated = pd.concat([annotated, annotated_unnaccented]) +annotated = annotated_unnaccented +``` + +```python +annotated = annotated.drop_duplicates() +``` + +```python +annotated = annotated.sort_values(['lexical_variant', 'section']) +``` + +```python +annotated +``` + +```python +annotated = annotated.drop_duplicates() +``` + +```python +sections = { + section.replace(' ', '_'): list(annotated.query('section == @section').lexical_variant) + for section in annotated.section.unique() +} +``` + +```python +for k, v in sections.items(): + print(unidecode(k.replace(' ', '_')), '=', v) + print() +``` + +```python +sections = { + section: unidecode(section.replace(' ', '_')) + for section in annotated.section.unique() +} +``` + +```python +for k, v in sections.items(): + print(f"{repr(k)}: {v},") +``` + +```python + +``` diff --git a/edsnlp/notebooks/sections/testing.md b/edsnlp/notebooks/sections/testing.md new file mode 100644 index 000000000..6f859706e --- /dev/null +++ b/edsnlp/notebooks/sections/testing.md @@ -0,0 +1,168 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.11.4 + kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +```python +%reload_ext autoreload +%autoreload 2 +``` + +```python +import pandas as pd +``` + +```python +import os +``` + +```python +import context +``` + +```python +from edsnlp.utils.brat import BratConnector +``` + +```python + +``` + +```python +import spacy +``` + +# Sections dataset + +We are using [Ivan Lerner's work at EDS](https://gitlab.eds.aphp.fr/IvanL/section_dataset). Make sure you clone the repo. + +```python +data_dir = '../../data/section_dataset/' +``` + +```python +brat = BratConnector(data_dir) +``` + +```python +texts, annotations = brat.get_brat() +``` + +```python +texts +``` + +```python +nlp = spacy.blank('fr') +``` + +```python +nlp.add_pipe('normaliser') +nlp.add_pipe('sections') +``` + +```python +df = texts.copy() +``` + +```python +df['doc'] = df.note_text.apply(nlp) +``` + +```python +def assign_id(row): + row.doc._.note_id = row.note_id +``` + +```python +df.apply(assign_id, axis=1); +``` + +```python +df['matches'] = df.doc.apply(lambda d: [dict( + lexical_variant=s.text, + label=s.label_, + start=s.start_char, + end=s.end_char +) for s in d._.section_titles]) +``` + +```python +df = df[['note_text', 'note_id', 'matches']].explode('matches') +``` + +```python +df = df.dropna() +``` + +```python +df[['lexical_variant', 'label', 'start', 'end']] = df.matches.apply(pd.Series) +``` + +```python +df = df.drop('matches', axis=1) +``` + +```python +df.head(20) +``` + +```python +df = df.rename(columns={'start': 'offset_begin', 'end': 'offset_end', 'label': 'label_value'}) +``` + +```python +df['label_name'] = df.label_value +``` + +```python +df['modifier_type'] = '' +df['modifier_result'] = '' +``` + +```python +from ipywidgets import Output, Button, VBox, Layout, Text, HTML +from IPython.display import display +from labeltool.labelling import GlobalLabels, Labels, Labelling + +out = Output() +``` + +```python +labels = Labels() + +for label in df.label_value.unique(): + labels.add(name = label, + color = 'green', + selection_type = 'button') +``` + +```python +labeller = Labelling( + df, + save_path='testing.pickle', + labels_dict=labels.dict, + from_save=True, + out=out, + display=display, +) +``` + +```python +labeller.run() +out +``` + +```python + +``` diff --git a/edsnlp/notebooks/sentences/context.py b/edsnlp/notebooks/sentences/context.py new file mode 100644 index 000000000..c26859b87 --- /dev/null +++ b/edsnlp/notebooks/sentences/context.py @@ -0,0 +1,5 @@ +import os +import sys + +REPO_PATH = os.path.abspath(os.path.join(os.path.dirname("__file__"), "..", "..")) +sys.path.insert(0, REPO_PATH) diff --git a/notebooks/sentences/sentences.md b/edsnlp/notebooks/sentences/sentences.md similarity index 100% rename from notebooks/sentences/sentences.md rename to edsnlp/notebooks/sentences/sentences.md diff --git a/edsnlp/notebooks/tnm/prototype.md b/edsnlp/notebooks/tnm/prototype.md new file mode 100644 index 000000000..e260d2d2f --- /dev/null +++ b/edsnlp/notebooks/tnm/prototype.md @@ -0,0 +1,63 @@ +--- +jupyter: + jupytext: + formats: md,ipynb + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.0 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +```python +%reload_ext autoreload +%autoreload 2 +``` + +```python +import spacy +from spacy import displacy +from spacy.tokens import Doc +``` + +# TNM mentions + +```python +nlp = spacy.blank("fr") +dates = nlp.add_pipe("eds.tnm") +``` + +```python +text = "patient a un pTNM : pT0N2M1" +``` + +```python +doc = nlp(text) +``` + +```python +tnms = doc.spans['tnm'] +``` + +```python +def display_tnm(doc: Doc): + doc.ents = doc.spans['tnm'] + return displacy.render(doc, style='ent') +``` + +```python +display_tnm(doc) +``` + +```python +for tnm in tnms: + print(f"{str(tnm):<25}{repr(tnm._.value)}") +``` + +```python + +``` diff --git a/edsnlp/notebooks/tokenizer/context.py b/edsnlp/notebooks/tokenizer/context.py new file mode 100644 index 000000000..c26859b87 --- /dev/null +++ b/edsnlp/notebooks/tokenizer/context.py @@ -0,0 +1,5 @@ +import os +import sys + +REPO_PATH = os.path.abspath(os.path.join(os.path.dirname("__file__"), "..", "..")) +sys.path.insert(0, REPO_PATH) diff --git a/edsnlp/notebooks/tokenizer/tokenizer.md b/edsnlp/notebooks/tokenizer/tokenizer.md new file mode 100644 index 000000000..aba1a457f --- /dev/null +++ b/edsnlp/notebooks/tokenizer/tokenizer.md @@ -0,0 +1,141 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.11.4 + kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +```python +%reload_ext autoreload +%autoreload 2 +``` + +```python +# Importation du "contexte", ie la bibliothèque sans installation +import context +``` + +```python +import spacy +``` + +```python +# One-shot import of all declared spaCy components + +``` + +# Baselines + +```python +import re +import spacy + +from spacy.tokenizer import Tokenizer +from spacy.util import compile_prefix_regex, compile_suffix_regex + +# Ajout de règles supplémentaires pour gérer les infix +def custom_tokenizer(nlp): + infix_re = re.compile(r'''[\,\?\:\;\‘\’\`\“\”\"\'~/\(\)\.\+=(->)\$]''') + prefix_re = compile_prefix_regex(nlp.Defaults.prefixes + ['-']) + suffix_re = compile_suffix_regex(nlp.Defaults.suffixes) + return Tokenizer( + nlp.vocab, + prefix_search=prefix_re.search, + suffix_search=suffix_re.search, + infix_finditer=infix_re.finditer, + ) + +def new_nlp(): + + nlp = spacy.blank('fr') + nlp.tokenizer = custom_tokenizer(nlp) + + return nlp +``` + +```python +nlp = new_nlp() +``` + +```python +# nlp.add_pipe('sentencizer') +nlp.add_pipe('matcher', config=dict(regex=dict(douleurs=['blème de locomotion', 'douleurs', 'IMV']))) +nlp.add_pipe('sections') +nlp.add_pipe('pollution') +``` + +```python +text = ( + "Le patient est admis pour des douleurs dans le bras droit, mais n'a pas de problème de locomotion. Test(et oui) " + "Historique d'AVC dans la famille. pourrait être un cas de rhume.\n" + "NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNBWbWbNbNbNBWbNbWbNbWBNbNbWbNbNBNbWbWbNbWBNbNbWbNBNbWbWbNb\n" + "IMV--deshabillé\n" + "Pourrait être un cas de rhume.\n" + "Motif :\n" + "-problème de locomotions==+test\n" + "Douleurs dans le bras droit." +) +``` + +```python +doc = nlp(text) +``` + +```python +doc.ents +``` + +```python +doc[19] +``` + +```python +doc._.sections +``` + +```python +doc._.clean_ +``` + +```python +doc[17]._.ascii_ +``` + +```python +doc._.clean_ +``` + +On peut tester l'extraction d'entité dans le texte nettoyé : + +```python +doc_clean = nlp(doc._.clean_) +``` + +```python +ent = doc_clean[64:68] +ent +``` + +Les deux textes ne sont plus alignés : + +```python +doc.text[ent.start_char:ent.end_char] +``` + +Mais la méthode `char_clean_span` permet de réaligner les deux représentations : + +```python +doc._.char_clean_span(ent.start_char, ent.end_char) +``` + +```python + +``` diff --git a/edsnlp/notebooks/utilities/brat.md b/edsnlp/notebooks/utilities/brat.md new file mode 100644 index 000000000..e64321554 --- /dev/null +++ b/edsnlp/notebooks/utilities/brat.md @@ -0,0 +1,99 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: "1.3" + jupytext_version: 1.11.4 + kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +```python +%reload_ext autoreload +%autoreload 2 +``` + +```python +# Importation du "contexte", ie la bibliothèque sans installation +import context +``` + +```python +import spacy +``` + +```python +import pandas as pd +``` + +```python +# One-shot import of all declared spaCy components +from edsnlp.utils.brat import BratConnector +``` + +# BRAT connector + +```python +brat = BratConnector('../../data/section_dataset/') +``` + +```python +texts = brat.read_texts() +``` + +```python +texts.head() +``` + +```python +brat.read_brat_annotation('BMI_4406356.txt.txt') +``` + +```python +texts, annotations = brat.get_brat() +``` + +```python +annotations +``` + +```python +nlp = spacy.blank('fr') +``` + +```python +docs = brat.brat2docs(nlp) +``` + +```python +doc = docs[0] +``` + +```python +doc.ents +``` + +```python +doc.ents[0] +``` + +```python +annotations.head() +``` + +```python +brat = BratConnector('test') +``` + +```python +brat.docs2brat(docs) +``` + +```python + +``` diff --git a/edsnlp/notebooks/utilities/context.py b/edsnlp/notebooks/utilities/context.py new file mode 100644 index 000000000..c26859b87 --- /dev/null +++ b/edsnlp/notebooks/utilities/context.py @@ -0,0 +1,5 @@ +import os +import sys + +REPO_PATH = os.path.abspath(os.path.join(os.path.dirname("__file__"), "..", "..")) +sys.path.insert(0, REPO_PATH) diff --git a/edsnlp/pipelines/base.py b/edsnlp/pipelines/base.py deleted file mode 100644 index a891113b9..000000000 --- a/edsnlp/pipelines/base.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import List, Optional, Tuple - -from spacy.tokens import Doc, Span - - -class BaseComponent(object): - """ - The `BaseComponent` adds a `set_extensions` method, - called at the creation of the object. - - It helps decouple the initialisation of the pipeline from - the creation of extensions, and is particularly usefull when - distributing EDSNLP on a cluster, since the serialisation mechanism - imposes that the extensions be reset. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.set_extensions() - - @classmethod - def set_extensions(cls) -> None: - """ - Set `Doc`, `Span` and `Token` extensions. - """ - pass - - def _boundaries( - self, doc: Doc, terminations: Optional[List[Span]] = None - ) -> List[Tuple[int, int]]: - """ - Create sub sentences based sentences and terminations found in text. - - Parameters - ---------- - doc: - spaCy Doc object - terminations: - List of tuples with (match_id, start, end) - - Returns - ------- - boundaries: - List of tuples with (start, end) of spans - """ - - if terminations is None: - terminations = [] - - sent_starts = [sent.start for sent in doc.sents] - termination_starts = [t.start for t in terminations] - - starts = sent_starts + termination_starts + [len(doc)] - - # Remove duplicates - starts = list(set(starts)) - - # Sort starts - starts.sort() - - boundaries = [(start, end) for start, end in zip(starts[:-1], starts[1:])] - - return boundaries diff --git a/pyproject.toml b/edsnlp/pyproject.toml similarity index 87% rename from pyproject.toml rename to edsnlp/pyproject.toml index 5f65bfd6c..376635b5f 100644 --- a/pyproject.toml +++ b/edsnlp/pyproject.toml @@ -111,14 +111,24 @@ where = ["."] "measurements" = "edsnlp.components:measurements" "drugs" = "edsnlp.components:drugs" "nested_ner" = "edsnlp.components:nested_ner" +"span_qualifier" = "edsnlp.components:span_qualifier" "adicap" = "edsnlp.components:adicap" "umls" = "edsnlp.components:umls" +"clean-entities" = "edsnlp.pipelines.clean_entities:CleanEntities" + +[project.entry-points."spacy_readers"] +"eds.Corpus.v1" = "edsnlp.corpus_reader:Corpus" [project.entry-points."spacy_architectures"] -"eds.stack_crf_ner_model.v1" = "edsnlp.models.stack_crf_ner:create_model" +"eds.stack_crf_ner_model.v1" = "edsnlp.pipelines.trainable.nested_ner.stack_crf_ner:create_model" +"eds.span_multi_classifier.v1" = "edsnlp.pipelines.trainable.span_qualifier.span_multi_classifier:create_model" [project.entry-points."spacy_scorers"] -"eds.nested_ner_scorer.v1" = "edsnlp.pipelines.trainable.nested_ner:make_nested_ner_scorer" +"eds.nested_ner_scorer.v1" = "edsnlp.pipelines.trainable.nested_ner.nested_ner:make_nested_ner_scorer" +"eds.span_qualifier_scorer.v1" = "edsnlp.pipelines.trainable.span_qualifier.factory:create_scorer" + +[project.entry-points."spacy_misc"] +"eds.candidate_span_qualifier_getter" = "edsnlp.pipelines.trainable.span_qualifier.factory:create_candidate_getter" [project.entry-points."spacy_languages"] "eds" = "edsnlp.language:EDSLanguage" diff --git a/scripts/adicap.py b/edsnlp/scripts/adicap.py similarity index 100% rename from scripts/adicap.py rename to edsnlp/scripts/adicap.py diff --git a/scripts/cim10.py b/edsnlp/scripts/cim10.py similarity index 100% rename from scripts/cim10.py rename to edsnlp/scripts/cim10.py diff --git a/scripts/conjugate_verbs.py b/edsnlp/scripts/conjugate_verbs.py similarity index 100% rename from scripts/conjugate_verbs.py rename to edsnlp/scripts/conjugate_verbs.py diff --git a/scripts/context.py b/edsnlp/scripts/context.py similarity index 100% rename from scripts/context.py rename to edsnlp/scripts/context.py diff --git a/scripts/serve.py b/edsnlp/scripts/serve.py similarity index 100% rename from scripts/serve.py rename to edsnlp/scripts/serve.py diff --git a/setup.py b/edsnlp/setup.py similarity index 100% rename from setup.py rename to edsnlp/setup.py diff --git a/tests/conftest.py b/edsnlp/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to edsnlp/tests/conftest.py diff --git a/tests/connectors/test_brat.py b/edsnlp/tests/connectors/test_brat.py similarity index 100% rename from tests/connectors/test_brat.py rename to edsnlp/tests/connectors/test_brat.py diff --git a/tests/connectors/test_labeltool.py b/edsnlp/tests/connectors/test_labeltool.py similarity index 100% rename from tests/connectors/test_labeltool.py rename to edsnlp/tests/connectors/test_labeltool.py diff --git a/tests/connectors/test_omop.py b/edsnlp/tests/connectors/test_omop.py similarity index 100% rename from tests/connectors/test_omop.py rename to edsnlp/tests/connectors/test_omop.py diff --git a/tests/matchers/test_phrase.py b/edsnlp/tests/matchers/test_phrase.py similarity index 100% rename from tests/matchers/test_phrase.py rename to edsnlp/tests/matchers/test_phrase.py diff --git a/tests/matchers/test_regex.py b/edsnlp/tests/matchers/test_regex.py similarity index 100% rename from tests/matchers/test_regex.py rename to edsnlp/tests/matchers/test_regex.py diff --git a/tests/matchers/test_simstring.py b/edsnlp/tests/matchers/test_simstring.py similarity index 100% rename from tests/matchers/test_simstring.py rename to edsnlp/tests/matchers/test_simstring.py diff --git a/tests/pipelines/core/test_contextual_matcher.py b/edsnlp/tests/pipelines/core/test_contextual_matcher.py similarity index 100% rename from tests/pipelines/core/test_contextual_matcher.py rename to edsnlp/tests/pipelines/core/test_contextual_matcher.py diff --git a/tests/pipelines/core/test_endlines.py b/edsnlp/tests/pipelines/core/test_endlines.py similarity index 100% rename from tests/pipelines/core/test_endlines.py rename to edsnlp/tests/pipelines/core/test_endlines.py diff --git a/tests/pipelines/core/test_matcher.py b/edsnlp/tests/pipelines/core/test_matcher.py similarity index 100% rename from tests/pipelines/core/test_matcher.py rename to edsnlp/tests/pipelines/core/test_matcher.py diff --git a/tests/pipelines/core/test_normalisation.py b/edsnlp/tests/pipelines/core/test_normalisation.py similarity index 100% rename from tests/pipelines/core/test_normalisation.py rename to edsnlp/tests/pipelines/core/test_normalisation.py diff --git a/tests/pipelines/core/test_sentences.py b/edsnlp/tests/pipelines/core/test_sentences.py similarity index 100% rename from tests/pipelines/core/test_sentences.py rename to edsnlp/tests/pipelines/core/test_sentences.py diff --git a/tests/pipelines/core/test_terminology.py b/edsnlp/tests/pipelines/core/test_terminology.py similarity index 100% rename from tests/pipelines/core/test_terminology.py rename to edsnlp/tests/pipelines/core/test_terminology.py diff --git a/tests/pipelines/misc/test_consultation_date.py b/edsnlp/tests/pipelines/misc/test_consultation_date.py similarity index 100% rename from tests/pipelines/misc/test_consultation_date.py rename to edsnlp/tests/pipelines/misc/test_consultation_date.py diff --git a/tests/pipelines/misc/test_dates.py b/edsnlp/tests/pipelines/misc/test_dates.py similarity index 100% rename from tests/pipelines/misc/test_dates.py rename to edsnlp/tests/pipelines/misc/test_dates.py diff --git a/tests/pipelines/misc/test_measurements.py b/edsnlp/tests/pipelines/misc/test_measurements.py similarity index 100% rename from tests/pipelines/misc/test_measurements.py rename to edsnlp/tests/pipelines/misc/test_measurements.py diff --git a/tests/pipelines/misc/test_reason.py b/edsnlp/tests/pipelines/misc/test_reason.py similarity index 100% rename from tests/pipelines/misc/test_reason.py rename to edsnlp/tests/pipelines/misc/test_reason.py diff --git a/tests/pipelines/misc/test_sections.py b/edsnlp/tests/pipelines/misc/test_sections.py similarity index 100% rename from tests/pipelines/misc/test_sections.py rename to edsnlp/tests/pipelines/misc/test_sections.py diff --git a/tests/pipelines/ner/test_adicap.py b/edsnlp/tests/pipelines/ner/test_adicap.py similarity index 100% rename from tests/pipelines/ner/test_adicap.py rename to edsnlp/tests/pipelines/ner/test_adicap.py diff --git a/tests/pipelines/ner/test_adicap_decoder.py b/edsnlp/tests/pipelines/ner/test_adicap_decoder.py similarity index 100% rename from tests/pipelines/ner/test_adicap_decoder.py rename to edsnlp/tests/pipelines/ner/test_adicap_decoder.py diff --git a/tests/pipelines/ner/test_cim10.py b/edsnlp/tests/pipelines/ner/test_cim10.py similarity index 100% rename from tests/pipelines/ner/test_cim10.py rename to edsnlp/tests/pipelines/ner/test_cim10.py diff --git a/tests/pipelines/ner/test_covid.py b/edsnlp/tests/pipelines/ner/test_covid.py similarity index 100% rename from tests/pipelines/ner/test_covid.py rename to edsnlp/tests/pipelines/ner/test_covid.py diff --git a/tests/pipelines/ner/test_drugs.py b/edsnlp/tests/pipelines/ner/test_drugs.py similarity index 100% rename from tests/pipelines/ner/test_drugs.py rename to edsnlp/tests/pipelines/ner/test_drugs.py diff --git a/tests/pipelines/ner/test_score.py b/edsnlp/tests/pipelines/ner/test_score.py similarity index 100% rename from tests/pipelines/ner/test_score.py rename to edsnlp/tests/pipelines/ner/test_score.py diff --git a/tests/pipelines/ner/test_tnm.py b/edsnlp/tests/pipelines/ner/test_tnm.py similarity index 100% rename from tests/pipelines/ner/test_tnm.py rename to edsnlp/tests/pipelines/ner/test_tnm.py diff --git a/tests/pipelines/ner/test_umls.py b/edsnlp/tests/pipelines/ner/test_umls.py similarity index 100% rename from tests/pipelines/ner/test_umls.py rename to edsnlp/tests/pipelines/ner/test_umls.py diff --git a/tests/pipelines/qualifiers/conftest.py b/edsnlp/tests/pipelines/qualifiers/conftest.py similarity index 100% rename from tests/pipelines/qualifiers/conftest.py rename to edsnlp/tests/pipelines/qualifiers/conftest.py diff --git a/tests/pipelines/qualifiers/test_family.py b/edsnlp/tests/pipelines/qualifiers/test_family.py similarity index 100% rename from tests/pipelines/qualifiers/test_family.py rename to edsnlp/tests/pipelines/qualifiers/test_family.py diff --git a/tests/pipelines/qualifiers/test_history.py b/edsnlp/tests/pipelines/qualifiers/test_history.py similarity index 100% rename from tests/pipelines/qualifiers/test_history.py rename to edsnlp/tests/pipelines/qualifiers/test_history.py diff --git a/tests/pipelines/qualifiers/test_hypothesis.py b/edsnlp/tests/pipelines/qualifiers/test_hypothesis.py similarity index 100% rename from tests/pipelines/qualifiers/test_hypothesis.py rename to edsnlp/tests/pipelines/qualifiers/test_hypothesis.py diff --git a/tests/pipelines/qualifiers/test_negation.py b/edsnlp/tests/pipelines/qualifiers/test_negation.py similarity index 100% rename from tests/pipelines/qualifiers/test_negation.py rename to edsnlp/tests/pipelines/qualifiers/test_negation.py diff --git a/tests/pipelines/qualifiers/test_reported_speech.py b/edsnlp/tests/pipelines/qualifiers/test_reported_speech.py similarity index 100% rename from tests/pipelines/qualifiers/test_reported_speech.py rename to edsnlp/tests/pipelines/qualifiers/test_reported_speech.py diff --git a/tests/pipelines/test_pipelines.py b/edsnlp/tests/pipelines/test_pipelines.py similarity index 100% rename from tests/pipelines/test_pipelines.py rename to edsnlp/tests/pipelines/test_pipelines.py diff --git a/tests/pipelines/trainable/test_nested_ner.py b/edsnlp/tests/pipelines/trainable/test_nested_ner.py similarity index 100% rename from tests/pipelines/trainable/test_nested_ner.py rename to edsnlp/tests/pipelines/trainable/test_nested_ner.py diff --git a/tests/processing/test_processing.py b/edsnlp/tests/processing/test_processing.py similarity index 100% rename from tests/processing/test_processing.py rename to edsnlp/tests/processing/test_processing.py diff --git a/tests/readme.md b/edsnlp/tests/readme.md similarity index 100% rename from tests/readme.md rename to edsnlp/tests/readme.md diff --git a/tests/test_conjugator.py b/edsnlp/tests/test_conjugator.py similarity index 100% rename from tests/test_conjugator.py rename to edsnlp/tests/test_conjugator.py diff --git a/tests/test_docs.py b/edsnlp/tests/test_docs.py similarity index 100% rename from tests/test_docs.py rename to edsnlp/tests/test_docs.py diff --git a/tests/test_language.py b/edsnlp/tests/test_language.py similarity index 100% rename from tests/test_language.py rename to edsnlp/tests/test_language.py diff --git a/tests/utils/test_examples.py b/edsnlp/tests/utils/test_examples.py similarity index 100% rename from tests/utils/test_examples.py rename to edsnlp/tests/utils/test_examples.py diff --git a/tests/utils/test_filter.py b/edsnlp/tests/utils/test_filter.py similarity index 100% rename from tests/utils/test_filter.py rename to edsnlp/tests/utils/test_filter.py diff --git a/tests/utils/test_quick_examples.py b/edsnlp/tests/utils/test_quick_examples.py similarity index 100% rename from tests/utils/test_quick_examples.py rename to edsnlp/tests/utils/test_quick_examples.py diff --git a/notebooks/export_pandas_to_brat.py b/notebooks/export_pandas_to_brat.py new file mode 100644 index 000000000..51ff17ed2 --- /dev/null +++ b/notebooks/export_pandas_to_brat.py @@ -0,0 +1,55 @@ +import pandas as pd +import re + +def export_pandas_to_brat( + ann_path, + txt_path, + df_to_convert, + label_column_name, + span_column_name, + term_column_name, + annotation_column_name = None, +): + """ + - ann_path: str path where to write the ann file. + - txt_path: str path where is stored the txt linked to ann file. Useful to check if there are newlines. + - df_to_convert: Pandas df containing at least a column of labels, a column of spans and a column of terms. + - label_column_name: str name of the column in df_to_convert containing the labels. This column should be filled with str only. + - span_column_name: str name of the column in df_to_convert containing the spans. This column should be filled with lists only, + first element of each list being the beginning of the span and second element being the end. + - term_column_name: str name of the column in df_to_convert containing the raw str from the raw text. This column should be filled with str only. + - annotation_column_name: OPTIONAL str name of the column in df_to_convert containing the annotations. This column should be filled with str only. + If None, no annotation will be saved. + """ + + SEP = "\t" + ANNOTATION_LABEL = "AnnotatorNotes" + brat_raw = "" + n_annotation = 0 + + with open(txt_path, "r") as f: + txt_raw = f.read() + + if annotation_column_name: + df_to_convert = df_to_convert[[label_column_name, span_column_name, term_column_name, annotation_column_name]] + else: + # Create an empty annotation column so that we can iter + # In a generic pandas dataframe + df_to_convert = df_to_convert[[label_column_name, span_column_name, term_column_name]] + df_to_convert[annotation_column_name] = "" + + # Iter through df to write each line of ann file + for index, (label, span, term, annotation) in df_to_convert.iterrows(): + term_raw = txt_raw[span[0]:span[1]] + if "\n" in term_raw: + span_str = str(span[0]) + "".join(" " + str(span[0] + newline_index.start()) + ";" + str(span[0] + newline_index.start() + 1) for newline_index in re.finditer("\n", term_raw)) + " " + str(span[1]) + else: + span_str = str(span[0]) + " " + str(span[1]) + brat_raw += "T" + str(index+1) + SEP + label + " " + span_str + SEP + term + "\n" + if len(annotation): + n_annotation += 1 + brat_raw += "#" + str(n_annotation) + SEP + ANNOTATION_LABEL + " " + "T" + str(index+1) + SEP + annotation + "\n" + + brat_raw = brat_raw[:-2] + with open(ann_path, "w") as f: + print(brat_raw, file=f) diff --git a/notebooks/get_stats_by_section_on_cim10.md b/notebooks/get_stats_by_section_on_cim10.md new file mode 100644 index 000000000..0564e8c3c --- /dev/null +++ b/notebooks/get_stats_by_section_on_cim10.md @@ -0,0 +1,1828 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.15.0 + kernelspec: + display_name: BioMedics_client + language: python + name: biomedics_client +--- + +## TODO +- [x] REMGARDER LE SEUIL pour la positivité +- [x] regarder les patients en communs +- [x] regarder Hémoglobine et DFG +- [x] Finish fine tuning of CODER-EDS. Just execute `/export/home/cse200093/Jacques_Bio/normalisation/py_files/train_coder.sh` file up to 1M iterations (To know the number of iteration, just take a look at where the weigths of CODER-EDS are saved, i.e at `/export/home/cse200093/Jacques_Bio/data_bio/coder_output`. The files are saved with the number of iterations in their names.). Evaluate this model then with the files in `/export/home/cse200093/Jacques_Bio/normalisation/notebooks/coder` for example. +- [X] Requêter les médicaments en structuré ! +- [X] Finir la normalisation des médicaments NER +- [ ] Cleaner le code et mettre sur GitHub +- [ ] Récupérer les figures +- [ ] Commencer à rédiger + + +```python +%reload_ext autoreload +%autoreload 2 +%reload_ext jupyter_black +sc.cancelAllJobs() +``` + +```python +import os + +os.environ["OMP_NUM_THREADS"] = "16" +``` + +```python +from edsteva import improve_performances + +spark, sc, sql = improve_performances( + to_add_conf=[ + ("spark.yarn.max.executor.failures", "10"), + ("spark.executor.memory", "32g"), + ("spark.driver.memory", "32g"), + ("spark.driver.maxResultSize", "16g"), + ("spark.default.parallelism", 160), + ("spark.shuffle.service.enabled", "true"), + ("spark.sql.shuffle.partitions", 160), + ("spark.yarn.am.memory", "4g"), + ("spark.yarn.max.executor.failures", 10), + ("spark.dynamicAllocation.enabled", True), + ("spark.dynamicAllocation.minExecutors", "20"), + ("spark.dynamicAllocation.maxExecutors", "20"), + ("spark.executor.cores", "8"), + ] +) +``` + +```python +import pandas as pd +from os.path import isfile, isdir, join, basename +from os import listdir, mkdir +import spacy +from edsnlp.processing import pipe +import matplotlib.pyplot as plt +import numpy as np +from matplotlib_venn import venn3, venn2 +import altair as alt +from functools import reduce +from knowledge import TO_BE_MATCHED + +import sys + +BRAT_DIR = "/export/home/cse200093/scratch/BioMedics/data/CRH" +RES_DIR = "/export/home/cse200093/scratch/BioMedics/data/bio_results" +RES_DRUG_DIR = "/export/home/cse200093/scratch/BioMedics/data/drug_results" +``` + + +# Only execute the following cells if you want to recreate the inference dataset (i.e dataset based on CIM10). PLEASE USE THE ENV `[2.4.3] K8s Py3 client` FOR THIS DATASET CREATION PART ! + + +### Functions + +```python +### CELLS TO CREATE THE DATASET CONTAINING ALL TXT FILES WE WANT TO STUDY: +### ALL PATIENTS WITH ONE LINE AT LEAST IN: +# - i2b2_observation_cim10 with correct CIM10 according to `TO_BE_MATCHED` +# - i2b2_observation_doc +# - i2b2_observation_lab (OPTIONAL) + +# SHOW DATASETS +sql("USE cse_200093_20210402") +sql("SHOW tables").show(10, False) + + +# Save txt function +def save_to_txt(path, txt): + with open(path, "w") as f: + print(txt, file=f) + + +def get_docs_df(cim10_list, min_len=1000): + ### If we filter on `i2b2_observation_lab` + # docs = sql("""SELECT doc.instance_num, doc.observation_blob, cim10.concept_cd FROM i2b2_observation_doc AS doc + # JOIN i2b2_observation_cim10 AS cim10 ON doc.encounter_num = cim10.encounter_num + # WHERE ((doc.concept_cd == 'CR:CRH-HOSPI' OR doc.concept_cd == 'CR:CRH-S') + # AND EXISTS (SELECT lab.encounter_num FROM i2b2_observation_lab AS lab + # WHERE lab.encounter_num = doc.encounter_num))""") + + ### If we don't filter on `i2b2_observation_lab` + docs = sql( + """SELECT doc.instance_num, doc.observation_blob, doc.encounter_num, doc.patient_num, visit.age_visit_in_years_num, visit.start_date, cim10.concept_cd FROM i2b2_observation_doc AS doc + JOIN i2b2_observation_cim10 AS cim10 ON doc.encounter_num = cim10.encounter_num JOIN i2b2_visit AS visit ON doc.encounter_num = visit.encounter_num + WHERE (doc.concept_cd == 'CR:CRH-HOSPI' OR doc.concept_cd == 'CR:CRH-S') + """ + ) + ### Filter on cim10_list and export to Pandas + docs_df = docs.filter(docs.concept_cd.isin(cim10_list)).toPandas().dropna() + ### Keep documents with some information at least + docs_df = docs_df.loc[docs_df["observation_blob"].apply(len) > min_len].reset_index( + drop=True + ) + docs_df = ( + docs_df.groupby("observation_blob") + .agg( + { + "instance_num": set, + "encounter_num": "first", + "patient_num": "first", + "age_visit_in_years_num": "first", + "start_date": "first", + "observation_blob": "first", + } + ) + .reset_index(drop=True) + ) + docs_df["instance_num"] = docs_df["instance_num"].apply( + lambda instance_num: "_".join(list(instance_num)) + ) + return docs_df + + +def get_bio_df(summary_docs): + bio = sql( + """SELECT bio.instance_num AS bio_id, bio.concept_cd, bio.units_cd, bio.nval_num, bio.tval_char, bio.quantity_num, bio.confidence_num, bio.encounter_num, bio.patient_num, bio.start_date, concept.name_char + FROM i2b2_observation_lab AS bio JOIN i2b2_concept AS concept ON bio.concept_cd = concept.concept_cd""" + ) + bio_dfs = {} + for disease in summary_docs.disease.unique(): + unique_visit = summary_docs[summary_docs.disease == disease][ + ["encounter_num"] + ].drop_duplicates() + unique_visit = spark.createDataFrame(unique_visit).hint("broadcast") + bio_df = bio.join(unique_visit, on="encounter_num").toPandas() + bio_df["disease"] = disease + bio_df["terms_linked_to_measurement"] = bio_df["name_char"].apply( + _get_term_from_c_name + ) + bio_df.loc[bio_df["units_cd"].isna(), "units_cd"] = "nounit" + bio_df = bio_df[~((bio_df.nval_num.isna()) & (bio_df.tval_char.isna()))] + display(bio_df) + bio_dfs[disease] = bio_df + + return bio_dfs + + +def get_med_df(summary_docs): + med = sql( + """SELECT med.instance_num AS med_id, med.concept_cd, med.valueflag_cd, med.encounter_num, med.patient_num, med.start_date, concept.name_char + FROM i2b2_observation_med AS med JOIN i2b2_concept AS concept ON med.concept_cd = concept.concept_cd""" + ) + med_dfs = {} + for disease in summary_docs.disease.unique(): + unique_visit = summary_docs[summary_docs.disease == disease][ + ["encounter_num"] + ].drop_duplicates() + unique_visit = spark.createDataFrame(unique_visit).hint("broadcast") + med_df = med.join(unique_visit, on="encounter_num").toPandas() + med_df["disease"] = disease + display(med_df) + med_dfs[disease] = med_df + + return med_dfs + + +def _get_term_from_c_name(c_name): + return c_name[c_name.index(":") + 1 :].split("_")[0].strip() +``` + +### Get Docs and Bio and Med + +```python +# Get docs and save It for each disease +docs_all_diseases = [] +for disease, disease_data in TO_BE_MATCHED.items(): + path_to_brat = join(BRAT_DIR, "raw", disease) + if not os.path.exists(path_to_brat): + mkdir(path_to_brat) + docs_df = get_docs_df(["CIM10:" + cim10 for cim10 in disease_data["CIM10"]]) + docs_df.apply(lambda row: save_to_txt(join(path_to_brat, row["instance_num"] + ".txt"), row["observation_blob"]), axis=1) + for file in os.listdir(path_to_brat): + if file.endswith(".txt"): + ann_file = os.path.join(path_to_brat, file[:-3] + "ann") + open(ann_file, mode='a').close() + print(disease + " processed and saved") + docs_df["disease"] = disease + docs_all_diseases.append(docs_df) +summary_df_docs = pd.concat(docs_all_diseases) +bio_from_structured_data = get_bio_df(summary_df_docs) +bio_from_structured_data = pd.concat(list(bio_from_structured_data.values())) +med_from_structured_data = get_med_df(summary_df_docs) +med_from_structured_data = pd.concat(list(med_from_structured_data.values())) +display(summary_df_docs) +display(bio_from_structured_data) +display(med_from_structured_data) +bio_from_structured_data.to_pickle(join(RES_DIR, "bio_from_structured_data.pkl")) +med_from_structured_data.to_pickle(join(RES_DRUG_DIR, "med_from_structured_data.pkl")) +summary_df_docs.to_pickle(join(BRAT_DIR, "summary_df_docs.pkl")) +``` + +```python +bio_from_structured_data["found"] = bio_from_structured_data["nval_num"].mask( + bio_from_structured_data["nval_num"].isna(), bio_from_structured_data["tval_char"] +) +bio_from_structured_data["gold"] = ( + bio_from_structured_data["found"].astype(str) + " " + bio_from_structured_data["units_cd"] +) +bio_from_structured_data = bio_from_structured_data.groupby( + ["disease", "encounter_num", "patient_num", "terms_linked_to_measurement"], + as_index=False, +).agg({"name_char": list, "gold": list}) +bio_from_structured_data.to_json(join(RES_DIR, "bio_from_structured_data.json")) +``` + + +# Summary description of the data + + +```python +import altair as alt + +summary_df_docs = pd.read_pickle(join(BRAT_DIR, "summary_df_docs.pkl")) +bio_from_structured_data = pd.read_pickle(join(RES_DIR, "bio_from_structured_data.pkl")) +med_from_structured_data = pd.read_pickle( + join(RES_DRUG_DIR, "med_from_structured_data.pkl") +) +``` + +## Number of docs/visit/patients + +```python +summary_df_docs.groupby("disease").agg( + {"instance_num": "nunique", "encounter_num": "nunique", "patient_num": "nunique"} +) +``` + +## Number of Bio/visit/patient + +```python +bio_from_structured_data.groupby("disease").agg( + {"bio_id": "nunique", "encounter_num": "nunique", "patient_num": "nunique"} +) +``` + +## Number of Med/visit/patient + +```python +med_from_structured_data.groupby("disease").agg( + {"med_id": "nunique", "encounter_num": "nunique", "patient_num": "nunique"} +) +``` + +## Age histogram + +```python +summary_df_docs["round_age"] = (summary_df_docs["age_visit_in_years_num"] * 2).round( + -1 +) / 2 +age_summary = summary_df_docs.groupby( + ["disease", "age_visit_in_years_num"], as_index=False +).agg({"patient_num": "nunique"}) +round_age_summary = summary_df_docs.groupby( + ["disease", "round_age"], as_index=False +).agg({"patient_num": "nunique"}) +total_patient = ( + summary_df_docs.groupby("disease", as_index=False) + .agg({"patient_num": "nunique"}) + .rename(columns={"patient_num": "total_patient"}) +) +age_summary = age_summary.merge(total_patient, on="disease") +age_summary["density"] = age_summary["patient_num"] / age_summary["total_patient"] +display(age_summary) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(round_age_summary).mark_bar(size=12, align="left").encode( + alt.X("round_age:Q").title("Age at stay"), + alt.Y("patient_num:Q").title("Number of patients"), + alt.Row("disease:N"), +).resolve_scale(y="independent").properties(height=200) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(round_age_summary).mark_area(interpolate="step-after").encode( + alt.X("round_age:Q").title("Age at stay"), + alt.Y("patient_num:Q").title("Number of patients"), + alt.Row("disease:N"), +).resolve_scale(y="independent").properties(height=200) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(age_summary).mark_area().encode( + alt.X("age_visit_in_years_num:Q").title("Age at stay"), + alt.Y("patient_num:Q").title("Number of patients"), + alt.Row("disease:N"), +).resolve_scale(y="independent").properties(height=200) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(age_summary).mark_area(interpolate="basis").encode( + alt.X("age_visit_in_years_num:Q").title("Age at stay"), + alt.Y("density:Q").title("Density"), + alt.Row("disease:N"), +).properties(height=200) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(age_summary).mark_bar().encode( + alt.X("age_visit_in_years_num:Q"), + alt.Y("density:Q"), + alt.Row("disease:N"), + color="disease:N", +).properties(height=200) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(age_summary).mark_area(opacity=0.4).encode( + alt.X("age_visit_in_years_num:Q"), alt.Y("density:Q").stack(None), color="disease:N" +).properties(height=200) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(age_summary).mark_area().encode( + alt.X("age_visit_in_years_num:Q"), + alt.Y("density:Q").stack(True), + color="disease:N", +).properties(height=200) +``` + +## Stay start histogramm + +```python +summary_df_docs["month_date"] = ( + summary_df_docs["start_date"].dt.strftime("%Y-%m").astype("datetime64[ns]") +) +month_date_summary = summary_df_docs.groupby( + ["disease", "month_date"], as_index=False +).agg({"encounter_num": "nunique"}) +total_visit = ( + summary_df_docs.groupby("disease", as_index=False) + .agg({"encounter_num": "nunique"}) + .rename(columns={"encounter_num": "total_visit"}) +) +month_date_summary = month_date_summary.merge(total_visit, on="disease") +month_date_summary["density"] = ( + month_date_summary["encounter_num"] / month_date_summary["total_visit"] +) +display(month_date_summary) +``` + +```python +alt.data_transformers.disable_max_rows() +alt.Chart(month_date_summary).mark_bar(align="left").encode( + alt.X("yearquarter(month_date):T") + .title("Time (Year)") + .axis(tickCount="year", labelAngle=0, grid=True, format="%Y"), + alt.Y("sum(encounter_num):Q").title("Number of stays"), + alt.Row("disease:N"), +).resolve_scale(y="independent").properties(height=200, width=600) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(month_date_summary).mark_area(interpolate="basis").encode( + alt.X("month_date:T").title("Time (Year)"), + alt.Y("density:Q").title("Density"), + alt.Row("disease:N"), +).properties(height=200, width=600) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(month_date_summary).mark_bar().encode( + alt.X("month_date:T").title("Time (Year)"), + alt.Y("density:Q").title("Density"), + alt.Row("disease:N"), + color="disease:N", +).properties(height=200) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(month_date_summary).mark_area(opacity=0.4).encode( + alt.X("month_date:T"), alt.Y("density:Q").stack(None), color="disease:N" +).properties(height=200, width=600) +``` + +```python +alt.data_transformers.disable_max_rows() + +alt.Chart(month_date_summary).mark_area().encode( + alt.X("month_date:T"), + alt.Y("density:Q").stack(True), + color="disease:N", +).properties(height=200) +``` + +# Please infer super_pipe on `BRAT_DIR` subfolders. Use the sbatch file in `/export/home/cse200093/Jacques_Bio/super_pipe/py_files/sbatch/main.sh`. res path should be in `RES_DIR`. NOW, PLEASE USE `jacques-spark` FOR THE NEXT CELLS. + + +## MED STRUCTURED + +```python +med_from_structured_data = pd.read_pickle( + join(RES_DRUG_DIR, "med_from_structured_data.pkl") +) +codes_to_keep = {"disease": [], "valueflag_cd": [], "med": []} +for disease, disease_data in TO_BE_MATCHED.items(): + for label, code_list in disease_data["ATC_codes"].items(): + for code in code_list: + codes_to_keep["disease"].append(disease) + codes_to_keep["valueflag_cd"].append(code) + codes_to_keep["med"].append(label) +filtered_med = med_from_structured_data.merge( + pd.DataFrame(codes_to_keep), on=["disease", "valueflag_cd"] +) +for disease in TO_BE_MATCHED.keys(): + path_to_res = join(RES_DRUG_DIR, disease) + if not os.path.exists(path_to_res): + mkdir(path_to_res) + filtered_med[filtered_med.disease == disease].to_pickle( + join(path_to_res, "filtered_med_from_structured_data.pkl") + ) +display(filtered_med) +filtered_med.to_pickle(join(RES_DRUG_DIR, "filtered_med_from_structured_data.pkl")) +``` + +## BIO STRUCTURED + +```python +bio_from_structured_data = pd.read_pickle(join(RES_DIR, "bio_from_structured_data.pkl")) +codes_to_keep = {"disease": [], "concept_cd": [], "bio": []} +for disease, disease_data in TO_BE_MATCHED.items(): + for label, code_list in disease_data["ANABIO_codes"].items(): + for code in code_list: + codes_to_keep["disease"].append(disease) + codes_to_keep["concept_cd"].append(f"LAB:{code}") + codes_to_keep["bio"].append(label) +filtered_bio = bio_from_structured_data.merge( + pd.DataFrame(codes_to_keep), on=["disease", "concept_cd"] +) +for disease in TO_BE_MATCHED.keys(): + path_to_res = join(RES_DIR, disease) + if not os.path.exists(path_to_res): + mkdir(path_to_res) + filtered_bio[filtered_bio.disease == disease].to_pickle( + join(path_to_res, "filtered_bio_from_structured_data.pkl") + ) +display(filtered_bio) +filtered_bio.to_pickle(join(RES_DIR, "filtered_bio_from_structured_data.pkl")) +``` + +```python +bio_from_structured_data = pd.read_json( + join(RES_DIR, "bio_from_structured_data.json"), + dtype={"encounter_num": str, "patient_num": str}, +).explode("label") +cuis_to_keep = {"disease": [], "label": [], "bio": []} +for disease, disease_data in TO_BE_MATCHED.items(): + for cui_dic in disease_data["CUI_per_section"].values(): + for cui_label, cui_list in cui_dic.items(): + for cui in cui_list: + print(cui) + cuis_to_keep["disease"].append(disease) + cuis_to_keep["label"].append(cui) + cuis_to_keep["bio"].append(cui_label) +filtered_bio_from_structured = bio_from_structured_data.merge( + pd.DataFrame(cuis_to_keep), on=["disease", "label"] +) +for disease in TO_BE_MATCHED.keys(): + path_to_res = join(RES_DIR, disease) + if not os.path.exists(path_to_res): + mkdir(path_to_res) + filtered_bio_from_structured[filtered_bio_from_structured.disease == disease].to_pickle( + join(path_to_res, "filtered_bio_from_structured_data.pkl") + ) +display(filtered_bio_from_structured) +filtered_bio_from_structured.to_pickle(join(RES_DIR, "filtered_bio_from_structured_data.pkl")) +``` + +## MED NLP + +```python +from tqdm import tqdm + + +# Check if we have to keep a match or not based on section and CUI +def keep_match(atc, atcs, atcs_to_keep): + if atc not in atcs_to_keep: + return None + for drug, atc_list in atcs.items(): + if atc in atc_list: + return drug + return None + + +# List of df by disease for concatenation +res_part_filtered_list = [] +res_part_df_list = [] +for disease, disease_data in TO_BE_MATCHED.items(): + ### Load each res dataset to concat them in one unique df + res_part_df = pd.read_pickle(join(RES_DRUG_DIR, disease, "norm_lev_match.pkl")) + res_part_df["disease"] = disease + res_part_df["source"] = res_part_df["source"] + ".ann" + + ### Filter ATC to keep + codes_to_keep = {"label": [], "med": []} + for label, code_list in disease_data["ATC_codes"].items(): + for code in code_list: + codes_to_keep["label"].append(code) + codes_to_keep["med"].append(label) + res_part_filtered = ( + res_part_df.explode("label") + .merge(pd.DataFrame(codes_to_keep), on="label") + .drop_duplicates( + subset=["term", "source", "span_converted", "norm_term", "disease"] + ) + ) + + ### Save for future concatenation + res_part_filtered.to_pickle(join(RES_DRUG_DIR, disease, "res_final_filtered.pkl")) + res_part_filtered_list.append(res_part_filtered) +res_filtered_df = pd.concat(res_part_filtered_list) +res_filtered_df.to_pickle(join(RES_DRUG_DIR, "res_final_filtered.pkl")) +display(res_filtered_df) +``` + +## BIO NLP WITHOUT SECTION + +```python +from tqdm import tqdm + + +# Check if we have to keep a match or not based on section and CUI +def keep_match(cui, cui_per_section, cuis_to_keep): + if cui not in cuis_to_keep: + return None + for bio, cui_list in cui_per_section["all"].items(): + if cui in cui_list: + return bio + return None + + +# List of df by disease for concatenation +res_part_filtered_list = [] +res_part_df_list = [] +for disease, disease_data in TO_BE_MATCHED.items(): + ### Load each res dataset to concat them in one unique df + res_part_df = pd.read_json(join(RES_DIR, disease, "norm_coder_all.json")) + res_part_df["disease"] = disease + + ### Filter CUIS to keep + cuis_to_keep = [ + cui + for cui_dic in disease_data["CUI_per_section"].values() + for cui_list in cui_dic.values() + for cui in cui_list + ] + res_part_filtered = [] + for source in tqdm(res_part_df["source"].unique()): + for _, row in res_part_df.loc[res_part_df["source"] == source].iterrows(): + for cui in row["label"]: + to_keep = keep_match( + cui, + disease_data["CUI_per_section"], + cuis_to_keep, + ) + if to_keep: + row["bio"] = to_keep + res_part_filtered.append(row) + + ### Save for future concatenation + res_part_df.to_pickle(join(RES_DIR, disease, "res_final.pkl")) + res_part_df_list.append(res_part_df) + pd.DataFrame(res_part_filtered).to_pickle( + join(RES_DIR, disease, "res_final_filtered.pkl") + ) + res_part_filtered_list += res_part_filtered +res_df = pd.concat(res_part_df_list) +res_filtered_df = pd.DataFrame(res_part_filtered_list) +res_df.to_pickle(join(RES_DIR, "res_final.pkl")) +res_filtered_df.to_pickle(join(RES_DIR, "res_final_filtered.pkl")) +display(res_df) +display(res_filtered_df) +``` + +## BIO NLP WITH SECTION + +```python +from tqdm import tqdm + +rule_based_section = False + +if rule_based_section: + # Load nlp pipe to detect sections + nlp_sections = spacy.blank("eds") + nlp_sections.add_pipe("eds.normalizer") + nlp_sections.add_pipe("eds.sections") + + +# Check if two spans are overlapping for section detection +def is_overlapping(a, b): + # Return true if a segment is overlapping b + # else False + return min(a[1], b[1]) > max(a[0], b[0]) + + +# Check if we have to keep a match or not based on section and CUI +def keep_match(cui, span, txt_section_part_df, cui_per_section, cuis_to_keep): + if cui not in cuis_to_keep: + return None + for section in cui_per_section.keys(): + if section == "all": + for bio, cui_list in cui_per_section["all"].items(): + if cui in cui_list: + return bio + elif section not in txt_section_part_df["label"].tolist(): + continue + else: + section_spans = ( + txt_section_part_df.loc[txt_section_part_df["label"] == section] + .apply(lambda row: [row["start"], row["end"]], axis=1) + .tolist() + ) + for section_span in section_spans: + if is_overlapping(span, section_span): + for bio, cui_list in cui_per_section[section].items(): + if cui in cui_list: + return bio + else: + continue + return None + + +# List of df by disease for concatenation +res_part_filtered_list = [] +txt_sections_part_df_list = [] +res_part_df_list = [] +for disease, disease_data in TO_BE_MATCHED.items(): + ### Load each res dataset to concat them in one unique df + res_part_df = pd.read_json(join(RES_DIR, disease, "norm_coder_all.json")) + res_part_df["disease"] = disease + + if rule_based_section: + ### Load txt files, detect sections and store it in df + # Load txt files in DataFrame + txt_files_part = [ + f + for f in listdir(join(BRAT_DIR, "raw", disease)) + if isfile(join(BRAT_DIR, "raw", disease, f)) + if f.endswith(".txt") + ] + txt_list_part = [] + for txt_file in txt_files_part: + with open(join(BRAT_DIR, "raw", disease, txt_file), "r") as file: + text = file.read() + txt_list_part.append([text, txt_file[:-3] + "ann"]) + txt_sections_part_df = pd.DataFrame( + txt_list_part, columns=["note_text", "note_id"] + ) + + # Infer nlp pipe to detect sections + txt_sections_part_df = pipe( + note=txt_sections_part_df, + nlp=nlp_sections, + n_jobs=-2, + additional_spans=["sections"], + ).drop(columns=["span_type", "lexical_variant"]) + else: + ### Load txt files, detect sections and store it in df + # Load txt files in DataFrame + txt_files_part = [ + f + for f in listdir(join(BRAT_DIR, "pred", disease)) + if isfile(join(BRAT_DIR, "pred", disease, f)) + if f.endswith(".ann") + ] + txt_list_part = [] + for txt_file in txt_files_part: + with open(join(BRAT_DIR, "pred", disease, txt_file), "r") as file: + lines = file.readlines() + start = 0 + section = "introduction" + for line in lines: + if "SECTION" in line and not ( + line.split(" ")[1].split(" ")[0] == section + ): + end = int(line.split(" ")[1].split(" ")[1]) + txt_list_part.append([txt_file, section, start, end]) + section = line.split(" ")[1].split(" ")[0] + start = end + txt_sections_part_df = pd.DataFrame( + txt_list_part, columns=["note_id", "label", "start", "end"] + ) + txt_sections_part_df["disease"] = disease + + ### Filter CUIS to keep + sections_to_keep = list(disease_data["CUI_per_section"].keys()) + cuis_to_keep = [ + cui + for cui_dic in disease_data["CUI_per_section"].values() + for cui_list in cui_dic.values() + for cui in cui_list + ] + print(cuis_to_keep) + res_part_filtered = [] + for source in tqdm(res_part_df["source"].unique()): + txt_sections_part_source_df = txt_sections_part_df.loc[ + (txt_sections_part_df["note_id"] == source) + # & (txt_sections_part_df["label"].isin(sections_to_keep)) + ] + for _, row in res_part_df.loc[res_part_df["source"] == source].iterrows(): + for cui in row["label"]: + to_keep = keep_match( + cui, + row["span_converted"], + txt_sections_part_source_df, + disease_data["CUI_per_section"], + cuis_to_keep, + ) + if to_keep: + row["bio"] = to_keep + res_part_filtered.append(row) + + ### Save for future concatenation + res_part_df.to_pickle(join(RES_DIR, disease, "res_final.pkl")) + res_part_df_list.append(res_part_df) + pd.DataFrame(res_part_filtered).to_pickle( + join(RES_DIR, disease, "res_final_filtered.pkl") + ) + res_part_filtered_list += res_part_filtered + txt_sections_part_df.to_pickle(join(RES_DIR, disease, "txt_sections_df.pkl")) + txt_sections_part_df_list.append(txt_sections_part_df) + +res_df = pd.concat(res_part_df_list) +res_filtered_df = pd.DataFrame(res_part_filtered_list) +txt_sections_df = pd.concat(txt_sections_part_df_list) +txt_sections_df.to_pickle(join(RES_DIR, "txt_sections_df.pkl")) +res_df.to_pickle(join(RES_DIR, "res_final.pkl")) +res_filtered_df.to_pickle(join(RES_DIR, "res_final_filtered.pkl")) +display(res_df) +display(res_filtered_df) +``` + +# Vizualize phenotype + +```python +def prepare_structured_CODERE_label_df(disease): + structured_filtered_res = pd.read_pickle( + join(RES_DIR, disease, "structured_filtered_res.pkl") + ) + summary_df_docs = pd.read_pickle(join(BRAT_DIR, "summary_df_docs.pkl")) + summary_df_docs = summary_df_docs[summary_df_docs.disease == disease] + structured_filtered_res = structured_filtered_res.merge( + summary_df_docs[["encounter_num", "patient_num"]], + on=["encounter_num", "patient_num"], + how="right", + ) + structured_filtered_res = structured_filtered_res.explode("gold") + structured_filtered_res["value"] = pd.to_numeric( + structured_filtered_res["gold"].str.split(" ").str.get(0), errors="coerce" + ) + structured_filtered_res["unit"] = ( + structured_filtered_res["gold"].str.split(" ").str.get(-1).str.lower() + ) + structured_patient_group = None + if len(TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys()) > 0: + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys(): + structured_filtered_res[bio] = structured_filtered_res.bio == bio + structured_filtered_res[f"{bio} positif"] = ( + structured_filtered_res.bio == bio + ) & (structured_filtered_res.value >= 1.0) + structured_patient_group = structured_filtered_res.groupby( + "patient_num", as_index=False + ).agg( + { + **{ + bio: "sum" + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys() + }, + **{ + f"{bio} positif": "sum" + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys() + }, + } + ) + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys(): + structured_patient_group[bio] = structured_patient_group[bio] >= 1 + structured_patient_group[f"{bio} positif"] = ( + structured_patient_group[f"{bio} positif"] >= 1 + ) + return structured_filtered_res, structured_patient_group +``` + +```python +def prepare_structured_df(disease): + summary_filtered_res = pd.read_pickle( + join(RES_DIR, disease, "filtered_bio_from_structured_data.pkl") + ) + summary_df_docs = pd.read_pickle(join(BRAT_DIR, "summary_df_docs.pkl")) + summary_df_docs = summary_df_docs[summary_df_docs.disease == disease] + summary_filtered_res = summary_filtered_res.merge( + summary_df_docs[["encounter_num", "patient_num"]], + on=["encounter_num", "patient_num"], + how="right", + ) + summary_filtered_res = summary_filtered_res.rename( + columns={"nval_num": "value", "units_cd": "unit"} + ) + summary_patient_group = None + if len(TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys()) > 0: + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys(): + summary_filtered_res[bio] = summary_filtered_res.bio == bio + summary_filtered_res[f"{bio} positif"] = ( + summary_filtered_res.bio == bio + ) & ( + (summary_filtered_res.value >= summary_filtered_res.confidence_num) + | (summary_filtered_res.tval_char.str.contains("posi", case=False)) + ) + summary_patient_group = summary_filtered_res.groupby( + "patient_num", as_index=False + ).agg( + { + **{ + bio: "sum" + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys() + }, + **{ + f"{bio} positif": "sum" + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys() + }, + } + ) + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys(): + summary_patient_group[bio] = summary_patient_group[bio] >= 1 + summary_patient_group[f"{bio} positif"] = ( + summary_patient_group[f"{bio} positif"] >= 1 + ) + + return summary_filtered_res, summary_patient_group +``` + +```python +def prepare_structured_med_df(disease): + summary_filtered_res = pd.read_pickle( + join(RES_DRUG_DIR, disease, "filtered_med_from_structured_data.pkl") + ) + summary_df_docs = pd.read_pickle(join(BRAT_DIR, "summary_df_docs.pkl")) + summary_df_docs = summary_df_docs[summary_df_docs.disease == disease] + summary_filtered_res = summary_filtered_res.merge( + summary_df_docs[["encounter_num", "patient_num"]], + on=["encounter_num", "patient_num"], + how="right", + ) + summary_patient_group = None + for med in TO_BE_MATCHED[disease]["ATC_codes"].keys(): + summary_filtered_res[med] = summary_filtered_res.med == med + summary_patient_group = summary_filtered_res.groupby( + "patient_num", as_index=False + ).agg( + { + **{med: "sum" for med in TO_BE_MATCHED[disease]["ATC_codes"].keys()}, + } + ) + for med in TO_BE_MATCHED[disease]["ATC_codes"].keys(): + summary_patient_group[med] = summary_patient_group[med] >= 1 + + return summary_filtered_res, summary_patient_group +``` + +```python +def prepare_nlp_med_df(disease): + res_filtered_df = pd.read_pickle( + join(RES_DRUG_DIR, disease, "res_final_filtered.pkl") + ) + res_filtered_df["instance_num"] = ( + res_filtered_df.source.str.split(".").str.get(0).str.split("_").str.get(0) + ) + summary_df_docs = pd.read_pickle(join(BRAT_DIR, "summary_df_docs.pkl")) + summary_df_docs = summary_df_docs[summary_df_docs.disease == disease] + summary_df_docs["instance_num"] = summary_df_docs.instance_num.str.split("_") + summary_df_docs = summary_df_docs.explode("instance_num") + res_filtered_df = res_filtered_df.merge( + summary_df_docs[["instance_num", "encounter_num", "patient_num"]], + on="instance_num", + how="right", + ) + patient_group = None + for med in TO_BE_MATCHED[disease]["ATC_codes"].keys(): + res_filtered_df[med] = res_filtered_df.med == med + patient_group = res_filtered_df.groupby("patient_num", as_index=False).agg( + { + **{med: "sum" for med in TO_BE_MATCHED[disease]["ATC_codes"].keys()}, + } + ) + for med in TO_BE_MATCHED[disease]["ATC_codes"].keys(): + patient_group[med] = patient_group[med] >= 1 + + return res_filtered_df, patient_group +``` + +```python +def prepare_nlp_df(disease): + res_filtered_df = pd.read_pickle(join(RES_DIR, disease, "res_final_filtered.pkl")) + res_filtered_df["instance_num"] = ( + res_filtered_df.source.str.split(".").str.get(0).str.split("_").str.get(0) + ) + summary_df_docs = pd.read_pickle(join(BRAT_DIR, "summary_df_docs.pkl")) + summary_df_docs = summary_df_docs[summary_df_docs.disease == disease] + summary_df_docs["instance_num"] = summary_df_docs.instance_num.str.split("_") + summary_df_docs = summary_df_docs.explode("instance_num") + res_filtered_df = res_filtered_df.merge( + summary_df_docs[["instance_num", "encounter_num", "patient_num"]], + on="instance_num", + how="right", + ) + res_filtered_df = res_filtered_df.explode("found") + res_filtered_df["comparator"] = res_filtered_df["found"].str.split(" ").str.get(0) + res_filtered_df["value"] = ( + res_filtered_df["found"].str.split(" ").str.get(1).astype(float) + ) + res_filtered_df["unit"] = res_filtered_df["found"].str.split(" ").str.get(2) + patient_group = None + if len(TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys()) > 0: + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys(): + res_filtered_df[bio] = res_filtered_df.bio == bio + res_filtered_df[f"{bio} positif"] = (res_filtered_df.bio == bio) & ( + res_filtered_df.value >= 1.0 + ) + patient_group = res_filtered_df.groupby("patient_num", as_index=False).agg( + { + **{ + bio: "sum" + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys() + }, + **{ + f"{bio} positif": "sum" + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys() + }, + } + ) + for bio in TO_BE_MATCHED[disease]["CUI_per_section"]["all"].keys(): + patient_group[bio] = patient_group[bio] >= 1 + patient_group[f"{bio} positif"] = patient_group[f"{bio} positif"] >= 1 + + return res_filtered_df, patient_group +``` + +```python +def plot_hist(unit_convert, possible_values, res_filtered_df, title: bool = False): + alt.data_transformers.disable_max_rows() + res_hists = [] + for bio, units in unit_convert.items(): + filtered_bio = res_filtered_df[["bio", "unit", "value"]][ + (res_filtered_df.bio == bio) & (res_filtered_df.unit.isin(units.keys())) + ].copy() + if not filtered_bio.empty: + for unit, rate in units.items(): + filtered_bio["value"] = filtered_bio["value"].mask( + filtered_bio["unit"] == unit, filtered_bio["value"] * rate + ) + outliers = filtered_bio[ + (filtered_bio["value"] > possible_values[bio]) + | (filtered_bio["value"] < 0) + ].copy() + outliers["Percentage"] = len(outliers) / len(filtered_bio) + outliers["MaxValue"] = possible_values[bio] + outliers["value"] = outliers["value"].mask( + outliers["value"] > outliers["MaxValue"], outliers["MaxValue"] + ) + outliers["value"] = outliers["value"].mask(outliers["value"] < 0, 0) + filtered_bio = filtered_bio[ + (filtered_bio.value >= 0) & (filtered_bio.value <= possible_values[bio]) + ] + res_density = ( + alt.Chart(filtered_bio) + .transform_density( + "value", + counts=True, + extent=[0, possible_values[bio]], + as_=["value", "density"], + ) + .mark_area() + .encode( + alt.X("value:Q"), + alt.Y("density:Q"), + alt.Tooltip(["value:Q", "density:Q"]), + ) + ) + res_box_plot = ( + alt.Chart(filtered_bio) + .mark_boxplot() + .encode(alt.X("value:Q").scale(domainMin=0)) + ) + res_outliers = ( + alt.Chart(outliers) + .mark_bar(color="grey") + .encode( + alt.X("value:Q"), + alt.Y("count()").title("Smoothed count"), + tooltip=[ + alt.Tooltip( + "MaxValue:Q", + title="Upper bound", + format=",", + ), + alt.Tooltip( + "count():Q", + title="Frequency over the maximum", + ), + alt.Tooltip( + "Percentage:Q", + format=".2%", + ), + ], + ) + ) + res_hist = ( + (res_density).properties(width=400, height=300) & res_box_plot + ).resolve_scale(x="shared") + else: + res_hist = ( + alt.Chart(pd.DataFrame([])) + .mark_text() + .properties(width=400, height=300) + ) + if title: + res_hist = res_hist.properties( + title=alt.TitleParams(text=bio, orient="top") + ) + res_hists.append(res_hist) + chart = reduce( + lambda bar_chart_1, bar_chart_2: (bar_chart_1 | bar_chart_2) + .resolve_scale(x="independent") + .resolve_scale(y="independent"), + res_hists, + ) + return chart +``` + +```python +def plot_venn(patient_group, bio_venn, english_title, method): + if len(bio_venn) == 2: + subsets = ( + ((patient_group[bio_venn["A"]]) & ~(patient_group[bio_venn["B"]])).sum(), + (~(patient_group[bio_venn["A"]]) & (patient_group[bio_venn["B"]])).sum(), + ((patient_group[bio_venn["A"]]) & (patient_group[bio_venn["B"]])).sum(), + ) + venn = venn2(subsets=subsets, set_labels=bio_venn.values()) + elif len(bio_venn) == 3: + subsets = ( + ( + (patient_group[bio_venn["A"]]) + & ~(patient_group[bio_venn["B"]]) + & ~(patient_group[bio_venn["C"]]) + ).sum(), + ( + ~(patient_group[bio_venn["A"]]) + & (patient_group[bio_venn["B"]]) + & ~(patient_group[bio_venn["C"]]) + ).sum(), + ( + (patient_group[bio_venn["A"]]) + & (patient_group[bio_venn["B"]]) + & ~(patient_group[bio_venn["C"]]) + ).sum(), + ( + ~(patient_group[bio_venn["A"]]) + & ~(patient_group[bio_venn["B"]]) + & (patient_group[bio_venn["C"]]) + ).sum(), + ( + (patient_group[bio_venn["A"]]) + & ~(patient_group[bio_venn["B"]]) + & (patient_group[bio_venn["C"]]) + ).sum(), + ( + ~(patient_group[bio_venn["A"]]) + & (patient_group[bio_venn["B"]]) + & (patient_group[bio_venn["C"]]) + ).sum(), + ( + (patient_group[bio_venn["A"]]) + & (patient_group[bio_venn["B"]]) + & (patient_group[bio_venn["C"]]) + ).sum(), + ) + venn = venn3(subsets=subsets, set_labels=bio_venn.values()) + + total_patients = patient_group.patient_num.nunique() + if len(bio_venn) == 3: + total_pos = patient_group[ + patient_group[bio_venn["A"]] + | patient_group[bio_venn["B"]] + | patient_group[bio_venn["C"]] + ].patient_num.nunique() + elif len(bio_venn) == 2: + total_pos = patient_group[ + patient_group[bio_venn["A"]] | patient_group[bio_venn["B"]] + ].patient_num.nunique() + for idx, subset in enumerate(venn.subset_labels): + if subset: + subset.set_text( + f"{subset.get_text()}\n{int(subset.get_text())/total_patients*100:.1f}%" + ) + plt.title( + f"N = {total_patients} patients studied with a {english_title} \n Detected from {method} = {total_pos} ({total_pos/total_patients * 100:.1f} %)" + ) + # plt.show() +``` + +```python +def plot_summary_med(nlp_patient_group, structured_patient_group, english_title): + nlp_summary = pd.DataFrame( + nlp_patient_group.sum().drop("patient_num"), columns=["Detected"] + ) + nlp_summary["Total"] = len(nlp_patient_group) + nlp_summary["Percentage"] = ( + nlp_summary["Detected"] / nlp_summary["Total"] * 100 + ).astype(float).round(2).astype(str) + " %" + nlp_summary.columns = pd.MultiIndex.from_product( + [ + ["NLP"], + nlp_summary.columns, + ] + ) + structued_summary = pd.DataFrame( + structured_patient_group.sum().drop("patient_num"), columns=["Detected"] + ) + structued_summary["Total"] = len(structured_patient_group) + structued_summary["Percentage"] = ( + (structued_summary["Detected"] / structued_summary["Total"] * 100) + .astype(float) + .round(2) + ).astype(str) + " %" + structued_summary.columns = pd.MultiIndex.from_product( + [ + ["Structured Data"], + structued_summary.columns, + ] + ) + nlp_structured_patient_group = ( + pd.concat([nlp_patient_group, structured_patient_group]) + .groupby("patient_num", as_index=False) + .max() + ) + nlp_structued_summary = pd.DataFrame( + nlp_structured_patient_group.sum().drop("patient_num"), columns=["Detected"] + ) + nlp_structued_summary["Total"] = len(nlp_structured_patient_group) + nlp_structued_summary["Percentage"] = ( + (nlp_structued_summary["Detected"] / nlp_structued_summary["Total"] * 100) + .astype(float) + .round(2) + ).astype(str) + " %" + nlp_structued_summary.columns = pd.MultiIndex.from_product( + [ + ["NLP + Structured Data"], + nlp_structued_summary.columns, + ] + ) + return pd.concat( + [structued_summary, nlp_summary, nlp_structued_summary], axis=1 + ).style.set_caption(english_title.capitalize()) +``` + +```python +Biology_nlp_hist = [] +Biology_structured_hist = [] +Biology_nlp_structured_hist = [] +unit_convert = { + "Créatininémie": {"µmol_per_l": 1, "µmol/l": 1, "nounit": 1}, + "Hémoglobine": {"g_per_dl": 1, "g/dl": 1}, + "CRP": {"mg_per_l": 1, "µg_per_l": 0.001, "ui_per_l": 1, "nounit": 1, "mg/l": 1}, + "INR": {"nounit": 1}, + "DFG": {"ml_per_min": 1, "ml/min": 1, "nounit": 1, "mL/min/1,73m²": 1}, +} +possible_values = { + "Créatininémie": 1000, + "Hémoglobine": 30, + "CRP": 300, + "INR": 10, + "DFG": 200, +} +``` + +## syndrome_des_anti-phospholipides + +```python +disease = "syndrome_des_anti-phospholipides" +english_title = "Antiphospholipid syndrome" +nlp_filtered_res, nlp_patient_group = prepare_nlp_df(disease) +structured_filtered_res, structured_patient_group = prepare_structured_df(disease) +_, nlp_patient_med_group = prepare_nlp_med_df(disease) +_, structured_patient_med_group = prepare_structured_med_df(disease) +structured_filtered_res["method"] = "structured_knowledge" +nlp_filtered_res["method"] = "nlp" +nlp_structured_filtered_res = pd.concat( + [ + structured_filtered_res[ + ["encounter_num", "patient_num", "value", "unit", "bio", "method"] + ], + nlp_filtered_res[ + ["encounter_num", "patient_num", "value", "unit", "bio", "method"] + ], + ] +) +nlp_structured_patient_group = ( + pd.concat([nlp_patient_group, structured_patient_group]) + .groupby("patient_num", as_index=False) + .max() +) +nlp_structured_patient_med_group = ( + pd.concat([nlp_patient_med_group, structured_patient_med_group]) + .groupby("patient_num", as_index=False) + .max() +) +``` + +```python +med_venn = dict(A="Héparine", B="Anticoagulants oraux") +plot_venn(nlp_patient_med_group, med_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_nlp_med.jpeg") +plt.show() +plot_venn( + structured_patient_med_group, med_venn, english_title, method="strctured data" +) +plt.savefig(f"figures/{disease}/venn_structured_med.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_med_group, + med_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_nlp_structured_med.jpeg") +plt.show() +``` + +```python +plot_summary_med(nlp_patient_med_group, structured_patient_med_group, english_title) +``` + +```python +nlp_hists = plot_hist(unit_convert, possible_values, nlp_filtered_res, True).properties( + title=english_title + " (NLP)" +) +strctured_hists = plot_hist( + unit_convert, possible_values, structured_filtered_res, False +).properties(title=english_title + " (structured data)") +nlp_strctured_hists = plot_hist( + unit_convert, possible_values, nlp_structured_filtered_res, False +).properties(title=english_title + " (NLP + structured data)") +chart = ( + (nlp_hists & strctured_hists & nlp_strctured_hists) + .resolve_scale(x="independent") + .resolve_scale(y="independent") + .configure_title(anchor="middle", fontSize=20, orient="left") +) +if not os.path.exists(f"figures/{disease}"): + os.makedirs(f"figures/{disease}") +chart.save(f"figures/{disease}/histogram.png") +chart.save(f"figures/{disease}/histogram.html") +# display(chart) +``` + +```python +Biology_nlp_hist.append( + plot_hist(unit_convert, possible_values, nlp_filtered_res, True).properties( + title=english_title + " (NLP)" + ) +) +Biology_structured_hist.append( + plot_hist(unit_convert, possible_values, structured_filtered_res, True).properties( + title=english_title + " (structured data)" + ) +) +Biology_nlp_structured_hist.append( + plot_hist( + unit_convert, possible_values, nlp_structured_filtered_res, True + ).properties(title=english_title + " (NLP + structured data)") +) +``` + +```python +bio_venn = dict( + A="Anti-cardiolipides", B="anti_B2GP1", C="anticoagulant_circulant_lupique" +) +plot_venn(nlp_patient_group, bio_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_nlp.jpeg") +plt.show() +plot_venn(structured_patient_group, bio_venn, english_title, method="strctured data") +plt.savefig(f"figures/{disease}/venn_structured.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_group, + bio_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_nlp_structured.jpeg") +plt.show() +``` + +```python +bio_venn = dict( + A="Anti-cardiolipides positif", + B="anti_B2GP1 positif", + C="anticoagulant_circulant_lupique positif", +) +plot_venn(nlp_patient_group, bio_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_pos_nlp.jpeg") +plt.show() +plot_venn(structured_patient_group, bio_venn, english_title, method="strctured data") +plt.savefig(f"figures/{disease}/venn_pos_structured.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_group, + bio_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_pos_nlp_structured.jpeg") +plt.show() +``` + +## Lupus + +FAN/AAN (C0587178), Anti-DNA Natif (C1262035) +Anti-Sm (C0201357) + +```python +disease = "lupus_erythemateux_dissemine" +english_title = "Lupus" +nlp_filtered_res, nlp_patient_group = prepare_nlp_df(disease) +structured_filtered_res, structured_patient_group = prepare_structured_df(disease) +_, nlp_patient_med_group = prepare_nlp_med_df(disease) +_, structured_patient_med_group = prepare_structured_med_df(disease) +structured_filtered_res["method"] = "structured_knowledge" +nlp_filtered_res["method"] = "nlp" +nlp_structured_filtered_res = pd.concat( + [ + structured_filtered_res[ + ["encounter_num", "patient_num", "value", "unit", "bio", "method"] + ], + nlp_filtered_res[ + ["encounter_num", "patient_num", "value", "unit", "bio", "method"] + ], + ] +) +nlp_structured_patient_group = ( + pd.concat([nlp_patient_group, structured_patient_group]) + .groupby("patient_num", as_index=False) + .max() +) +``` + +```python +plot_summary_med(nlp_patient_med_group, structured_patient_med_group, english_title) +``` + +```python +nlp_hists = plot_hist(unit_convert, possible_values, nlp_filtered_res, True).properties( + title=english_title + " (NLP)" +) +strctured_hists = plot_hist( + unit_convert, possible_values, structured_filtered_res, False +).properties(title=english_title + " (structured data)") +nlp_strctured_hists = plot_hist( + unit_convert, possible_values, nlp_structured_filtered_res, False +).properties(title=english_title + " (NLP + structured data)") +chart = ( + (nlp_hists & strctured_hists & nlp_strctured_hists) + .resolve_scale(x="independent") + .resolve_scale(y="independent") + .configure_title(anchor="middle", fontSize=20, orient="left") +) +if not os.path.exists(f"figures/{disease}"): + os.makedirs(f"figures/{disease}") +chart.save(f"figures/{disease}/histogram.png") +chart.save(f"figures/{disease}/histogram.html") +# display(chart) +``` + +```python +Biology_nlp_hist.append( + plot_hist(unit_convert, possible_values, nlp_filtered_res).properties( + title=english_title + " (NLP)" + ) +) +Biology_structured_hist.append( + plot_hist(unit_convert, possible_values, structured_filtered_res).properties( + title=english_title + " (structured data)" + ) +) +Biology_nlp_structured_hist.append( + plot_hist(unit_convert, possible_values, nlp_structured_filtered_res).properties( + title=english_title + " (NLP + structured data)" + ) +) +``` + +```python +bio_venn = dict(A="Facteur anti-nucléaire", B="Anti-DNA natif", C="Anti-Sm") +plot_venn(nlp_patient_group, bio_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_nlp.jpeg") +plt.show() +plot_venn(structured_patient_group, bio_venn, english_title, method="strctured data") +plt.savefig(f"figures/{disease}/venn_structured.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_group, + bio_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_nlp_structured.jpeg") +plt.show() +``` + +```python +bio_venn = dict( + A="Facteur anti-nucléaire positif", B="Anti-DNA natif positif", C="Anti-Sm positif" +) +plot_venn(nlp_patient_group, bio_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_pos_nlp.jpeg") +plt.show() +plot_venn(structured_patient_group, bio_venn, english_title, method="strctured data") +plt.savefig(f"figures/{disease}/venn_pos_structured.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_group, + bio_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_pos_nlp_structured.jpeg") +plt.show() +``` + +```python +bio_venn = dict(A="Facteur anti-nucléaire", B="Anti-DNA natif") +plot_venn(nlp_patient_group, bio_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_2_nlp.jpeg") +plt.show() +plot_venn(structured_patient_group, bio_venn, english_title, method="strctured data") +plt.savefig(f"figures/{disease}/venn_2_structured.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_group, + bio_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_2_nlp_structured.jpeg") +plt.show() +``` + +```python +bio_venn = dict(A="Facteur anti-nucléaire positif", B="Anti-DNA natif positif") +plot_venn(nlp_patient_group, bio_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_pos_2_nlp.jpeg") +plt.show() +plot_venn(structured_patient_group, bio_venn, english_title, method="strctured data") +plt.savefig(f"figures/{disease}/venn_pos_2_structured.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_group, + bio_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_pos_2_nlp_structured.jpeg") +plt.show() +``` + +## Sclérodermie systémique + +```python +disease = "sclerodermie_systemique" +english_title = "systemic sclerosis" +nlp_filtered_res, nlp_patient_group = prepare_nlp_df(disease) +structured_filtered_res, structured_patient_group = prepare_structured_df(disease) +_, nlp_patient_med_group = prepare_nlp_med_df(disease) +_, structured_patient_med_group = prepare_structured_med_df(disease) +structured_filtered_res["method"] = "structured_knowledge" +nlp_filtered_res["method"] = "nlp" +nlp_structured_filtered_res = pd.concat( + [ + structured_filtered_res[ + ["encounter_num", "patient_num", "value", "unit", "bio", "method"] + ], + nlp_filtered_res[ + ["encounter_num", "patient_num", "value", "unit", "bio", "method"] + ], + ] +) +nlp_structured_patient_group = ( + pd.concat([nlp_patient_group, structured_patient_group]) + .groupby("patient_num", as_index=False) + .max() +) +``` + +```python +plot_summary_med(nlp_patient_med_group, structured_patient_med_group, english_title) +``` + +```python +nlp_hists = plot_hist(unit_convert, possible_values, nlp_filtered_res, True).properties( + title=english_title + " (NLP)" +) +strctured_hists = plot_hist( + unit_convert, possible_values, structured_filtered_res, False +).properties(title=english_title + " (structured data)") +nlp_strctured_hists = plot_hist( + unit_convert, possible_values, nlp_structured_filtered_res, False +).properties(title=english_title + " (NLP + structured data)") +chart = ( + (nlp_hists & strctured_hists & nlp_strctured_hists) + .resolve_scale(x="independent") + .resolve_scale(y="independent") + .configure_title(anchor="middle", fontSize=20, orient="left") +) +if not os.path.exists(f"figures/{disease}"): + os.makedirs(f"figures/{disease}") +chart.save(f"figures/{disease}/histogram.png") +chart.save(f"figures/{disease}/histogram.html") +# display(chart) +``` + +```python +Biology_nlp_hist.append( + plot_hist(unit_convert, possible_values, nlp_filtered_res).properties( + title=english_title + " (NLP)" + ) +) +Biology_structured_hist.append( + plot_hist(unit_convert, possible_values, structured_filtered_res).properties( + title=english_title + " (structured data)" + ) +) +Biology_nlp_structured_hist.append( + plot_hist(unit_convert, possible_values, nlp_structured_filtered_res).properties( + title=english_title + " (NLP + structured data)" + ) +) +``` + +```python +bio_venn = dict(A="Anti-RNA pol 3", B="Anti-SCL 70") +plot_venn(nlp_patient_group, bio_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_nlp.jpeg") +plt.show() +plot_venn(structured_patient_group, bio_venn, english_title, method="strctured data") +plt.savefig(f"figures/{disease}/venn_structured.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_group, + bio_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_nlp_structured.jpeg") +plt.show() +``` + +```python +bio_venn = dict(A="Anti-RNA pol 3 positif", B="Anti-SCL 70 positif") +plot_venn(nlp_patient_group, bio_venn, english_title, method="discharge summaries") +plt.savefig(f"figures/{disease}/venn_pos_nlp.jpeg") +plt.show() +plot_venn(structured_patient_group, bio_venn, english_title, method="strctured data") +plt.savefig(f"figures/{disease}/venn_pos_structured.jpeg") +plt.show() +plot_venn( + nlp_structured_patient_group, + bio_venn, + english_title, + method="discharge summaries and structured data", +) +plt.savefig(f"figures/{disease}/venn_pos_nlp_structured.jpeg") +plt.show() +``` + +### Maladie de Takayasu + +```python +disease = "maladie_de_takayasu" +english_title = "Takayasu´s disease" +nlp_filtered_res, _ = prepare_nlp_df(disease) +structured_filtered_res, _ = prepare_structured_df(disease) +_, nlp_patient_med_group = prepare_nlp_med_df(disease) +_, structured_patient_med_group = prepare_structured_med_df(disease) +structured_filtered_res["method"] = "structured_knowledge" +nlp_filtered_res["method"] = "nlp" +nlp_structured_filtered_res = pd.concat( + [ + structured_filtered_res[ + ["encounter_num", "patient_num", "value", "unit", "bio", "method"] + ], + nlp_filtered_res[ + ["encounter_num", "patient_num", "value", "unit", "bio", "method"] + ], + ] +) +``` + +```python +plot_summary_med(nlp_patient_med_group, structured_patient_med_group, english_title) +``` + +```python +nlp_hists = plot_hist(unit_convert, possible_values, nlp_filtered_res, True).properties( + title=english_title + " (NLP)" +) +strctured_hists = plot_hist( + unit_convert, possible_values, structured_filtered_res, False +).properties(title=english_title + " (structured data)") +nlp_strctured_hists = plot_hist( + unit_convert, possible_values, nlp_structured_filtered_res, False +).properties(title=english_title + " (NLP + structured data)") +chart = ( + (nlp_hists & strctured_hists & nlp_strctured_hists) + .resolve_scale(x="independent") + .resolve_scale(y="independent") + .configure_title(anchor="middle", fontSize=20, orient="left") +) +if not os.path.exists(f"figures/{disease}"): + os.makedirs(f"figures/{disease}") +chart.save(f"figures/{disease}/histogram.png") +chart.save(f"figures/{disease}/histogram.html") +# display(chart) +``` + +```python +Biology_nlp_hist.append( + plot_hist(unit_convert, possible_values, nlp_filtered_res).properties( + title=english_title + " (NLP)" + ) +) +Biology_structured_hist.append( + plot_hist(unit_convert, possible_values, structured_filtered_res).properties( + title=english_title + " (structured data)" + ) +) +Biology_nlp_structured_hist.append( + plot_hist(unit_convert, possible_values, nlp_structured_filtered_res).properties( + title=english_title + " (NLP + structured data)" + ) +) +``` + +```python +chart = reduce( + lambda bar_chart_1, bar_chart_2: (bar_chart_1 & bar_chart_2) + .resolve_scale(x="independent") + .resolve_scale(y="independent"), + Biology_nlp_hist, +).configure_title(orient="left", anchor="middle", fontSize=20) +chart.save("figures/histogram_nlp.png") +chart.save("figures/histogram_nlp.html") +# display(chart) +``` + +```python +chart = reduce( + lambda bar_chart_1, bar_chart_2: (bar_chart_1 & bar_chart_2) + .resolve_scale(x="independent") + .resolve_scale(y="independent"), + Biology_structured_hist, +).configure_title(orient="left", anchor="middle", fontSize=20) +chart.save("figures/histogram_structured.png") +chart.save("figures/histogram_structured.html") +# display(chart) +``` + +```python +chart = reduce( + lambda bar_chart_1, bar_chart_2: (bar_chart_1 & bar_chart_2) + .resolve_scale(x="independent") + .resolve_scale(y="independent"), + Biology_nlp_structured_hist, +).configure_title(orient="left", anchor="middle", fontSize=20) +chart.save("figures/histogram_nlp_structured.png") +chart.save("figures/histogram_nlp_structured.html") +# display(chart) +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python + +``` + +```python +res_filtered_df[res_filtered_df.bio == "DFG"].unit.value_counts() +``` + +```python +"Anti-B2GP1" =[ + "E9627", + "A7854", + "H3772", + "E5157", + "I2042", + "I1882", + "X9708", + "H6269", + "X5898", + "X2761", + "A7855", + "H9650", + "E9626", + "I2043", + "I1883", + "X9707", + "H6270", + "X5899", + "I5970", + "H5543", + "H6271", + "J9678", + "J9704", + "J9705", + "K8345", +] +``` + +```python +structured_filtered_res.explode("name_char").name_char.str.split(":").str.get(0).unique() +``` + +```python +structured_res = pd.read_json( + join(BRAT_DIR, "summary_df_bio.json"), + dtype={"encounter_num": str, "patient_num": str}, +).explode("label") +``` + +```python +structured_res = structured_res.explode("name_char") +structured_res[structured_res.name_char.str.contains("F8160")] +``` diff --git a/notebooks/knowledge.py b/notebooks/knowledge.py new file mode 100644 index 000000000..4eb183062 --- /dev/null +++ b/notebooks/knowledge.py @@ -0,0 +1,650 @@ +TO_BE_MATCHED = { + "lupus_erythemateux_dissemine": { + "CIM10": ["M320", "M321", "M328", "M329"], + "ATC_codes" : { + "Corticothérapie systémique": ["H02AB"], + "Endoxan": ["L01AA01"], + "Cellcept": ["L04AA06"], + "Rituximab": ["L01XC02"], + "Belimumab": ["L04AA26"], + "Methotrexate": ["L04AX03"], + "Plaquenil": ["P01BA02"], + "Vaccins Prevenar": ["J07AL02"], + "Vaccins Pneumovax": ["J07AL01"], + "Vaccins Grippe": ["J07BB"], + }, + "ANABIO_codes": { + "CRP": ["E6332", "A0248", "F5581", "J7381", "F2631"], + "Créatininémie": [ + "H9412", + "F2621", + "H9411", + "H4038", + "H9421", + "A0094", + "J4687", + "C0697", + "H9427", + "H9426", + "E3180", + "A7813", + "H9417", + "F9410", + "H9424", + "G1975", + "H9413", + "H9418", + "J1002", + "H9419", + "G7834", + "H9415", + "H9425", + "G1974", + "H9422", + "H9416", + "F9409", + "H9414", + "H9423", + "H9420", + ], + "DFG": ["F8160", "G6921"], + "Hémoglobine": [ + "J6562", + "J4764", + "A9882", + "B1946", + "C1549", + "B1947", + "Z0363", + "I4392", + "C8745", + "E9823", + "I7893", + "B1945", + "F8179", + "A0163", + ], + "Facteur anti-nucléaire": [ + "A7947", + "A7941", + "A7953", + "C2707", + "C2757", + "C2762", + "C2758", + "C2763", + "C2759", + "C2764", + "C2760", + "C2765", + "I1951", + "I1952", + "I1953", + "I1954", + "I1955", + "I1956", + "I1839", + ], + "Anti-DNA natif": [ + "A7844", + "A7847", + "C2683", + "C2684", + "A7850", + "B2594", + "E6639", + "B2599", + "H8136", + "C8243", + "C8245", + "I2221", + "I2222", + "L7308", + "C8765", + "A7838", + "H9653", + "C2748", + "A7835", + "B2595", + "C5008", + "C2685", + "X5893", + "B2600", + "A7841", + "L4997", + "L4998", + "D0198", + "X4089", + "X4807", + ], + "Anti-Sm": [ + "E6951", + "C8704", + "H6257", + "B3391", + "A7983", + "C5061", + "C7978", + "H8580", + "B3393", + "L7304", + "B3389", + ], + }, + "CUI_per_section": { + "all": { + "Facteur anti-nucléaire": [ + "C0587178", + "C1271804", + "C1273464", + "C1277811", + ], + "Anti-DNA natif": ["C1262035", "C0282056"], + "Anti-Sm": ["C0201357"], + "Hémoglobine": ["C0518015"], + "CRP": ["C0201657"], + "Créatininémie": ["C0201975"], + "DFG": ["C0017654", "C2733005"], + }, + "SECTION_examen_complementaire": {}, + }, + }, + "syndrome_des_anti-phospholipides": { + "CIM10": ["D686"], + "ATC_codes" : { + "Héparine": ["B01AB01"], + "Anticoagulants oraux": ["B01AF02"], + }, + "ANABIO_codes": { + "CRP": ["E6332", "A0248", "F5581", "J7381", "F2631"], + "Créatininémie": [ + "H9412", + "F2621", + "H9411", + "H4038", + "H9421", + "A0094", + "J4687", + "C0697", + "H9427", + "H9426", + "E3180", + "A7813", + "H9417", + "F9410", + "H9424", + "G1975", + "H9413", + "H9418", + "J1002", + "H9419", + "G7834", + "H9415", + "H9425", + "G1974", + "H9422", + "H9416", + "F9409", + "H9414", + "H9423", + "H9420", + ], + "Hémoglobine": [ + "J6562", + "J4764", + "A9882", + "B1946", + "C1549", + "B1947", + "Z0363", + "I4392", + "C8745", + "E9823", + "I7893", + "B1945", + "F8179", + "A0163", + ], + "INR": ["A0269"], + "Anti-cardiolipides": [ + "H5544", + "E2153", + "C3960", + "H3773", + "E2154", + "C2686", + "L5048", + "I1880", + "X9706", + "A7856", + "H6272", + "K9986", + "L7125", + "X5896", + "X8627", + "E2155", + "C6288", + "H3774", + "E2156", + "C6289", + "L5049", + "I1881", + "X9705", + "A7857", + "H6273", + "K9987", + "L7124", + "X5897", + "I5969", + "H5542", + "H6274", + "L5050", + "J9703", + "X4100", + "K8344", + ], + "Anti_phospholipides": [ + "B4504", + "E2293", + "E2297", + "E2294", + "L1016", + "C7890", + "B4505", + "E2295", + "E2298", + "E2296", + "L1017", + "C7891", + "C4781", + "X5900", + "F8079", + "I6159", + "F8080", + "I6160", + "L5296", + "F8139", + "I6150", + "L5297", + "F8140", + "I6149", + "L5298", + "X2612", + "X4808", + "B2604", + "I6157", + "B2605", + "I6158", + "K8346", + ], + "anti_B2GP1": [ + "E9627", + "A7854", + "H3772", + "E5157", + "I2042", + "L5051", + "I1882", + "X9708", + "H6269", + "L7127", + "X5898", + "X2761", + "A7855", + "H9650", + "E9626", + "I2043", + "L5052", + "I1883", + "X9707", + "H6270", + "L7126", + "X5899", + "I5970", + "H5543", + "H6271", + "J9678", + "L5053", + "J9704", + "J9705", + "K8345", + ], + "anticoagulant_circulant_lupique": [ + "A7747", + "A7746", + "A7749", + "F0800", + "F0803", + "A1793", + "F0807", + "A7748", + "C4797", + "A7750", + "A1791", + "D0020", + "K9945", + "K9946", + "K9947", + "K9948", + "J7800", + "J7801", + "J7802", + "J7803", + "J7804", + "J7805", + "B3819", + "B5606", + "B3821", + "B3817", + "E2945", + "B9000", + "B9001", + "B9002", + "E5356", + "B8999", + "B3820", + "B5607", + "B3822", + "E5357", + "B3818", + "E5344", + "H5107", + "C5050", + "X2278", + "X5408", + "E5358", + "E5360", + "E5359", + "G1945", + "G1946", + "G1947", + "G1948", + "G1949", + "G1950", + "G1951", + "X4109", + "C2770", + "C2773", + "E5355", + "E5948", + "E5412", + "X4143", + "B9005", + "B9003", + "B9004", + "B8082", + "B8084", + "B8083", + "B9995", + "B9997", + "C1407", + "C1411", + "C1409", + "C1406", + "A1809", + "A0358", + "E5413", + "A0354", + "A1810", + "A3966", + "G1987", + "H5105", + "J3287", + "J3283", + "J3282", + "J3284", + "J3301", + "J3300", + "A0351", + "L8492", + "A0355", + "F0786", + "A0352", + "A0356", + "A3965", + "A3964", + "A0353", + "H5217", + "J3299", + "J3298", + "J3297", + "J3296", + "J3295", + "J3294", + "J3292", + "J3290", + "L8493", + "B4084", + "C2267", + "B9990", + "X4145", + "J3288", + "J2389", + "J2397", + "J2395", + "J2394", + "J2396", + "J2393", + "J2392", + "J2391", + "J2386", + "J2388", + "J2390", + "J2387", + "J2384", + "J2383", + "K2941", + "K2942", + "K2943", + "J2385", + "X2155", + "X4796", + "L3541", + "L7206", + "L7207", + ], + }, + "CUI_per_section": { + "all": { + "Anti-cardiolipides": ["C0201535", "C0455311"], + "Anti_phospholipides": ["C0201534"], + "anti_B2GP1": ["C1295005", "C1303280"], + "anticoagulant_circulant_lupique": [ + "C0455328", + "C1142517", + "C1277823", + "C0522828", + ], + "INR": ["C0525032"], + "Hémoglobine": ["C0518015"], + "CRP": ["C0201657"], + "Créatininémie": ["C0201975"], + }, + "SECTION_examen_complementaire": {}, + }, + }, + "sclerodermie_systemique": { + "CIM10": ["M340", "M341", "M348", "M349"], + "ATC_codes" : { + "IgIV": ["J06BA02"], + "Corticothérapie systémique": ["H02AB"], + "Endoxan": ["L01AA01"], + "Cellcept": ["L04AA06"], + "Rituximab": ["L01XC02"], + "Belimumab": ["L04AA26"], + "Methotrexate": ["L04AX03"], + "Plaquenil": ["P01BA02"], + "Vaccins Prevenar": ["J07AL02"], + "Vaccins Pneumovax": ["J07AL01"], + "Vaccins Grippe": ["J07BB"], + }, + "ANABIO_codes": { + "CRP": ["E6332", "A0248", "F5581", "J7381", "F2631"], + "Créatininémie": [ + "H9412", + "F2621", + "H9411", + "H4038", + "H9421", + "A0094", + "J4687", + "C0697", + "H9427", + "H9426", + "E3180", + "A7813", + "H9417", + "F9410", + "H9424", + "G1975", + "H9413", + "H9418", + "J1002", + "H9419", + "G7834", + "H9415", + "H9425", + "G1974", + "H9422", + "H9416", + "F9409", + "H9414", + "H9423", + "H9420", + ], + "DFG": ["F8160", "G6921"], + "Hémoglobine": [ + "J6562", + "J4764", + "A9882", + "B1946", + "C1549", + "B1947", + "Z0363", + "I4392", + "C8745", + "E9823", + "I7893", + "B1945", + "F8179", + "A0163", + ], + "Anti-RNA pol 3": [ + "C4950", + "C6911", + "E6593", + "I2082", + "K9937", + "L1626", + "E7376", + "H8235", + ], + "Anti-SCL 70": [ + "E6950", + "C8702", + "H6256", + "B4528", + "C5060", + "A7981", + "C9997", + "H8582", + "B4530", + "L7302", + "B4527", + "H8573", + "C2711", + "K5223", + ], + }, + "CUI_per_section": { + "all": { + "Anti-RNA pol 3": ["C1295034"], + "Anti-SCL 70": ["C0523317"], + "Anti-centromères": ["C0201361"], + "Hémoglobine": ["C0518015"], + "CRP": ["C0201657"], + "Créatininémie": ["C0201975"], + "DFG": ["C0017654", "C2733005"], + }, + "SECTION_examen_complementaire": {}, + }, + }, + "maladie_de_takayasu": { + "CIM10": ["M314"], + "ATC_codes" : { + "Corticothérapie systémique": ["H02AB"], + "Endoxan": ["L01AA01"], + "Tocilizumab": ["L04AC07"], + "Cellcept": ["L04AA06"], + "Rituximab": ["L01XC02"], + "Belimumab": ["L04AA26"], + "Methotrexate": ["L04AX03"], + "Plaquenil": ["P01BA02"], + "IgIV": ["J06BA02"], + "Vaccins Prevenar": ["J07AL02"], + "Vaccins Pneumovax": ["J07AL01"], + "Vaccins Grippe": ["J07BB"], + }, + "ANABIO_codes": { + "CRP": ["E6332", "A0248", "F5581", "J7381", "F2631"], + "Créatininémie": [ + "H9412", + "F2621", + "H9411", + "H4038", + "H9421", + "A0094", + "J4687", + "C0697", + "H9427", + "H9426", + "E3180", + "A7813", + "H9417", + "F9410", + "H9424", + "G1975", + "H9413", + "H9418", + "J1002", + "H9419", + "G7834", + "H9415", + "H9425", + "G1974", + "H9422", + "H9416", + "F9409", + "H9414", + "H9423", + "H9420", + ], + "DFG": ["F8160", "G6921"], + "Hémoglobine": [ + "J6562", + "J4764", + "A9882", + "B1946", + "C1549", + "B1947", + "Z0363", + "I4392", + "C8745", + "E9823", + "I7893", + "B1945", + "F8179", + "A0163", + ], + }, + "CUI_per_section": { + "all": { + "Hémoglobine": ["C0518015"], + "CRP": ["C0201657"], + "Créatininémie": ["C0201975"], + "DFG": ["C0017654", "C2733005"], + }, + "SECTION_examen_complementaire": {}, + }, + }, +} diff --git a/tests/resources/brat_data/subfolder/doc-1.ann b/tests/resources/brat_data/subfolder/doc-1.ann deleted file mode 100644 index b9bc7af1e..000000000 --- a/tests/resources/brat_data/subfolder/doc-1.ann +++ /dev/null @@ -1,24 +0,0 @@ -R1 lieu Arg1:T8 Arg2:T9 -T1 sosy 30 38 douleurs -T2 localisation 39 57 dans le bras droit -T3 anatomie 47 57 bras droit -T4 pathologie 75 83;85 98 problème de locomotion -A1 assertion T4 absent -T5 pathologie 114 117 AVC -A2 etat T5 passé -A3 assertion T5 non-associé -T6 pathologie 159 164 rhume -A4 etat T6 présent -A5 assertion T6 hypothétique -T7 pathologie 291 296 rhume -A6 etat T7 présent -A7 assertion T7 hypothétique -T8 sosy 306 314 Douleurs -T9 localisation 315 333 dans le bras droit -T10 anatomie 323 333 bras droit -T11 sosy 378 386 anomalie -#1 AnnotatorNotes T7 Repetition -R2 lieu Arg1:T1 Arg2:T2 -A8 assertion T11 absent -E1 MyArg1:T3 MyArg2:T1 -E2 MyArg1:T1 MyArg2:E1 diff --git a/tests/resources/brat_data/subfolder/doc-1.txt b/tests/resources/brat_data/subfolder/doc-1.txt deleted file mode 100644 index 37cfaa255..000000000 --- a/tests/resources/brat_data/subfolder/doc-1.txt +++ /dev/null @@ -1,10 +0,0 @@ -Le patient est admis pour des douleurs dans le bras droit, mais n'a pas de problème -de locomotion. -Historique d'AVC dans la famille. pourrait être un cas de rhume. -NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNBWbWbNbNbNBWbNbWbNbWBNbNbWbNbNBNbWbWbNbWBNbNbWbNBNbWbWbNb -Pourrait être un cas de rhume. -Motif : -Douleurs dans le bras droit. -ANTÉCÉDENTS -Le patient est déjà venu -Pas d'anomalie détectée. From 7926b24f8f9e57440b93134d64d488296a894881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Fri, 1 Dec 2023 15:35:50 +0000 Subject: [PATCH 11/11] Initiate repo from scratch --- NER_model/configs/rebus/__.cfg | 2 +- NER_model/configs/rebus/config_1907.cfg | 2 +- NER_model/configs/rebus/config_backup2.cfg | 2 +- NER_model/project.yml | 6 +- NER_model/scripts/convert.py | 12 +- NER_model/scripts/infer.py | 25 +- NER_model/scripts/new_evaluate.py | 14 +- NER_model/scripts/save_to_brat.py | 154 +++++---- Normalisation/drugs/exception.py | 25 +- Normalisation/drugs/normalisation.py | 115 ++++--- Normalisation/extract_measurement/config.py | 7 +- .../extract_measurements_from_brat.py | 265 ++++++++++----- .../extract_pandas_from_brat.py | 55 ++-- Normalisation/extract_measurement/main.py | 24 +- .../measurements_patterns.py | 17 +- Normalisation/inference/config.py | 19 +- .../inference/extract_pandas_from_brat.py | 55 ++-- .../inference/get_normalization_with_coder.py | 53 +-- Normalisation/inference/main.py | 137 ++++---- Normalisation/inference/text_preprocessor.py | 22 +- Normalisation/training/data_util.py | 105 ++++-- Normalisation/training/extract_bert.py | 6 +- .../training/extract_pandas_from_brat.py | 55 ++-- .../training/generate_term_embeddings.py | 53 ++- .../training/generate_term_embeddings.sh | 12 +- .../generate_term_embeddings_coder_eds.py | 51 ++- .../generate_term_embeddings_coder_eds.sh | 12 +- ...enerate_term_embeddings_coder_eds_cased.py | 51 ++- ...enerate_term_embeddings_coder_eds_cased.sh | 12 +- .../training/generate_umls_embeddings.py | 57 +++- .../training/generate_umls_embeddings.sh | 12 +- .../generate_umls_normalized_embeddings.py | 19 +- .../generate_umls_normalized_embeddings.sh | 12 +- ...te_umls_normalized_embeddings_coder_eds.py | 19 +- ...te_umls_normalized_embeddings_coder_eds.sh | 12 +- ...s_normalized_embeddings_coder_eds_cased.py | 17 +- ...s_normalized_embeddings_coder_eds_cased.sh | 12 +- Normalisation/training/get_matches.py | 19 +- Normalisation/training/get_matches.sh | 10 +- Normalisation/training/load_umls.py | 37 ++- .../training/load_umls_normalized.py | 43 ++- Normalisation/training/loss.py | 58 ++-- Normalisation/training/model.py | 101 +++--- Normalisation/training/sampler_util.py | 10 +- Normalisation/training/train.py | 310 ++++++++++++------ Normalisation/training/trans.py | 8 +- bash_scripts/NER_model/expe_data_size.sh | 10 +- bash_scripts/NER_model/expe_hyperparams.sh | 4 +- bash_scripts/NER_model/expe_model_lang.sh | 10 +- bash_scripts/NER_model/infer.sh | 8 +- bash_scripts/NER_model/save.sh | 6 +- bash_scripts/NER_model/test.sh | 6 +- bash_scripts/NER_model/train.sh | 4 +- bash_scripts/NER_model/train_v1.sh | 4 +- bash_scripts/Normalisation/infer_coder.sh | 8 +- .../Normalisation/infer_coder_quaero.sh | 4 +- .../2023-06-16_15:03:31/config.cfg | 18 + .../2023-06-16_15:03:31/sbatch.sh | 14 + .../2023-06-16_15:07:58/config.cfg | 18 + .../2023-06-16_15:07:58/sbatch.sh | 14 + .../2023-06-16_15:09:08/config.cfg | 18 + .../2023-06-16_15:09:08/sbatch.sh | 14 + .../2023-06-16_15:09:57/config.cfg | 18 + .../2023-06-16_15:09:57/sbatch.sh | 14 + .../2023-06-16_15:11:04/config.cfg | 18 + .../2023-06-16_15:11:04/sbatch.sh | 14 + .../2023-06-16_15:16:23/config.cfg | 18 + .../2023-06-16_15:16:23/sbatch.sh | 14 + .../2023-06-16_15:20:28/config.cfg | 18 + .../2023-06-16_15:20:28/sbatch.sh | 14 + .../2023-06-16_15:24:27/config.cfg | 18 + .../2023-06-16_15:24:27/sbatch.sh | 14 + .../2023-06-19_12:50:04/config.cfg | 18 + .../2023-06-19_12:50:04/sbatch.sh | 14 + .../2023-06-23_15:33:55/config.cfg | 18 + .../2023-06-23_15:33:55/sbatch.sh | 14 + .../2023-06-23_15:34:32/config.cfg | 18 + .../2023-06-23_15:34:32/sbatch.sh | 14 + .../2023-06-23_15:35:28/config.cfg | 18 + .../2023-06-23_15:35:28/sbatch.sh | 14 + .../2023-06-23_15:36:41/config.cfg | 18 + .../2023-06-23_15:36:41/sbatch.sh | 14 + .../2023-07-06_15:09:43/config.cfg | 18 + .../2023-07-06_15:09:43/sbatch.sh | 14 + .../2023-07-06_15:16:12/config.cfg | 18 + .../2023-07-06_15:16:12/sbatch.sh | 14 + .../2023-07-06_15:20:05/config.cfg | 18 + .../2023-07-06_15:20:05/sbatch.sh | 14 + .../2023-07-06_15:31:19/config.cfg | 18 + .../2023-07-06_15:31:19/sbatch.sh | 14 + .../2023-07-07_09:59:30/config.cfg | 18 + .../2023-07-07_09:59:30/sbatch.sh | 14 + .../2023-07-10_14:47:39/config.cfg | 18 + .../2023-07-10_14:47:39/sbatch.sh | 14 + .../2023-07-11_15:31:59/config.cfg | 18 + .../2023-07-11_15:31:59/sbatch.sh | 14 + .../2023-07-12_15:42:51/config.cfg | 18 + .../2023-07-12_15:42:51/sbatch.sh | 14 + .../2023-07-17_12:17:49/config.cfg | 18 + .../2023-07-17_12:17:49/sbatch.sh | 14 + .../2023-07-19_15:24:15/config.cfg | 18 + .../2023-07-19_15:24:15/sbatch.sh | 14 + .../2023-07-21_09:07:59/config.cfg | 18 + .../2023-07-21_09:07:59/sbatch.sh | 14 + .../2023-07-24_07:41:05/config.cfg | 18 + .../2023-07-24_07:41:05/sbatch.sh | 14 + .../2023-07-24_09:35:24/config.cfg | 18 + .../2023-07-24_09:35:24/sbatch.sh | 14 + .../2023-07-25_14:10:30/config.cfg | 18 + .../2023-07-25_14:10:30/sbatch.sh | 14 + .../2023-07-25_14:10:58/config.cfg | 18 + .../2023-07-25_14:10:58/sbatch.sh | 14 + .../2023-07-25_14:13:54/config.cfg | 18 + .../2023-07-25_14:13:54/sbatch.sh | 14 + .../2023-11-23_19:14:27/config.cfg | 18 + .../2023-11-23_19:14:27/sbatch.sh | 14 + .../2023-11-23_19:17:42/config.cfg | 18 + .../2023-11-23_19:17:42/sbatch.sh | 14 + .../2023-11-23_19:19:16/config.cfg | 18 + .../2023-11-23_19:19:16/sbatch.sh | 14 + .../2023-11-27_13:49:17/config.cfg | 18 + .../2023-11-27_13:49:17/sbatch.sh | 14 + bash_scripts/Normalisation/train_coder.sh | 2 +- edsnlp/edsnlp/__init__.py | 11 +- edsnlp/edsnlp/connectors/brat.py | 3 +- edsnlp/edsnlp/evaluate.py | 127 ++++--- .../pipelines/misc/measurements/factory.py | 11 +- .../misc/measurements/measurements.py | 51 ++- .../pipelines/misc/measurements/patterns.py | 2 +- .../trainable/nested_ner/nested_ner.py | 4 +- .../span_qualifier/span_qualifier.py | 13 +- edsnlp/edsnlp/utils/filter.py | 2 +- edsnlp/tests/pipelines/core/test_matcher.py | 2 +- notebooks/connectors/omop.md | 99 ------ notebooks/context.py | 5 - notebooks/dates/context.py | 5 - notebooks/dates/prototype.md | 89 ----- notebooks/dates/user-guide.md | 93 ------ notebooks/endlines/endlines-example.md | 182 ---------- notebooks/example.txt | 11 - notebooks/export_pandas_to_brat.py | 61 +++- notebooks/get_stats_by_section_on_cim10.md | 6 +- notebooks/knowledge.py | 8 +- notebooks/normalizer/context.py | 5 - notebooks/normalizer/profiling.md | 235 ------------- notebooks/normalizer/prototype.md | 109 ------ notebooks/pipeline.md | 193 ----------- notebooks/premier-pipeline.md | 260 --------------- notebooks/sections/annotated_sections.csv | 121 ------- notebooks/sections/context.py | 5 - notebooks/sections/section-dataset.md | 158 --------- notebooks/sections/sections.xlsx | Bin 12717 -> 0 bytes notebooks/sections/testing.md | 168 ---------- notebooks/tnm/prototype.md | 63 ---- notebooks/tokenizer/context.py | 5 - notebooks/tokenizer/tokenizer.md | 141 -------- notebooks/utilities/brat.md | 99 ------ notebooks/utilities/context.py | 5 - 158 files changed, 2580 insertions(+), 3018 deletions(-) create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:03:31/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:03:31/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:07:58/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:07:58/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:08/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:08/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:57/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:57/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:11:04/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:11:04/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:16:23/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:16:23/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:20:28/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:20:28/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:24:27/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-16_15:24:27/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-19_12:50:04/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-19_12:50:04/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-23_15:33:55/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-23_15:33:55/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-23_15:34:32/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-23_15:34:32/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-23_15:35:28/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-23_15:35:28/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-23_15:36:41/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-06-23_15:36:41/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-06_15:09:43/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-06_15:09:43/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-06_15:16:12/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-06_15:16:12/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-06_15:20:05/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-06_15:20:05/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-06_15:31:19/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-06_15:31:19/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-07_09:59:30/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-07_09:59:30/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-10_14:47:39/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-10_14:47:39/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-11_15:31:59/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-11_15:31:59/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-12_15:42:51/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-12_15:42:51/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-17_12:17:49/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-17_12:17:49/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-19_15:24:15/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-19_15:24:15/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-21_09:07:59/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-21_09:07:59/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-24_07:41:05/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-24_07:41:05/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-24_09:35:24/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-24_09:35:24/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:30/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:30/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:58/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:58/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-25_14:13:54/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-07-25_14:13:54/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-11-23_19:14:27/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-11-23_19:14:27/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-11-23_19:17:42/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-11-23_19:17:42/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-11-23_19:19:16/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-11-23_19:19:16/sbatch.sh create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-11-27_13:49:17/config.cfg create mode 100644 bash_scripts/Normalisation/log_train_coder/2023-11-27_13:49:17/sbatch.sh delete mode 100644 notebooks/connectors/omop.md delete mode 100644 notebooks/context.py delete mode 100644 notebooks/dates/context.py delete mode 100644 notebooks/dates/prototype.md delete mode 100644 notebooks/dates/user-guide.md delete mode 100644 notebooks/endlines/endlines-example.md delete mode 100644 notebooks/example.txt delete mode 100644 notebooks/normalizer/context.py delete mode 100644 notebooks/normalizer/profiling.md delete mode 100644 notebooks/normalizer/prototype.md delete mode 100644 notebooks/pipeline.md delete mode 100644 notebooks/premier-pipeline.md delete mode 100644 notebooks/sections/annotated_sections.csv delete mode 100644 notebooks/sections/context.py delete mode 100644 notebooks/sections/section-dataset.md delete mode 100644 notebooks/sections/sections.xlsx delete mode 100644 notebooks/sections/testing.md delete mode 100644 notebooks/tnm/prototype.md delete mode 100644 notebooks/tokenizer/context.py delete mode 100644 notebooks/tokenizer/tokenizer.md delete mode 100644 notebooks/utilities/brat.md delete mode 100644 notebooks/utilities/context.py diff --git a/NER_model/configs/rebus/__.cfg b/NER_model/configs/rebus/__.cfg index 849b3a6ec..7922b33a5 100644 --- a/NER_model/configs/rebus/__.cfg +++ b/NER_model/configs/rebus/__.cfg @@ -104,7 +104,7 @@ use_fast = true [corpora.train] @readers = "eds-medic.Corpus.v1" path = ${paths.train} -max_length = 384 +max_length = 384 gold_preproc = false limit = 0 augmenter = null diff --git a/NER_model/configs/rebus/config_1907.cfg b/NER_model/configs/rebus/config_1907.cfg index 1f063303b..cf9399a90 100644 --- a/NER_model/configs/rebus/config_1907.cfg +++ b/NER_model/configs/rebus/config_1907.cfg @@ -104,7 +104,7 @@ use_fast = true [corpora.train] @readers = "eds-medic.Corpus.v1" path = ${paths.train} -max_length = 384 +max_length = 384 gold_preproc = false limit = 0 augmenter = null diff --git a/NER_model/configs/rebus/config_backup2.cfg b/NER_model/configs/rebus/config_backup2.cfg index 275ab1b7c..3fe9a03d5 100644 --- a/NER_model/configs/rebus/config_backup2.cfg +++ b/NER_model/configs/rebus/config_backup2.cfg @@ -105,7 +105,7 @@ use_fast = true [corpora.train] @readers = "eds-medic.Corpus.v1" path = ${paths.train} -max_length = 384 +max_length = 384 gold_preproc = false limit = 0 augmenter = null diff --git a/NER_model/project.yml b/NER_model/project.yml index 4b305f59a..153ca9344 100644 --- a/NER_model/project.yml +++ b/NER_model/project.yml @@ -15,9 +15,9 @@ vars: train: "data/NLP_diabeto/train" test: "data/NLP_diabeto/test" dev: "data/NLP_diabeto/val" -# jeu suyr lequel tester le modèle : +# jeu suyr lequel tester le modèle : # brat_data/QUAERO_FrenchMed/corpus/train/EMEA/ - + corpus: "corpus" training: "training" seed: 0 @@ -94,7 +94,7 @@ commands: # outputs: # - "${vars.corpus}/output.spacy" # - "${vars.training}/test_metrics.json" - + - name: "evaluate" help: "Evaluate the model and export metrics" script: diff --git a/NER_model/scripts/convert.py b/NER_model/scripts/convert.py index 6cae21498..874418d6c 100644 --- a/NER_model/scripts/convert.py +++ b/NER_model/scripts/convert.py @@ -5,11 +5,12 @@ import spacy import srsly import typer -from edsnlp.connectors.brat import BratConnector from spacy.language import Language from spacy.tokens import Doc, DocBin from spacy.util import filter_spans +from edsnlp.connectors.brat import BratConnector + if not Doc.has_extension("context"): Doc.set_extension("context", default=dict()) if not Doc.has_extension("note_id"): @@ -58,7 +59,7 @@ def convert_jsonl( n_limit: int, ) -> spacy.tokens.DocBin: db = DocBin(store_user_data=True) - + if n_limit is not None: nb_docs = 0 for annot in srsly.read_jsonl(input_path): @@ -95,7 +96,6 @@ def convert_jsonl( db.add(doc) - return db @@ -114,7 +114,6 @@ def convert_brat( if hasattr(doc, "text"): db.add(doc) - return db @@ -137,17 +136,18 @@ def convert( ), ) -> None: nlp = get_nlp(lang) - + if os.path.isdir(input_path): db = convert_brat(nlp, input_path, n_limit) else: db = convert_jsonl(nlp, input_path, n_limit) - + typer.echo(f"The saved dataset contains {len(db)} documents.") if not os.path.exists(output_path.parent): os.makedirs(output_path.parent) print(f"Folder: {output_path.parent} has been created") db.to_disk(output_path) + if __name__ == "__main__": typer.run(convert) diff --git a/NER_model/scripts/infer.py b/NER_model/scripts/infer.py index f99783277..a7f9de603 100644 --- a/NER_model/scripts/infer.py +++ b/NER_model/scripts/infer.py @@ -4,10 +4,11 @@ import spacy import typer -from edsnlp.connectors.brat import BratConnector from spacy.tokens import DocBin from tqdm import tqdm +from edsnlp.connectors.brat import BratConnector + def main( model: Optional[Path] = typer.Option(None, help="Path to the model"), @@ -46,7 +47,15 @@ def main( doc.user_data = { k: v for k, v in doc.user_data.items() - if "note_id" in k or "context" in k or "split" in k or "Action" in k or "Allergie" in k or "Certainty" in k or "Temporality" in k or "Family" in k or "Negation" in k + if "note_id" in k + or "context" in k + or "split" in k + or "Action" in k + or "Allergie" in k + or "Certainty" in k + or "Temporality" in k + or "Family" in k + or "Negation" in k } predicted.append(doc) # predicted[0].ents[i]._.negation donne None au lieu de False/True @@ -56,7 +65,17 @@ def main( out_db.to_disk(output) elif format == "brat": print("Output format is BRAT") - BratConnector(output, attributes=["Negation", "Family", "Temporality", "Certainty", "Action", "Allergie"]).docs2brat(predicted) + BratConnector( + output, + attributes=[ + "Negation", + "Family", + "Temporality", + "Certainty", + "Action", + "Allergie", + ], + ).docs2brat(predicted) if __name__ == "__main__": diff --git a/NER_model/scripts/new_evaluate.py b/NER_model/scripts/new_evaluate.py index 31b82e373..51520ecc5 100644 --- a/NER_model/scripts/new_evaluate.py +++ b/NER_model/scripts/new_evaluate.py @@ -5,8 +5,6 @@ import srsly import typer -from spacy.scorer import Scorer -from spacy.training import Example from spacy import util from spacy.cli._util import Arg, Opt, import_code, setup_gpu from spacy.cli.evaluate import ( @@ -14,11 +12,15 @@ print_textcats_auc_per_cat, render_parses, ) +from spacy.scorer import Scorer from spacy.tokens import DocBin +from spacy.training import Example from thinc.api import fix_random_seed from wasabi import Printer + from edsnlp.evaluate import evaluate_test + # fmt: off def evaluate_cli( model: str = Arg(..., help="Model name or path"), # noqa: E501 @@ -103,15 +105,15 @@ def evaluate( } pred_docs.append(doc) pred_docs.sort(key=lambda doc: doc._.note_id) - + if docbin is not None: output_db = DocBin(store_user_data=True) for doc in pred_docs: output_db.add(doc) output_db.to_disk(docbin) - + scores = evaluate_test(gold_docs, pred_docs) - + metrics = { "TOK": "token_acc", "TAG": "tag_acc", @@ -187,7 +189,7 @@ def print_prf_per_type( aligns=("l", "r", "r", "r", "r"), title=f"{name} (per {type})", ) - + def handle_scores_per_type( scores: Dict[str, Any], data: Dict[str, Any] = {}, diff --git a/NER_model/scripts/save_to_brat.py b/NER_model/scripts/save_to_brat.py index 0588c6574..d4596cc0b 100644 --- a/NER_model/scripts/save_to_brat.py +++ b/NER_model/scripts/save_to_brat.py @@ -1,14 +1,14 @@ -import pandas as pd +import os +import re +from pathlib import Path +from typing import Any, Dict, Optional + import numpy as np +import pandas as pd import spacy -from edsnlp.connectors.brat import BratConnector -import re import srsly import typer -from spacy.scorer import Scorer - -from spacy.tokens import Doc -from spacy.training import Example +from eds_medic.corpus_reader import Corpus from spacy import util from spacy.cli._util import Arg, Opt, import_code, setup_gpu from spacy.cli.evaluate import ( @@ -16,46 +16,63 @@ print_textcats_auc_per_cat, render_parses, ) - -import re -from pathlib import Path -from typing import Any, Dict, Optional -from tqdm import tqdm - -import os -from spacy.tokens import DocBin +from spacy.scorer import Scorer +from spacy.tokens import Doc, DocBin +from spacy.training import Example from thinc.api import fix_random_seed +from tqdm import tqdm from wasabi import Printer -from eds_medic.corpus_reader import Corpus - - +from edsnlp.connectors.brat import BratConnector def evaluate_cli( model: str = Arg(..., help="Model name or path"), # noqa: E501 - data_path: Path = Arg(..., help="Location of binary evaluation data in .spacy format", exists=True), # noqa: E501 - output: Optional[Path] = Opt(None, "--output", "-o", help="Output JSON file for metrics", dir_okay=False), # noqa: E501 - docbin: Optional[Path] = Opt(None, "--docbin", help="Output Doc Bin path", dir_okay=False), # noqa: E501 - code_path: Optional[Path] = Opt(None, "--code", "-c", help="Path to Python file with additional code (registered functions) to be imported"), # noqa: E501 + data_path: Path = Arg( + ..., help="Location of binary evaluation data in .spacy format", exists=True + ), # noqa: E501 + output: Optional[Path] = Opt( + None, "--output", "-o", help="Output JSON file for metrics", dir_okay=False + ), # noqa: E501 + docbin: Optional[Path] = Opt( + None, "--docbin", help="Output Doc Bin path", dir_okay=False + ), # noqa: E501 + code_path: Optional[Path] = Opt( + None, + "--code", + "-c", + help="Path to Python file with additional code (registered functions) to be imported", + ), # noqa: E501 use_gpu: int = Opt(-1, "--gpu-id", "-g", help="GPU ID or -1 for CPU"), # noqa: E501 - gold_preproc: bool = Opt(False, "--gold-preproc", "-G", help="Use gold preprocessing"), # noqa: E501 - displacy_path: Optional[Path] = Opt(None, "--displacy-path", "-dp", help="Directory to output rendered parses as HTML", exists=True, file_okay=False), # noqa: E501 - displacy_limit: int = Opt(25, "--displacy-limit", "-dl", help="Limit of parses to render as HTML"), # noqa: E501 + gold_preproc: bool = Opt( + False, "--gold-preproc", "-G", help="Use gold preprocessing" + ), # noqa: E501 + displacy_path: Optional[Path] = Opt( + None, + "--displacy-path", + "-dp", + help="Directory to output rendered parses as HTML", + exists=True, + file_okay=False, + ), # noqa: E501 + displacy_limit: int = Opt( + 25, "--displacy-limit", "-dl", help="Limit of parses to render as HTML" + ), # noqa: E501 ): - - save(model, + + save( + model, ### A DECOMMENTER ### - #data_path = '../data/attr2/test', - #output_brat ='../data/attr2/pred', + # data_path = '../data/attr2/test', + # output_brat ='../data/attr2/pred', # data_path = "/export/home/"cse200093/Jacques_Bio/data_bio/brat_annotated_bio_val/test", # output_brat = "/export/home/cse200093/Jacques_Bio/data_bio/brat_annotated_bio_val/test_eds-medic", - #data_path = '/export/home/cse200093/RV_Inter_conf/unnested_sosydiso_qualifiers_final/test_, - #output_brat = '/export/home/cse200093/RV_Inter_conf/unnested_sosydiso_qualifiers_final/pred', - #data_path = '/export/home/cse200093/RV_Inter_conf/unnested_final/test', - #output_brat = '/export/home/cse200093/RV_Inter_conf/unnested_final/pred', - data_path='/export/home/cse200093/Jacques_Bio/data_bio/super_pipe_get_stats_by_section_on_cim10/pred/syndrome_des_anti-phospholipides_init', - output_brat='/export/home/cse200093/Jacques_Bio/data_bio/super_pipe_get_stats_by_section_on_cim10/pred/syndrome_des_anti-phospholipides_pred2', + # data_path = '/export/home/cse200093/RV_Inter_conf/unnested_sosydiso_qualifiers_final/test_, + # output_brat = '/export/home/cse200093/RV_Inter_conf/unnested_sosydiso_qualifiers_final/pred', + # data_path = '/export/home/cse200093/RV_Inter_conf/unnested_final/test', + # output_brat = '/export/home/cse200093/RV_Inter_conf/unnested_final/pred', + data_path="/export/home/cse200093/Jacques_Bio/data_bio/super_pipe_get_stats_by_section_on_cim10/pred/syndrome_des_anti-phospholipides_init", + output_brat="/export/home/cse200093/Jacques_Bio/data_bio/super_pipe_get_stats_by_section_on_cim10/pred/syndrome_des_anti-phospholipides_pred2", output=output, docbin=docbin, use_gpu=use_gpu, @@ -65,7 +82,7 @@ def evaluate_cli( silent=False, ) - + def save( model: str, output_brat: str, @@ -81,49 +98,60 @@ def save( ): setup_gpu(use_gpu, silent) - #brat = BratConnector(data_path, attributes = {"Disorders_type":"Disorders_type",'SOSY_type':'SOSY_type','Chemical_and_drugs_type':'Chemical_and_drugs_type', - # 'Concept_type':'Concept_type','negation':'negation','hypothetique':'hypothetique', 'family':'family','Medical_Procedure_type':'Medical_Procedure_type','gender_type':'gender_type'}) - brat = BratConnector(data_path, attributes = {"Negation":"Negation","Family": "Family", "Temporality":"Temporality","Certainty":"Certainty","Action":"Action"}) + # brat = BratConnector(data_path, attributes = {"Disorders_type":"Disorders_type",'SOSY_type':'SOSY_type','Chemical_and_drugs_type':'Chemical_and_drugs_type', + # 'Concept_type':'Concept_type','negation':'negation','hypothetique':'hypothetique', 'family':'family','Medical_Procedure_type':'Medical_Procedure_type','gender_type':'gender_type'}) + brat = BratConnector( + data_path, + attributes={ + "Negation": "Negation", + "Family": "Family", + "Temporality": "Temporality", + "Certainty": "Certainty", + "Action": "Action", + }, + ) empty = spacy.blank("fr") df_gold = brat.brat2docs(empty) df_gold.sort(key=lambda doc: doc.text) - - - - print('-- Model running --') - #model_path = '/export/home/cse200093/Pierre_Medic/NEURAL_BASED_NER/inference_model/model-best' + print("-- Model running --") + # model_path = '/export/home/cse200093/Pierre_Medic/NEURAL_BASED_NER/inference_model/model-best' df_txt = [doc.text for doc in df_gold] model = spacy.load(model) - model.add_pipe('clean-entities') - #caanot find clean entities... --> from edsmedic.... import clean_entites + model.add_pipe("clean-entities") + # caanot find clean entities... --> from edsmedic.... import clean_entites df_txt_pred = [] for doc in tqdm(df_txt, desc="Processing documents"): doc = model(doc) doc._.trf_data = None - df_txt_pred.append(doc) + df_txt_pred.append(doc) for i in range(len(df_txt_pred)): df_txt_pred[i]._.note_id = df_gold[i]._.note_id - - - #for doc in df_txt: - #for ent in doc.ents: - #if ent._.Action: - #print(ent, ent._.Action) - - print('-- try saving --') - - print('path: ',output_brat) - brat = BratConnector(output_brat, attributes = {"Negation":"Negation","Family": "Family", "Temporality":"Temporality","Certainty":"Certainty","Action":"Action"}) + + # for doc in df_txt: + # for ent in doc.ents: + # if ent._.Action: + # print(ent, ent._.Action) + + print("-- try saving --") + + print("path: ", output_brat) + brat = BratConnector( + output_brat, + attributes={ + "Negation": "Negation", + "Family": "Family", + "Temporality": "Temporality", + "Certainty": "Certainty", + "Action": "Action", + }, + ) brat.docs2brat(df_txt_pred) - - print('-- saved -- ') - - + print("-- saved -- ") + - if __name__ == "__main__": typer.run(evaluate_cli) diff --git a/Normalisation/drugs/exception.py b/Normalisation/drugs/exception.py index d01134991..9f653ff1e 100644 --- a/Normalisation/drugs/exception.py +++ b/Normalisation/drugs/exception.py @@ -1,14 +1,15 @@ exception_list = { - 'glucocorticoid': ['cortico', - 'corticoides', - 'corticoide' - 'corticos', - 'corticotherapie', - 'corticotherapies', - 'glucocorticoides', - 'glucocorticoide', - 'corticostéroides', - 'corticostéroide'], - 'rituximab': ['ritux','rtx'], - 'prevenar': ['prevenar13'], + "glucocorticoid": [ + "cortico", + "corticoides", + "corticoide" "corticos", + "corticotherapie", + "corticotherapies", + "glucocorticoides", + "glucocorticoide", + "corticostéroides", + "corticostéroide", + ], + "rituximab": ["ritux", "rtx"], + "prevenar": ["prevenar13"], } diff --git a/Normalisation/drugs/normalisation.py b/Normalisation/drugs/normalisation.py index 17ce3499d..25e23ef4d 100644 --- a/Normalisation/drugs/normalisation.py +++ b/Normalisation/drugs/normalisation.py @@ -1,29 +1,30 @@ -import pandas as pd -import numpy as np import re -import spacy -import Levenshtein -from unidecode import unidecode -from tqdm import tqdm -import duckdb -from edsnlp.connectors import BratConnector from collections import defaultdict -from exception import exception_list -from sklearn.preprocessing import MultiLabelBinarizer +import duckdb +import Levenshtein import numpy as np -from sklearn.metrics import precision_score, recall_score, f1_score +import pandas as pd +import spacy +from exception import exception_list from levenpandas import fuzzymerge +from sklearn.metrics import f1_score, precision_score, recall_score +from sklearn.preprocessing import MultiLabelBinarizer +from tqdm import tqdm +from unidecode import unidecode + +from edsnlp.connectors import BratConnector + class DrugNormaliser: - def __init__(self, df_path, drug_dict, method="exact", max_pred=5, atc_len = 7): - if df_path.endswith('json'): + def __init__(self, df_path, drug_dict, method="exact", max_pred=5, atc_len=7): + if df_path.endswith("json"): self.df = pd.read_json(df_path) else: self.df = self.gold_generation(df_path) self.drug_dict = drug_dict # self.df['drug'] = self.df['drug'].apply(lambda x: re.sub(r'\W+', '',x.lower())) - self.df['norm_term'] = self.df['norm_term'].apply(lambda x: unidecode(x)) + self.df["norm_term"] = self.df["norm_term"].apply(lambda x: unidecode(x)) # self.df['score'] = None merged_dict = {} @@ -34,60 +35,70 @@ def __init__(self, df_path, drug_dict, method="exact", max_pred=5, atc_len = 7): # Check if the shortened ATC code already exists in the merged dictionary if shortened_code in merged_dict: # Merge the arrays - merged_dict[shortened_code] = list(set(merged_dict[shortened_code] + values)) + merged_dict[shortened_code] = list( + set(merged_dict[shortened_code] + values) + ) else: # Add a new entry for the shortened ATC code merged_dict[shortened_code] = values - merged_dict = pd.DataFrame.from_dict({"norm_term": merged_dict}, "index").T.explode("norm_term").reset_index().rename(columns={"index": "label"}) + merged_dict = ( + pd.DataFrame.from_dict({"norm_term": merged_dict}, "index") + .T.explode("norm_term") + .reset_index() + .rename(columns={"index": "label"}) + ) merged_dict.norm_term = merged_dict.norm_term.str.split(",") merged_dict = merged_dict.explode("norm_term").reset_index(drop=True) self.drug_dict = merged_dict self.method = method self.max_pred = max_pred - - - - + def get_gold(self): return self.df - + def get_dict(self): return self.drug_dict - + def gold_generation(self, df_path): doc_list = BratConnector(df_path).brat2docs(spacy.blank("fr")) drug_list = [] for doc in doc_list: for ent in doc.ents: - if ent.label_ == 'Chemical_and_drugs': + if ent.label_ == "Chemical_and_drugs": if not ent._.Tech: - drug_list.append([ent.text, doc._.note_id, [ent.start, ent.end], ent.text.lower().strip()]) + drug_list.append( + [ + ent.text, + doc._.note_id, + [ent.start, ent.end], + ent.text.lower().strip(), + ] + ) - drug_list_df = pd.DataFrame(drug_list, columns=['term', 'source', 'span_converted', 'norm_term']) + drug_list_df = pd.DataFrame( + drug_list, columns=["term", "source", "span_converted", "norm_term"] + ) drug_list_df.span_converted = drug_list_df.span_converted.astype(str) return drug_list_df - - - -# def exact_match(self, drug_name, atc, names): -# matching_atc = [] -# matching_names = [] -# for name in names: -# if drug_name == name: -# matching_atc.append(atc) -# matching_names.append(name) -# return matching_atc, matching_names - -# def levenshtein_match(self, drug_name, name): -# return Levenshtein.ratio(drug_name, name) - -# def dice_match(self, word1, word2): -# intersection = len(set(word1) & set(word2)) -# coefficient = (2 * intersection) / (len(word1) + len(word2)) -# return coefficient + # def exact_match(self, drug_name, atc, names): + # matching_atc = [] + # matching_names = [] + # for name in names: + # if drug_name == name: + # matching_atc.append(atc) + # matching_names.append(name) + # return matching_atc, matching_names + + # def levenshtein_match(self, drug_name, name): + # return Levenshtein.ratio(drug_name, name) + + # def dice_match(self, word1, word2): + # intersection = len(set(word1) & set(word2)) + # coefficient = (2 * intersection) / (len(word1) + len(word2)) + # return coefficient def normalize(self, threshold=0.85): # self.df['pred_atc'] = [None]*len(self.df) @@ -117,7 +128,8 @@ def normalize(self, threshold=0.85): ["term", "source", "span_converted", "norm_term"], as_index=False ).agg({"label": list}) return self.df - + + # if self.method =='lev': # for index, row in self.df.iterrows(): # drug_name = row['drug'] @@ -153,7 +165,6 @@ def normalize(self, threshold=0.85): # self.df.at[index, 'score'] = matching_scores[:self.max_pred] # return self.df - # def acc(self, verbose = False): # correct_predictions = self.df.apply(lambda row: row['ATC'][:len(row['ATC'])] in [x[:len(row['ATC'])] for x in row['pred_atc']], axis=1).sum() @@ -167,15 +178,15 @@ def normalize(self, threshold=0.85): # def get_good_predictions(self): # good_predictions = self.df.apply(lambda row: row['ATC'][:len(row['ATC'])] in [x[:len(row['ATC'])] for x in row['pred_atc']], axis=1) # return self.df[good_predictions] - + # def get_bad_predictions(self): # bad_predictions = self.df.apply(lambda row: row['ATC'][:len(row['ATC'])] not in [x[:len(row['ATC'])] for x in row['pred_atc']], axis=1) # return self.df[bad_predictions] - + # def get_no_predictions(self): # no_predictions = self.df.apply(lambda row: len(row['pred_atc'])==0, axis=1) # return self.df[no_predictions] - + # def metrics(self, verbose = True): # y_true = self.df['ATC'] @@ -204,7 +215,7 @@ def normalize(self, threshold=0.85): # results[atc]['TP'] = TP # results[atc]['FP'] = FP # results[atc]['FN'] = FN - + # #we get the micro_average # total_TP = sum([results[atc]['TP'] for atc in unique_atc]) # total_FP = sum([results[atc]['FP'] for atc in unique_atc]) @@ -232,7 +243,7 @@ def normalize(self, threshold=0.85): # f1 = 2*precision*recall/(precision+recall) # else: # f1 = 0 - + # total_precision += precision # total_recall += recall # total_f1 += f1 @@ -241,6 +252,6 @@ def normalize(self, threshold=0.85): # total_recall = total_recall/len(unique_atc) # total_f1 = total_f1/len(unique_atc) - + # print(f' MICRO : The precision is {precision_micro}, the recall is {recall_micro} and the f1 score is {f1_micro}') # print(f' MACRO : The precision is {total_precision}, the recall is {total_recall} and the f1 score is {total_f1}') diff --git a/Normalisation/extract_measurement/config.py b/Normalisation/extract_measurement/config.py index 8a319e172..109539330 100644 --- a/Normalisation/extract_measurement/config.py +++ b/Normalisation/extract_measurement/config.py @@ -1,6 +1,5 @@ -from measurements_patterns import * import pandas as pd - +from measurements_patterns import * ################################ # ## MEASUREMENTS PIPE CONFIG ### @@ -10,7 +9,9 @@ measurements_pipe_labels_to_remove = labels_to_remove measurements_pipe_labels_linkable_to_measurement = labels_linkable_to_measurement measurements_pipe_config_normalizer_from_label_key = config_normalizer_from_label_key -measurements_pipe_config_measurements_from_label_key = config_measurements_from_label_key +measurements_pipe_config_measurements_from_label_key = ( + config_measurements_from_label_key +) measurements_pipe_config_normalizer_from_tables = config_normalizer_from_tables measurements_pipe_config_measurements_from_tables = config_measurements_from_tables measurements_only_tables = False diff --git a/Normalisation/extract_measurement/extract_measurements_from_brat.py b/Normalisation/extract_measurement/extract_measurements_from_brat.py index 06f3e2856..9f921626a 100644 --- a/Normalisation/extract_measurement/extract_measurements_from_brat.py +++ b/Normalisation/extract_measurement/extract_measurements_from_brat.py @@ -1,25 +1,34 @@ +import math +import random +import re +import sys import time +from itertools import combinations +from os import listdir +from os.path import basename, isdir, isfile, join +from statistics import mean from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union -from typing_extensions import TypedDict -from measurements_patterns import regex_convert_spans, label_key, labels_to_remove, labels_linkable_to_measurement, config_normalizer_from_label_key, config_measurements_from_label_key, config_normalizer_from_tables, config_measurements_from_tables -from scipy.stats import bootstrap -import spacy -import pandas as pd -import re -import random import matplotlib.pyplot as plt import numpy as np -import math -from statistics import mean -from os.path import isfile, isdir, join, basename -from os import listdir -from itertools import combinations -from edsnlp.processing import pipe +import pandas as pd +import spacy +from extract_pandas_from_brat import extract_pandas +from measurements_patterns import ( + config_measurements_from_label_key, + config_measurements_from_tables, + config_normalizer_from_label_key, + config_normalizer_from_tables, + label_key, + labels_linkable_to_measurement, + labels_to_remove, + regex_convert_spans, +) +from scipy.stats import bootstrap from tqdm import tqdm +from typing_extensions import TypedDict -import sys -from extract_pandas_from_brat import extract_pandas +from edsnlp.processing import pipe class UnitConfig(TypedDict): @@ -28,30 +37,36 @@ class UnitConfig(TypedDict): followed_by: Optional[str] = None ui_decomposition: Dict[str, int] + class UnitlessRange(TypedDict): min: Optional[int] max: Optional[int] unit: str + class UnitlessPatternConfig(TypedDict): terms: List[str] ranges: List[UnitlessRange] + class SimpleMeasurementConfigWithoutRegistry(TypedDict): value_range: str value: Union[float, int] unit: str + class ValuelessPatternConfig(TypedDict): terms: Optional[List[str]] regex: Optional[List[str]] measurement: SimpleMeasurementConfigWithoutRegistry + class MeasureConfig(TypedDict): unit: str unitless_patterns: Optional[List[UnitlessPatternConfig]] valueless_patterns: Optional[List[ValuelessPatternConfig]] + class MeasurementsPipeConfig(TypedDict): measurements: Union[List[str], Tuple[str], Dict[str, MeasureConfig]] units_config: Dict[str, UnitConfig] @@ -75,21 +90,35 @@ def __init__( regex_convert_spans: Optional[str] = regex_convert_spans, label_key: Optional[str] = label_key, labels_to_remove: Optional[List[str]] = labels_to_remove, - labels_linkable_to_measurement: Optional[List[str]] = labels_linkable_to_measurement, - config_normalizer_from_label_key: Optional[Dict[str, bool]] = config_normalizer_from_label_key, - config_measurements_from_label_key: Optional[MeasurementsPipeConfig] = config_measurements_from_label_key, - config_normalizer_from_tables: Optional[Dict[str, bool]] = config_normalizer_from_tables, - config_measurements_from_tables: Optional[MeasurementsPipeConfig] = config_measurements_from_tables + labels_linkable_to_measurement: Optional[ + List[str] + ] = labels_linkable_to_measurement, + config_normalizer_from_label_key: Optional[ + Dict[str, bool] + ] = config_normalizer_from_label_key, + config_measurements_from_label_key: Optional[ + MeasurementsPipeConfig + ] = config_measurements_from_label_key, + config_normalizer_from_tables: Optional[ + Dict[str, bool] + ] = config_normalizer_from_tables, + config_measurements_from_tables: Optional[ + MeasurementsPipeConfig + ] = config_measurements_from_tables, ): print("--------------- Loading extraction pipe ---------------") self.regex_convert_spans = re.compile(regex_convert_spans) self.label_key = label_key self.labels_to_remove = labels_to_remove self.labels_linkable_to_measurement = labels_linkable_to_measurement - self.nlp_from_label_key = self.load_nlp(config_normalizer_from_label_key, config_measurements_from_label_key) - self.nlp_from_tables = self.load_nlp(config_normalizer_from_tables, config_measurements_from_tables) + self.nlp_from_label_key = self.load_nlp( + config_normalizer_from_label_key, config_measurements_from_label_key + ) + self.nlp_from_tables = self.load_nlp( + config_normalizer_from_tables, config_measurements_from_tables + ) print("--------------- Extraction pipe loaded ---------------") - + def load_nlp(self, config_normalizer, config_measurements): nlp = spacy.blank("eds") nlp.add_pipe("eds.normalizer", config=config_normalizer) @@ -105,13 +134,19 @@ def convert_spans(span): span_start = int(span_match.group(1)) span_end = int(span_match.group(2)) return [span_start, span_end] - + df = extract_pandas(IN_BRAT_DIR=brat_dir) - df = df.loc[df["label"].isin([self.label_key] + self.labels_to_remove + self.labels_linkable_to_measurement)] + df = df.loc[ + df["label"].isin( + [self.label_key] + + self.labels_to_remove + + self.labels_linkable_to_measurement + ) + ] df["span_converted"] = df["span"].apply(convert_spans) df = df[["term", "source", "span_converted", "label"]] return df - + @classmethod def is_overlapping(cls, a, b): # Return crop parts in a if a and b overlaps, 0, 0 if not @@ -119,7 +154,7 @@ def is_overlapping(cls, a, b): return max(a[0], b[0]), min(a[1], b[1]) else: return 0, 0 - + def remove_labels_from_label_key(self, df): def get_parts_to_crop(old_parts_to_crop, new_part_to_crop): # From old_part_to_crops which is a list of segments and new_part_to_crop @@ -138,7 +173,6 @@ def get_parts_to_crop(old_parts_to_crop, new_part_to_crop): old_parts_to_crop.append(new_part_to_crop) return old_parts_to_crop - def crop_with_parts_to_crop(parts_to_crop, to_crop, to_crop_span): # parts_to_crop contains a list of segments to crop in to_crop (str) parts_to_crop.insert(0, [to_crop_span[0], to_crop_span[0]]) @@ -152,22 +186,30 @@ def crop_with_parts_to_crop(parts_to_crop, to_crop, to_crop_span): for i in range(len(parts_to_crop) - 1) ] return "".join(res) - + label_key_df = df.loc[df["label"] == self.label_key].sort_values("source") - specific_label_df = df.loc[df["label"].isin(self.labels_to_remove + self.labels_linkable_to_measurement)] + specific_label_df = df.loc[ + df["label"].isin( + self.labels_to_remove + self.labels_linkable_to_measurement + ) + ] res = {"term_labels_removed": [], "terms_linked_to_measurement": []} label_keys = [] source = None - for label_key in tqdm(label_key_df.itertuples(index=False), total = label_key_df.shape[0]): + for label_key in tqdm( + label_key_df.itertuples(index=False), total=label_key_df.shape[0] + ): new_source = label_key.source - if new_source != source: - temp_df = specific_label_df.loc[(specific_label_df["source"] == new_source)] + if new_source != source: + temp_df = specific_label_df.loc[ + (specific_label_df["source"] == new_source) + ] source = new_source labels_linkable_to_measurement = [] parts_to_crop = [] for label in temp_df.itertuples(index=False): - + crop_start, crop_end = self.is_overlapping( label_key.span_converted, label.span_converted ) @@ -184,36 +226,49 @@ def crop_with_parts_to_crop(parts_to_crop, to_crop, to_crop_span): ) if label.label in self.labels_linkable_to_measurement: labels_linkable_to_measurement.append(label.term) - res["term_labels_removed"].append(crop_with_parts_to_crop( - parts_to_crop, label_key.term, label_key.span_converted - )) + res["term_labels_removed"].append( + crop_with_parts_to_crop( + parts_to_crop, label_key.term, label_key.span_converted + ) + ) res["terms_linked_to_measurement"].append(labels_linkable_to_measurement) label_keys.append(label_key) res = pd.DataFrame(label_keys).join(pd.DataFrame(res)) return res.reset_index(drop=True) - + def get_measurements_from_label_key(self, df): - df_for_nlp_from_label_key = pd.DataFrame({ - "note_text": df["term_labels_removed"], - "note_id": df.index - }) + df_for_nlp_from_label_key = pd.DataFrame( + {"note_text": df["term_labels_removed"], "note_id": df.index} + ) df_for_nlp_from_label_key = pipe( note=df_for_nlp_from_label_key, - nlp = self.nlp_from_label_key, + nlp=self.nlp_from_label_key, n_jobs=-1, - additional_spans = ["measurements"], - extensions = ["value"], + additional_spans=["measurements"], + extensions=["value"], + ) + df_for_nlp_from_label_key = ( + df_for_nlp_from_label_key.groupby("note_id") + .agg({"note_id": "first", "value": list, "lexical_variant": list}) + .reset_index(drop=True) + .rename(columns={"value": "found"}) + ) + df = pd.merge( + df, df_for_nlp_from_label_key, left_index=True, right_on="note_id" ) - df_for_nlp_from_label_key = df_for_nlp_from_label_key.groupby("note_id").agg({"note_id":"first", "value":list, "lexical_variant":list}).reset_index(drop=True).rename(columns={"value":"found"}) - df = pd.merge(df, df_for_nlp_from_label_key, left_index=True, right_on="note_id") df["found"] = df["found"].fillna("").apply(list) return df.reset_index(drop=True) - - def get_measurements_from_tables(self, df, df_labels_of_interest, brat_dir, only_tables): - - # Treat each txt files + + def get_measurements_from_tables( + self, df, df_labels_of_interest, brat_dir, only_tables + ): + + # Treat each txt files txt_files = [ - f for f in listdir(brat_dir) if isfile(join(brat_dir, f)) if f.endswith(".txt") + f + for f in listdir(brat_dir) + if isfile(join(brat_dir, f)) + if f.endswith(".txt") ] ann_files = [f[:-3] + "ann" for f in txt_files] text_df = {"note_text": [], "note_id": []} @@ -225,16 +280,16 @@ def get_measurements_from_tables(self, df, df_labels_of_interest, brat_dir, only text_df = pd.DataFrame(text_df) df_for_nlp_from_table = pipe( note=text_df, - nlp = self.nlp_from_tables, + nlp=self.nlp_from_tables, n_jobs=-1, - additional_spans = ["measurements"], - extensions = ["value"], + additional_spans=["measurements"], + extensions=["value"], ) # Load discriminative dataframe (in other words df containing terms with a label to remove) so that we can drop matches when one overlaps one of these words discriminative_df = df_labels_of_interest.loc[ df_labels_of_interest["label"].isin(self.labels_to_remove) ] - + def get_measurements_from_tables_one_file(df_for_nlp_from_table, ann_file): # Select label_keys from the ann_file # and check if our matcher from tables @@ -249,8 +304,14 @@ def get_measurements_from_tables_one_file(df_for_nlp_from_table, ann_file): .copy() .reset_index(drop=True) ) - df_for_nlp_from_table_part = df_for_nlp_from_table.loc[df_for_nlp_from_table["note_id"] == ann_file].copy().reset_index(drop=True) - for measurement_from_table in df_for_nlp_from_table_part.itertuples(index=False): + df_for_nlp_from_table_part = ( + df_for_nlp_from_table.loc[df_for_nlp_from_table["note_id"] == ann_file] + .copy() + .reset_index(drop=True) + ) + for measurement_from_table in df_for_nlp_from_table_part.itertuples( + index=False + ): measurement_span = [ measurement_from_table.start, measurement_from_table.end, @@ -278,13 +339,19 @@ def get_measurements_from_tables_one_file(df_for_nlp_from_table, ann_file): # DataFrame with merged doc and tables matches result_df_per_file = [] - for ann_file in tqdm(ann_files, total = len(ann_files)): - result_df_per_file.append(get_measurements_from_tables_one_file(df_for_nlp_from_table, ann_file)) - + for ann_file in tqdm(ann_files, total=len(ann_files)): + result_df_per_file.append( + get_measurements_from_tables_one_file(df_for_nlp_from_table, ann_file) + ) + result_df = pd.concat(result_df_per_file, ignore_index=True) if only_tables: - result_df = result_df.loc[result_df["new_found"].astype(bool)].reset_index(drop=True) - result_df = result_df.drop(columns=["found"]).rename(columns={"new_found":"found"}) + result_df = result_df.loc[result_df["new_found"].astype(bool)].reset_index( + drop=True + ) + result_df = result_df.drop(columns=["found"]).rename( + columns={"new_found": "found"} + ) return result_df else: result_df["found"] = result_df.apply( @@ -293,56 +360,78 @@ def get_measurements_from_tables_one_file(df_for_nlp_from_table, ann_file): axis=1, ) return result_df.drop(columns=["new_found"]) - + def prepare_df_for_normalization(self, df): # This method converts SimpleMeasurement objects to strings # So that It can be exported to json # Moreover, for each term, if any terms_linked_to_measurement are found, # We fill the cell with a list of 1 item: # this term cropped by the found measures from It - + # Fill empty terms_linked_to_measurement - mask_empty_labels_linkable_to_measurement = (df["terms_linked_to_measurement"].str.len() == 0) - df_empty_labels_linkable_to_measurement = df[mask_empty_labels_linkable_to_measurement][["term", "lexical_variant"]] - df.loc[mask_empty_labels_linkable_to_measurement, "terms_linked_to_measurement"] = df_empty_labels_linkable_to_measurement.apply(lambda row: - [re - .compile( - r"\b(?:" + "|".join(row["lexical_variant"]) + r")\b", - re.IGNORECASE - ) - .sub("", row["term"])], - axis=1) - + mask_empty_labels_linkable_to_measurement = ( + df["terms_linked_to_measurement"].str.len() == 0 + ) + df_empty_labels_linkable_to_measurement = df[ + mask_empty_labels_linkable_to_measurement + ][["term", "lexical_variant"]] + df.loc[ + mask_empty_labels_linkable_to_measurement, "terms_linked_to_measurement" + ] = df_empty_labels_linkable_to_measurement.apply( + lambda row: [ + re.compile( + r"\b(?:" + "|".join(row["lexical_variant"]) + r")\b", re.IGNORECASE + ).sub("", row["term"]) + ], + axis=1, + ) + # Convert SimpleMeasurement to str - df["found"] = df["found"].apply(lambda measurements: [measurement.value_range + " " + str(measurement.value) + " " + measurement.unit - for measurement in measurements]) - df = df.drop(columns=["label", "term_labels_removed", "note_id", "lexical_variant"]) + df["found"] = df["found"].apply( + lambda measurements: [ + measurement.value_range + + " " + + str(measurement.value) + + " " + + measurement.unit + for measurement in measurements + ] + ) + df = df.drop( + columns=["label", "term_labels_removed", "note_id", "lexical_variant"] + ) return df - + def __call__(self, brat_dir, only_tables): - print("--------------- Converting BRAT files to Pandas DataFrame... ---------------") + print( + "--------------- Converting BRAT files to Pandas DataFrame... ---------------" + ) tic = time.time() df_labels_of_interest = self.extract_pandas_labels_of_interest(brat_dir) - tac=time.time() + tac = time.time() print(f"Converting BRAT files to Pandas DataFrame : {tac-tic:.2f} sec") print("--------------- Removing labels from label keys... ---------------") tic = time.time() df = self.remove_labels_from_label_key(df_labels_of_interest) - tac=time.time() + tac = time.time() print(f"Removing labels from label keys : {tac-tic:.2f} sec") - print("--------------- Extracting measurements from label keys... ---------------") + print( + "--------------- Extracting measurements from label keys... ---------------" + ) tic = time.time() df = self.get_measurements_from_label_key(df) - tac=time.time() + tac = time.time() print(f"Extracting measurements from label keys : {tac-tic:.2f} sec") print("--------------- Extracting measurements from tables... ---------------") tic = time.time() - df = self.get_measurements_from_tables(df, df_labels_of_interest, brat_dir, only_tables) - tac=time.time() + df = self.get_measurements_from_tables( + df, df_labels_of_interest, brat_dir, only_tables + ) + tac = time.time() print(f"Extracting measurements from tables : {tac-tic:.2f} sec") print("--------------- Formatting table for normalization... ---------------") tic = time.time() df = self.prepare_df_for_normalization(df) - tac=time.time() + tac = time.time() print(f"Formatting table for normalization : {tac-tic:.2f} sec") return df diff --git a/Normalisation/extract_measurement/extract_pandas_from_brat.py b/Normalisation/extract_measurement/extract_pandas_from_brat.py index b1836f95d..82e99e239 100644 --- a/Normalisation/extract_measurement/extract_pandas_from_brat.py +++ b/Normalisation/extract_measurement/extract_pandas_from_brat.py @@ -2,54 +2,58 @@ # coding: utf-8 # %% -# # Build prediction file -# +# # Build prediction file +# # From our files with NER prediction, extract a pandas data frame to work on entities easily -# +# # %% -from os.path import isfile, isdir, join, basename -from os import listdir -import pandas as pd -import numpy as np -import re - import collections import math +import re +from os import listdir +from os.path import basename, isdir, isfile, join +import numpy as np +import pandas as pd -def extract_pandas(IN_BRAT_DIR, - OUT_DF = None, - labels = None): - assert isdir(IN_BRAT_DIR) +def extract_pandas(IN_BRAT_DIR, OUT_DF=None, labels=None): - ENTITY_REGEX = re.compile('^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$') + assert isdir(IN_BRAT_DIR) + ENTITY_REGEX = re.compile("^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$") data = [] patients = [] # extract all ann_files from IN_BRAT_DIR - ann_files = [f for f in listdir(IN_BRAT_DIR) if isfile(join(IN_BRAT_DIR, f)) if f.endswith('.ann')] + ann_files = [ + f + for f in listdir(IN_BRAT_DIR) + if isfile(join(IN_BRAT_DIR, f)) + if f.endswith(".ann") + ] for ann_file in ann_files: ann_path = join(IN_BRAT_DIR, ann_file) - txt_path = ann_path[:-4]+'.txt' + txt_path = ann_path[:-4] + ".txt" # sanity check assert isfile(ann_path) assert isfile(txt_path) - # Read text file to get patient number : - with open(txt_path, 'r', encoding='utf-8') as f_txt:lines_txt = f_txt.readlines() + # Read text file to get patient number : + with open(txt_path, "r", encoding="utf-8") as f_txt: + lines_txt = f_txt.readlines() patient_num = lines_txt[0][:-1] patients.append(patient_num) # Read ann file - with open(ann_path, 'r', encoding='utf-8') as f_in:lines = f_in.readlines() - + with open(ann_path, "r", encoding="utf-8") as f_in: + lines = f_in.readlines() + for line in lines: entity_match = ENTITY_REGEX.match(line.strip()) if entity_match is not None: @@ -59,11 +63,12 @@ def extract_pandas(IN_BRAT_DIR, term = entity_match.group(4) if labels is None: data.append([ann_id, term, label, basename(ann_path), offsets]) - elif label in labels: + elif label in labels: data.append([ann_id, term, label, basename(ann_path), offsets]) - - columns = ['ann_id', 'term', 'label', 'source', 'span'] + + columns = ["ann_id", "term", "label", "source", "span"] dataset_df = pd.DataFrame(data=list(data), columns=columns) - if OUT_DF: dataset_df.to_csv(OUT_DF) - + if OUT_DF: + dataset_df.to_csv(OUT_DF) + return dataset_df diff --git a/Normalisation/extract_measurement/main.py b/Normalisation/extract_measurement/main.py index 6db8294fd..cf9e7288b 100644 --- a/Normalisation/extract_measurement/main.py +++ b/Normalisation/extract_measurement/main.py @@ -1,11 +1,13 @@ import os + import typer + os.environ["OMP_NUM_THREADS"] = "16" -from extract_measurements_from_brat import ExtractMeasurements -from config import * from pathlib import Path import pandas as pd +from config import * +from extract_measurements_from_brat import ExtractMeasurements def extract_measurements_cli( @@ -13,15 +15,15 @@ def extract_measurements_cli( output_dir: Path, ): df = ExtractMeasurements( - regex_convert_spans = measurements_pipe_regex_convert_spans, - label_key = measurements_pipe_label_key, - labels_to_remove = measurements_pipe_labels_to_remove, - labels_linkable_to_measurement = measurements_pipe_labels_linkable_to_measurement, - config_normalizer_from_label_key = measurements_pipe_config_normalizer_from_label_key, - config_measurements_from_label_key = measurements_pipe_config_measurements_from_label_key, - config_normalizer_from_tables = measurements_pipe_config_normalizer_from_tables, - config_measurements_from_tables = measurements_pipe_config_measurements_from_tables, - )(brat_dir = input_dir, only_tables = measurements_only_tables) + regex_convert_spans=measurements_pipe_regex_convert_spans, + label_key=measurements_pipe_label_key, + labels_to_remove=measurements_pipe_labels_to_remove, + labels_linkable_to_measurement=measurements_pipe_labels_linkable_to_measurement, + config_normalizer_from_label_key=measurements_pipe_config_normalizer_from_label_key, + config_measurements_from_label_key=measurements_pipe_config_measurements_from_label_key, + config_normalizer_from_tables=measurements_pipe_config_normalizer_from_tables, + config_measurements_from_tables=measurements_pipe_config_measurements_from_tables, + )(brat_dir=input_dir, only_tables=measurements_only_tables) if not os.path.exists(output_dir): os.makedirs(output_dir) df.to_json(output_dir / "pred_with_extraction.json") diff --git a/Normalisation/extract_measurement/measurements_patterns.py b/Normalisation/extract_measurement/measurements_patterns.py index cb02efed0..5d9885a60 100644 --- a/Normalisation/extract_measurement/measurements_patterns.py +++ b/Normalisation/extract_measurement/measurements_patterns.py @@ -1,5 +1,12 @@ -from edsnlp.pipelines.misc.measurements.patterns import common_measurements, number_terms, value_range_terms, units_config, unit_divisors, stopwords_unitless, stopwords_measure_unit - +from edsnlp.pipelines.misc.measurements.patterns import ( + common_measurements, + number_terms, + stopwords_measure_unit, + stopwords_unitless, + unit_divisors, + units_config, + value_range_terms, +) ####################################### # ## CONFIG TO PRETREAT THE BRAT DIR ### @@ -202,7 +209,8 @@ positive_symbols_from_tables = ("\+", "p") # To match symbols, we create regex positive_regex_from_tables = [ - r"^[^a-zA-Z0-9]*(?:% s)" % "|".join(positive_symbols_from_tables) + r"[^a-zA-Z0-9]*$" + r"^[^a-zA-Z0-9]*(?:% s)" % "|".join(positive_symbols_from_tables) + + r"[^a-zA-Z0-9]*$" ] # Terms which will make the measurements pipe match a negative measurement @@ -224,7 +232,8 @@ negative_symbols_from_tables = ("\-", "n") # To match symbols, we create regex negative_regex_from_tables = [ - r"^[^a-zA-Z0-9]*(?:% s)" % "|".join(negative_symbols_from_tables) + r"[^a-zA-Z0-9]*$" + r"^[^a-zA-Z0-9]*(?:% s)" % "|".join(negative_symbols_from_tables) + + r"[^a-zA-Z0-9]*$" ] # Terms which will make the measurements pipe match a normal measurement diff --git a/Normalisation/inference/config.py b/Normalisation/inference/config.py index a86a34cff..081273cc8 100644 --- a/Normalisation/inference/config.py +++ b/Normalisation/inference/config.py @@ -1,10 +1,11 @@ import pandas as pd - ###################### # ## GENERAL CONFIG ### # ##################### -umls_path = "/export/home/cse200093/scratch/BioMedics/data/umls/bio_str_SNOMEDCT_US.json" +umls_path = ( + "/export/home/cse200093/scratch/BioMedics/data/umls/bio_str_SNOMEDCT_US.json" +) labels_column_name = "CUI" # Name of the column which contains the CUIs synonyms_column_name = "STR" @@ -18,8 +19,12 @@ column_name_to_normalize = "term" # Name of the preceding column of interest. Default should be # "terms_linked_to_measurement" to make the entire pipe work -coder_model_name_or_path = "/export/home/cse200093/scratch/word-embedding/coder_eds/model_967662.pth" -coder_tokenizer_name_or_path = "/export/home/cse200093/scratch/word-embedding/finetuning-camembert-2021-07-29" +coder_model_name_or_path = ( + "/export/home/cse200093/scratch/word-embedding/coder_eds/model_967662.pth" +) +coder_tokenizer_name_or_path = ( + "/export/home/cse200093/scratch/word-embedding/finetuning-camembert-2021-07-29" +) coder_device = "cuda:0" coder_save_umls_embeddings_dir = False # set to False if you don't want to save @@ -29,9 +34,9 @@ # set to False if you don't want to save coder_save_data_embeddings_dir = False # set to False if you don't want to save -coder_normalize=True -coder_summary_method="CLS" -coder_tqdm_bar=True +coder_normalize = True +coder_summary_method = "CLS" +coder_tqdm_bar = True coder_cased = True coder_batch_size = 128 coder_stopwords = [ diff --git a/Normalisation/inference/extract_pandas_from_brat.py b/Normalisation/inference/extract_pandas_from_brat.py index c9a959582..ac04c0d09 100644 --- a/Normalisation/inference/extract_pandas_from_brat.py +++ b/Normalisation/inference/extract_pandas_from_brat.py @@ -2,55 +2,59 @@ # coding: utf-8 # %% -# # Build prediction file -# +# # Build prediction file +# # From our files with NER prediction, extract a pandas data frame to work on entities easily -# +# # %% -from os.path import isfile, isdir, join, basename -from os import listdir -import pandas as pd -import numpy as np -import re - import collections import math +import re +from os import listdir +from os.path import basename, isdir, isfile, join +import numpy as np +import pandas as pd -def extract_pandas(IN_BRAT_DIR, - OUT_DF = None, - labels = None): - assert isdir(IN_BRAT_DIR) +def extract_pandas(IN_BRAT_DIR, OUT_DF=None, labels=None): - ENTITY_REGEX = re.compile('^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$') + assert isdir(IN_BRAT_DIR) + ENTITY_REGEX = re.compile("^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$") data = [] patients = [] # extract all ann_files from IN_BRAT_DIR - ann_files = [f for f in listdir(IN_BRAT_DIR) if isfile(join(IN_BRAT_DIR, f)) if f.endswith('.ann')] + ann_files = [ + f + for f in listdir(IN_BRAT_DIR) + if isfile(join(IN_BRAT_DIR, f)) + if f.endswith(".ann") + ] for ann_file in ann_files: ann_path = join(IN_BRAT_DIR, ann_file) - txt_path = ann_path[:-4]+'.txt' + txt_path = ann_path[:-4] + ".txt" # sanity check assert isfile(ann_path) assert isfile(txt_path) - # Read text file to get patient number : - with open(txt_path, 'r', encoding='utf-8') as f_txt:lines_txt = f_txt.readlines() + # Read text file to get patient number : + with open(txt_path, "r", encoding="utf-8") as f_txt: + lines_txt = f_txt.readlines() patient_num = lines_txt[0][:-1] patients.append(patient_num) # Read ann file - with open(ann_path, 'r', encoding='utf-8') as f_in:lines = f_in.readlines() - + with open(ann_path, "r", encoding="utf-8") as f_in: + lines = f_in.readlines() + for line in lines: entity_match = ENTITY_REGEX.match(line.strip()) if entity_match is not None: @@ -60,11 +64,12 @@ def extract_pandas(IN_BRAT_DIR, term = entity_match.group(4) if labels is None: data.append([ann_id, term, label, basename(ann_path), offsets]) - elif label in labels: + elif label in labels: data.append([ann_id, term, label, basename(ann_path), offsets]) - - columns = ['ann_id', 'term', 'label', 'source', 'span'] + + columns = ["ann_id", "term", "label", "source", "span"] dataset_df = pd.DataFrame(data=list(data), columns=columns) - if OUT_DF: dataset_df.to_csv(OUT_DF) - + if OUT_DF: + dataset_df.to_csv(OUT_DF) + return dataset_df diff --git a/Normalisation/inference/get_normalization_with_coder.py b/Normalisation/inference/get_normalization_with_coder.py index c3a0a5ed3..4526c1633 100644 --- a/Normalisation/inference/get_normalization_with_coder.py +++ b/Normalisation/inference/get_normalization_with_coder.py @@ -1,17 +1,27 @@ +import argparse import os +import pathlib import sys import time -import pathlib -import argparse + +import numpy as np import torch from torch import nn -import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig, AdamW, get_linear_schedule_with_warmup, get_cosine_schedule_with_warmup, get_constant_schedule_with_warmup from tqdm import tqdm, trange -import sys +from transformers import ( + AdamW, + AutoConfig, + AutoModel, + AutoTokenizer, + get_constant_schedule_with_warmup, + get_cosine_schedule_with_warmup, + get_linear_schedule_with_warmup, +) + sys.path.append("/export/home/cse200093/scratch/BioMedics/normalisation/training") -class CoderNormalizer(): + +class CoderNormalizer: def __init__( self, model_name_or_path: str, @@ -27,7 +37,7 @@ def __init__( self.model = torch.load(model_name_or_path).to(self.device) self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_name_or_path) self.model_from_transformers = False - + def get_bert_embed( self, phrase_list, @@ -83,14 +93,9 @@ def get_bert_embed( if tqdm_bar: pbar.close() return output - + def get_sim_results( - self, - res_embeddings, - umls_embeddings, - umls_labels, - umls_des, - split_size=200 + self, res_embeddings, umls_embeddings, umls_labels, umls_des, split_size=200 ): label_matches = [] des_matches = [] @@ -103,23 +108,29 @@ def get_sim_results( label_matches.extend(label_matches_split) des_matches.extend(des_matches_split) return label_matches, des_matches - + def __call__( self, umls_labels_list, umls_des_list, data_list, - save_umls_embeddings_dir = False, - save_data_embeddings_dir = False, + save_umls_embeddings_dir=False, + save_data_embeddings_dir=False, normalize=True, summary_method="CLS", tqdm_bar=False, - coder_batch_size = 128, + coder_batch_size=128, ): - umls_embeddings = self.get_bert_embed(umls_des_list, normalize, summary_method, tqdm_bar, coder_batch_size) - res_embeddings = self.get_bert_embed(data_list, normalize, summary_method, tqdm_bar, coder_batch_size) + umls_embeddings = self.get_bert_embed( + umls_des_list, normalize, summary_method, tqdm_bar, coder_batch_size + ) + res_embeddings = self.get_bert_embed( + data_list, normalize, summary_method, tqdm_bar, coder_batch_size + ) if save_umls_embeddings_dir: torch.save(umls_embeddings, save_umls_embeddings_dir) if save_data_embeddings_dir: torch.save(res_embeddings, save_data_embeddings_dir) - return self.get_sim_results(res_embeddings, umls_embeddings, umls_labels_list, umls_des_list) + return self.get_sim_results( + res_embeddings, umls_embeddings, umls_labels_list, umls_des_list + ) diff --git a/Normalisation/inference/main.py b/Normalisation/inference/main.py index f628bc561..cffeab945 100644 --- a/Normalisation/inference/main.py +++ b/Normalisation/inference/main.py @@ -1,40 +1,44 @@ import os + import typer + os.environ["OMP_NUM_THREADS"] = "16" -from text_preprocessor import TextPreprocessor -from get_normalization_with_coder import CoderNormalizer -from config import * import pickle from pathlib import Path import pandas as pd +from config import * +from get_normalization_with_coder import CoderNormalizer +from text_preprocessor import TextPreprocessor + def coder_wrapper(df): # This wrapper is needed to preprocess terms # and in case the cells contains list of terms instead of one unique term df = df.reset_index(drop=True) - text_preprocessor = TextPreprocessor( - cased=coder_cased, - stopwords=coder_stopwords - ) + text_preprocessor = TextPreprocessor(cased=coder_cased, stopwords=coder_stopwords) coder_normalizer = CoderNormalizer( - model_name_or_path = coder_model_name_or_path, - tokenizer_name_or_path = coder_tokenizer_name_or_path, - device = coder_device + model_name_or_path=coder_model_name_or_path, + tokenizer_name_or_path=coder_tokenizer_name_or_path, + device=coder_device, ) - + # Preprocess UMLS print("--- Preprocessing UMLS ---") umls_df = pd.read_json(umls_path) - - umls_df[synonyms_column_name] = umls_df[synonyms_column_name].apply(lambda term: - text_preprocessor( - text = term, - remove_stopwords = coder_remove_stopwords_umls, - remove_special_characters = coder_remove_special_characters_umls) - ) + + umls_df[synonyms_column_name] = umls_df[synonyms_column_name].apply( + lambda term: text_preprocessor( + text=term, + remove_stopwords=coder_remove_stopwords_umls, + remove_special_characters=coder_remove_special_characters_umls, + ) + ) umls_df = ( - umls_df.loc[(~umls_df[synonyms_column_name].str.isnumeric()) & (umls_df[synonyms_column_name] != "")] + umls_df.loc[ + (~umls_df[synonyms_column_name].str.isnumeric()) + & (umls_df[synonyms_column_name] != "") + ] .groupby([synonyms_column_name]) .agg({labels_column_name: set, synonyms_column_name: "first"}) .reset_index(drop=True) @@ -47,54 +51,76 @@ def coder_wrapper(df): if coder_save_umls_labels_dir: with open(coder_save_umls_labels_dir, "wb") as f: pickle.dump(coder_umls_labels_list, f) - + # Preprocessing and inference on terms print("--- Preprocessing terms ---") if type(df[column_name_to_normalize].iloc[0]) == str: - coder_data_list = df[column_name_to_normalize].apply(lambda term: - text_preprocessor( - text = term, - remove_stopwords = coder_remove_stopwords_terms, - remove_special_characters = coder_remove_special_characters_terms) - ).tolist() + coder_data_list = ( + df[column_name_to_normalize] + .apply( + lambda term: text_preprocessor( + text=term, + remove_stopwords=coder_remove_stopwords_terms, + remove_special_characters=coder_remove_special_characters_terms, + ) + ) + .tolist() + ) print("--- CODER inference ---") coder_res = coder_normalizer( - umls_labels_list = coder_umls_labels_list, - umls_des_list = coder_umls_des_list, - data_list = coder_data_list, - save_umls_embeddings_dir = coder_save_umls_embeddings_dir, - save_data_embeddings_dir = coder_save_data_embeddings_dir, - normalize = coder_normalize, - summary_method = coder_summary_method, - tqdm_bar = coder_tqdm_bar, - coder_batch_size = coder_batch_size, + umls_labels_list=coder_umls_labels_list, + umls_des_list=coder_umls_des_list, + data_list=coder_data_list, + save_umls_embeddings_dir=coder_save_umls_embeddings_dir, + save_data_embeddings_dir=coder_save_data_embeddings_dir, + normalize=coder_normalize, + summary_method=coder_summary_method, + tqdm_bar=coder_tqdm_bar, + coder_batch_size=coder_batch_size, ) df[["label", "des"]] = pd.DataFrame(zip(*coder_res)) else: - exploded_term_df = pd.DataFrame({ - "id": df.index, - column_name_to_normalize: df[column_name_to_normalize] - }).explode(column_name_to_normalize).reset_index(drop=True) - coder_data_list = exploded_term_df[column_name_to_normalize].apply(lambda term: - text_preprocessor( - text = term, - remove_stopwords = coder_remove_stopwords_terms, - remove_special_characters = coder_remove_special_characters_terms) - ).tolist() + exploded_term_df = ( + pd.DataFrame( + {"id": df.index, column_name_to_normalize: df[column_name_to_normalize]} + ) + .explode(column_name_to_normalize) + .reset_index(drop=True) + ) + coder_data_list = ( + exploded_term_df[column_name_to_normalize] + .apply( + lambda term: text_preprocessor( + text=term, + remove_stopwords=coder_remove_stopwords_terms, + remove_special_characters=coder_remove_special_characters_terms, + ) + ) + .tolist() + ) print("--- CODER inference ---") coder_res = coder_normalizer( - umls_labels_list = coder_umls_labels_list, - umls_des_list = coder_umls_des_list, - data_list = coder_data_list, - save_umls_embeddings_dir = coder_save_umls_embeddings_dir, - save_data_embeddings_dir = coder_save_data_embeddings_dir, - normalize = coder_normalize, - summary_method = coder_summary_method, - tqdm_bar = coder_tqdm_bar, - coder_batch_size = coder_batch_size, + umls_labels_list=coder_umls_labels_list, + umls_des_list=coder_umls_des_list, + data_list=coder_data_list, + save_umls_embeddings_dir=coder_save_umls_embeddings_dir, + save_data_embeddings_dir=coder_save_data_embeddings_dir, + normalize=coder_normalize, + summary_method=coder_summary_method, + tqdm_bar=coder_tqdm_bar, + coder_batch_size=coder_batch_size, ) exploded_term_df[["label", "des"]] = pd.DataFrame(zip(*coder_res)) - df = pd.merge(df.drop(columns=[column_name_to_normalize]), exploded_term_df, left_index = True, right_on = "id").drop(columns=["id"]).reset_index(drop=True) + df = ( + pd.merge( + df.drop(columns=[column_name_to_normalize]), + exploded_term_df, + left_index=True, + right_on="id", + ) + .drop(columns=["id"]) + .reset_index(drop=True) + ) return df @@ -107,5 +133,6 @@ def coder_inference_cli( if res_path: df.to_json(output_dir) + if __name__ == "__main__": typer.run(coder_inference_cli) diff --git a/Normalisation/inference/text_preprocessor.py b/Normalisation/inference/text_preprocessor.py index 269e0bb58..a9a7b40c5 100644 --- a/Normalisation/inference/text_preprocessor.py +++ b/Normalisation/inference/text_preprocessor.py @@ -1,16 +1,16 @@ -from unidecode import unidecode import re -class TextPreprocessor(): - def __init__( - self, - cased, - stopwords - ): +from unidecode import unidecode + + +class TextPreprocessor: + def __init__(self, cased, stopwords): self.cased = cased - self.regex_stopwords = re.compile(r"\b(?:" + "|".join(stopwords) + r")\b", re.IGNORECASE) + self.regex_stopwords = re.compile( + r"\b(?:" + "|".join(stopwords) + r")\b", re.IGNORECASE + ) self.regex_special_characters = re.compile(r"[^a-zA-Z0-9\s]", re.IGNORECASE) - + def normalize(self, txt, remove_stopwords, remove_special_characters): if not self.cased: txt = unidecode( @@ -35,6 +35,6 @@ def normalize(self, txt, remove_stopwords, remove_special_characters): if remove_special_characters: txt = self.regex_special_characters.sub(" ", txt) return re.sub(" +", " ", txt).strip() - - def __call__(self, text, remove_stopwords = False, remove_special_characters = False): + + def __call__(self, text, remove_stopwords=False, remove_special_characters=False): return self.normalize(text, remove_stopwords, remove_special_characters) diff --git a/Normalisation/training/data_util.py b/Normalisation/training/data_util.py index 196878228..afef4f48a 100644 --- a/Normalisation/training/data_util.py +++ b/Normalisation/training/data_util.py @@ -1,16 +1,17 @@ +import json import os +from pathlib import Path +from random import sample +from time import time + +import ipdb import numpy as np import pandas as pd -from transformers import AutoTokenizer from load_umls import UMLS -from torch.utils.data import Dataset, DataLoader -from random import sample from sampler_util import FixedLengthBatchSampler, my_collate_fn +from torch.utils.data import DataLoader, Dataset from torch.utils.data.sampler import RandomSampler -import ipdb -from time import time -import json -from pathlib import Path +from transformers import AutoTokenizer def pad(list_ids, pad_length, pad_mark=0): @@ -26,25 +27,34 @@ def pad(list_ids, pad_length, pad_mark=0): def my_sample(lst, lst_length, start, length): start = start % lst_length if start + length < lst_length: - return lst[start:start + length] - return lst[start:] + lst[0:start + length - lst_length] + return lst[start : start + length] + return lst[start:] + lst[0 : start + length - lst_length] class UMLSDataset(Dataset): - def __init__(self, umls_folder, model_name_or_path, lang, json_save_path=None, max_lui_per_cui=8, max_length=32): + def __init__( + self, + umls_folder, + model_name_or_path, + lang, + json_save_path=None, + max_lui_per_cui=8, + max_length=32, + ): self.umls = UMLS(umls_folder, lang_range=lang) self.len = len(self.umls.rel) self.max_lui_per_cui = max_lui_per_cui self.max_length = max_length - self.tokenizer = AutoTokenizer.from_pretrained("/export/home/cse200093/scratch/word-embedding/finetuning-camembert-2021-07-29") + self.tokenizer = AutoTokenizer.from_pretrained( + "/export/home/cse200093/scratch/word-embedding/finetuning-camembert-2021-07-29" + ) self.json_save_path = json_save_path self.calculate_class_count() def calculate_class_count(self): print("Calculate class count") - self.cui2id = {cui: index for index, - cui in enumerate(self.umls.cui2str.keys())} + self.cui2id = {cui: index for index, cui in enumerate(self.umls.cui2str.keys())} self.re_set = set() self.rel_set = set() @@ -78,7 +88,9 @@ def calculate_class_count(self): print("STY:", len(self.sty2id)) def tokenize_one(self, string): - return self.tokenizer.encode_plus(string, max_length=self.max_length, truncation=True)['input_ids'] + return self.tokenizer.encode_plus( + string, max_length=self.max_length, truncation=True + )["input_ids"] # @profile def __getitem__(self, index): @@ -101,22 +113,29 @@ def __getitem__(self, index): cui2_index_list = [] sty2_index_list = [] - cui2 = my_sample(self.umls.cui, self.umls.cui_count, - index * self.max_lui_per_cui, use_len * 2) + cui2 = my_sample( + self.umls.cui, + self.umls.cui_count, + index * self.max_lui_per_cui, + use_len * 2, + ) sample_index = 0 while len(str2_list) < use_len: if sample_index < len(cui2): use_cui2 = cui2[sample_index] else: sample_index = 0 - cui2 = my_sample(self.umls.cui, self.umls.cui_count, - index * self.max_lui_per_cui, use_len * 2) + cui2 = my_sample( + self.umls.cui, + self.umls.cui_count, + index * self.max_lui_per_cui, + use_len * 2, + ) use_cui2 = cui2[sample_index] # if not "\t".join([cui0, use_cui2, re, rel]) in self.umls.rel: # TOO SLOW! if True: cui2_index_list.append(self.cui2id[use_cui2]) - sty2_index_list.append( - self.sty2id[self.umls.cui2sty[use_cui2]]) + sty2_index_list.append(self.sty2id[self.umls.cui2sty[use_cui2]]) str2_list.append(sample(self.umls.cui2str[use_cui2], 1)[0]) sample_index += 1 @@ -124,23 +143,30 @@ def __getitem__(self, index): # print(str1_list) # print(str2_list) - input_ids = [self.tokenize_one(s) - for s in str0_list + str1_list + str2_list] + input_ids = [self.tokenize_one(s) for s in str0_list + str1_list + str2_list] input_ids = pad(input_ids, self.max_length) input_ids_0 = input_ids[0:use_len] - input_ids_1 = input_ids[use_len:2 * use_len] - input_ids_2 = input_ids[2 * use_len:] + input_ids_1 = input_ids[use_len : 2 * use_len] + input_ids_2 = input_ids[2 * use_len :] cui0_index = self.cui2id[cui0] cui1_index = self.cui2id[cui1] re_index = self.re2id[re] rel_index = self.rel2id[rel] - return input_ids_0, input_ids_1, input_ids_2, \ - [cui0_index] * use_len, [cui1_index] * use_len, cui2_index_list, \ - [sty0_index] * use_len, [sty1_index] * use_len, sty2_index_list, \ - [re_index] * use_len, \ - [rel_index] * use_len + return ( + input_ids_0, + input_ids_1, + input_ids_2, + [cui0_index] * use_len, + [cui1_index] * use_len, + cui2_index_list, + [sty0_index] * use_len, + [sty1_index] * use_len, + sty2_index_list, + [re_index] * use_len, + [rel_index] * use_len, + ) def __len__(self): return self.len @@ -149,16 +175,22 @@ def __len__(self): def fixed_length_dataloader(umls_dataset, fixed_length=96, num_workers=0): base_sampler = RandomSampler(umls_dataset) batch_sampler = FixedLengthBatchSampler( - sampler=base_sampler, fixed_length=fixed_length, drop_last=True) - dataloader = DataLoader(umls_dataset, batch_sampler=batch_sampler, - collate_fn=my_collate_fn, num_workers=num_workers, pin_memory=True) + sampler=base_sampler, fixed_length=fixed_length, drop_last=True + ) + dataloader = DataLoader( + umls_dataset, + batch_sampler=batch_sampler, + collate_fn=my_collate_fn, + num_workers=num_workers, + pin_memory=True, + ) return dataloader if __name__ == "__main__": - umls_dataset = UMLSDataset(umls_folder="../umls", - model_name_or_path="../biobert_v1.1", - lang=None) + umls_dataset = UMLSDataset( + umls_folder="../umls", model_name_or_path="../biobert_v1.1", lang=None + ) ipdb.set_trace() umls_dataloader = fixed_length_dataloader(umls_dataset, num_workers=4) now_time = time() @@ -168,7 +200,8 @@ def fixed_length_dataloader(umls_dataset, fixed_length=96, num_workers=0): if index < 10: for item in batch: print(item.shape) - #print(batch) + # print(batch) else: import sys + sys.exit() diff --git a/Normalisation/training/extract_bert.py b/Normalisation/training/extract_bert.py index be4a7fd6c..7b6ab9b28 100644 --- a/Normalisation/training/extract_bert.py +++ b/Normalisation/training/extract_bert.py @@ -1,8 +1,8 @@ -import torch -import sys import os +import sys +import torch -model = torch.load(sys.argv[1], map_location=torch.device('cpu')) +model = torch.load(sys.argv[1], map_location=torch.device("cpu")) bert_model = model.bert torch.save(bert_model, sys.argv[2]) diff --git a/Normalisation/training/extract_pandas_from_brat.py b/Normalisation/training/extract_pandas_from_brat.py index c9a959582..ac04c0d09 100644 --- a/Normalisation/training/extract_pandas_from_brat.py +++ b/Normalisation/training/extract_pandas_from_brat.py @@ -2,55 +2,59 @@ # coding: utf-8 # %% -# # Build prediction file -# +# # Build prediction file +# # From our files with NER prediction, extract a pandas data frame to work on entities easily -# +# # %% -from os.path import isfile, isdir, join, basename -from os import listdir -import pandas as pd -import numpy as np -import re - import collections import math +import re +from os import listdir +from os.path import basename, isdir, isfile, join +import numpy as np +import pandas as pd -def extract_pandas(IN_BRAT_DIR, - OUT_DF = None, - labels = None): - assert isdir(IN_BRAT_DIR) +def extract_pandas(IN_BRAT_DIR, OUT_DF=None, labels=None): - ENTITY_REGEX = re.compile('^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$') + assert isdir(IN_BRAT_DIR) + ENTITY_REGEX = re.compile("^(.\d+)\t([^ ]+) ([^\t]+)\t(.*)$") data = [] patients = [] # extract all ann_files from IN_BRAT_DIR - ann_files = [f for f in listdir(IN_BRAT_DIR) if isfile(join(IN_BRAT_DIR, f)) if f.endswith('.ann')] + ann_files = [ + f + for f in listdir(IN_BRAT_DIR) + if isfile(join(IN_BRAT_DIR, f)) + if f.endswith(".ann") + ] for ann_file in ann_files: ann_path = join(IN_BRAT_DIR, ann_file) - txt_path = ann_path[:-4]+'.txt' + txt_path = ann_path[:-4] + ".txt" # sanity check assert isfile(ann_path) assert isfile(txt_path) - # Read text file to get patient number : - with open(txt_path, 'r', encoding='utf-8') as f_txt:lines_txt = f_txt.readlines() + # Read text file to get patient number : + with open(txt_path, "r", encoding="utf-8") as f_txt: + lines_txt = f_txt.readlines() patient_num = lines_txt[0][:-1] patients.append(patient_num) # Read ann file - with open(ann_path, 'r', encoding='utf-8') as f_in:lines = f_in.readlines() - + with open(ann_path, "r", encoding="utf-8") as f_in: + lines = f_in.readlines() + for line in lines: entity_match = ENTITY_REGEX.match(line.strip()) if entity_match is not None: @@ -60,11 +64,12 @@ def extract_pandas(IN_BRAT_DIR, term = entity_match.group(4) if labels is None: data.append([ann_id, term, label, basename(ann_path), offsets]) - elif label in labels: + elif label in labels: data.append([ann_id, term, label, basename(ann_path), offsets]) - - columns = ['ann_id', 'term', 'label', 'source', 'span'] + + columns = ["ann_id", "term", "label", "source", "span"] dataset_df = pd.DataFrame(data=list(data), columns=columns) - if OUT_DF: dataset_df.to_csv(OUT_DF) - + if OUT_DF: + dataset_df.to_csv(OUT_DF) + return dataset_df diff --git a/Normalisation/training/generate_term_embeddings.py b/Normalisation/training/generate_term_embeddings.py index 01cd978f7..8d854dfa0 100644 --- a/Normalisation/training/generate_term_embeddings.py +++ b/Normalisation/training/generate_term_embeddings.py @@ -1,14 +1,17 @@ -from gensim import models import os import sys + +from gensim import models + sys.path.append("/export/home/cse200093/Jacques_Bio/normalisation/py_files") -from load_umls import UMLS -import torch -import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig -import tqdm import pickle + +import numpy as np import pandas as pd +import torch +import tqdm +from load_umls import UMLS +from transformers import AutoConfig, AutoModel, AutoTokenizer from unidecode import unidecode batch_size = 128 @@ -16,7 +19,9 @@ # Defining the model # coder_all -model_checkpoint = '/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29' +model_checkpoint = ( + "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" +) tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) model = AutoModel.from_pretrained(model_checkpoint).to(device) @@ -25,12 +30,20 @@ EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/data_embeddings_normalized_snomed_camembert_eds.pt" # Method to generate embeddings -def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False): +def get_bert_embed( + phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False +): input_ids = [] for phrase in phrase_list: - input_ids.append(tok.encode_plus( - phrase, max_length=32, add_special_tokens=True, - truncation=True, pad_to_max_length=True)['input_ids']) + input_ids.append( + tok.encode_plus( + phrase, + max_length=32, + add_special_tokens=True, + truncation=True, + pad_to_max_length=True, + )["input_ids"] + ) m.eval() count = len(input_ids) @@ -39,15 +52,17 @@ def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tq if tqdm_bar: pbar = tqdm.tqdm(total=count) while now_count < count: - input_gpu_0 = torch.LongTensor(input_ids[now_count:min( - now_count + batch_size, count)]).to(device) + input_gpu_0 = torch.LongTensor( + input_ids[now_count : min(now_count + batch_size, count)] + ).to(device) if summary_method == "CLS": embed = m(input_gpu_0)[1] if summary_method == "MEAN": embed = torch.mean(m(input_gpu_0)[0], dim=1) if normalize: - embed_norm = torch.norm( - embed, p=2, dim=1, keepdim=True).clamp(min=1e-12) + embed_norm = torch.norm(embed, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) embed = embed / embed_norm if now_count == 0: output = embed @@ -60,6 +75,7 @@ def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tq pbar.close() return output + # Normalisation of words def normalize(txt): return unidecode( @@ -70,6 +86,7 @@ def normalize(txt): .replace("antigenes ", "antigene ") ) + # Loading our data to match data_df = pd.read_json(DATA_DIR) data_df["term"] = data_df["term"].apply(normalize) @@ -80,5 +97,7 @@ def normalize(txt): .reset_index(drop=True) ) -umls_embedding = get_bert_embed(data_df["term"].tolist(), model, tokenizer, tqdm_bar=False) -torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) \ No newline at end of file +umls_embedding = get_bert_embed( + data_df["term"].tolist(), model, tokenizer, tqdm_bar=False +) +torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) diff --git a/Normalisation/training/generate_term_embeddings.sh b/Normalisation/training/generate_term_embeddings.sh index 63ee366bb..277dd37d2 100644 --- a/Normalisation/training/generate_term_embeddings.sh +++ b/Normalisation/training/generate_term_embeddings.sh @@ -1,14 +1,14 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=generate_umls_embeddings.sh -#SBATCH -t 48:00:00 +#SBATCH -t 48:00:00 #SBATCH --gres=gpu:t4:1 -#SBATCH -N1-1 +#SBATCH -N1-1 #SBATCH -c2 -#SBATCH --mem=40000 +#SBATCH --mem=40000 #SBATCH -p gpuT4 #SBATCH --output=./log/%x-%j.out #SBATCH --error=./log/%x-%j.err -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -17,6 +17,6 @@ conda activate pierrenv cd /export/home/cse200093/Jacques_Bio/normalisation/py_files which python python ./generate_term_embeddings.py -#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. #which python #python ./main.py diff --git a/Normalisation/training/generate_term_embeddings_coder_eds.py b/Normalisation/training/generate_term_embeddings_coder_eds.py index 8eb8aada0..6624e68ec 100644 --- a/Normalisation/training/generate_term_embeddings_coder_eds.py +++ b/Normalisation/training/generate_term_embeddings_coder_eds.py @@ -1,14 +1,17 @@ -from gensim import models import os import sys + +from gensim import models + sys.path.append("/export/home/cse200093/Jacques_Bio/normalisation/py_files") -from load_umls import UMLS -import torch -import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig -import tqdm import pickle + +import numpy as np import pandas as pd +import torch +import tqdm +from load_umls import UMLS +from transformers import AutoConfig, AutoModel, AutoTokenizer from unidecode import unidecode batch_size = 128 @@ -16,7 +19,9 @@ # Defining the model # coder_all -model_checkpoint = "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +model_checkpoint = ( + "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +) tokenizer_path = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" model = torch.load(model_checkpoint).to(device) tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) @@ -26,12 +31,20 @@ EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/data_embeddings_normalized_snomed_coder_eds.pt" # Method to generate embeddings -def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False): +def get_bert_embed( + phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False +): input_ids = [] for phrase in phrase_list: - input_ids.append(tok.encode_plus( - phrase, max_length=32, add_special_tokens=True, - truncation=True, pad_to_max_length=True)['input_ids']) + input_ids.append( + tok.encode_plus( + phrase, + max_length=32, + add_special_tokens=True, + truncation=True, + pad_to_max_length=True, + )["input_ids"] + ) m.eval() count = len(input_ids) @@ -40,15 +53,17 @@ def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tq if tqdm_bar: pbar = tqdm.tqdm(total=count) while now_count < count: - input_gpu_0 = torch.LongTensor(input_ids[now_count:min( - now_count + batch_size, count)]).to(device) + input_gpu_0 = torch.LongTensor( + input_ids[now_count : min(now_count + batch_size, count)] + ).to(device) if summary_method == "CLS": embed = m.bert(input_gpu_0)[1] if summary_method == "MEAN": embed = torch.mean(m.bert(input_gpu_0)[0], dim=1) if normalize: - embed_norm = torch.norm( - embed, p=2, dim=1, keepdim=True).clamp(min=1e-12) + embed_norm = torch.norm(embed, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) embed = embed / embed_norm if now_count == 0: output = embed @@ -61,6 +76,7 @@ def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tq pbar.close() return output + # Normalisation of words def normalize(txt): return unidecode( @@ -71,6 +87,7 @@ def normalize(txt): .replace("antigenes ", "antigene ") ) + # Loading our data to match data_df = pd.read_json(DATA_DIR) data_df["term"] = data_df["term"].apply(normalize) @@ -81,5 +98,7 @@ def normalize(txt): .reset_index(drop=True) ) -umls_embedding = get_bert_embed(data_df["term"].tolist(), model, tokenizer, tqdm_bar=False) +umls_embedding = get_bert_embed( + data_df["term"].tolist(), model, tokenizer, tqdm_bar=False +) torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) diff --git a/Normalisation/training/generate_term_embeddings_coder_eds.sh b/Normalisation/training/generate_term_embeddings_coder_eds.sh index 5a62c2140..979085094 100644 --- a/Normalisation/training/generate_term_embeddings_coder_eds.sh +++ b/Normalisation/training/generate_term_embeddings_coder_eds.sh @@ -1,14 +1,14 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=generate_umls_embeddings_coder_eds.sh -#SBATCH -t 48:00:00 +#SBATCH -t 48:00:00 #SBATCH --gres=gpu:t4:1 -#SBATCH -N1-1 +#SBATCH -N1-1 #SBATCH -c2 -#SBATCH --mem=40000 +#SBATCH --mem=40000 #SBATCH -p gpuT4 #SBATCH --output=./log/%x-%j.out #SBATCH --error=./log/%x-%j.err -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -17,6 +17,6 @@ conda activate pierrenv cd /export/home/cse200093/Jacques_Bio/normalisation/py_files which python python ./generate_term_embeddings_coder_eds.py -#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. #which python #python ./main.py diff --git a/Normalisation/training/generate_term_embeddings_coder_eds_cased.py b/Normalisation/training/generate_term_embeddings_coder_eds_cased.py index 47a070e38..1c4f95bcb 100644 --- a/Normalisation/training/generate_term_embeddings_coder_eds_cased.py +++ b/Normalisation/training/generate_term_embeddings_coder_eds_cased.py @@ -1,14 +1,17 @@ -from gensim import models import os import sys + +from gensim import models + sys.path.append("/export/home/cse200093/Jacques_Bio/normalisation/py_files") -from load_umls import UMLS -import torch -import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig -import tqdm import pickle + +import numpy as np import pandas as pd +import torch +import tqdm +from load_umls import UMLS +from transformers import AutoConfig, AutoModel, AutoTokenizer from unidecode import unidecode batch_size = 128 @@ -16,7 +19,9 @@ # Defining the model # coder_all -model_checkpoint = "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +model_checkpoint = ( + "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +) tokenizer_path = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" model = torch.load(model_checkpoint).to(device) tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) @@ -26,12 +31,20 @@ EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/data_embeddings_normalized_snomed_coder_eds_2_cased.pt" # Method to generate embeddings -def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False): +def get_bert_embed( + phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False +): input_ids = [] for phrase in phrase_list: - input_ids.append(tok.encode_plus( - phrase, max_length=32, add_special_tokens=True, - truncation=True, pad_to_max_length=True)['input_ids']) + input_ids.append( + tok.encode_plus( + phrase, + max_length=32, + add_special_tokens=True, + truncation=True, + pad_to_max_length=True, + )["input_ids"] + ) m.eval() count = len(input_ids) @@ -40,15 +53,17 @@ def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tq if tqdm_bar: pbar = tqdm.tqdm(total=count) while now_count < count: - input_gpu_0 = torch.LongTensor(input_ids[now_count:min( - now_count + batch_size, count)]).to(device) + input_gpu_0 = torch.LongTensor( + input_ids[now_count : min(now_count + batch_size, count)] + ).to(device) if summary_method == "CLS": embed = m.bert(input_gpu_0)[1] if summary_method == "MEAN": embed = torch.mean(m.bert(input_gpu_0)[0], dim=1) if normalize: - embed_norm = torch.norm( - embed, p=2, dim=1, keepdim=True).clamp(min=1e-12) + embed_norm = torch.norm(embed, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) embed = embed / embed_norm if now_count == 0: output = embed @@ -61,6 +76,7 @@ def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tq pbar.close() return output + # Normalisation of words def normalize(txt): return unidecode( @@ -73,6 +89,7 @@ def normalize(txt): .replace("Antigenes ", "Antigene ") ) + # Loading our data to match data_df = pd.read_json(DATA_DIR) data_df["term"] = data_df["term"].apply(normalize) @@ -83,5 +100,7 @@ def normalize(txt): .reset_index(drop=True) ) -umls_embedding = get_bert_embed(data_df["term"].tolist(), model, tokenizer, tqdm_bar=False) +umls_embedding = get_bert_embed( + data_df["term"].tolist(), model, tokenizer, tqdm_bar=False +) torch.save(umls_embedding, EMBEDDINGS_SAVE_DIR) diff --git a/Normalisation/training/generate_term_embeddings_coder_eds_cased.sh b/Normalisation/training/generate_term_embeddings_coder_eds_cased.sh index 71b9f609e..b921d9de4 100644 --- a/Normalisation/training/generate_term_embeddings_coder_eds_cased.sh +++ b/Normalisation/training/generate_term_embeddings_coder_eds_cased.sh @@ -1,14 +1,14 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=generate_umls_embeddings_coder_eds_cased.sh -#SBATCH -t 48:00:00 +#SBATCH -t 48:00:00 #SBATCH --gres=gpu:t4:1 -#SBATCH -N1-1 +#SBATCH -N1-1 #SBATCH -c2 -#SBATCH --mem=40000 +#SBATCH --mem=40000 #SBATCH -p gpuT4 #SBATCH --output=./log/%x-%j.out #SBATCH --error=./log/%x-%j.err -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -17,6 +17,6 @@ conda activate pierrenv cd /export/home/cse200093/Jacques_Bio/normalisation/py_files which python python ./generate_term_embeddings_coder_eds_cased.py -#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. #which python #python ./main.py diff --git a/Normalisation/training/generate_umls_embeddings.py b/Normalisation/training/generate_umls_embeddings.py index b9ec1dbdc..ca35acf08 100644 --- a/Normalisation/training/generate_umls_embeddings.py +++ b/Normalisation/training/generate_umls_embeddings.py @@ -1,37 +1,50 @@ print("BONJOUR") -from gensim import models import os import sys + +from gensim import models + sys.path.append("/export/home/cse200093/Jacques_Bio/normalisation/py_files") -from load_umls import UMLS -import torch +import pickle + import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig +import torch import tqdm -import pickle +from load_umls import UMLS +from transformers import AutoConfig, AutoModel, AutoTokenizer batch_size = 128 device = "cuda:0" # Defining the model # coder_all -model_checkpoint = '/export/home/cse200093/coder_all' +model_checkpoint = "/export/home/cse200093/coder_all" tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) model = AutoModel.from_pretrained(model_checkpoint).to(device) # Defining save paths -DES_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_des.pkl" +DES_SAVE_DIR = ( + "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_des.pkl" +) LABEL_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_label.pkl" EMBEDDINGS_SAVE_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_embeddings.pt" # Method to generate embeddings -def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False): +def get_bert_embed( + phrase_list, m, tok, normalize=True, summary_method="CLS", tqdm_bar=False +): input_ids = [] for phrase in phrase_list: - input_ids.append(tok.encode_plus( - phrase, max_length=32, add_special_tokens=True, - truncation=True, pad_to_max_length=True)['input_ids']) + input_ids.append( + tok.encode_plus( + phrase, + max_length=32, + add_special_tokens=True, + truncation=True, + pad_to_max_length=True, + )["input_ids"] + ) m.eval() count = len(input_ids) @@ -40,15 +53,17 @@ def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tq if tqdm_bar: pbar = tqdm.tqdm(total=count) while now_count < count: - input_gpu_0 = torch.LongTensor(input_ids[now_count:min( - now_count + batch_size, count)]).to(device) + input_gpu_0 = torch.LongTensor( + input_ids[now_count : min(now_count + batch_size, count)] + ).to(device) if summary_method == "CLS": embed = m(input_gpu_0)[1] if summary_method == "MEAN": embed = torch.mean(m(input_gpu_0)[0], dim=1) if normalize: - embed_norm = torch.norm( - embed, p=2, dim=1, keepdim=True).clamp(min=1e-12) + embed_norm = torch.norm(embed, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) embed = embed / embed_norm if now_count == 0: output = embed @@ -61,15 +76,20 @@ def get_bert_embed(phrase_list, m, tok, normalize=True, summary_method="CLS", tq pbar.close() return output + def get_umls(): umls_label = [] umls_label_set = set() umls_des = [] - umls = UMLS("/export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB", lang_range=['ENG', 'FRA'], only_load_dict=True) + umls = UMLS( + "/export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB", + lang_range=["ENG", "FRA"], + only_load_dict=True, + ) umls.load_sty() umls_sty = umls.cui2sty for cui in tqdm.tqdm(umls.cui2str): - if not cui in umls_label_set and umls_sty[cui] == 'Laboratory Procedure': + if not cui in umls_label_set and umls_sty[cui] == "Laboratory Procedure": tmp_str = list(umls.cui2str[cui]) umls_label.extend([cui] * len(tmp_str)) umls_des.extend(tmp_str) @@ -77,6 +97,7 @@ def get_umls(): print(len(umls_des)) return umls_label, umls_des + umls_label, umls_des = get_umls() umls_embedding = get_bert_embed(umls_des, model, tokenizer, tqdm_bar=False) @@ -91,4 +112,4 @@ def get_umls(): # Save umls_labels open_file = open(LABEL_SAVE_DIR, "wb") pickle.dump(umls_label, open_file) -open_file.close()""" \ No newline at end of file +open_file.close()""" diff --git a/Normalisation/training/generate_umls_embeddings.sh b/Normalisation/training/generate_umls_embeddings.sh index 6d5fb80b9..a22ed4f59 100644 --- a/Normalisation/training/generate_umls_embeddings.sh +++ b/Normalisation/training/generate_umls_embeddings.sh @@ -1,14 +1,14 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=generate_umls_embeddings.sh -#SBATCH -t 48:00:00 +#SBATCH -t 48:00:00 #SBATCH --gres=gpu:t4:1 -#SBATCH -N1-1 +#SBATCH -N1-1 #SBATCH -c2 -#SBATCH --mem=40000 +#SBATCH --mem=40000 #SBATCH -p gpuT4 #SBATCH --output=./log/%x-%j.out #SBATCH --error=./log/%x-%j.err -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -17,6 +17,6 @@ conda activate pierrenv cd /export/home/cse200093/Jacques_Bio/normalisation/py_files which python python ./generate_umls_embeddings.py -#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. #which python #python ./main.py diff --git a/Normalisation/training/generate_umls_normalized_embeddings.py b/Normalisation/training/generate_umls_normalized_embeddings.py index 5aece0b85..b40813721 100644 --- a/Normalisation/training/generate_umls_normalized_embeddings.py +++ b/Normalisation/training/generate_umls_normalized_embeddings.py @@ -1,21 +1,24 @@ -from gensim import models import os +import pickle +import re import sys -import torch + import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig -import tqdm -import pickle import pandas as pd +import torch +import tqdm +from gensim import models +from transformers import AutoConfig, AutoModel, AutoTokenizer from unidecode import unidecode -import re batch_size = 128 device = "cuda:0" # Defining the model # coder_all -model_checkpoint = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" +model_checkpoint = ( + "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" +) tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) model = AutoModel.from_pretrained(model_checkpoint).to(device) @@ -168,4 +171,4 @@ def normalize(txt): # Save umls_labels open_file = open(LABEL_SAVE_DIR, "wb") pickle.dump(umls_label, open_file) -open_file.close() \ No newline at end of file +open_file.close() diff --git a/Normalisation/training/generate_umls_normalized_embeddings.sh b/Normalisation/training/generate_umls_normalized_embeddings.sh index cb4f53ce2..e8b419dff 100644 --- a/Normalisation/training/generate_umls_normalized_embeddings.sh +++ b/Normalisation/training/generate_umls_normalized_embeddings.sh @@ -1,15 +1,15 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=generate_umls_normalized_embeddings.sh -#SBATCH -t 24:00:00 +#SBATCH -t 24:00:00 #SBATCH --gres=gpu:t4:1 -#SBATCH -N1-1 +#SBATCH -N1-1 #SBATCH -c2 -#SBATCH --mem=40000 +#SBATCH --mem=40000 #SBATCH -p gpuT4 #SBATCH -w bbs-edsg28-p012 #SBATCH --output=./log/%x-%j.out #SBATCH --error=./log/%x-%j.err -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -20,6 +20,6 @@ which python pwd python /export/home/cse200093/Jacques_Bio/normalisation/py_files/generate_umls_normalized_embeddings.py echo end -#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. #which python #python ./main.py diff --git a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.py b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.py index d24864c01..88e7f2721 100644 --- a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.py +++ b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.py @@ -1,21 +1,24 @@ -from gensim import models import os +import pickle +import re import sys -import torch + import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig -import tqdm -import pickle import pandas as pd +import torch +import tqdm +from gensim import models +from transformers import AutoConfig, AutoModel, AutoTokenizer from unidecode import unidecode -import re batch_size = 128 device = "cuda:0" # Defining the model # coder_eds -model_checkpoint = "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +model_checkpoint = ( + "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +) tokenizer_path = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" model = torch.load(model_checkpoint).to(device) tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) @@ -169,4 +172,4 @@ def normalize(txt): # Save umls_labels open_file = open(LABEL_SAVE_DIR, "wb") pickle.dump(umls_label, open_file) -open_file.close() \ No newline at end of file +open_file.close() diff --git a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.sh b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.sh index 095730118..b9535ac5d 100644 --- a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.sh +++ b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds.sh @@ -1,15 +1,15 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=generate_umls_normalized_embeddings_coder_eds.sh -#SBATCH -t 24:00:00 +#SBATCH -t 24:00:00 #SBATCH --gres=gpu:t4:1 -#SBATCH -N1-1 +#SBATCH -N1-1 #SBATCH -c2 -#SBATCH --mem=40000 +#SBATCH --mem=40000 #SBATCH -p gpuT4 #SBATCH -w bbs-edsg28-p009 #SBATCH --output=./log/%x-%j.out #SBATCH --error=./log/%x-%j.err -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -20,6 +20,6 @@ which python pwd python /export/home/cse200093/Jacques_Bio/normalisation/py_files/generate_umls_normalized_embeddings_coder_eds.py echo end -#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. #which python #python ./main.py diff --git a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.py b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.py index 562853917..0a3e89943 100644 --- a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.py +++ b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.py @@ -1,21 +1,24 @@ -from gensim import models import os +import pickle +import re import sys -import torch + import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig -import tqdm -import pickle import pandas as pd +import torch +import tqdm +from gensim import models +from transformers import AutoConfig, AutoModel, AutoTokenizer from unidecode import unidecode -import re batch_size = 128 device = "cuda:0" # Defining the model # coder_eds -model_checkpoint = "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +model_checkpoint = ( + "/export/home/cse200093/Jacques_Bio/data_bio/coder_output/model_150000.pth" +) tokenizer_path = "/export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29" model = torch.load(model_checkpoint).to(device) tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) diff --git a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.sh b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.sh index 1fc64c7ec..6edb1868a 100644 --- a/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.sh +++ b/Normalisation/training/generate_umls_normalized_embeddings_coder_eds_cased.sh @@ -1,15 +1,15 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=generate_umls_normalized_embeddings_coder_eds_cased.sh -#SBATCH -t 24:00:00 +#SBATCH -t 24:00:00 #SBATCH --gres=gpu:t4:1 -#SBATCH -N1-1 +#SBATCH -N1-1 #SBATCH -c2 -#SBATCH --mem=40000 +#SBATCH --mem=40000 #SBATCH -p gpuT4 #SBATCH -w bbs-edsg28-p009 #SBATCH --output=./log/%x-%j.out #SBATCH --error=./log/%x-%j.err -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -20,6 +20,6 @@ which python pwd python /export/home/cse200093/Jacques_Bio/normalisation/py_files/generate_umls_normalized_embeddings_coder_eds_cased.py echo end -#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. #which python #python ./main.py diff --git a/Normalisation/training/get_matches.py b/Normalisation/training/get_matches.py index 3cad3241c..734bed0dd 100644 --- a/Normalisation/training/get_matches.py +++ b/Normalisation/training/get_matches.py @@ -1,12 +1,13 @@ -from gensim import models import os +import pickle import sys -import torch + import numpy as np -from transformers import AutoTokenizer, AutoModel, AutoConfig -import tqdm -import pickle import pandas as pd +import torch +import tqdm +from gensim import models +from transformers import AutoConfig, AutoModel, AutoTokenizer device = "cpu" EMBEDDINGS_DIR = "/export/home/cse200093/Jacques_Bio/data_bio/normalisation_embeddings/umls_normalized_embeddings_coder_eds.pt" @@ -21,14 +22,14 @@ # LOAD CORRESPONDANCE FILE BETWEEN EMBEDDINGS AND CUIS AND DES with open(LABEL_DIR, "rb") as f: - umls_labels= pickle.load(f) - + umls_labels = pickle.load(f) + with open(DES_DIR, "rb") as f: - umls_des= pickle.load(f) + umls_des = pickle.load(f) sim = torch.matmul(res_embeddings, umls_embeddings.t()) most_similar = torch.max(sim, dim=1)[1].tolist() label_matches = [umls_labels[idx] for idx in most_similar] des_matches = [umls_des[idx] for idx in most_similar] print(label_matches) -print(des_matches) \ No newline at end of file +print(des_matches) diff --git a/Normalisation/training/get_matches.sh b/Normalisation/training/get_matches.sh index d80a6937b..61a5375d0 100644 --- a/Normalisation/training/get_matches.sh +++ b/Normalisation/training/get_matches.sh @@ -1,14 +1,14 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=get_matches.sh -#SBATCH -t 48:00:00 +#SBATCH -t 48:00:00 #SBATCH --gres=gpu:t4:1 -#SBATCH -N1-1 +#SBATCH -N1-1 #SBATCH -c2 #SBATCH --mem=40000 #SBATCH -p gpuT4 #SBATCH --output=./log/%x-%j.out #SBATCH --error=./log/%x-%j.err -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -17,6 +17,6 @@ conda activate pierrenv cd /export/home/cse200093/Jacques_Bio/normalisation/py_files which python python ./get_matches.py -#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. +#export PYTHONPATH=/export/home/cse200093/nlstruct-master/:. #which python #python ./main.py diff --git a/Normalisation/training/load_umls.py b/Normalisation/training/load_umls.py index a3ab6c84c..6f0c7fe14 100644 --- a/Normalisation/training/load_umls.py +++ b/Normalisation/training/load_umls.py @@ -1,8 +1,11 @@ import os -from tqdm import tqdm import re from random import shuffle -#import ipdb + +from tqdm import tqdm + +# import ipdb + def byLineReader(filename): with open(filename, "r", encoding="utf-8") as f: @@ -14,7 +17,9 @@ def byLineReader(filename): class UMLS(object): - def __init__(self, umls_path, source_range=None, lang_range=['ENG'], only_load_dict=False): + def __init__( + self, umls_path, source_range=None, lang_range=["ENG"], only_load_dict=False + ): self.umls_path = umls_path self.source_range = source_range self.lang_range = lang_range @@ -36,11 +41,11 @@ def load(self): self.cui2str = {} self.str2cui = {} self.code2cui = {} - #self.lui_status = {} + # self.lui_status = {} read_count = 0 for line in tqdm(reader, ascii=True): if self.type == "txt": - l = [t.replace("\"", "") for t in line.split(",")] + l = [t.replace('"', "") for t in line.split(",")] else: l = line.strip().split("|") if len(l) < 3: @@ -53,7 +58,9 @@ def load(self): code = l[13] string = l[14] - if (self.source_range is None or source in self.source_range) and (self.lang_range is None or lang in self.lang_range): + if (self.source_range is None or source in self.source_range) and ( + self.lang_range is None or lang in self.lang_range + ): if not lui in self.lui_set: read_count += 1 self.str2cui[string] = cui @@ -84,7 +91,7 @@ def load_rel(self): self.rel = set() for line in tqdm(reader, ascii=True): if self.type == "txt": - l = [t.replace("\"", "") for t in line.split(",")] + l = [t.replace('"', "") for t in line.split(",")] else: l = line.strip().split("|") cui0 = l[0] @@ -108,7 +115,7 @@ def load_sty(self): self.cui2sty = {} for line in tqdm(reader, ascii=True): if self.type == "txt": - l = [t.replace("\"", "") for t in line.split(",")] + l = [t.replace('"', "") for t in line.split(",")] else: l = line.strip().split("|") cui = l[0] @@ -118,14 +125,16 @@ def load_sty(self): print("sty count:", len(self.cui2sty)) - def clean(self, term, lower=True, clean_NOS=True, clean_bracket=True, clean_dash=True): + def clean( + self, term, lower=True, clean_NOS=True, clean_bracket=True, clean_dash=True + ): term = " " + term + " " if lower: term = term.lower() if clean_NOS: term = term.replace(" NOS ", " ").replace(" nos ", " ") if clean_bracket: - term = re.sub(u"\\(.*?\\)", "", term) + term = re.sub("\\(.*?\\)", "", term) if clean_dash: term = term.replace("-", " ") term = " ".join([w for w in term.split() if w]) @@ -152,13 +161,13 @@ def search(self, code=None, string_list=None, max_number=-1): result_by_code = self.search_by_code(code) if result_by_code is not None: if max_number > 0: - return result_by_code[0:min(len(result_by_code), max_number)] + return result_by_code[0 : min(len(result_by_code), max_number)] return result_by_code return None result_by_string = self.search_by_string_list(string_list) if result_by_string is not None: if max_number > 0: - return result_by_string[0:min(len(result_by_string), max_number)] + return result_by_string[0 : min(len(result_by_string), max_number)] return result_by_string return None @@ -166,8 +175,8 @@ def search(self, code=None, string_list=None, max_number=-1): if __name__ == "__main__": umls = UMLS("E:\\code\\research\\umls") # print(umls.search_by_code("282299006")) - #print(umls.search_by_string_list(["Backache", "aching muscles in back"])) - #print(umls.search(code="95891005", max_number=10)) + # print(umls.search_by_string_list(["Backache", "aching muscles in back"])) + # print(umls.search(code="95891005", max_number=10)) # ipdb.set_trace() """ diff --git a/Normalisation/training/load_umls_normalized.py b/Normalisation/training/load_umls_normalized.py index 38910d638..277aa9a6c 100644 --- a/Normalisation/training/load_umls_normalized.py +++ b/Normalisation/training/load_umls_normalized.py @@ -1,14 +1,17 @@ import os -from tqdm import tqdm import re from random import shuffle -#import ipdb + +from tqdm import tqdm + +# import ipdb ### THIS LOADING PROCESS DEFERS FROM load_umls.py IN THE WAY THAT source_range LETS YOU ### SELECT ALL SYNONYMS FROM ONE CUI IF THIS CUI HAVE AT LEAST ONE SYNONYM ### FROM A SOURCE OF source_range ### THUS, ALL SYNONYMS ARE NOT FROM THE SOURCES OF source_range + def byLineReader(filename): with open(filename, "r", encoding="utf-8") as f: line = f.readline() @@ -19,7 +22,9 @@ def byLineReader(filename): class UMLS(object): - def __init__(self, umls_path, source_range=None, lang_range=['ENG'], only_load_dict=False): + def __init__( + self, umls_path, source_range=None, lang_range=["ENG"], only_load_dict=False + ): self.umls_path = umls_path self.source_range = source_range self.lang_range = lang_range @@ -41,14 +46,14 @@ def load(self): self.cui2str = {} self.str2cui = {} self.code2cui = {} - #self.lui_status = {} - + # self.lui_status = {} + # Select all CUIs which have at least one synonym in source_range cuis2keep = [] if self.source_range is not None: for line in tqdm(reader, ascii=True): if self.type == "txt": - l = [t.replace("\"", "") for t in line.split(",")] + l = [t.replace('"', "") for t in line.split(",")] else: l = line.strip().split("|") if len(l) < 3: @@ -57,12 +62,12 @@ def load(self): source = l[11] if source in self.source_range: cuis2keep.append(cui) - + reader = byLineReader(os.path.join(self.umls_path, "MRCONSO." + self.type)) read_count = 0 for line in tqdm(reader, ascii=True): if self.type == "txt": - l = [t.replace("\"", "") for t in line.split(",")] + l = [t.replace('"', "") for t in line.split(",")] else: l = line.strip().split("|") if len(l) < 3: @@ -75,7 +80,9 @@ def load(self): code = l[13] string = l[14] - if (self.source_range is None or source in cuis2keep) and (self.lang_range is None or lang in self.lang_range): + if (self.source_range is None or source in cuis2keep) and ( + self.lang_range is None or lang in self.lang_range + ): if not lui in self.lui_set: read_count += 1 self.str2cui[string] = cui @@ -106,7 +113,7 @@ def load_rel(self): self.rel = set() for line in tqdm(reader, ascii=True): if self.type == "txt": - l = [t.replace("\"", "") for t in line.split(",")] + l = [t.replace('"', "") for t in line.split(",")] else: l = line.strip().split("|") cui0 = l[0] @@ -130,7 +137,7 @@ def load_sty(self): self.cui2sty = {} for line in tqdm(reader, ascii=True): if self.type == "txt": - l = [t.replace("\"", "") for t in line.split(",")] + l = [t.replace('"', "") for t in line.split(",")] else: l = line.strip().split("|") cui = l[0] @@ -140,14 +147,16 @@ def load_sty(self): print("sty count:", len(self.cui2sty)) - def clean(self, term, lower=True, clean_NOS=True, clean_bracket=True, clean_dash=True): + def clean( + self, term, lower=True, clean_NOS=True, clean_bracket=True, clean_dash=True + ): term = " " + term + " " if lower: term = term.lower() if clean_NOS: term = term.replace(" NOS ", " ").replace(" nos ", " ") if clean_bracket: - term = re.sub(u"\\(.*?\\)", "", term) + term = re.sub("\\(.*?\\)", "", term) if clean_dash: term = term.replace("-", " ") term = " ".join([w for w in term.split() if w]) @@ -174,13 +183,13 @@ def search(self, code=None, string_list=None, max_number=-1): result_by_code = self.search_by_code(code) if result_by_code is not None: if max_number > 0: - return result_by_code[0:min(len(result_by_code), max_number)] + return result_by_code[0 : min(len(result_by_code), max_number)] return result_by_code return None result_by_string = self.search_by_string_list(string_list) if result_by_string is not None: if max_number > 0: - return result_by_string[0:min(len(result_by_string), max_number)] + return result_by_string[0 : min(len(result_by_string), max_number)] return result_by_string return None @@ -188,8 +197,8 @@ def search(self, code=None, string_list=None, max_number=-1): if __name__ == "__main__": umls = UMLS("E:\\code\\research\\umls") # print(umls.search_by_code("282299006")) - #print(umls.search_by_string_list(["Backache", "aching muscles in back"])) - #print(umls.search(code="95891005", max_number=10)) + # print(umls.search_by_string_list(["Backache", "aching muscles in back"])) + # print(umls.search(code="95891005", max_number=10)) # ipdb.set_trace() """ diff --git a/Normalisation/training/loss.py b/Normalisation/training/loss.py index 9f2d8ca56..4b638f612 100644 --- a/Normalisation/training/loss.py +++ b/Normalisation/training/loss.py @@ -3,23 +3,21 @@ class AMSoftmax(nn.Module): - def __init__(self, - in_feats, - n_classes=10, - m=0.35, - s=30): + def __init__(self, in_feats, n_classes=10, m=0.35, s=30): super(AMSoftmax, self).__init__() self.m = m self.s = s self.in_feats = in_feats - self.W = torch.nn.Parameter(torch.randn(in_feats, n_classes), requires_grad=True) + self.W = torch.nn.Parameter( + torch.randn(in_feats, n_classes), requires_grad=True + ) self.ce = nn.CrossEntropyLoss() nn.init.xavier_normal_(self.W, gain=1) def forward(self, x, label): - #print(x.shape, lb.shape, self.in_feats) - #assert x.size()[0] == label.size()[0] - #assert x.size()[1] == self.in_feats + # print(x.shape, lb.shape, self.in_feats) + # assert x.size()[0] == label.size()[0] + # assert x.size()[1] == self.in_feats x_norm = torch.norm(x, p=2, dim=1, keepdim=True).clamp(min=1e-12) x_norm = torch.div(x, x_norm) w_norm = torch.norm(self.W, p=2, dim=0, keepdim=True).clamp(min=1e-12) @@ -41,6 +39,7 @@ def predict(self, x): costh = torch.mm(x_norm, w_norm) return costh + class MultiSimilarityLoss(nn.Module): def __init__(self): super(MultiSimilarityLoss, self).__init__() @@ -51,7 +50,7 @@ def __init__(self): self.scale_neg = 50.0 def forward(self, feats, labels): - #assert feats.size(0) == labels.size(0), \ + # assert feats.size(0) == labels.size(0), \ # f"feats.size(0): {feats.size(0)} is not equal to labels.size(0): {labels.size(0)}" batch_size = feats.size(0) @@ -64,41 +63,58 @@ def forward(self, feats, labels): epsilon = 1e-5 loss = [] - #unique_label, inverse_indices = torch.unique_consecutive(labels, return_inverse=True) + # unique_label, inverse_indices = torch.unique_consecutive(labels, return_inverse=True) for i in range(batch_size): pos_pair_ = sim_mat[i][labels == labels[i]] pos_pair_ = pos_pair_[pos_pair_ < 1 - epsilon] neg_pair_ = sim_mat[i][labels != labels[i]] - #print(pos_pair_) - #print(neg_pair_) - + # print(pos_pair_) + # print(neg_pair_) + if len(neg_pair_) >= 1: pos_pair = pos_pair_[pos_pair_ - self.margin < max(neg_pair_)] if len(pos_pair) >= 1: - pos_loss = 1.0 / self.scale_pos * torch.log( - 1 + torch.sum(torch.exp(-self.scale_pos * (pos_pair - self.thresh)))) + pos_loss = ( + 1.0 + / self.scale_pos + * torch.log( + 1 + + torch.sum( + torch.exp(-self.scale_pos * (pos_pair - self.thresh)) + ) + ) + ) loss.append(pos_loss) if len(pos_pair_) >= 1: neg_pair = neg_pair_[neg_pair_ + self.margin > min(pos_pair_)] if len(neg_pair) >= 1: - neg_loss = 1.0 / self.scale_neg * torch.log( - 1 + torch.sum(torch.exp(self.scale_neg * (neg_pair - self.thresh)))) + neg_loss = ( + 1.0 + / self.scale_neg + * torch.log( + 1 + + torch.sum( + torch.exp(self.scale_neg * (neg_pair - self.thresh)) + ) + ) + ) loss.append(neg_loss) - #print(labels, len(loss)) + # print(labels, len(loss)) if len(loss) == 0: return torch.zeros([], requires_grad=True).to(feats.device) loss = sum(loss) / batch_size return loss -if __name__ == '__main__': + +if __name__ == "__main__": criteria = AMSoftmax(20, 5) a = torch.randn(10, 20) - lb = torch.randint(0, 5, (10, ), dtype=torch.long) + lb = torch.randint(0, 5, (10,), dtype=torch.long) loss = criteria(a, lb) loss.backward() diff --git a/Normalisation/training/model.py b/Normalisation/training/model.py index 4de2df06b..4fe752953 100644 --- a/Normalisation/training/model.py +++ b/Normalisation/training/model.py @@ -1,23 +1,28 @@ -#from transformers import BertConfig, BertPreTrainedModel, BertTokenizer, BertModel -from transformers import AutoConfig -from transformers import AutoModelForPreTraining -from transformers import AutoTokenizer -from transformers import AutoModel -from transformers.modeling_utils import SequenceSummary -from torch import nn -import torch.nn.functional as F +# from transformers import BertConfig, BertPreTrainedModel, BertTokenizer, BertModel import torch +import torch.nn.functional as F from loss import AMSoftmax from pytorch_metric_learning import losses, miners +from torch import nn from trans import TransE +from transformers import AutoConfig, AutoModel, AutoModelForPreTraining, AutoTokenizer +from transformers.modeling_utils import SequenceSummary class UMLSPretrainedModel(nn.Module): - def __init__(self, device, model_name_or_path, - cui_label_count, rel_label_count, sty_label_count, - re_weight=1.0, sty_weight=0.1, - cui_loss_type="ms_loss", - trans_loss_type="TransE", trans_margin=1.0): + def __init__( + self, + device, + model_name_or_path, + cui_label_count, + rel_label_count, + sty_label_count, + re_weight=1.0, + sty_weight=0.1, + cui_loss_type="ms_loss", + trans_loss_type="TransE", + trans_margin=1.0, + ): super(UMLSPretrainedModel, self).__init__() self.device = device @@ -45,8 +50,7 @@ def __init__(self, device, model_name_or_path, self.cui_loss_fn = nn.CrossEntropyLoss() self.linear = nn.Linear(self.feature_dim, self.cui_label_count) if self.cui_loss_type == "am_softmax": - self.cui_loss_fn = AMSoftmax( - self.feature_dim, self.cui_label_count) + self.cui_loss_fn = AMSoftmax(self.feature_dim, self.cui_label_count) if self.cui_loss_type == "ms_loss": self.cui_loss_fn = losses.MultiSimilarityLoss(alpha=2, beta=50) self.miner = miners.MultiSimilarityMiner(epsilon=0.1) @@ -54,12 +58,13 @@ def __init__(self, device, model_name_or_path, self.trans_loss_type = trans_loss_type if self.trans_loss_type == "TransE": self.re_loss_fn = TransE(trans_margin) - self.re_embedding = nn.Embedding( - self.rel_label_count, self.feature_dim) + self.re_embedding = nn.Embedding(self.rel_label_count, self.feature_dim) self.standard_dataloader = None - self.sequence_summary = SequenceSummary(AutoConfig.from_pretrained(model_name_or_path)) # Now only used for XLNet + self.sequence_summary = SequenceSummary( + AutoConfig.from_pretrained(model_name_or_path) + ) # Now only used for XLNet def softmax(self, logits, label): loss = self.cui_loss_fn(logits, label) @@ -94,22 +99,28 @@ def get_sentence_feature(self, input_ids): pooled_output = self.sequence_summary(outputs[0]) return pooled_output - # @profile - def forward(self, - input_ids_0, input_ids_1, input_ids_2, - cui_label_0, cui_label_1, cui_label_2, - sty_label_0, sty_label_1, sty_label_2, - re_label): + def forward( + self, + input_ids_0, + input_ids_1, + input_ids_2, + cui_label_0, + cui_label_1, + cui_label_2, + sty_label_0, + sty_label_1, + sty_label_2, + re_label, + ): input_ids = torch.cat((input_ids_0, input_ids_1, input_ids_2), 0) cui_label = torch.cat((cui_label_0, cui_label_1, cui_label_2)) sty_label = torch.cat((sty_label_0, sty_label_1, sty_label_2)) - #print(input_ids.shape, cui_label.shape, sty_label.shape) + # print(input_ids.shape, cui_label.shape, sty_label.shape) use_len = input_ids_0.shape[0] - pooled_output = self.get_sentence_feature( - input_ids) # (3 * pair) * re_label + pooled_output = self.get_sentence_feature(input_ids) # (3 * pair) * re_label logits_sty = self.linear_sty(pooled_output) sty_loss = self.sty_loss_fn(logits_sty, sty_label) @@ -120,14 +131,13 @@ def forward(self, cui_loss = self.calculate_loss(pooled_output, logits, cui_label) cui_0_output = pooled_output[0:use_len] - cui_1_output = pooled_output[use_len:2 * use_len] - cui_2_output = pooled_output[2 * use_len:] + cui_1_output = pooled_output[use_len : 2 * use_len] + cui_2_output = pooled_output[2 * use_len :] re_output = self.re_embedding(re_label) - re_loss = self.re_loss_fn( - cui_0_output, cui_1_output, cui_2_output, re_output) + re_loss = self.re_loss_fn(cui_0_output, cui_1_output, cui_2_output, re_output) loss = self.sty_weight * sty_loss + cui_loss + self.re_weight * re_loss - #print(sty_loss.device, cui_loss.device, re_loss.device) + # print(sty_loss.device, cui_loss.device, re_loss.device) return loss, (sty_loss, cui_loss, re_loss) @@ -136,7 +146,7 @@ def predict(self, input_ids): if self.loss_type == "softmax": return self.predict_by_softmax(input_ids) if self.loss_type == "am_softmax": - return self.predict_by_amsoftmax(input_ids) + return self.predict_by_amsoftmax(input_ids) def predict_by_softmax(self, input_ids): pooled_output = self.get_sentence_feature(input_ids) @@ -155,24 +165,31 @@ def init_standard_feature(self): input_ids = batch[0].to(self.device) outputs = self.get_sentence_feature(input_ids) normalized_standard_feature = torch.norm( - outputs, p=2, dim=1, keepdim=True).clamp(min=1e-12) + outputs, p=2, dim=1, keepdim=True + ).clamp(min=1e-12) normalized_standard_feature = torch.div( - outputs, normalized_standard_feature) + outputs, normalized_standard_feature + ) if index == 0: self.standard_feature = normalized_standard_feature else: self.standard_feature = torch.cat( - (self.standard_feature, normalized_standard_feature), 0) + (self.standard_feature, normalized_standard_feature), 0 + ) assert self.standard_feature.shape == ( - self.num_label, self.feature_dim), self.standard_feature.shape + self.num_label, + self.feature_dim, + ), self.standard_feature.shape return None def predict_by_cosine(self, input_ids): pooled_output = self.get_sentence_feature(input_ids) - normalized_feature = torch.norm( - pooled_output, p=2, dim=1, keepdim=True).clamp(min=1e-12) + normalized_feature = torch.norm(pooled_output, p=2, dim=1, keepdim=True).clamp( + min=1e-12 + ) normalized_feature = torch.div(pooled_output, normalized_feature) - sim_mat = torch.matmul(normalized_feature, torch.t( - self.standard_feature)) # batch_size * num_label - return torch.max(sim_mat, dim=1)[1], sim_mat \ No newline at end of file + sim_mat = torch.matmul( + normalized_feature, torch.t(self.standard_feature) + ) # batch_size * num_label + return torch.max(sim_mat, dim=1)[1], sim_mat diff --git a/Normalisation/training/sampler_util.py b/Normalisation/training/sampler_util.py index 01c0259b2..379457bdf 100644 --- a/Normalisation/training/sampler_util.py +++ b/Normalisation/training/sampler_util.py @@ -1,6 +1,6 @@ import torch -from torch.utils.data import Dataset, DataLoader -from torch.utils.data.sampler import Sampler, RandomSampler +from torch.utils.data import DataLoader, Dataset +from torch.utils.data.sampler import RandomSampler, Sampler """ class TmpDataset(Dataset): @@ -14,6 +14,7 @@ def __len__(self): return self.len """ + class FixedLengthBatchSampler(Sampler): def __init__(self, sampler, fixed_length, drop_last): self.sampler = sampler @@ -25,10 +26,10 @@ def __iter__(self): batch = [] now_length = 0 for idx in self.sampler: - #print(batch, now_length) + # print(batch, now_length) sample_length = len(self.sampler.data_source[idx][-1]) * 3 if now_length + sample_length > self.fixed_length: - #print(batch, now_length) + # print(batch, now_length) yield batch batch = [] now_length = 0 @@ -38,6 +39,7 @@ def __iter__(self): if len(batch) > 0 and not self.drop_last: yield batch + def my_collate_fn(batch): type_count = len(batch[0]) batch_size = sum([len(item[-1]) for item in batch]) diff --git a/Normalisation/training/train.py b/Normalisation/training/train.py index 584b152aa..ebb3a43be 100644 --- a/Normalisation/training/train.py +++ b/Normalisation/training/train.py @@ -1,49 +1,66 @@ -from data_util import UMLSDataset, fixed_length_dataloader -from model import UMLSPretrainedModel -from transformers import AdamW, get_linear_schedule_with_warmup, get_cosine_schedule_with_warmup, get_constant_schedule_with_warmup -from tqdm import tqdm, trange -import torch -from torch import nn -import time -import os -import numpy as np import argparse -import time +import os import pathlib -#import ipdb +import time + +import numpy as np +import torch +from data_util import UMLSDataset, fixed_length_dataloader +from model import UMLSPretrainedModel + +# import ipdb # try: # from torch.utils.tensorboard import SummaryWriter # except: from tensorboardX import SummaryWriter +from torch import nn +from tqdm import tqdm, trange +from transformers import ( + AdamW, + get_constant_schedule_with_warmup, + get_cosine_schedule_with_warmup, + get_linear_schedule_with_warmup, +) def train(args, model, train_dataloader, umls_dataset): - writer = SummaryWriter(comment='umls') + writer = SummaryWriter(comment="umls") t_total = args.max_steps no_decay = ["bias", "LayerNorm.weight"] optimizer_grouped_parameters = [ { - "params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], + "params": [ + p + for n, p in model.named_parameters() + if not any(nd in n for nd in no_decay) + ], "weight_decay": args.weight_decay, }, - {"params": [p for n, p in model.named_parameters() if any( - nd in n for nd in no_decay)], "weight_decay": 0.0}, + { + "params": [ + p + for n, p in model.named_parameters() + if any(nd in n for nd in no_decay) + ], + "weight_decay": 0.0, + }, ] - optimizer = AdamW(optimizer_grouped_parameters, - lr=args.learning_rate, eps=args.adam_epsilon) + optimizer = AdamW( + optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon + ) args.warmup_steps = int(args.warmup_steps) - if args.schedule == 'linear': + if args.schedule == "linear": scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total ) - if args.schedule == 'constant': + if args.schedule == "constant": scheduler = get_constant_schedule_with_warmup( optimizer, num_warmup_steps=args.warmup_steps ) - if args.schedule == 'cosine': + if args.schedule == "cosine": scheduler = get_cosine_schedule_with_warmup( optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total ) @@ -54,8 +71,7 @@ def train(args, model, train_dataloader, umls_dataset): print(" Instantaneous batch size per GPU =", args.train_batch_size) print( " Total train batch size (w. parallel, distributed & accumulation) =", - args.train_batch_size - * args.gradient_accumulation_steps, + args.train_batch_size * args.gradient_accumulation_steps, ) print(" Gradient Accumulation steps =", args.gradient_accumulation_steps) @@ -66,14 +82,14 @@ def train(args, model, train_dataloader, umls_dataset): global_step = args.shift best_batch_loss = 0.033 - + while True: model.train() epoch_iterator = tqdm(train_dataloader, desc="Iteration", ascii=True) - batch_loss = 0. - batch_sty_loss = 0. - batch_cui_loss = 0. - batch_re_loss = 0. + batch_loss = 0.0 + batch_sty_loss = 0.0 + batch_cui_loss = 0.0 + batch_re_loss = 0.0 for _, batch in enumerate(epoch_iterator): input_ids_0 = batch[0].to(args.device) input_ids_1 = batch[1].to(args.device) @@ -92,11 +108,18 @@ def train(args, model, train_dataloader, umls_dataset): # for item in batch: # print(item.shape) - loss, (sty_loss, cui_loss, re_loss) = \ - model(input_ids_0, input_ids_1, input_ids_2, - cui_label_0, cui_label_1, cui_label_2, - sty_label_0, sty_label_1, sty_label_2, - re_label) + loss, (sty_loss, cui_loss, re_loss) = model( + input_ids_0, + input_ids_1, + input_ids_2, + cui_label_0, + cui_label_1, + cui_label_2, + sty_label_0, + sty_label_1, + sty_label_2, + re_label, + ) batch_loss = float(loss.item()) batch_sty_loss = float(sty_loss.item()) batch_cui_loss = float(cui_loss.item()) @@ -104,26 +127,32 @@ def train(args, model, train_dataloader, umls_dataset): # tensorboardX writer.add_scalar( - 'rel_count', train_dataloader.batch_sampler.rel_sampler_count, global_step=global_step) - writer.add_scalar('batch_loss', batch_loss, - global_step=global_step) - writer.add_scalar('batch_sty_loss', batch_sty_loss, - global_step=global_step) - writer.add_scalar('batch_cui_loss', batch_cui_loss, - global_step=global_step) - writer.add_scalar('batch_re_loss', batch_re_loss, - global_step=global_step) + "rel_count", + train_dataloader.batch_sampler.rel_sampler_count, + global_step=global_step, + ) + writer.add_scalar("batch_loss", batch_loss, global_step=global_step) + writer.add_scalar("batch_sty_loss", batch_sty_loss, global_step=global_step) + writer.add_scalar("batch_cui_loss", batch_cui_loss, global_step=global_step) + writer.add_scalar("batch_re_loss", batch_re_loss, global_step=global_step) if args.gradient_accumulation_steps > 1: loss = loss / args.gradient_accumulation_steps loss.backward() - epoch_iterator.set_description("Rel_count: %s, Loss: %0.4f, Sty: %0.4f, Cui: %0.4f, Re: %0.4f" % - (train_dataloader.batch_sampler.rel_sampler_count, batch_loss, batch_sty_loss, batch_cui_loss, batch_re_loss)) + epoch_iterator.set_description( + "Rel_count: %s, Loss: %0.4f, Sty: %0.4f, Cui: %0.4f, Re: %0.4f" + % ( + train_dataloader.batch_sampler.rel_sampler_count, + batch_loss, + batch_sty_loss, + batch_cui_loss, + batch_re_loss, + ) + ) if (global_step + 1) % args.gradient_accumulation_steps == 0: - torch.nn.utils.clip_grad_norm_( - model.parameters(), args.max_grad_norm) + torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm) optimizer.step() scheduler.step() # Update learning rate schedule model.zero_grad() @@ -131,43 +160,65 @@ def train(args, model, train_dataloader, umls_dataset): global_step += 1 if batch_loss < best_batch_loss: best_batch_loss = batch_loss - save_path = os.path.join( - args.output_dir, f'model_{global_step}.pth') + save_path = os.path.join(args.output_dir, f"model_{global_step}.pth") torch.save(model, save_path) # re_embedding if args.use_re: - writer.add_embedding(model.re_embedding.weight, metadata=umls_dataset.re2id.keys( - ), global_step=global_step, tag="re embedding") + writer.add_embedding( + model.re_embedding.weight, + metadata=umls_dataset.re2id.keys(), + global_step=global_step, + tag="re embedding", + ) else: # print(len(umls_dataset.rel2id)) # print(model.re_embedding.weight.shape) - writer.add_embedding(model.re_embedding.weight, metadata=umls_dataset.rel2id.keys( - ), global_step=global_step, tag="rel embedding") + writer.add_embedding( + model.re_embedding.weight, + metadata=umls_dataset.rel2id.keys(), + global_step=global_step, + tag="rel embedding", + ) # sty_parameter - writer.add_embedding(model.linear_sty.weight, metadata=umls_dataset.sty2id.keys( - ), global_step=global_step, tag="sty weight") + writer.add_embedding( + model.linear_sty.weight, + metadata=umls_dataset.sty2id.keys(), + global_step=global_step, + tag="sty weight", + ) if global_step % args.save_step == 0 and global_step > 0: - save_path = os.path.join( - args.output_dir, f'model_{global_step}.pth') + save_path = os.path.join(args.output_dir, f"model_{global_step}.pth") torch.save(model, save_path) # re_embedding if args.use_re: - writer.add_embedding(model.re_embedding.weight, metadata=umls_dataset.re2id.keys( - ), global_step=global_step, tag="re embedding") + writer.add_embedding( + model.re_embedding.weight, + metadata=umls_dataset.re2id.keys(), + global_step=global_step, + tag="re embedding", + ) else: # print(len(umls_dataset.rel2id)) # print(model.re_embedding.weight.shape) - writer.add_embedding(model.re_embedding.weight, metadata=umls_dataset.rel2id.keys( - ), global_step=global_step, tag="rel embedding") + writer.add_embedding( + model.re_embedding.weight, + metadata=umls_dataset.rel2id.keys(), + global_step=global_step, + tag="rel embedding", + ) # sty_parameter - writer.add_embedding(model.linear_sty.weight, metadata=umls_dataset.sty2id.keys( - ), global_step=global_step, tag="sty weight") - + writer.add_embedding( + model.linear_sty.weight, + metadata=umls_dataset.sty2id.keys(), + global_step=global_step, + tag="sty weight", + ) + if args.max_steps > 0 and global_step > args.max_steps: return None @@ -180,7 +231,7 @@ def run(args): np.random.seed(args.seed) # numpy torch.backends.cudnn.deterministic = True # cudnn - #args.output_dir = args.output_dir + "_" + str(int(time.time())) + # args.output_dir = args.output_dir + "_" + str(int(time.time())) # dataloader if args.lang == "eng": @@ -188,12 +239,17 @@ def run(args): if args.lang == "all": lang = None if args.lang == "eng_fr": - lang = ["ENG","FRE"] + lang = ["ENG", "FRE"] # assert args.model_name_or_path.find("bio") == -1, "Should use multi-language model" umls_dataset = UMLSDataset( - umls_folder=args.umls_dir, model_name_or_path=args.model_name_or_path, lang=lang, json_save_path=args.output_dir) + umls_folder=args.umls_dir, + model_name_or_path=args.model_name_or_path, + lang=lang, + json_save_path=args.output_dir, + ) umls_dataloader = fixed_length_dataloader( - umls_dataset, fixed_length=args.train_batch_size, num_workers=args.num_workers) + umls_dataset, fixed_length=args.train_batch_size, num_workers=args.num_workers + ) if args.use_re: rel_label_count = len(umls_dataset.re2id) @@ -208,30 +264,35 @@ def run(args): save_list.append(int(f[6:-4])) if len(save_list) > 0: args.shift = max(save_list) - if os.path.exists(os.path.join(args.output_dir, 'last_model.pth')): - model = torch.load(os.path.join( - args.output_dir, 'last_model.pth')).to(args.device) + if os.path.exists(os.path.join(args.output_dir, "last_model.pth")): + model = torch.load(os.path.join(args.output_dir, "last_model.pth")).to( + args.device + ) model_load = True else: - model = torch.load(os.path.join( - args.output_dir, f'model_{max(save_list)}.pth')).to(args.device) + model = torch.load( + os.path.join(args.output_dir, f"model_{max(save_list)}.pth") + ).to(args.device) model_load = True if not model_load: if not os.path.exists(args.output_dir): os.makedirs(args.output_dir) - model = UMLSPretrainedModel(device=args.device, model_name_or_path=args.model_name_or_path, - cui_label_count=len(umls_dataset.cui2id), - rel_label_count=rel_label_count, - sty_label_count=len(umls_dataset.sty2id), - re_weight=args.re_weight, - sty_weight=args.sty_weight).to(args.device) + model = UMLSPretrainedModel( + device=args.device, + model_name_or_path=args.model_name_or_path, + cui_label_count=len(umls_dataset.cui2id), + rel_label_count=rel_label_count, + sty_label_count=len(umls_dataset.sty2id), + re_weight=args.re_weight, + sty_weight=args.sty_weight, + ).to(args.device) args.shift = 0 model_load = True if args.do_train: - torch.save(args, os.path.join(args.output_dir, 'training_args.bin')) + torch.save(args, os.path.join(args.output_dir, "training_args.bin")) train(args, model, umls_dataloader, umls_dataset) - torch.save(model, os.path.join(args.output_dir, 'last_model.pth')) + torch.save(model, os.path.join(args.output_dir, "last_model.pth")) return None @@ -271,9 +332,14 @@ def main(): help="The maximum total input sequence length after tokenization. Sequences longer " "than this will be truncated, sequences shorter will be padded.", ) - parser.add_argument("--do_train", default=True, type=bool, help="Whether to run training.") parser.add_argument( - "--train_batch_size", default=256, type=int, help="Batch size per GPU/CPU for training.", + "--do_train", default=True, type=bool, help="Whether to run training." + ) + parser.add_argument( + "--train_batch_size", + default=256, + type=int, + help="Batch size per GPU/CPU for training.", ) parser.add_argument( "--gradient_accumulation_steps", @@ -281,39 +347,67 @@ def main(): default=8, help="Number of updates steps to accumulate before performing a backward/update pass.", ) - parser.add_argument("--learning_rate", default=2e-5, - type=float, help="The initial learning rate for Adam.") - parser.add_argument("--weight_decay", default=0.01, - type=float, help="Weight decay if we apply some.") - parser.add_argument("--adam_epsilon", default=1e-8, - type=float, help="Epsilon for Adam optimizer.") - parser.add_argument("--max_grad_norm", default=1.0, - type=float, help="Max gradient norm.") + parser.add_argument( + "--learning_rate", + default=2e-5, + type=float, + help="The initial learning rate for Adam.", + ) + parser.add_argument( + "--weight_decay", + default=0.01, + type=float, + help="Weight decay if we apply some.", + ) + parser.add_argument( + "--adam_epsilon", default=1e-8, type=float, help="Epsilon for Adam optimizer." + ) + parser.add_argument( + "--max_grad_norm", default=1.0, type=float, help="Max gradient norm." + ) parser.add_argument( "--max_steps", default=1000000, type=int, help="If > 0: set total number of training steps to perform. Override num_train_epochs.", ) - parser.add_argument("--warmup_steps", default=10000, - help="Linear warmup over warmup_steps or a float.") - parser.add_argument("--device", type=str, default='cuda:0', help="device") - parser.add_argument("--seed", type=int, default=72, - help="random seed for initialization") - parser.add_argument("--schedule", type=str, default="linear", - choices=["linear", "cosine", "constant"], help="Schedule.") - parser.add_argument("--trans_margin", type=float, default=1.0, - help="Margin of TransE.") - parser.add_argument("--use_re", default=False, type=bool, - help="Whether to use re or rel.") - parser.add_argument("--num_workers", default=1, type=int, - help="Num workers for data loader, only 0 can be used for Windows") - parser.add_argument("--lang", default='eng', type=str, choices=["eng", "all", "eng_fr"], - help="language range, eng or all") - parser.add_argument("--sty_weight", type=float, default=0.0, - help="Weight of sty.") - parser.add_argument("--re_weight", type=float, default=1.0, - help="Weight of re.") + parser.add_argument( + "--warmup_steps", + default=10000, + help="Linear warmup over warmup_steps or a float.", + ) + parser.add_argument("--device", type=str, default="cuda:0", help="device") + parser.add_argument( + "--seed", type=int, default=72, help="random seed for initialization" + ) + parser.add_argument( + "--schedule", + type=str, + default="linear", + choices=["linear", "cosine", "constant"], + help="Schedule.", + ) + parser.add_argument( + "--trans_margin", type=float, default=1.0, help="Margin of TransE." + ) + parser.add_argument( + "--use_re", default=False, type=bool, help="Whether to use re or rel." + ) + parser.add_argument( + "--num_workers", + default=1, + type=int, + help="Num workers for data loader, only 0 can be used for Windows", + ) + parser.add_argument( + "--lang", + default="eng", + type=str, + choices=["eng", "all", "eng_fr"], + help="language range, eng or all", + ) + parser.add_argument("--sty_weight", type=float, default=0.0, help="Weight of sty.") + parser.add_argument("--re_weight", type=float, default=1.0, help="Weight of re.") args = parser.parse_args() diff --git a/Normalisation/training/trans.py b/Normalisation/training/trans.py index 2987758b3..cc2ec75ca 100644 --- a/Normalisation/training/trans.py +++ b/Normalisation/training/trans.py @@ -1,6 +1,6 @@ import torch -from torch import nn import torch.nn.functional as F +from torch import nn class TransE(nn.Module): @@ -11,4 +11,8 @@ def __init__(self, margin=1.0): def forward(self, cui_0, cui_1, cui_2, re): pos = cui_0 + re - cui_1 neg = cui_0 + re - cui_2 - return torch.mean(F.relu(self.margin + torch.norm(pos, p=2, dim=1) - torch.norm(neg, p=2, dim=1))) + return torch.mean( + F.relu( + self.margin + torch.norm(pos, p=2, dim=1) - torch.norm(neg, p=2, dim=1) + ) + ) diff --git a/bash_scripts/NER_model/expe_data_size.sh b/bash_scripts/NER_model/expe_data_size.sh index 80687a7c8..3b395e0e9 100644 --- a/bash_scripts/NER_model/expe_data_size.sh +++ b/bash_scripts/NER_model/expe_data_size.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 24:00:00 +#SBATCH -t 24:00:00 #SBATCH --gres=gpu:v100:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 @@ -32,9 +32,9 @@ do echo ----------------- echo REMOVE MODEL LAST echo ----------------- - + rm -rf ./training/expe_data_size/model_$i/model-last - + echo ----------------- echo INFER TEST DOCS WITH MODEL TRAINED ON $i DOCS echo ----------------- @@ -52,4 +52,4 @@ done echo --Training_done--- -echo --------------- \ No newline at end of file +echo --------------- diff --git a/bash_scripts/NER_model/expe_hyperparams.sh b/bash_scripts/NER_model/expe_hyperparams.sh index 8836f08b9..569d22a8e 100644 --- a/bash_scripts/NER_model/expe_hyperparams.sh +++ b/bash_scripts/NER_model/expe_hyperparams.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 24:00:00 +#SBATCH -t 24:00:00 #SBATCH --gres=gpu:v100:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 diff --git a/bash_scripts/NER_model/expe_model_lang.sh b/bash_scripts/NER_model/expe_model_lang.sh index 5de9f166c..ec447e7e3 100644 --- a/bash_scripts/NER_model/expe_model_lang.sh +++ b/bash_scripts/NER_model/expe_model_lang.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 24:00:00 +#SBATCH -t 24:00:00 #SBATCH --gres=gpu:v100:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 @@ -37,9 +37,9 @@ do echo ----------------- echo REMOVE $lang_model MODEL LAST echo ----------------- - + rm -rf ./training/expe_lang_model/model_$lang_model/model-last - + echo ----------------- echo INFER $lang_model TEST DOCS echo ----------------- @@ -52,7 +52,7 @@ do echo ----------------- python ./scripts/evaluate.py ./training/expe_lang_model/model_$lang_model/model-best ./corpus/test.spacy --output ./training/expe_lang_model/model_$lang_model/test_metrics.json --docbin ./data/NLP_diabeto/expe_lang_model/pred_model_$lang_model.spacy --gpu-id 0 - + done echo --Training_done--- diff --git a/bash_scripts/NER_model/infer.sh b/bash_scripts/NER_model/infer.sh index 5c6e88961..e047369e9 100644 --- a/bash_scripts/NER_model/infer.sh +++ b/bash_scripts/NER_model/infer.sh @@ -1,13 +1,13 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 24:00:00 +#SBATCH -t 24:00:00 #SBATCH --gres=gpu:t4:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 #SBATCH --partition gpuT4 #SBATCH --output=logs/slurm-%j-stdout.log #SBATCH --error=logs/slurm-%j-stderr.log -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : @@ -18,4 +18,4 @@ cd '/export/home/cse200093/Jacques_Bio/BioMedics/eds-medic' #python ./scripts/infer.py --model ~/RV_Inter_conf/model-best/ --input ~/RV_Inter_conf/unnested_sosydiso_qualifiers_final/test_ --output ~/RV_Inter_conf/usqf_pred/ --format brat -python ./scripts/infer.py --model ./training/model-best/ --input ../data/lupus_erythemateux_dissemine_raw --output ../data/lupus_erythemateux_dissemine_pred/ --format brat \ No newline at end of file +python ./scripts/infer.py --model ./training/model-best/ --input ../data/lupus_erythemateux_dissemine_raw --output ../data/lupus_erythemateux_dissemine_pred/ --format brat diff --git a/bash_scripts/NER_model/save.sh b/bash_scripts/NER_model/save.sh index 8f2ae5fd3..c9f4b9c9f 100644 --- a/bash_scripts/NER_model/save.sh +++ b/bash_scripts/NER_model/save.sh @@ -1,13 +1,13 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 24:00:00 +#SBATCH -t 24:00:00 #SBATCH --gres=gpu:v100:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 #SBATCH --partition gpuV100 #SBATCH --output=logs/slurm-%j-stdout.log #SBATCH --error=logs/slurm-%j-stderr.log -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : diff --git a/bash_scripts/NER_model/test.sh b/bash_scripts/NER_model/test.sh index 8adda0786..9ea204864 100644 --- a/bash_scripts/NER_model/test.sh +++ b/bash_scripts/NER_model/test.sh @@ -1,13 +1,13 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 1:00:00 +#SBATCH -t 1:00:00 #SBATCH --gres=gpu:t4:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 #SBATCH --partition gpuT4 #SBATCH --output=logs/slurm-%j-stdout.log #SBATCH --error=logs/slurm-%j-stderr.log -#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable source $HOME/.user_conda/miniconda/etc/profile.d/conda.sh # appel de ce script # your code here : diff --git a/bash_scripts/NER_model/train.sh b/bash_scripts/NER_model/train.sh index 30ff967b2..8a595abde 100644 --- a/bash_scripts/NER_model/train.sh +++ b/bash_scripts/NER_model/train.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 1:00:00 +#SBATCH -t 1:00:00 #SBATCH --gres=gpu:v100:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 diff --git a/bash_scripts/NER_model/train_v1.sh b/bash_scripts/NER_model/train_v1.sh index 85ffea383..ba40c59c8 100644 --- a/bash_scripts/NER_model/train_v1.sh +++ b/bash_scripts/NER_model/train_v1.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 48:00:00 +#SBATCH -t 48:00:00 #SBATCH --gres=gpu:v100:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 diff --git a/bash_scripts/Normalisation/infer_coder.sh b/bash_scripts/Normalisation/infer_coder.sh index 0c093f9fe..2e9a09a26 100644 --- a/bash_scripts/Normalisation/infer_coder.sh +++ b/bash_scripts/Normalisation/infer_coder.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 48:00:00 +#SBATCH -t 48:00:00 #SBATCH --gres=gpu:v100:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 @@ -24,7 +24,7 @@ echo ----------------- echo NORMALIZE MALADIE TAKAYASU DOCS echo ----------------- -python normalisation/inference/main.py data/bio_results/maladie_de_takayasu/pred_with_extraction.json data/bio_results/maladie_de_takayasu/norm_coder_all.json +python normalisation/inference/main.py data/bio_results/maladie_de_takayasu/pred_with_extraction.json data/bio_results/maladie_de_takayasu/norm_coder_all.json echo ----------------- echo NORMALIZE SCLERODERMIE SYSTEMIQUE DOCS @@ -40,4 +40,4 @@ python normalisation/inference/main.py data/bio_results/syndrome_des_anti-phosph echo --NORMALIZATION_FINISHED--- -echo --------------- \ No newline at end of file +echo --------------- diff --git a/bash_scripts/Normalisation/infer_coder_quaero.sh b/bash_scripts/Normalisation/infer_coder_quaero.sh index 626960a85..1d079150c 100644 --- a/bash_scripts/Normalisation/infer_coder_quaero.sh +++ b/bash_scripts/Normalisation/infer_coder_quaero.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash #SBATCH --job-name=ner_med_training -#SBATCH -t 48:00:00 +#SBATCH -t 48:00:00 #SBATCH --gres=gpu:v100:1 #SBATCH --cpus-per-task=2 #SBATCH --mem=20000 diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:03:31/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:03:31/config.cfg new file mode 100644 index 000000000..b306974ac --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:03:31/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-16_15:03:31" +stdout_path = "log_train_coder/2023-06-16_15:03:31/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-16_15:03:31/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:03:31/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:03:31/sbatch.sh new file mode 100644 index 000000000..fe3e9629a --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:03:31/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-16_15:03:31/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-16_15:03:31/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 2 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:07:58/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:07:58/config.cfg new file mode 100644 index 000000000..6929599b8 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:07:58/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-16_15:07:58" +stdout_path = "log_train_coder/2023-06-16_15:07:58/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-16_15:07:58/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:07:58/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:07:58/sbatch.sh new file mode 100644 index 000000000..d7e97a535 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:07:58/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-16_15:07:58/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-16_15:07:58/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 2 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:08/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:08/config.cfg new file mode 100644 index 000000000..aa4607ccb --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:08/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-16_15:09:08" +stdout_path = "log_train_coder/2023-06-16_15:09:08/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-16_15:09:08/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:08/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:08/sbatch.sh new file mode 100644 index 000000000..1d549b2e4 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:08/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-16_15:09:08/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-16_15:09:08/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 2 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:57/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:57/config.cfg new file mode 100644 index 000000000..3d9017273 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:57/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-16_15:09:57" +stdout_path = "log_train_coder/2023-06-16_15:09:57/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-16_15:09:57/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:57/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:57/sbatch.sh new file mode 100644 index 000000000..c9ff7bea2 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:09:57/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-16_15:09:57/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-16_15:09:57/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 2 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:11:04/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:11:04/config.cfg new file mode 100644 index 000000000..263b2a090 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:11:04/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-16_15:11:04" +stdout_path = "log_train_coder/2023-06-16_15:11:04/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-16_15:11:04/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:11:04/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:11:04/sbatch.sh new file mode 100644 index 000000000..bc62fdd39 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:11:04/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-16_15:11:04/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-16_15:11:04/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 2 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:16:23/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:16:23/config.cfg new file mode 100644 index 000000000..0ed036c20 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:16:23/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-16_15:16:23" +stdout_path = "log_train_coder/2023-06-16_15:16:23/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-16_15:16:23/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:16:23/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:16:23/sbatch.sh new file mode 100644 index 000000000..28b201c13 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:16:23/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-16_15:16:23/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-16_15:16:23/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:20:28/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:20:28/config.cfg new file mode 100644 index 000000000..5fe1c7048 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:20:28/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-16_15:20:28" +stdout_path = "log_train_coder/2023-06-16_15:20:28/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-16_15:20:28/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:20:28/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:20:28/sbatch.sh new file mode 100644 index 000000000..0c54f6bbb --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:20:28/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-16_15:20:28/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-16_15:20:28/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 256 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:24:27/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:24:27/config.cfg new file mode 100644 index 000000000..434cc5593 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:24:27/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-16_15:24:27" +stdout_path = "log_train_coder/2023-06-16_15:24:27/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-16_15:24:27/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:24:27/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:24:27/sbatch.sh new file mode 100644 index 000000000..37a0d5bf0 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-16_15:24:27/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-16_15:24:27/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-16_15:24:27/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 256 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-19_12:50:04/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-19_12:50:04/config.cfg new file mode 100644 index 000000000..088c843c7 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-19_12:50:04/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-19_12:50:04" +stdout_path = "log_train_coder/2023-06-19_12:50:04/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-19_12:50:04/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-19_12:50:04/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-19_12:50:04/sbatch.sh new file mode 100644 index 000000000..9e50d458d --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-19_12:50:04/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-19_12:50:04/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-19_12:50:04/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 256 --lang eng_fr --do_train False \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:33:55/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:33:55/config.cfg new file mode 100644 index 000000000..ffb6a1cd1 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:33:55/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-23_15:33:55" +stdout_path = "log_train_coder/2023-06-23_15:33:55/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-23_15:33:55/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:33:55/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:33:55/sbatch.sh new file mode 100644 index 000000000..c09a8b63e --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:33:55/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-23_15:33:55/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-23_15:33:55/slurm-%j-stderr.log +export PATH=/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/condabin:/export/home/cse200093/.user_conda/miniconda/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:34:32/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:34:32/config.cfg new file mode 100644 index 000000000..2725c1d82 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:34:32/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-23_15:34:32" +stdout_path = "log_train_coder/2023-06-23_15:34:32/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-23_15:34:32/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:34:32/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:34:32/sbatch.sh new file mode 100644 index 000000000..1e18d936d --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:34:32/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-23_15:34:32/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-23_15:34:32/slurm-%j-stderr.log +export PATH=/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/condabin:/export/home/cse200093/.user_conda/miniconda/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:35:28/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:35:28/config.cfg new file mode 100644 index 000000000..fa8c39962 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:35:28/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "t4" +gpu_type_upper = "T4" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-23_15:35:28" +stdout_path = "log_train_coder/2023-06-23_15:35:28/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-23_15:35:28/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:35:28/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:35:28/sbatch.sh new file mode 100644 index 000000000..100f2afec --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:35:28/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:t4:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuT4 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-23_15:35:28/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-23_15:35:28/slurm-%j-stderr.log +export PATH=/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/condabin:/export/home/cse200093/.user_conda/miniconda/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:36:41/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:36:41/config.cfg new file mode 100644 index 000000000..22df67a2f --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:36:41/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-06-23_15:36:41" +stdout_path = "log_train_coder/2023-06-23_15:36:41/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-06-23_15:36:41/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:36:41/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:36:41/sbatch.sh new file mode 100644 index 000000000..46f72a5b9 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-06-23_15:36:41/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-06-23_15:36:41/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-06-23_15:36:41/slurm-%j-stderr.log +export PATH=/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/bin:/export/home/cse200093/.user_conda/miniconda/condabin:/export/home/cse200093/.user_conda/miniconda/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/word-embedding/finetuning-camembert-2021-07-29 --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:09:43/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:09:43/config.cfg new file mode 100644 index 000000000..8bd254a0f --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:09:43/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-06_15:09:43" +stdout_path = "log_train_coder/2023-07-06_15:09:43/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-06_15:09:43/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:09:43/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:09:43/sbatch.sh new file mode 100644 index 000000000..d66224380 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:09:43/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-06_15:09:43/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-06_15:09:43/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/CODER/pretrain/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:16:12/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:16:12/config.cfg new file mode 100644 index 000000000..40731a6a2 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:16:12/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-06_15:16:12" +stdout_path = "log_train_coder/2023-07-06_15:16:12/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-06_15:16:12/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:16:12/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:16:12/sbatch.sh new file mode 100644 index 000000000..27e43ce78 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:16:12/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-06_15:16:12/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-06_15:16:12/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:20:05/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:20:05/config.cfg new file mode 100644 index 000000000..8def8edbb --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:20:05/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-06_15:20:05" +stdout_path = "log_train_coder/2023-07-06_15:20:05/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-06_15:20:05/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:20:05/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:20:05/sbatch.sh new file mode 100644 index 000000000..c808e7b5a --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:20:05/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-06_15:20:05/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-06_15:20:05/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:31:19/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:31:19/config.cfg new file mode 100644 index 000000000..ced3ce2f2 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:31:19/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-06_15:31:19" +stdout_path = "log_train_coder/2023-07-06_15:31:19/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-06_15:31:19/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:31:19/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:31:19/sbatch.sh new file mode 100644 index 000000000..ccda82d23 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-06_15:31:19/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-06_15:31:19/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-06_15:31:19/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-07_09:59:30/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-07_09:59:30/config.cfg new file mode 100644 index 000000000..615370d61 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-07_09:59:30/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-07_09:59:30" +stdout_path = "log_train_coder/2023-07-07_09:59:30/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-07_09:59:30/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-07_09:59:30/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-07_09:59:30/sbatch.sh new file mode 100644 index 000000000..f6a2bb776 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-07_09:59:30/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-07_09:59:30/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-07_09:59:30/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-10_14:47:39/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-10_14:47:39/config.cfg new file mode 100644 index 000000000..50b82a4e7 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-10_14:47:39/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-10_14:47:39" +stdout_path = "log_train_coder/2023-07-10_14:47:39/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-10_14:47:39/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-10_14:47:39/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-10_14:47:39/sbatch.sh new file mode 100644 index 000000000..b16c5fa41 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-10_14:47:39/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-10_14:47:39/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-10_14:47:39/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-11_15:31:59/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-11_15:31:59/config.cfg new file mode 100644 index 000000000..01404eb60 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-11_15:31:59/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-11_15:31:59" +stdout_path = "log_train_coder/2023-07-11_15:31:59/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-11_15:31:59/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-11_15:31:59/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-11_15:31:59/sbatch.sh new file mode 100644 index 000000000..efae43f97 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-11_15:31:59/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-11_15:31:59/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-11_15:31:59/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-12_15:42:51/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-12_15:42:51/config.cfg new file mode 100644 index 000000000..991526d27 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-12_15:42:51/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-12_15:42:51" +stdout_path = "log_train_coder/2023-07-12_15:42:51/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-12_15:42:51/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-12_15:42:51/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-12_15:42:51/sbatch.sh new file mode 100644 index 000000000..7692efd8d --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-12_15:42:51/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-12_15:42:51/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-12_15:42:51/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-17_12:17:49/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-17_12:17:49/config.cfg new file mode 100644 index 000000000..8ce67533a --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-17_12:17:49/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-17_12:17:49" +stdout_path = "log_train_coder/2023-07-17_12:17:49/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-17_12:17:49/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-17_12:17:49/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-17_12:17:49/sbatch.sh new file mode 100644 index 000000000..0dab12cc4 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-17_12:17:49/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-17_12:17:49/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-17_12:17:49/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-19_15:24:15/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-19_15:24:15/config.cfg new file mode 100644 index 000000000..abd5ddae8 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-19_15:24:15/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-19_15:24:15" +stdout_path = "log_train_coder/2023-07-19_15:24:15/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-19_15:24:15/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-19_15:24:15/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-19_15:24:15/sbatch.sh new file mode 100644 index 000000000..6b503dc36 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-19_15:24:15/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-19_15:24:15/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-19_15:24:15/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-21_09:07:59/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-21_09:07:59/config.cfg new file mode 100644 index 000000000..ed7765f3b --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-21_09:07:59/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-21_09:07:59" +stdout_path = "log_train_coder/2023-07-21_09:07:59/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-21_09:07:59/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-21_09:07:59/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-21_09:07:59/sbatch.sh new file mode 100644 index 000000000..730e5ea2b --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-21_09:07:59/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-21_09:07:59/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-21_09:07:59/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-24_07:41:05/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-24_07:41:05/config.cfg new file mode 100644 index 000000000..ce41a51d0 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-24_07:41:05/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-24_07:41:05" +stdout_path = "log_train_coder/2023-07-24_07:41:05/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-24_07:41:05/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-24_07:41:05/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-24_07:41:05/sbatch.sh new file mode 100644 index 000000000..3f57f42dd --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-24_07:41:05/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-24_07:41:05/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-24_07:41:05/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-24_09:35:24/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-24_09:35:24/config.cfg new file mode 100644 index 000000000..b32d76599 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-24_09:35:24/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-24_09:35:24" +stdout_path = "log_train_coder/2023-07-24_09:35:24/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-24_09:35:24/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-24_09:35:24/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-24_09:35:24/sbatch.sh new file mode 100644 index 000000000..91fa89096 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-24_09:35:24/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-24_09:35:24/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-24_09:35:24/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:30/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:30/config.cfg new file mode 100644 index 000000000..210c588e2 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:30/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-25_14:10:30" +stdout_path = "log_train_coder/2023-07-25_14:10:30/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-25_14:10:30/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:30/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:30/sbatch.sh new file mode 100644 index 000000000..d3be7688e --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:30/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-25_14:10:30/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-25_14:10:30/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:58/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:58/config.cfg new file mode 100644 index 000000000..7978f212e --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:58/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-25_14:10:58" +stdout_path = "log_train_coder/2023-07-25_14:10:58/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-25_14:10:58/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:58/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:58/sbatch.sh new file mode 100644 index 000000000..49ba426da --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:10:58/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-25_14:10:58/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-25_14:10:58/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:13:54/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:13:54/config.cfg new file mode 100644 index 000000000..fe8789d09 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:13:54/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "log_train_coder/2023-07-25_14:13:54" +stdout_path = "log_train_coder/2023-07-25_14:13:54/slurm-%j-stdout.log" +stderr_path = "log_train_coder/2023-07-25_14:13:54/slurm-%j-stderr.log" +copy_scratch = true +python = "/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:13:54/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:13:54/sbatch.sh new file mode 100644 index 000000000..333d467bf --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-07-25_14:13:54/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=log_train_coder/2023-07-25_14:13:54/slurm-%j-stdout.log +#SBATCH --error=log_train_coder/2023-07-25_14:13:54/slurm-%j-stderr.log +export PATH=/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/envs/pierrenv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/export/home/cse200093/Jacques_Bio/normalisation/py_files' +python /export/home/cse200093/Jacques_Bio/normalisation/py_files/train.py --umls_dir /export/home/cse200093/deep_mlg_normalization/resources/umls/2021AB/ --model_name_or_path /export/home/cse200093/Jacques_Bio/data_bio/coder_output --output_dir /export/home/cse200093/Jacques_Bio/data_bio/coder_output --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:14:27/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:14:27/config.cfg new file mode 100644 index 000000000..98c52d2eb --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:14:27/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:14:27" +stdout_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:14:27/slurm-%j-stdout.log" +stderr_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:14:27/slurm-%j-stderr.log" +copy_scratch = true +python = "/data/scratch/cse200093/BioMedics/.venv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:14:27/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:14:27/sbatch.sh new file mode 100644 index 000000000..8944674d7 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:14:27/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:14:27/slurm-%j-stdout.log +#SBATCH --error=BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:14:27/slurm-%j-stderr.log +export PATH=/data/scratch/cse200093/BioMedics/.venv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/condabin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/data/scratch/cse200093' +python BioMedics/normalisation/py_files/train.py --umls_dir BioMedics/data/umls/2021AB/ --model_name_or_path word-embedding/coder_eds --output_dir word-embedding/coder_eds --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:17:42/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:17:42/config.cfg new file mode 100644 index 000000000..f13fb4dd4 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:17:42/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:17:42" +stdout_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:17:42/slurm-%j-stdout.log" +stderr_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:17:42/slurm-%j-stderr.log" +copy_scratch = true +python = "/data/scratch/cse200093/BioMedics/.venv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:17:42/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:17:42/sbatch.sh new file mode 100644 index 000000000..7986f55ea --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:17:42/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:17:42/slurm-%j-stdout.log +#SBATCH --error=BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:17:42/slurm-%j-stderr.log +export PATH=/data/scratch/cse200093/BioMedics/.venv/bin:/data/scratch/cse200093/BioMedics/.venv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/condabin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/data/scratch/cse200093' +python BioMedics/normalisation/py_files/train.py --umls_dir BioMedics/data/umls/2021AB/ --model_name_or_path word-embedding/coder_eds --output_dir word-embedding/coder_eds --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:19:16/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:19:16/config.cfg new file mode 100644 index 000000000..48dba8d5a --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:19:16/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:19:16" +stdout_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:19:16/slurm-%j-stdout.log" +stderr_path = "BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:19:16/slurm-%j-stderr.log" +copy_scratch = true +python = "/data/scratch/cse200093/BioMedics/.venv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:19:16/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:19:16/sbatch.sh new file mode 100644 index 000000000..3e59882f9 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-11-23_19:19:16/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:19:16/slurm-%j-stdout.log +#SBATCH --error=BioMedics/normalisation/py_files/log_train_coder/2023-11-23_19:19:16/slurm-%j-stderr.log +export PATH=/data/scratch/cse200093/BioMedics/.venv/bin:/data/scratch/cse200093/BioMedics/.venv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/condabin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/data/scratch/cse200093' +python BioMedics/normalisation/py_files/train.py --umls_dir BioMedics/data/umls/2021AB/ --model_name_or_path word-embedding/coder_eds --output_dir word-embedding/coder_eds --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-11-27_13:49:17/config.cfg b/bash_scripts/Normalisation/log_train_coder/2023-11-27_13:49:17/config.cfg new file mode 100644 index 000000000..03c7eb448 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-11-27_13:49:17/config.cfg @@ -0,0 +1,18 @@ +[slurm] +job_name = "slurm-job-cse200093" +job_duration = "72:00:00" +gpu_type = "v100" +gpu_type_upper = "V100" +n_gpu = 1 +n_node = 1 +n_cpu = 5 +mem = 40000 +hdfs = false +log_path = "BioMedics/bash_scripts/Coder_model/log_train_coder/2023-11-27_13:49:17" +stdout_path = "BioMedics/bash_scripts/Coder_model/log_train_coder/2023-11-27_13:49:17/slurm-%j-stdout.log" +stderr_path = "BioMedics/bash_scripts/Coder_model/log_train_coder/2023-11-27_13:49:17/slurm-%j-stderr.log" +copy_scratch = true +python = "/data/scratch/cse200093/BioMedics/.venv/bin/python" +conda = null + +[slurm.conf] \ No newline at end of file diff --git a/bash_scripts/Normalisation/log_train_coder/2023-11-27_13:49:17/sbatch.sh b/bash_scripts/Normalisation/log_train_coder/2023-11-27_13:49:17/sbatch.sh new file mode 100644 index 000000000..3f4b2b0e9 --- /dev/null +++ b/bash_scripts/Normalisation/log_train_coder/2023-11-27_13:49:17/sbatch.sh @@ -0,0 +1,14 @@ +#!/bin/bash +#SBATCH --job-name=slurm-job-cse200093 +#SBATCH --time 72:00:00 +#SBATCH --gres=gpu:v100:1 +#SBATCH --nodes=1 +#SBATCH --cpus-per-task=5 +#SBATCH --mem=40000 +#SBATCH --partition gpuV100 +#SBATCH --container-image /scratch/images/sparkhadoop.sqsh --container-mounts=/export/home/$USER:/export/home/$USER,/export/home/share:/export/home/share,/data/scratch/$USER:/data/scratch/$USER --container-mount-home --container-writable --container-workdir=/ +#SBATCH --output=BioMedics/bash_scripts/Coder_model/log_train_coder/2023-11-27_13:49:17/slurm-%j-stdout.log +#SBATCH --error=BioMedics/bash_scripts/Coder_model/log_train_coder/2023-11-27_13:49:17/slurm-%j-stderr.log +export PATH=/data/scratch/cse200093/BioMedics/.venv/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/export/home/cse200093/.user_conda/miniconda/condabin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/accumulo-client/bin:/usr/hdp/current/atlas-server/bin:/usr/hdp/current/beacon-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/falcon-client/bin:/usr/hdp/current/flume-server/bin:/usr/hdp/current/hadoop-client/bin:/usr/hdp/current/hbase-client/bin:/usr/hdp/current/hadoop-hdfs-client/bin:/usr/hdp/current/hadoop-mapreduce-client/bin:/usr/hdp/current/hadoop-yarn-client/bin:/usr/hdp/current/hive-client/bin:/usr/hdp/current/hive-hcatalog/bin:/usr/hdp/current/hive-server2/bin:/usr/hdp/current/kafka-broker/bin:/usr/hdp/current/mahout-client/bin:/usr/hdp/current/oozie-client/bin:/usr/hdp/current/oozie-server/bin:/usr/hdp/current/phoenix-client/bin:/usr/hdp/current/pig-client/bin:/usr/hdp/share/hst/hst-agent/python-wrap:/usr/hdp/current/slider-client/bin:/usr/hdp/current/sqoop-client/bin:/usr/hdp/current/sqoop-server/bin:/usr/hdp/current/storm-slider-client/bin:/usr/hdp/current/zookeeper-client/bin:/usr/hdp/current/zookeeper-server/bin:/export/home/opt/jupyterhub/conda/bin:/export/home/opt/jupyterhub/node/bin:/export/home/opt/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/hdp/current/spark-2.4.3-client/bin:/usr/local/hadoop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/opt/apps/texlive-20190227/2018/bin/x86_64-linux:/export/home/cse200093/.local/bin:/export/home/cse200093/bin:$PATH +cd '/data/scratch/cse200093' +python BioMedics/normalisation/training/train.py --umls_dir BioMedics/data/umls/2021AB/ --model_name_or_path word-embedding/coder_eds --output_dir word-embedding/coder_eds --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr \ No newline at end of file diff --git a/bash_scripts/Normalisation/train_coder.sh b/bash_scripts/Normalisation/train_coder.sh index febcf16d9..934075b76 100755 --- a/bash_scripts/Normalisation/train_coder.sh +++ b/bash_scripts/Normalisation/train_coder.sh @@ -4,4 +4,4 @@ cd ~/scratch source BioMedics/.venv/bin/activate conda deactivate -eds-toolbox slurm submit --config BioMedics/normalisation/training/train_coder_slurm.cfg -c "python BioMedics/normalisation/training/train.py --umls_dir BioMedics/data/umls/2021AB/ --model_name_or_path word-embedding/coder_eds --output_dir word-embedding/coder_eds --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr" \ No newline at end of file +eds-toolbox slurm submit --config BioMedics/normalisation/training/train_coder_slurm.cfg -c "python BioMedics/normalisation/training/train.py --umls_dir BioMedics/data/umls/2021AB/ --model_name_or_path word-embedding/coder_eds --output_dir word-embedding/coder_eds --gradient_accumulation_steps 8 --train_batch_size 1024 --lang eng_fr" diff --git a/edsnlp/edsnlp/__init__.py b/edsnlp/edsnlp/__init__.py index 98edc125a..4175a4257 100644 --- a/edsnlp/edsnlp/__init__.py +++ b/edsnlp/edsnlp/__init__.py @@ -2,20 +2,23 @@ EDS-NLP """ -import spacy -from . import patch_spacy_dot_components # isort: skip from pathlib import Path -from .evaluate import evaluate +import spacy + from . import extensions +from .evaluate import evaluate from .language import * +from . import patch_spacy_dot_components # isort: skip + + __version__ = "0.8.0" BASE_DIR = Path(__file__).parent -for ext in ["Allergie","Action","Certainty","Temporality","Negation","Family"]: +for ext in ["Allergie", "Action", "Certainty", "Temporality", "Negation", "Family"]: if not spacy.tokens.Span.has_extension(ext): spacy.tokens.Span.set_extension(ext, default=None) diff --git a/edsnlp/edsnlp/connectors/brat.py b/edsnlp/edsnlp/connectors/brat.py index bf0218552..6dceecd2a 100644 --- a/edsnlp/edsnlp/connectors/brat.py +++ b/edsnlp/edsnlp/connectors/brat.py @@ -425,7 +425,7 @@ def brat2docs(self, nlp: Language, run_pipe=False) -> List[Doc]: doc.spans[group_name] = group docs.append(doc) - + if self.span_groups is None: self.span_groups = sorted(list(encountered_span_groups)) if self.attr_map is None: @@ -542,4 +542,3 @@ def get_brat(self) -> Tuple[pd.DataFrame, pd.DataFrame]: ) return texts, annotations - \ No newline at end of file diff --git a/edsnlp/edsnlp/evaluate.py b/edsnlp/edsnlp/evaluate.py index c980cc02c..985055041 100644 --- a/edsnlp/edsnlp/evaluate.py +++ b/edsnlp/edsnlp/evaluate.py @@ -1,12 +1,14 @@ -from typing import Optional, Any, Dict, List, Iterable import time +from copy import deepcopy +from timeit import default_timer as timer +from typing import Any, Dict, Iterable, List, Optional + +import numpy as np from spacy.language import _copy_examples from spacy.tokens import Doc -from spacy.training import validate_examples, Example -from copy import deepcopy +from spacy.training import Example, validate_examples from tqdm import tqdm -import numpy as np -from timeit import default_timer as timer + def get_annotation(docs): full_annot = [] @@ -31,8 +33,16 @@ def overlap(start_g, end_g, start_p, end_p, exact): return 1 else: return 0 + + def compute_scores( - ents_gold, ents_pred, boostrap_level="entity", exact=True, n_draw=500, alpha=0.05, digits=2 + ents_gold, + ents_pred, + boostrap_level="entity", + exact=True, + n_draw=500, + alpha=0.05, + digits=2, ): docs = [doc[0] for doc in ents_gold] gold_labels = [ @@ -55,8 +65,8 @@ def compute_scores( results[label]["TP"] = 0 results[label]["FP"] = 0 results[label]["FN"] = 0 - results_by_doc = {doc : deepcopy(results) for doc in docs} - + results_by_doc = {doc: deepcopy(results) for doc in docs} + for i in range(len(ents_gold)): # iterate through doc # list of doc, inside each of them is a quadrupet ['text','label','start_char','stop_char'] doc_id = ents_gold[i][0] @@ -120,7 +130,7 @@ def compute_scores( for key, value in results_list.items(): for k, v in value.items(): results_list[key][k] = [v] - + total_words = 0 for entity in results_list.keys(): total_words += results[entity]["TP"] @@ -138,19 +148,21 @@ def compute_scores( "FN": [sum(results[entity]["FN"] for entity in results.keys())], "FP": [sum(results[entity]["FP"] for entity in results.keys())], } - + # Bootstrap per doc - if boostrap_level == 'doc': + if boostrap_level == "doc": for i in tqdm(range(1, n_draw)): draw = np.random.choice( - docs, - size=len(docs), - replace=True, - ) - micro_avg_draw = {"TP":0, "FN": 0, "FP": 0} - results_draw = { # we create a dic with the labels of the dataset (CHEM, BIO...) - label: {} for label in pred_labels.union(gold_labels) - } + docs, + size=len(docs), + replace=True, + ) + micro_avg_draw = {"TP": 0, "FN": 0, "FP": 0} + results_draw = ( + { # we create a dic with the labels of the dataset (CHEM, BIO...) + label: {} for label in pred_labels.union(gold_labels) + } + ) for label in results_draw.keys(): results_draw[label]["Precision"] = 0 results_draw[label]["TP"] = 0 @@ -158,28 +170,22 @@ def compute_scores( results_draw[label]["FN"] = 0 for doc in draw: for label in results_by_doc[doc].keys(): - micro_avg_draw["TP"]+=results_by_doc[doc][label]["TP"] - results_draw[label]["TP"]+=results_by_doc[doc][label]["TP"] - micro_avg_draw["FN"]+=results_by_doc[doc][label]["FN"] - results_draw[label]["FN"]+=results_by_doc[doc][label]["FN"] - micro_avg_draw["FP"]+=results_by_doc[doc][label]["FP"] - results_draw[label]["FP"]+=results_by_doc[doc][label]["FP"] + micro_avg_draw["TP"] += results_by_doc[doc][label]["TP"] + results_draw[label]["TP"] += results_by_doc[doc][label]["TP"] + micro_avg_draw["FN"] += results_by_doc[doc][label]["FN"] + results_draw[label]["FN"] += results_by_doc[doc][label]["FN"] + micro_avg_draw["FP"] += results_by_doc[doc][label]["FP"] + results_draw[label]["FP"] += results_by_doc[doc][label]["FP"] for entity in results_list.keys(): - results_list[entity]["TP"].append( - results_draw[entity]["TP"] - ) - results_list[entity]["FN"].append( - results_draw[entity]["FN"] - ) - results_list[entity]["FP"].append( - results_draw[entity]["FP"] - ) + results_list[entity]["TP"].append(results_draw[entity]["TP"]) + results_list[entity]["FN"].append(results_draw[entity]["FN"]) + results_list[entity]["FP"].append(results_draw[entity]["FP"]) micro_avg["TP"].append(micro_avg_draw["TP"]) micro_avg["FN"].append(micro_avg_draw["FN"]) micro_avg["FP"].append(micro_avg_draw["FP"]) - + # Bootstrap per entities - if boostrap_level == 'entity': + if boostrap_level == "entity": for i in tqdm(range(1, n_draw)): draw = np.random.choice( label_to_draw, @@ -212,30 +218,33 @@ def compute_scores( results_list[entity]["Recall"] = [] results_list[entity]["F1"] = [] for i in range(n_draw): - results_list[entity]["N_entity"].append(results_list[entity]["TP"][i] + results_list[entity]["FP"][i] + results_list[entity]["FN"][i]) + results_list[entity]["N_entity"].append( + results_list[entity]["TP"][i] + + results_list[entity]["FP"][i] + + results_list[entity]["FN"][i] + ) if results_list[entity]["TP"][i] + results_list[entity]["FP"][i] != 0: results_list[entity]["Precision"].append( results_list[entity]["TP"][i] - / ( - results_list[entity]["TP"][i] - + results_list[entity]["FP"][i] - ) * 100 + / (results_list[entity]["TP"][i] + results_list[entity]["FP"][i]) + * 100 ) else: - results_list[entity]["Precision"].append(int(results_list[entity]["TP"][i] == 0) * 100) + results_list[entity]["Precision"].append( + int(results_list[entity]["TP"][i] == 0) * 100 + ) if (results_list[entity]["TP"][i] + results_list[entity]["FN"][i]) != 0: results_list[entity]["Recall"].append( results_list[entity]["TP"][i] - / ( - results_list[entity]["TP"][i] - + results_list[entity]["FN"][i] - ) * 100 + / (results_list[entity]["TP"][i] + results_list[entity]["FN"][i]) + * 100 ) else: - results_list[entity]["Recall"].append(int(results_list[entity]["TP"][i] == 0) * 100) + results_list[entity]["Recall"].append( + int(results_list[entity]["TP"][i] == 0) * 100 + ) if ( - results_list[entity]["Precision"][i] - + results_list[entity]["Recall"][i] + results_list[entity]["Precision"][i] + results_list[entity]["Recall"][i] ) != 0: results_list[entity]["F1"].append( 2 @@ -314,7 +323,12 @@ def compute_scores( str(round(f1, digits)) + " (" + str(f1_down) + "-" + str(f1_up) + ")" ) result_panel[key]["N_entity"] = ( - str(n_entity) + " (" + str(int(n_entity_up)) + "-" + str(int(n_entity_down)) + ")" + str(n_entity) + + " (" + + str(int(n_entity_up)) + + "-" + + str(int(n_entity_down)) + + ")" ) print(f"With alpha = {alpha} and {n_draw} draws") output = f"With alpha = {alpha} and {n_draw} draws\n" @@ -326,9 +340,18 @@ def compute_scores( output += "-" * 30 # print(output) - result_panel["ents_per_type"] = {label: {"p": value["Precision"], "r": value["Recall"], "f": value["F1"], "n_entity": value["N_entity"]} for label, value in result_panel.items()} + result_panel["ents_per_type"] = { + label: { + "p": value["Precision"], + "r": value["Recall"], + "f": value["F1"], + "n_entity": value["N_entity"], + } + for label, value in result_panel.items() + } return result_panel + def evaluate_test( gold_docs: List[Doc], pred_docs: List[Doc], @@ -356,7 +379,7 @@ def evaluate_test( ents_pred, ents_gold = get_annotation(pred_docs), get_annotation(gold_docs) ents_pred.sort(key=lambda l: l[0]) ents_gold.sort(key=lambda l: l[0]) - + scores = compute_scores( ents_gold, ents_pred, diff --git a/edsnlp/edsnlp/pipelines/misc/measurements/factory.py b/edsnlp/edsnlp/pipelines/misc/measurements/factory.py index 9dc0f89c9..d21ad9a06 100644 --- a/edsnlp/edsnlp/pipelines/misc/measurements/factory.py +++ b/edsnlp/edsnlp/pipelines/misc/measurements/factory.py @@ -1,11 +1,10 @@ -from typing import Dict, List, Optional, Union, Tuple -from typing_extensions import Literal +from typing import Dict, List, Optional, Tuple, Union + from spacy.language import Language -from edsnlp.pipelines.base import ( - SpanGetterArg, - SpanSetterArg, -) +from typing_extensions import Literal + import edsnlp.pipelines.misc.measurements.patterns as patterns +from edsnlp.pipelines.base import SpanGetterArg, SpanSetterArg from edsnlp.pipelines.misc.measurements.measurements import ( MeasureConfig, MeasurementsMatcher, diff --git a/edsnlp/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/edsnlp/pipelines/misc/measurements/measurements.py index 9a9265252..b095ea4c2 100644 --- a/edsnlp/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/edsnlp/pipelines/misc/measurements/measurements.py @@ -13,7 +13,6 @@ from edsnlp.matchers.phrase import EDSPhraseMatcher from edsnlp.matchers.regex import RegexMatcher -from edsnlp.pipelines.misc.measurements.patterns import common_measurements from edsnlp.pipelines.base import ( BaseNERComponent, SpanGetterArg, @@ -22,6 +21,7 @@ validate_span_getter, ) from edsnlp.pipelines.misc.measurements import patterns +from edsnlp.pipelines.misc.measurements.patterns import common_measurements from edsnlp.utils.filter import align_spans, filter_spans __all__ = ["MeasurementsMatcher"] @@ -228,8 +228,8 @@ def __getattr__(self, other_unit): @classmethod def verify(cls, ent): return True - - + + class RangeMeasurement(Measurement): def __init__(self, from_value, to_value, unit, registry): super().__init__() @@ -293,6 +293,7 @@ def __getitem__(self, item: int): def verify(cls, ent): return True + class MeasurementsMatcher(BaseNERComponent): def __init__( self, @@ -315,7 +316,9 @@ def __init__( span_getter: Optional[SpanGetterArg] = None, merge_mode: Literal["intersect", "align", "union"] = "intersect", extract_ranges: bool = False, - range_patterns: List[Tuple[Optional[str], Optional[str]]] = patterns.range_patterns, # noqa: E501 + range_patterns: List[ + Tuple[Optional[str], Optional[str]] + ] = patterns.range_patterns, # noqa: E501 as_ents: bool = False, ): """ @@ -454,27 +457,21 @@ def __init__( self.parse_tables = parse_tables self.parse_doc = parse_doc self.span_getter = ( - validate_span_getter(span_getter) - if span_getter is not None - else None + validate_span_getter(span_getter) if span_getter is not None else None ) self.merge_mode = merge_mode self.extract_ranges = extract_ranges self.range_patterns = range_patterns - + if span_setter is None: span_setter = { "ents": as_ents, "measurements": True, - **{ - name: [name] - for name in self.measure_names.values() - } + **{name: [name] for name in self.measure_names.values()}, } super().__init__(nlp=nlp, name=name, span_setter=span_setter) - # INTERVALS self.regex_matcher.add( "interval", @@ -1468,7 +1465,7 @@ def merge_adjacent_measurements(cls, measurements: List[Span]) -> List[Span]: merged.append(ent) return merged - + def merge_measurements_in_ranges(self, measurements: List[Span]) -> List[Span]: """ Aggregates extracted measurements together when they are adjacent to handle @@ -1492,7 +1489,7 @@ def merge_measurements_in_ranges(self, measurements: List[Span]) -> List[Span]: last = merged[-1] from_text = last.doc[last.start - 1].norm_ if last.start > 0 else None - to_text = get_text(last.doc[last.end: ent.start], "NORM", True) + to_text = get_text(last.doc[last.end : ent.start], "NORM", True) matching_patterns = [ (a, b) for a, b in self.range_patterns @@ -1504,10 +1501,10 @@ def merge_measurements_in_ranges(self, measurements: List[Span]) -> List[Span]: last._.value, ent._.value ) merged[-1] = last = last.doc[ - last.start - if matching_patterns[0][0] is None - else last.start - 1: ent.end - ] + last.start + if matching_patterns[0][0] is None + else last.start - 1 : ent.end + ] last.label_ = ent.label_ last._.set(last.label_, new_value) except (AttributeError, TypeError): @@ -1516,11 +1513,11 @@ def merge_measurements_in_ranges(self, measurements: List[Span]) -> List[Span]: merged.append(ent) return merged - + def merge_with_existing( - self, - extracted: List[Span], - existing: List[Span], + self, + extracted: List[Span], + existing: List[Span], ) -> List[Span]: """ Merges the extracted measurements with the existing measurements in the @@ -1555,9 +1552,9 @@ def merge_with_existing( elif self.merge_mode == "union": extracted = [*extracted, *existing] - + return extracted - + def __call__(self, doc): """ Adds measurements to document's "measurements" SpanGroup. @@ -1575,11 +1572,11 @@ def __call__(self, doc): measurements = self.extract_measurements(doc) measurements = self.merge_adjacent_measurements(measurements) measurements = self.merge_measurements_in_ranges(measurements) - + if self.span_getter is not None: existing = list(get_spans(doc, self.span_getter)) measurements = self.merge_with_existing(measurements, existing) - + doc.spans["measurements"] = measurements # for backward compatibility diff --git a/edsnlp/edsnlp/pipelines/misc/measurements/patterns.py b/edsnlp/edsnlp/pipelines/misc/measurements/patterns.py index 352acd521..4e5e6e06b 100644 --- a/edsnlp/edsnlp/pipelines/misc/measurements/patterns.py +++ b/edsnlp/edsnlp/pipelines/misc/measurements/patterns.py @@ -2390,4 +2390,4 @@ ("Entre", "et"), ("entre", "et"), (None, "à"), -] \ No newline at end of file +] diff --git a/edsnlp/edsnlp/pipelines/trainable/nested_ner/nested_ner.py b/edsnlp/edsnlp/pipelines/trainable/nested_ner/nested_ner.py index d3900a939..144fe5a80 100644 --- a/edsnlp/edsnlp/pipelines/trainable/nested_ner/nested_ner.py +++ b/edsnlp/edsnlp/pipelines/trainable/nested_ner/nested_ner.py @@ -41,7 +41,7 @@ def nested_ner_scorer(examples: Iterable[Example], **cfg): """ labels = set(cfg["labels"]) if "labels" in cfg is not None else None spans_labels = cfg["spans_labels"] - + total_pred_spans = set() total_gold_spans = set() if labels is not None: @@ -426,4 +426,4 @@ def examples_to_truth(self, examples: List[Example]) -> Ints2d: continue spans.add((eg_idx, label_idx, span.start, span.end)) truths = self.model.ops.asarray(list(spans)) - return truths \ No newline at end of file + return truths diff --git a/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_qualifier.py b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_qualifier.py index 6dd11616f..a4e64ec8e 100644 --- a/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_qualifier.py +++ b/edsnlp/edsnlp/pipelines/trainable/span_qualifier/span_qualifier.py @@ -58,7 +58,11 @@ def span_qualifier_scorer(examples: Iterable[Example], **cfg): value = BINDING_GETTERS[qualifier](span) if value: labels["ALL"][0].append((eg_idx, span_idx, qualifier, value)) - key_str = f"{qualifier[2:]}" if value is True else f"{qualifier[2:]}-{value}" + key_str = ( + f"{qualifier[2:]}" + if value is True + else f"{qualifier[2:]}-{value}" + ) labels[key_str][0].append((eg_idx, span_idx, value)) doc_spans, *_, doc_qlf = candidate_getter(eg.reference) @@ -67,7 +71,11 @@ def span_qualifier_scorer(examples: Iterable[Example], **cfg): value = BINDING_GETTERS[qualifier](span) if value: labels["ALL"][1].append((eg_idx, span_idx, qualifier, value)) - key_str = f"{qualifier[2:]}" if value is True else f"{qualifier[2:]}-{value}" + key_str = ( + f"{qualifier[2:]}" + if value is True + else f"{qualifier[2:]}-{value}" + ) labels[key_str][1].append((eg_idx, span_idx, value)) def prf(pred, gold): @@ -79,6 +87,7 @@ def prf(pred, gold): "p": 1 if tp == np else (tp / np), "r": 1 if tp == ng else (tp / ng), } + results = {name: prf(pred, gold) for name, (pred, gold) in labels.items()} results = dict(sorted(results.items())) return {"qual_f": results["ALL"]["f"], "qual_per_type": results} diff --git a/edsnlp/edsnlp/utils/filter.py b/edsnlp/edsnlp/utils/filter.py index 9d8240d97..4fc6d5a69 100644 --- a/edsnlp/edsnlp/utils/filter.py +++ b/edsnlp/edsnlp/utils/filter.py @@ -303,4 +303,4 @@ def get_span_group(doclike: Union[Doc, Span], group: str) -> List[Span]: span for span in doclike.doc.spans.get(group, ()) if span.start >= doclike.start and span.end <= doclike.end - ] \ No newline at end of file + ] diff --git a/edsnlp/tests/pipelines/core/test_matcher.py b/edsnlp/tests/pipelines/core/test_matcher.py index c1be81b23..5dda35bc5 100644 --- a/edsnlp/tests/pipelines/core/test_matcher.py +++ b/edsnlp/tests/pipelines/core/test_matcher.py @@ -1,9 +1,9 @@ import pytest from pytest import fixture +from tests.conftest import text from thinc.config import ConfigValidationError from edsnlp.pipelines.core.matcher import GenericMatcher -from tests.conftest import text @fixture diff --git a/notebooks/connectors/omop.md b/notebooks/connectors/omop.md deleted file mode 100644 index beab4d523..000000000 --- a/notebooks/connectors/omop.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -jupyter: - jupytext: - formats: md,ipynb - main_language: python - text_representation: - extension: .md - format_name: markdown - format_version: "1.3" - jupytext_version: 1.13.0 - kernelspec: - display_name: "Python 3.9.5 64-bit ('.env': venv)" - name: python3 ---- - -```python -import context -``` - -```python -import spacy -``` - -```python -from edsnlp.connectors.omop import OmopConnector -``` - -```python - -``` - -# Date detection - -```python -text = ( - "Le patient est arrivé le 23 août (23/08/2021). " - "Il dit avoir eu mal au ventre hier. " - "L'année dernière, on lui avait prescrit du doliprane." -) -``` - -```python -nlp = spacy.blank('fr') -``` - -```python -nlp.add_pipe('normalizer') -nlp.add_pipe('matcher', config=dict(regex=dict(word=r"(\w+)"))) -``` - -```python -doc = nlp(text) -``` - -```python -doc._.note_id = 0 -``` - -```python -docs = [] - -for i in range(10): - doc = nlp(f"Doc{i:02}" + text) - doc._.note_id = i - docs.append(doc) -``` - -```python -connector = OmopConnector(nlp) -``` - -```python -note, note_nlp = connector.docs2omop(docs) -``` - -```python -note -``` - -```python -new_docs = connector.omop2docs(note, note_nlp) -``` - -```python -new_docs[0].text == docs[0].text -``` - -```python -len(docs[0].ents) == len(new_docs[0].ents) -``` - -```python -for e, o in zip(new_docs[0].ents, docs[0].ents): - assert e.text == o.text -``` - -```python - -``` diff --git a/notebooks/context.py b/notebooks/context.py deleted file mode 100644 index 4b1f5cb4e..000000000 --- a/notebooks/context.py +++ /dev/null @@ -1,5 +0,0 @@ -import os -import sys - -REPO_PATH = os.path.abspath(os.path.join(os.path.dirname("__file__"), "..")) -sys.path.insert(0, REPO_PATH) diff --git a/notebooks/dates/context.py b/notebooks/dates/context.py deleted file mode 100644 index cdd4b6470..000000000 --- a/notebooks/dates/context.py +++ /dev/null @@ -1,5 +0,0 @@ -import os -import sys - -REPO_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -sys.path.insert(0, REPO_PATH) diff --git a/notebooks/dates/prototype.md b/notebooks/dates/prototype.md deleted file mode 100644 index 97e588ef4..000000000 --- a/notebooks/dates/prototype.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -jupyter: - jupytext: - formats: md,ipynb - text_representation: - extension: .md - format_name: markdown - format_version: '1.3' - jupytext_version: 1.13.8 - kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -```python -%reload_ext autoreload -%autoreload 2 -``` - -```python -import spacy -from spacy import displacy -from spacy.tokens import Doc -``` - -```python -from edsnlp.utils.colors import create_colors -``` - -# Dates - -```python -nlp = spacy.blank('fr') -dates = nlp.add_pipe('eds.dates', config=dict(detect_periods=True)) -``` - -```python -text = "le 5 janvier à 15h32 cette année il y a trois semaines pdt 1 mois" -``` - -```python -doc = nlp(text) -``` - -```python -ds = doc.spans['dates'] -``` - -```python -colors = create_colors(['absolute', 'relative', 'duration']) - -def display_dates(doc: Doc): - doc.ents = doc.spans['dates'] - return displacy.render(doc, style='ent', options=dict(colors=colors)) -``` - -```python -display_dates(doc) -``` - -```python -for date in ds: - print(f"{str(date):<25}{repr(date._.date)}") -``` - -```python -for date in ds: - print(f"{str(date):<25}{date._.date.dict(exclude_none=True)}") -``` - -```python -for date in ds: - print(f"{str(date):<25}{date._.date.to_datetime()}") -``` - -```python -for date in ds: - print(f"{str(date):<25}{date._.date.norm()}") -``` - -```python -for p in doc.spans['periods']: - print(f"{str(p):<40}{p._.period.dict()}") -``` - -```python - -``` diff --git a/notebooks/dates/user-guide.md b/notebooks/dates/user-guide.md deleted file mode 100644 index 052dbecfb..000000000 --- a/notebooks/dates/user-guide.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -jupyter: - jupytext: - formats: md,ipynb - main_language: python - text_representation: - extension: .md - format_name: markdown - format_version: '1.3' - jupytext_version: 1.13.8 - kernelspec: - display_name: 'Python 3.9.5 64-bit (''.env'': venv)' - name: python3 ---- - -```python -import context -``` - -```python -from edsnlp.pipelines.misc.dates import Dates, terms -``` - -```python -from datetime import datetime -``` - -```python -import spacy -``` - -# Date detection - -```python -text = ( - "Le patient est arrivé le 23 août (23/08/2021). " - "Il dit avoir eu mal au ventre hier. " - "L'année dernière, on lui avait prescrit du doliprane." -) -``` - -```python -nlp = spacy.blank('fr') -``` - -```python -doc = nlp(text) -``` - -```python -dates = Dates( - nlp, - absolute=terms.absolute, - relative=terms.relative, - no_year=terms.no_year, -) -``` - -```python -dates(doc) -``` - -```python -doc.spans -``` - -```python -print(f"{'expression':<20} label") -print(f"{'----------':<20} -----") - -for span in doc.spans['dates']: - print(f"{span.text:<20} {span._.date}") -``` - -Lorsque la date du document n'est pas connue, le label des dates relatives (hier, il y a quinze jours, etc) devient `TD±` - -Si on renseigne l'extension `note_datetime` : - -```python -doc._.note_datetime = datetime(2020, 10, 10) -``` - -```python -dates(doc) -``` - -```python -print(f"{'expression':<20} label") -print(f"{'----------':<20} -----") - -for span in doc.spans['dates']: - print(f"{span.text:<20} {span._.date}") -``` diff --git a/notebooks/endlines/endlines-example.md b/notebooks/endlines/endlines-example.md deleted file mode 100644 index d8a1236ad..000000000 --- a/notebooks/endlines/endlines-example.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -jupyter: - jupytext: - text_representation: - extension: .md - format_name: markdown - format_version: "1.3" - jupytext_version: 1.13.0 - kernelspec: - display_name: "Python 3.7.1 64-bit ('env_debug': conda)" - name: python3 ---- - -```python -%load_ext autoreload -%autoreload 2 -``` - -```python -import spacy -``` - -```python -from edsnlp.pipelines.endlines.endlinesmodel import EndLinesModel -``` - -```python -import pandas as pd -``` - -```python -from spacy import displacy -``` - -# Train - -```python -nlp = spacy.blank("fr") -``` - -```python -text = r"""Le patient est arrivé hier soir. -Il est accompagné par son fils - -ANTECEDENTS -Il a fait une TS en 2010; -Fumeur, il est arreté il a 5 mois -Chirurgie de coeur en 2011 -CONCLUSION -Il doit prendre -le medicament indiqué 3 fois par jour. Revoir médecin -dans 1 mois. -DIAGNOSTIC : - -Antecedents Familiaux: -- 1. Père avec diabete - -""" -``` - -```python -doc = nlp(text) -``` - -```python -text2 = """J'aime le \nfromage...\n""" -doc2 = nlp(text2) -``` - -```python -text3 = '\nIntervention(s) - acte(s) réalisé(s) :\nParathyroïdectomie élective le [DATE]' -doc3 = nlp(text3) -``` - -```python -corpus = [doc,doc2, doc3] -``` - -```python -endlines = EndLinesModel(nlp = nlp) -``` - -```python -df = endlines.fit_and_predict(corpus) -df.head() -``` - -```python -pd.set_option("max_columns",None) -``` - -```python -# Save model -PATH= "/path_to_model" -endlines.save() -``` - -# Predict - -```python -df2 = pd.DataFrame({"A1":[12646014,4191891561709484510 , 1668228190683662995], - "A2":[12646065887601541794,4191891561709484510 , 1668228190683662995], - "A3": ["UPPER","DIGIT","sdf"], - "A4": ["DIGIT","ENUMERATION","STRONG_PUNCT"], - "B1": [.5,.7,10.2], - "B2": [.0,.2,-10.2], - "BLANK_LINE":[False,True,False]}) -df2 = endlines.predict(df2) -df2 -``` - -# Set spans in training data (for viz) - -```python -set_spans = endlines.set_spans -``` - -```python -set_spans(corpus, df) -``` - -```python -df.loc[df.DOC_ID==1] -``` - -```python -doc_exemple = corpus[1] -``` - -```python -doc_exemple.spans -``` - -```python -doc_exemple.ents = tuple(doc_exemple.spans['new_lines']) -``` - -```python -displacy.render(doc_exemple, style="ent", options={"colors":{"end_line":"green","space":"red"}}) -``` - -# Pipe spacy (inference) - -```python - -``` - -```python -nlp = spacy.blank("fr") -``` - -```python -nlp.add_pipe("endlines", config=dict(model_path = PATH)) -``` - -```python -docs2 = list(nlp.pipe([text,text2,text3])) -``` - -```python -doc_exemple = docs2[1] -``` - -```python -doc_exemple -``` - -```python -from edsnlp.utils.filter import filter_spans -spaces = tuple(s for s in doc_exemple.spans['new_lines'] if s.label_=="space") -ents = doc_exemple.ents + spaces -ents_f = filter_spans(ents) -doc_exemple.ents = ents_f -``` - -```python -displacy.render(doc_exemple, style="ent", options={"colors":{"space":"red"}}) -``` - -```python - -``` diff --git a/notebooks/example.txt b/notebooks/example.txt deleted file mode 100644 index cbd694c9b..000000000 --- a/notebooks/example.txt +++ /dev/null @@ -1,11 +0,0 @@ -Motif : -Le patient est admis le 29 août pour des difficultés respiratoires. - -Antécédents familiaux : -Le père est asthmatique, sans traitement particulier. - -HISTOIRE DE LA MALADIE -Le patient dit avoir de la toux depuis trois jours. Elle a empiré jusqu'à nécessiter un passage aux urgences. - -Conclusion -Possible infection au coronavirus diff --git a/notebooks/export_pandas_to_brat.py b/notebooks/export_pandas_to_brat.py index 51ff17ed2..bd370325e 100644 --- a/notebooks/export_pandas_to_brat.py +++ b/notebooks/export_pandas_to_brat.py @@ -1,6 +1,8 @@ -import pandas as pd import re +import pandas as pd + + def export_pandas_to_brat( ann_path, txt_path, @@ -8,7 +10,7 @@ def export_pandas_to_brat( label_column_name, span_column_name, term_column_name, - annotation_column_name = None, + annotation_column_name=None, ): """ - ann_path: str path where to write the ann file. @@ -21,35 +23,68 @@ def export_pandas_to_brat( - annotation_column_name: OPTIONAL str name of the column in df_to_convert containing the annotations. This column should be filled with str only. If None, no annotation will be saved. """ - + SEP = "\t" ANNOTATION_LABEL = "AnnotatorNotes" brat_raw = "" n_annotation = 0 - + with open(txt_path, "r") as f: txt_raw = f.read() - + if annotation_column_name: - df_to_convert = df_to_convert[[label_column_name, span_column_name, term_column_name, annotation_column_name]] + df_to_convert = df_to_convert[ + [ + label_column_name, + span_column_name, + term_column_name, + annotation_column_name, + ] + ] else: # Create an empty annotation column so that we can iter # In a generic pandas dataframe - df_to_convert = df_to_convert[[label_column_name, span_column_name, term_column_name]] + df_to_convert = df_to_convert[ + [label_column_name, span_column_name, term_column_name] + ] df_to_convert[annotation_column_name] = "" - + # Iter through df to write each line of ann file for index, (label, span, term, annotation) in df_to_convert.iterrows(): - term_raw = txt_raw[span[0]:span[1]] + term_raw = txt_raw[span[0] : span[1]] if "\n" in term_raw: - span_str = str(span[0]) + "".join(" " + str(span[0] + newline_index.start()) + ";" + str(span[0] + newline_index.start() + 1) for newline_index in re.finditer("\n", term_raw)) + " " + str(span[1]) + span_str = ( + str(span[0]) + + "".join( + " " + + str(span[0] + newline_index.start()) + + ";" + + str(span[0] + newline_index.start() + 1) + for newline_index in re.finditer("\n", term_raw) + ) + + " " + + str(span[1]) + ) else: span_str = str(span[0]) + " " + str(span[1]) - brat_raw += "T" + str(index+1) + SEP + label + " " + span_str + SEP + term + "\n" + brat_raw += ( + "T" + str(index + 1) + SEP + label + " " + span_str + SEP + term + "\n" + ) if len(annotation): n_annotation += 1 - brat_raw += "#" + str(n_annotation) + SEP + ANNOTATION_LABEL + " " + "T" + str(index+1) + SEP + annotation + "\n" - + brat_raw += ( + "#" + + str(n_annotation) + + SEP + + ANNOTATION_LABEL + + " " + + "T" + + str(index + 1) + + SEP + + annotation + + "\n" + ) + brat_raw = brat_raw[:-2] with open(ann_path, "w") as f: print(brat_raw, file=f) diff --git a/notebooks/get_stats_by_section_on_cim10.md b/notebooks/get_stats_by_section_on_cim10.md index 0564e8c3c..21e26beea 100644 --- a/notebooks/get_stats_by_section_on_cim10.md +++ b/notebooks/get_stats_by_section_on_cim10.md @@ -147,7 +147,7 @@ def get_docs_df(cim10_list, min_len=1000): def get_bio_df(summary_docs): bio = sql( - """SELECT bio.instance_num AS bio_id, bio.concept_cd, bio.units_cd, bio.nval_num, bio.tval_char, bio.quantity_num, bio.confidence_num, bio.encounter_num, bio.patient_num, bio.start_date, concept.name_char + """SELECT bio.instance_num AS bio_id, bio.concept_cd, bio.units_cd, bio.nval_num, bio.tval_char, bio.quantity_num, bio.confidence_num, bio.encounter_num, bio.patient_num, bio.start_date, concept.name_char FROM i2b2_observation_lab AS bio JOIN i2b2_concept AS concept ON bio.concept_cd = concept.concept_cd""" ) bio_dfs = {} @@ -171,7 +171,7 @@ def get_bio_df(summary_docs): def get_med_df(summary_docs): med = sql( - """SELECT med.instance_num AS med_id, med.concept_cd, med.valueflag_cd, med.encounter_num, med.patient_num, med.start_date, concept.name_char + """SELECT med.instance_num AS med_id, med.concept_cd, med.valueflag_cd, med.encounter_num, med.patient_num, med.start_date, concept.name_char FROM i2b2_observation_med AS med JOIN i2b2_concept AS concept ON med.concept_cd = concept.concept_cd""" ) med_dfs = {} @@ -1369,7 +1369,7 @@ plt.show() ## Lupus FAN/AAN (C0587178), Anti-DNA Natif (C1262035) -Anti-Sm (C0201357) +Anti-Sm (C0201357) ```python disease = "lupus_erythemateux_dissemine" diff --git a/notebooks/knowledge.py b/notebooks/knowledge.py index 4eb183062..efad84253 100644 --- a/notebooks/knowledge.py +++ b/notebooks/knowledge.py @@ -1,7 +1,7 @@ TO_BE_MATCHED = { "lupus_erythemateux_dissemine": { "CIM10": ["M320", "M321", "M328", "M329"], - "ATC_codes" : { + "ATC_codes": { "Corticothérapie systémique": ["H02AB"], "Endoxan": ["L01AA01"], "Cellcept": ["L04AA06"], @@ -151,7 +151,7 @@ }, "syndrome_des_anti-phospholipides": { "CIM10": ["D686"], - "ATC_codes" : { + "ATC_codes": { "Héparine": ["B01AB01"], "Anticoagulants oraux": ["B01AF02"], }, @@ -465,7 +465,7 @@ }, "sclerodermie_systemique": { "CIM10": ["M340", "M341", "M348", "M349"], - "ATC_codes" : { + "ATC_codes": { "IgIV": ["J06BA02"], "Corticothérapie systémique": ["H02AB"], "Endoxan": ["L01AA01"], @@ -571,7 +571,7 @@ }, "maladie_de_takayasu": { "CIM10": ["M314"], - "ATC_codes" : { + "ATC_codes": { "Corticothérapie systémique": ["H02AB"], "Endoxan": ["L01AA01"], "Tocilizumab": ["L04AC07"], diff --git a/notebooks/normalizer/context.py b/notebooks/normalizer/context.py deleted file mode 100644 index cdd4b6470..000000000 --- a/notebooks/normalizer/context.py +++ /dev/null @@ -1,5 +0,0 @@ -import os -import sys - -REPO_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -sys.path.insert(0, REPO_PATH) diff --git a/notebooks/normalizer/profiling.md b/notebooks/normalizer/profiling.md deleted file mode 100644 index 7021ec8a2..000000000 --- a/notebooks/normalizer/profiling.md +++ /dev/null @@ -1,235 +0,0 @@ ---- -jupyter: - jupytext: - formats: md,ipynb - text_representation: - extension: .md - format_name: markdown - format_version: "1.3" - jupytext_version: 1.13.4 - kernelspec: - display_name: "Python 3.9.5 64-bit ('.venv': venv)" - language: python - name: python3 ---- - -```python -%reload_ext autoreload -%autoreload 2 -``` - -```python -import context -``` - -```python - -``` - -```python -import spacy -``` - -# Date detection - -```python -text = ( - "Le patient est admis pour des douleurs dans le bras droit, mais n'a pas de problème de locomotion. " - "Historique d'AVC dans la famille. pourrait être un cas de rhume.\n" - "NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNBWbWbNbNbNBWbNbWbNbWBNbNbWbNbNBNbWbWbNbWBNbNbWbNBNbWbWbNb\n" - "Pourrait être un cas de rhume.\n" - "Motif :\n" - "Douleurs dans le bras droit.\n" - "ANTÉCÉDENTS\n" - "Le patient est déjà venu\n" - "Pas d'anomalie détectée.\n\n" -) * 10 -``` - -```python -nlp = spacy.blank('fr') -# nlp.add_pipe('lowercase') -# nlp.add_pipe('accents') -# nlp.add_pipe('pollution') -# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) -nlp.add_pipe('sentences') -nlp.add_pipe( - "matcher", - name="matcher", - config=dict( - attr='TEXT', - regex=dict(anomalie=r"anomalie"), - ), -) -nlp.add_pipe('negation', config=dict(attr='TEXT')) -``` - -```python -%%timeit -nlp(text) -``` - -```python -nlp = spacy.blank('fr') -nlp.add_pipe('lowercase') -# nlp.add_pipe('accents') -# nlp.add_pipe('pollution') -# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) -nlp.add_pipe('sentences') -nlp.add_pipe( - "matcher", - name="matcher", - config=dict( - attr='TEXT', - regex=dict(anomalie=r"anomalie"), - ), -) -nlp.add_pipe('negation', config=dict(attr='TEXT')) -``` - -```python -%%timeit -nlp(text) -``` - -```python -nlp = spacy.blank('fr') -nlp.add_pipe('lowercase') -nlp.add_pipe('accents') -# nlp.add_pipe('pollution') -# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) -nlp.add_pipe('sentences') -nlp.add_pipe( - "matcher", - name="matcher", - config=dict( - attr='TEXT', - regex=dict(anomalie=r"anomalie"), - ), -) -nlp.add_pipe('negation', config=dict(attr='TEXT')) -``` - -```python -%%timeit -nlp(text) -``` - -```python -nlp = spacy.blank('fr') -nlp.add_pipe('lowercase') -nlp.add_pipe('accents') -nlp.add_pipe('pollution') -# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) -nlp.add_pipe('sentences') -nlp.add_pipe( - "matcher", - name="matcher", - config=dict( - attr='TEXT', - regex=dict(anomalie=r"anomalie"), - ), -) -nlp.add_pipe('negation', config=dict(attr='TEXT')) -``` - -```python -%%timeit -nlp(text) -``` - -```python -nlp = spacy.blank('fr') -nlp.add_pipe('normalizer') -nlp.add_pipe('sentences') -nlp.add_pipe( - "matcher", - name="matcher", - config=dict( - attr='TEXT', - regex=dict(anomalie=r"anomalie"), - ), -) -nlp.add_pipe('negation', config=dict(attr='TEXT')) -``` - -```python -%%timeit -nlp(text) -``` - -```python -nlp = spacy.blank('fr') -# nlp.add_pipe('lowercase') -# nlp.add_pipe('accents') -# nlp.add_pipe('pollution') -# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) -nlp.add_pipe('normalizer') -nlp.add_pipe('sentences') -nlp.add_pipe( - "matcher", - name="matcher", - config=dict( - attr='CUSTOM_NORM', - regex=dict(anomalie=r"anomalie"), - ), -) -nlp.add_pipe('negation', config=dict(attr='TEXT')) -``` - -```python -%%timeit -nlp(text) -``` - -```python -nlp = spacy.blank('fr') -# nlp.add_pipe('lowercase') -# nlp.add_pipe('accents') -# nlp.add_pipe('pollution') -# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) -nlp.add_pipe('normalizer') -nlp.add_pipe('sentences') -nlp.add_pipe( - "matcher", - name="matcher", - config=dict( - attr='CUSTOM_NORM', - regex=dict(anomalie=r"anomalie"), - ), -) -nlp.add_pipe('negation', config=dict(attr='CUSTOM_NORM')) -``` - -```python -%%timeit -nlp(text) -``` - -```python -nlp = spacy.blank('fr') -# nlp.add_pipe('lowercase') -# nlp.add_pipe('accents') -# nlp.add_pipe('pollution') -# nlp.add_pipe('normalizer', config=dict(lowercase=False, accents=False, pollution=False)) -nlp.add_pipe('normalizer') -nlp.add_pipe('sentences') -nlp.add_pipe( - "matcher", - name="matcher", - config=dict( - attr='CUSTOM_NORM', - regex=dict(anomalie=r"anomalie"), - ), -) -nlp.add_pipe('negation', config=dict(attr='CUSTOM_NORM')) -``` - -```python -%%timeit -nlp(text) -``` - -```python - -``` diff --git a/notebooks/normalizer/prototype.md b/notebooks/normalizer/prototype.md deleted file mode 100644 index 832bd1d75..000000000 --- a/notebooks/normalizer/prototype.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -jupyter: - jupytext: - formats: md,ipynb - text_representation: - extension: .md - format_name: markdown - format_version: "1.3" - jupytext_version: 1.13.4 - kernelspec: - display_name: "Python 3.9.5 64-bit ('.venv': venv)" - language: python - name: python3 ---- - -```python -import context -``` - -```python -import spacy -from spacy.matcher import Matcher -``` - -```python -from spacy.tokens import Span -``` - -```python -from edsnlp.matchers.exclusion import ExclusionMatcher -``` - -```python - -``` - -```python -Span.set_extension('normalized_variant', getter=lambda s: ''.join([t.text + t.whitespace_ for t in s if not t._.excluded]).rstrip(' ')) -``` - -# Test normalisation - -```python -text = "Le patient est atteint d'une pneumopathie à NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNB coronavirus" -``` - -```python -phrase = "pneumopathie à coronavirus" -``` - -## Clean doc method - -```python -nlp = spacy.blank('fr') -nlp.add_pipe('normalizer', config=dict(lowercase=False, quotes=False, accents=False, pollution=True)) -nlp.add_pipe('matcher', config=dict(terms=dict(covid=[phrase]), attr="CUSTOM_NORM")) -``` - -```python -%timeit doc = nlp(text) -``` - -## Matcher method - -```python -nlp = spacy.blank('fr') -nlp.add_pipe('pollution') -``` - -```python -def set_ents(doc, matcher): - doc.ents = list(matcher(doc, as_spans=True)) -``` - -```python -doc_pattern = nlp(phrase) -``` - -```python -matcher = ExclusionMatcher(nlp.vocab, attr="LOWER") -``` - -```python -matcher.add('covid', [doc_pattern]) -``` - -```python -%%timeit -doc = nlp(text) -set_ents(doc, matcher) -``` - -```python -doc = nlp(text) -set_ents(doc, matcher) -``` - -```python -for token in doc: - print(token.norm_, token._.excluded) -``` - -```python -doc.ents[0]._.normalized_variant -``` - -```python - -``` diff --git a/notebooks/pipeline.md b/notebooks/pipeline.md deleted file mode 100644 index 570c07470..000000000 --- a/notebooks/pipeline.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -jupyter: - jupytext: - formats: ipynb,md - text_representation: - extension: .md - format_name: markdown - format_version: "1.3" - jupytext_version: 1.11.2 - kernelspec: - display_name: "[2.4.3] Py3" - language: python - name: pyspark-2.4.3 ---- - -```python -%reload_ext autoreload -%autoreload 2 -``` - -```python -# Importation du "contexte", ie la bibliothèque sans installation -import context -``` - -```python -import spacy -``` - -```python -# One-shot import of all declared spaCy components - -``` - -```python -a = spacy.registry.get('factories','charlson') -``` - -```python -a() -``` - -# Baselines - -```python -text = ( - "Le patient est admis pour des douleurs dans le bras droit. mais n'a pas de problème de locomotion. \n" - "Historique d'AVC dans la famille mais pas chez les voisins\n" - "mais ne semble pas en être un\n" - "Charlson 7.\n" - "Pourrait être un cas de rhume du fait d'un hiver rigoureux.\n" - "Motif :\n" - "Douleurs dans le bras droit." -) -``` - -```python -nlp = spacy.blank('fr') -nlp.add_pipe('sentencizer') -# nlp.add_pipe('sentences') -nlp.add_pipe('matcher', config=dict(terms=dict(tabac=['Tabac']))) -nlp.add_pipe('normalizer') -nlp.add_pipe('hypothesis') -nlp.add_pipe('family', config=dict(on_ents_only=False)) -#nlp.add_pipe('charlson') -``` - -```python -text = "Tabac:\n" -``` - -```python -doc = nlp(text) -``` - -```python -print([(token.text, token._.hypothesis_) for token in doc if token._.hypothesis==True]) -``` - -```python -doc.ents[0].end -``` - -```python -from edsnlp.utils.inclusion import check_inclusion -ents = [ent for ent in doc.ents if check_inclusion(ent, 0, 2)] -ents -``` - -```python -print([(ent.text, ent.start, ent.end) for ent in doc.ents]) -``` - -```python -import thinc - -registered_func = spacy.registry.get("misc", "score_norm") -``` - -```python -@spacy.registry.misc("score_normalization.charlson") -def score_normalization(extracted_score): - """ - Charlson score normalization. - If available, returns the integer value of the Charlson score. - """ - score_range = list(range(0, 30)) - if (extracted_score is not None) and (int(extracted_score) in score_range): - return int(extracted_score) - -charlson_config = dict( - score_name = 'charlson', - regex = [r'charlson'], - value_extract = r"(\d+)", - score_normalization = "score_normalization.charlson" -) - -nlp = spacy.blank('fr') -nlp.add_pipe('sentences') -nlp.add_pipe('normalizer') -nlp.add_pipe('score', config = charlson_config) -``` - -```python -# nlp.add_pipe('sentencizer') -nlp.add_pipe('sentences') -nlp.add_pipe('normalizer') -nlp.add_pipe('matcher', config=dict(terms=dict(douleurs=['probleme de locomotion', 'douleurs']), attr='NORM')) -nlp.add_pipe('sections') -nlp.add_pipe('pollution') -``` - -```python -text = ( - "Le patient est admis pour des douleurs dans le bras droit, mais n'a pas de problème de locomotion. " - "Historique d'AVC dans la famille. pourrait être un cas de rhume.\n" - "NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNBWbWbNbNbNBWbNbWbNbWBNbNbWbNbNBNbWbWbNbWBNbNbWbNBNbWbWbNb\n" - "Pourrait être un cas de rhume.\n" - "Motif :\n" - "Douleurs dans le bras droit." -) -``` - -```python -doc = nlp(text) -``` - -```python -doc.ents[0]._.after_snippet -``` - -```python -doc._.sections -``` - -```python -doc._.clean_ -``` - -```python -doc[17]._.ascii_ -``` - -```python -doc._.clean_ -``` - -On peut tester l'extraction d'entité dans le texte nettoyé : - -```python -doc._.clean_[165:181] -``` - -Les deux textes ne sont plus alignés : - -```python -doc.text[165:181] -``` - -Mais la méthode `char_clean_span` permet de réaligner les deux représentations : - -```python -span = doc._.char_clean_span(165, 181) -span -``` - -```python -doc._.sections[0] -``` - -```python - -``` diff --git a/notebooks/premier-pipeline.md b/notebooks/premier-pipeline.md deleted file mode 100644 index 0fdad6a5c..000000000 --- a/notebooks/premier-pipeline.md +++ /dev/null @@ -1,260 +0,0 @@ ---- -jupyter: - jupytext: - formats: md,ipynb - text_representation: - extension: .md - format_name: markdown - format_version: "1.3" - jupytext_version: 1.13.5 - kernelspec: - display_name: "Python 3.9.5 64-bit ('.env': venv)" - name: python3 ---- - - - -# EDS-NLP – Présentation - - - -## Texte d'exemple - -```python -with open('example.txt', 'r') as f: - text = f.read() -``` - -```python -print(text) -``` - -## Définition d'un pipeline spaCy - -```python slideshow={"slide_type": "slide"} -# Importation de spaCy -import spacy -``` - -```python -# Chargement des composants EDS-NLP - -``` - -```python -# Création de l'instance spaCy -nlp = spacy.blank('fr') - -# Normalisation des accents, de la casse et autres caractères spéciaux -nlp.add_pipe('normalizer') -# Détection des fins de phrases -nlp.add_pipe('sentences') - -# Extraction d'entités nommées -nlp.add_pipe( - 'matcher', - config=dict( - terms=dict(respiratoire=[ - 'difficultes respiratoires', - 'asthmatique', - 'toux', - ]), - regex=dict( - covid=r'(?i)(?:infection\sau\s)?(covid[\s\-]?19|corona[\s\-]?virus)', - traitement=r'(?i)traitements?|medicaments?'), - attr='NORM', - ), -) - -nlp.add_pipe('dates') - -# Qualification des entités -nlp.add_pipe('negation') -nlp.add_pipe('hypothesis') -nlp.add_pipe('family') -nlp.add_pipe('rspeech') -``` - -## Application du pipeline - -```python -doc = nlp(text) -``` - -```python -doc -``` - -Les traitements effectués par EDS-NLP (et spaCy en général) sont non-destructifs : - -```python -# Non-destruction -doc.text == text -``` - -Pour des tâches comme la normalisation, EDS-NLP ajoute des attributs aux tokens, sans perte d'information : - -```python -# Normalisation -print(f"{'texte':<15}", 'normalisation') -print(f"{'-----':<15}", '-------------') -for token in doc[3:15]: - print(f"{token.text:<15}", f"{token.norm_}") -``` - -Le pipeline que nous avons appliqué a extrait des entités avec le `matcher`. - -Les entités détectées se retrouvent dans l'attribut `ents` : - -```python -doc.ents -``` - -EDS-NLP étant fondée sur spaCy, on peut utiliser tous les outils proposés autour de cette bibliothèque : - -```python -from spacy import displacy -``` - -```python -displacy.render( - doc, - style='ent', - options={'colors': dict(respiratoire='green', covid='orange')}, -) -``` - -Focalisons-nous sur la première entité : - -```python -entity = doc.ents[0] -``` - -```python -entity -``` - -Chaque entité a été qualifiée par les pipelines de négation, hypothèse, etc. Ces pipelines utilisent des extensions spaCy pour stocker leur résultat : - -```python -entity._.negated -``` - -Le pipeline n'a pas détecté de négation pour cette entité. - -## Application du pipleline sur une table de textes - -Les textes seront le plus souvent disponibles sous la forme d'un DataFrame pandas, qu'on peut simuler ici : - -```python -import pandas as pd -``` - -```python -note = pd.DataFrame(dict(note_text=[text] * 10)) -note['note_id'] = range(len(note)) -note = note[['note_id', 'note_text']] -``` - -```python -note -``` - -On peut appliquer la pipeline à l'ensemble des documents en utilisant la fonction `nlp.pipe`, qui permet d'accélérer les traitements en les appliquant en parallèle : - -```python -# Ici on crée une liste qui va contenir les documents traités par spaCy -docs = list(nlp.pipe(note.note_text)) -``` - -On veut récupérer les entités détectées et les information associées (empans, qualification, etc) : - -```python -def get_entities(doc): - """Extract a list of qualified entities from a spaCy Doc object""" - entities = [] - - for ent in doc.ents: - entity = dict( - start=ent.start_char, - end=ent.end_char, - label=ent.label_, - lexical_variant=ent.text, - negated=ent._.negated, - hypothesis=ent._.hypothesis, - ) - - entities.append(entity) - - return entities -``` - -```python -note['entities'] = [get_entities(doc) for doc in nlp.pipe(note.note_text)] -``` - -```python -note -``` - -On peut maintenant récupérer les entités détectées au format `NOTE_NLP` (ou similaire) : - -```python -# Sélection des colonnes -note_nlp = note[['note_id', 'entities']] - -# "Explosion" des listes d'entités, et suppression des lignes vides (documents sans entité) -note_nlp = note_nlp.explode('entities').dropna() - -# Re-création de l'index, pour des raisons internes à pandas -note_nlp = note_nlp.reset_index(drop=True) -``` - -```python -note_nlp -``` - -Il faut maintenant passer d'une colonne de dictionnaires à une table `NOTE_NLP` : - -```python -note_nlp = note_nlp[['note_id']].join(pd.json_normalize(note_nlp.entities)) -``` - -```python -note_nlp -``` - -On peut aggréger la qualification des entités en une unique colonne : - -```python -# Création d'une colonne "discard" -> si l'entité est niée ou hypothétique, on la supprime des résultats -note_nlp['discard'] = note_nlp[['negated', 'hypothesis']].max(axis=1) -``` - -```python -note_nlp -``` - -```python - -``` - -```python - -``` - -```python - -``` - -```python -%%timeit -for text in texts: - nlp(text) -``` - -```python -%%timeit -for text in nlp.pipe(texts, n_process=-1): - pass -``` diff --git a/notebooks/sections/annotated_sections.csv b/notebooks/sections/annotated_sections.csv deleted file mode 100644 index f22f5541c..000000000 --- a/notebooks/sections/annotated_sections.csv +++ /dev/null @@ -1,121 +0,0 @@ -lexical_variant,section -ENTREE, -OBSERVATION, -PRISE EN CHARGE, -CONCLUSION, -CORRESPONDANTS, -Antécédents, -Motif, -Résumé clinique, -Traitement en cours, -Evolution depuis la dernière consultation, -Suivi, -Examen clinique, -Examens complémentaires, -Conclusion, -Synthèse, -Histoire de la maladie actuelle, -Traitement, -Habitus, -Résultats d'examens, -Indication de l'acte, -Facteurs de risque, -Résultat de la coronarographie, -ANTECEDENTS :, -MODE DE VIE :, -TRAITEMENT ACTUEL :, -HISTOIRE DU POIDS :, -EXAMEN CLINIQUE :, -ENQUETE ACTIVITE PHYSIQUE :, -SYNTHESE MEDICALE/CONCLUSION, -ANTÉCÉDENTS :, -ENQUÊTE ACTIVITÉ PHYSIQUE :, -SYNTÈSE MÉDICALE / CONCLUSION, -DIAGNOSTIC, -INTERVENTION, -RAPPEL CLINIQUE, -DESCRIPTION DETAILLEE, -Motif de l'hospitalisation :, -Histoire de la maladie :, -Examen clinique à l'entrée :, -Examen(s) complémentaire(s) :, -Intervention(s) - acte(s) réalisé(s) :, -Suites opératoires :, -Conduite à tenir :, -Prescriptions de sortie :, -Conclusion :, -Antécédents :, -CONCLUSION :, -MOTIF D'HOSPITALISATION, -MODE DE VIE, -HABITUS, -ANTECEDENTS, -HISTOIRE RECENTE, -EXAMEN CLINIQUE A L'ENTREE, -HEMOPATHIE, -EVOLUTION, -CONCLUSION DE SORTIE, -CONSIGNES A LA SORTIE, -RESUME, -TRAITEMENT A L'ENTREE, -POSE DE CATHETER CENTRAL, -STATUT FONCTIONNEL DE SORTIE, -TRAITEMENT DE SORTIE, -FACTEURS DE RISQUES, -Antécédents personnels, -HISTOIRE DE LA MALADIE, -EXAMENS COMPLEMENTAIRES, -AU TOTAL, -Motif de l'hospitalisation, -Diagnostics, -Prescriptions médicales de sortie, -Soins infirmiers, -Traitement de sortie :, -EXAMEN CLINIQUE, -Examen clinique , -EXAMENS COMPLÉMENTAIRES, -Actes réalisés, -VACCINATIONS, -Examen clinique :, -Examens complémentaires :, -ANTÉCÉDENTS, -TRAITEMENT À L'ENTRÉE, -EXAMEN CLINIQUE À L'ENTRÉE, -TRAITEMENT A L'ENTRÉE, -EXAMENS COMPLÉMENTAIRES RÉALISÉS PENDANT LE SÉJOUR, -ÉVOLUTION, -PROJET DIAGNOSTIQUE ET THÉRAPEUTIQUE, -Contexte familial et social, -Antécédents familiaux, -Grossesse - Période néonatale, -Histoire de la maladie, -Examen clinique à l'entrée, -Examens complémentaires à l'entrée, -Conclusion à l'entrée, -Attitude thérapeutique initiale, -Dépistages, -Conclusion de sortie, -Destination de sortie, -Traitement de sortie, -Planification des soins, -Scores à l'entrée, -Mode de vie, -Traitements à l'entrée, -Données biométriques et paramètres vitaux à l'entrée, -Evolution, -Scores à la sortie, -"Situation sociale, mode de vie", -Histoire de la maladie - Explorations, -Histoire récente de la maladie, -Examens para-cliniques, -Diagnostic retenu, -Diagnostics :, -Prescriptions médicales de sortie :, -Actes réalisés :, -Antécédents médicaux et chirurgicaux, -Vaccinations, -Mode de vie - Scolarité, -EXAMEN CLINIQUE A L'ENTRÉE, -PARAMÈTRES VITAUX ET DONNÉES BIOMÉTRIQUES A L'ENTRÉE, -PLANIFICATION DES SOINS / SUITES À DONNER, -PARAMETRES VITAUX ET DONNEES BIOMETRIQUES A L'ENTREE, diff --git a/notebooks/sections/context.py b/notebooks/sections/context.py deleted file mode 100644 index c26859b87..000000000 --- a/notebooks/sections/context.py +++ /dev/null @@ -1,5 +0,0 @@ -import os -import sys - -REPO_PATH = os.path.abspath(os.path.join(os.path.dirname("__file__"), "..", "..")) -sys.path.insert(0, REPO_PATH) diff --git a/notebooks/sections/section-dataset.md b/notebooks/sections/section-dataset.md deleted file mode 100644 index 951523002..000000000 --- a/notebooks/sections/section-dataset.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -jupyter: - jupytext: - formats: ipynb,md - text_representation: - extension: .md - format_name: markdown - format_version: '1.3' - jupytext_version: 1.11.4 - kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -```python -%reload_ext autoreload -%autoreload 2 -``` - -```python -import pandas as pd -``` - -```python -import os -``` - -```python -import context -``` - -```python -from edsnlp.utils.brat import BratConnector -``` - -# Sections dataset - - -Réutilisation du [travail réalisé par Ivan Lerner à l'EDS](https://gitlab.eds.aphp.fr/IvanL/section_dataset). - -```python -data_dir = '../../data/section_dataset/' -``` - -```python -brat = BratConnector(data_dir) -``` - -```python -texts, annotations = brat.get_brat() -``` - -```python -df = annotations[['lexical_variant']].drop_duplicates() -``` - -```python -df['section'] = '' -``` - -```python -df.to_csv('sections.tsv', sep='\t', index=False) -``` - -```python -annotated = pd.read_csv('sections.tsv', sep='\t') -``` - -```python -annotated.to_csv('annotated_sections.csv', index=False) -``` - -```python -annotated = pd.read_excel('sections.xlsx', sheet_name='Annotation', engine='openpyxl') -``` - -```python -annotated.columns = ['lexical_variant', 'section', 'keep', 'comment'] -``` - -```python -annotated.keep = annotated.keep.fillna('Oui') == 'Oui' -``` - -```python -annotated = annotated.query('keep')[['lexical_variant', 'section']] -``` - -```python -annotated.merge(annotations, on='lexical_variant').section.value_counts() -``` - -```python -annotated.lexical_variant = annotated.lexical_variant.str.lower() -``` - -```python -annotated_unnaccented = annotated.copy() -``` - -```python -from unidecode import unidecode -``` - -```python -annotated_unnaccented.lexical_variant = annotated_unnaccented.lexical_variant.apply(unidecode) -``` - -```python -# annotated = pd.concat([annotated, annotated_unnaccented]) -annotated = annotated_unnaccented -``` - -```python -annotated = annotated.drop_duplicates() -``` - -```python -annotated = annotated.sort_values(['lexical_variant', 'section']) -``` - -```python -annotated -``` - -```python -annotated = annotated.drop_duplicates() -``` - -```python -sections = { - section.replace(' ', '_'): list(annotated.query('section == @section').lexical_variant) - for section in annotated.section.unique() -} -``` - -```python -for k, v in sections.items(): - print(unidecode(k.replace(' ', '_')), '=', v) - print() -``` - -```python -sections = { - section: unidecode(section.replace(' ', '_')) - for section in annotated.section.unique() -} -``` - -```python -for k, v in sections.items(): - print(f"{repr(k)}: {v},") -``` - -```python - -``` diff --git a/notebooks/sections/sections.xlsx b/notebooks/sections/sections.xlsx deleted file mode 100644 index fc1df11b44bdc2118b5a595cdf336fd8b0c69710..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12717 zcmeHt1zTKOvUU?(8V~NS!QGtz!8<^3cXxLQ?(Qyu1OmZ>ySp}SL4r%r+h^pSnVk8) zKX7OF^K7r`U29dXvR7Fi8U`Bx2S5M-0OSBMo}dXkC;$Kk768BkAiUC+u(xwDwR6!| z_jE9I)_vn)YeQBD^NKDX@Cx$&f4Bd`Bk)Om$gZ0ey>Tf@K4&=-gWe?H#?WKeDmT~PlfFG1p2p7C zt+l}Cmmn3j`~+2Q%T9{I5r&9f%Z7bHW*z{?LJ4jeZemC3S@!TTIFi9@$=R;?=BocF z5HuAZ4a!%Vt8;I*+KEHH%y7Ou<{T`k{>VK1!Sq=17V+5w&(!DIo~if}H*n&BESD|* z+_OCd!8r`w5)4>tIl;Iha;|=LfOvlOo(J~~_aPZIc9xenE_)e}=5C0aP{V(X0uzXG zEa7CacoR+Zt7-)moKq-9x{-R0O85-1vzHfWfbzeYX#o%{0dH5)8e)p52*Dk+ zMCj@S!O$}P?S7BLD`3Hx!$Ic{pThBU#_K_6BlD#ipFtb)>t6&QH48q=FGjge{1l%0CN`ay zl35*CPd?0w*@LPSoS1j)6EXwT`Pcp*>N(Fxay)x^5PPZy&xV4cNXyqC9_z105-6OU zb_p4g7Ko7$01%)&Y*_woC+_x6)<*XB)_-`jf7%Qb#HB&F{NLU_slJo%g)HgF&*7}@ zS#GZ}mRwjUjzLFg2t)PsD>URBepl;+3@y5v^YSdv_FnSl3;Q<&l^$ zURXA#0~FpXTDn(Yh?%&kLa`mZW8H9&Xc4pEoaknjf$@&-g@{#98%scYLWvts+@)xX4R zwTZuhLBe<|6=)?pqwdyjge8=48~>=eK1!Xa42$ePrnPQPXK3 zBiT%~x0lqT7b;;m`lGIbo&B2!tSt)(Dd}cx^d#EJ9l>9BhHmjz9Vt$yWC4?G^wAX* z3v-rG)N{~Uft^V@m5Bs8l{r<6u$(in0rbs;9&3xt^jO7G5wqT`Cb?g+Q`xKK(Mw@f zIxD&^T)P;+1t~wgH{>dBT<4Tbwa9x@9nEqHl(&*C>(>w~4pJ#kibM|M9q-{UZS{3HJ&=jUSZ5fzZCTmXRfQwA z%nuKa#4&08y$(HXaQ6J_YCa;d;CEhBq4h?4iV8lSd>F1T@!k1h6NfKVdcd2)_=8wy z;JS4wK*G>e-t4Z}QnK+rGPAqlb=(le@~TyF2j6ATCRMgw>IGR-uZNAIOR4?4N8}Pq zEBe;K=IW}Pmm!giCG2%J-UD3flWD#+j)Wlzg)g+ZihJQ^54onL5pP+q#0Bkdg`rno z)6l5-6L7lciSjfLFlJ6+#H>4$7hBdvy)%0-vbCj-jl@%&<+oOFZdIDnp&|RKC6=Ew z$FbUz5ao2{F!ZTU-s&LL>I;S({mXopaV_i!`xmfcVj7b0^`4N(A+xt1Yi2X;2Hnb0 z6b5r&`BFiJJCVXuK?$hAqs|tFPNpVmE>4zq=FWetYSEhS?6<$+fN?!u0?%leCue7HEcBzOpkqEo zgBBBWso+l%DLJ^2wN#}T29H}lJut30Jqlb6c+m}zPL&A4z8ckRD;ykgp5RO4vCre- z^TupQG<6iXzscgEq>22bGi>B4@S*s2;f0v3#IttCQ!8R+N~S>C{`C2>FGe+U6D$sn zo&TUA5;wr09R<`_Pfnp@z0jN|tU3Rx7Xtzze^<9AzoH@azug-{+XklfMVOkZ9jr{b zi#+ify(53I2x;}dIEh=2`)QZl);i^W$$EXdv|~j;laR;HA>ybif@0Ty|I^Gy{Kha# zb!6bV$CLLQ@0rBsvq=3OX|bL9OD$IM$-HeN7K?1sGZ~9Z>&P-#6+5#nqdzv!%eifU zh`Y7B!?Be*Z;mVNgUu+6Y(vdT&Vj$_A+6=X0Jz&6HA{fQ+(@fKy(PK9K)%aqoLg{T z{_Wm{pX(Qjz46D+m-X(}`Z`~W?o|hpvE}Neh+q&E$_lHNeIsI2n?VptZN|XaLz)A| zEiOwgA9tBUeHMegyIB!Y71feeBig2op}?Tqt@>UJ9L0lko++MrW{9KQj8m>YXg;&F zSL+KWOzizvbJK*~n2M0+!S?OaH3~(PL?aO~5=3?q&r&HhMrVQsb9O<8JnL zB&d9#j!sgs6p#O1T<0j^@Xso>pOeeP7oqB;oeCnDe6<|*FLFRx66MS&{fxA_w|FoB zzwWhPn{D9IS|~jyL{Ir+9M}9scrCl-oEjh&N~#QYct`rg-S`8U!ZhmqU5?_4?Xe^Y zYv}3qt=in~srYT48{rw#(XC!;TK^g!J@QT;g6(bbbT2JP_OEv{aa3#(3(gvA7Bw6A68g7E#xD?!JgUWno?ULOWiu zz1P^;eyeqSl(5xsV;twBROrOb!ZY8epeLY@aO$C|e};^>!vm%KkcL!6{pdZVU%b~j zbnz;$+!A@2T)RX)&6QJH!zn@8Iu%@VpIma+wr_TG4;Zg^+{hYiv5iLaI7|?E- z>=pL*B+`of4NWkz4qA@wrTiZJl10{TTv7^K9cVXKCnzvtyeT@bIklXPhxq-S8s31Y zxh6`M7l;ElcRZvcx4>*NT^lK*Fel@f37ig~$P`d(mFIpGlUOA(1qHy}i5IO;ubg+l zL?*KUIjBIW-8o?eQ6M=T$6rbHi*>4r>A1C;FHA;Ukdn0Iavr29`Q3hRt zW(p^>AJ?^$^%$V6fyBGlkvlf&Ee<%mb5RtfH{{SxyduQR^vd2}*`72={MZfRIIBFy zcur_-=Ygm^a9fFRc_;B`bO$4-Cl||NaIowaZn0ZH1yGE7DVtVgOZ*8fCqo!5f3FtP zjWxW&VplF*6sw*CmBf^O<>z~Fq^IQk1};H*?~?H^nvLZW2~x$8CZ9>@k_YCx1ue6@n6{FanLV9#28|FZe;gOiI7bR<<)HX)hgwn^QB!` zfVKn%mMVjpG&EjP`M8>wqumL|BtH<^ysxJvZ`_^h+@BJ@)_fL8_F=kec`8Q066CEp zg4fOcxN!q(?m$+5 zsLx^Xp+#M815GoQEPJ{WGY8KFp!yE;8-EB^F6qH9mc%K1yf=>X6^x`UZyf8TLk)L% z%_97%9ozx9Ba?BKYIaX^ip#pNYrBkAnw=k@DY2j)n(W)cw2|>zCE|soWVVR5D3(d5 zWL;LG_!8RbLg=~&0(navp4m90*-9IUKp(fmYDH^A&0*>*lHRDgq50e2(FV*BUxmpQ z*r6!frZL&!8Rw`h>f$C`1VsmNFeoF(TeZ^h>2_(chpv@AJW7e27=2I3xw2{4?%zwlFny zasEfv@V5)L|IHaf;<_TcS+S*Vp`Rr@yp!fbQ6$uOD4Ia~&_?&ma4Tu?x7bhKUx`dR zcXu5ocDb`21S<&5P!pJDTV93k7~@)eIrCm-?hvcxJzT{`WEPA!oO$y-6IxMG?&Cnb z=4<5IP!*~tydJ;arP^4;ig@9@rMDkFS%-c_5l()pw6eX>hp%T*Y&d1w@zn_C7gE)F zK92Pz6|^hr7?-`62zkpFZy;PK%Qq->auHx8cI1AOYwe2)Q))Xpa(RRJpGH9568$(A z4giQD_@nUmx5(GU!qnE3p{2X;`zsb0+?Zd|MaE$`W_$+d+PM zJ}cX$qEBWf(ie6L2+2FM@&)m!dPwaJ%5u;^&Z}t&|6y_QtmlaD)6qiHAIA5LwWkxg zm^d|n&vVdbMoI}(UHK*OGa2aZXlQ0OAOqSj3QQ=EUj~oIM*$SDuY~2H! zy)7Sl(B2d>V{GCHD2r@rm!C+BkA53A6%I1$W%+`yxUmva!iIU4sW`HYO&knPPqj{w zQ;x-Wt3xZsmQF0);g8l|^Nz+O1bk9*EgVXN*=5=lQTi~=Jziz8cy0)yxC;vBbd5!~ zZJnGr1gd$zGlX`02J&7`{PthnelF+SQ6MfpfzChaU$+Y6gA^AjbEx3-% z&-j2O+Q(nBegOYuV5$T)X#pcN)^x!AWLLFv>HG<9n1uP8cZ^=U)dttYHP<+?{xeG3 zE7O~Hu15p(AY35i++%$!Nt*fUkK1XUPk-I#@bTAWBMXUd2A40vgzMuasfhRUkv$6sn$R1a z^W&<4i9A8So9li5$Fo7B_M}2+Vqq$_y>XDw@u~rv%VakxY4K+bV?1)BSy7C|Z|Ogy z1_64yWL7<{wLiMG>C2;m9-e*!8!eShlE80(nFdU`zB8+dZZ zT~~o2u{O5hbZoCy^Cm>~K8~5-83ymUMq&n*@^*D6tS^%Hr>=o%de4qX@Jph78}hHs!>n`<6je zn0|=5E=5t7u&Q57NxZQCibAL_neT~hZ}t8D2c)ODAU{~_ev%uZmWX#I41)z@dvA;s zWNsD>@d`Z5b`=t32mE@`$fXjTa}WuA;xZ^Dze^A)*RicOB5-&6enVe{ah#)njY!ck zswU6GVr&*W0GmVhNgb|!J^m`4%KY$w-t_i`*tF65iqE12e6~{VNlQJ?*5EAH!?XDu zJPek&+~E)FsuOl@$NhQRee!%7>OG?=?BDzbo1nKd`9Q6ZMmnG#C@`0(k+K zSz@`iEN5ycdT3gLvXB5S4l{C8DQ5@D9D5|A2A~0?Zm_x2=~Q)cujhse&}NW4*X*Yk zz|@r1fZNxhY#+){p8#k|F>&NaxX3$!7loEundbD!r{NHp!3vlk2&65dFOO%@6vsK{ zt0{`%gnmTsTF_Z=X1+*%Dv8LRhRZyV6T{kXI)QT~MIDf~n0KT7avDD10oUBK8-zPW zpPd^cnF+k(QAyPrmO?wHb8f=SQ2-iII2RM-DTv zMmjpDm6S0Y*N}&9Sc%zw2~C5u4!T7y0{5LQmyD*+c`1j;tUykKQ;=p2BWSBC3+>4+ zhPfiO% z$$Lj$0$&9`{%y0;qA<^$E$jYPP!+^C*rtfEG5&eGA365WbzED3wZQouXPV27@ zy;NP<>ldVuqX0>ifq!>r&Muxdrhm(h>p?5AS*+;3pJ)BQJSsN9&`Ab@l9;CDY(bWd zs`|k6-VgfdyxP;1FKcXRA#*o%;7U(~8&+XwvsiOYeb=BO_~m+qA(&QVcuEFj5PP%I zSy}67B&!uy0`;ccG)$gF)aU7i&-2y7j$YpR>XZTTwn1ng78qCTqM%y~HAd8k!L zqXSpW>>FJr-Y#WI1g+$5hkPSb*n%>oj@%DGm|i8du2L?f$=r?R@I9<$#q2vYV*yDx zp{ffk(83p>i4-VojjhAmLqtmD+ca$e5kqQHFpv1B94L3n_^$ zQlb2EIR-s5@p@qSYbmq6*A_wWpIyLyQfMK;eSK~%Z?H9h)5G2ZZ_XzIIf3sh!q3s2)B28xvD-$M~P% zlV3=1T<7p#G|F^0coKtZ(v$e}q>F2Q4Y_=5e%VGHj|NY2z}I{Y5@Z7v@=^`D? z-LnNw0TeUS<=y9NCQA{rA88iB-)_C-v=6_l$7J;1W_&sto5d+3GQx{~b=ezFvd zNNIs7ur-ayPh+m}upB?Tkjtu#TYqxe3h-!J($JYZOrdA#JodUTz8;pDj%L=2 zz{blo$5nk6oSwsyrTHFEqrKaE&%=9Lp=JM@i73N$o@+0LdN9)#-7xM-3wR8TOXoIi zc1lR7p=z%{yWK7e>``89&wXvNO{%y2bf&lB=K9ukrRC+h<;8Z+_V!!kfZI%LN2%QY z*OSBhqsyA3qk$OPTcxD=s@s+_f0#M@70@~mJmXc8Cw10rx|z|r^znw zdTi$85zYC@V9VxvQ)`CT!Mg1wcYL0?@zY)P5@%Jd)=FUJ#41404eV}yH}|YzazsP} ztzq6yutGXkw`60tkJcZ1c(y(i^RU#Sne8)~xBldt0e3?BbX-f4Y1;Tpsg*?klbZHe zi)W=i&vqVoWj*uemudY=mR`v>W1SbB&;CN3J;SKcz823;GzSNz!J8+748( zxw%*v3ulv^zWeg6Y=dL(GB>i>otw+=tyEGOX}HnA{g(2l6Z&KdL$GWg88$=aDsHG= zZI72D-TXY7W%5_ea)RDhQYD(lh0VM;)9o{)j^FJ}vRolT-;$XT+0+*!Aod(QSn3OGtFd-D0k|T8OxL_g!MQLQ$#8Qc@(6K3lUWJp%qjI4{Lb2nUqYo!a z0F8|la$kj~iPB7o_Twk%#HzuW7KDBbV;^4>P`IlzsEdQo9r+l>0U0B<{UkW`x&onN zHnG=@K*YZIr(ZLVQ}{`y1jihPxWl(?nwrb}RM=|f+krsi;o8kZia1{pN=H$;@^LR1 zOjt{!Aa*5e6`a<9pko`+Wc4oOWEgdBJ$dC_d~r5+ZlVp%Zu0p2QT9CQVWSsilw4qh z5$kq_{woIWSQfFAhj8%=8{Xs~sQ{)*GcjHS%^x73)Iqa@s;EW(M>r>RG#ng#VwLXP z7R zmt{axd2o63d!Ay@Dto87U+Q@`Iqww6r?$~J)t3YUsVj|m34BVEdAUgy1O*oF{?Sx6rciB(9s6P1aYT0W+rFwALfP)V=E`06(nTLeJ;`* zd4n=#pE<8a=+@<)ncyLlS)dPhg?x{t4pD}C(|A%EA40UcMN!~0`-V1afconv%Y#&v zmcDGe{K#j{yJ5N@%_}M3kbS*nxKGQ z*d{{-aBO@u%06R7OnkGfG~t}IcgL}HJIc@*M3@=*f$A}#60ps1?BjR!aPrUCFrW(! z3%u=UA!FH0|2yd+llf=rVO2~W3$mSPp^&=)O9rHgc=2E|^J8b7@35~xPG#iEVu+H8 zenbLGM7lRD2h}#*yYqe3QcRaN}!9_AQGyMSXAD`tRCzqPtVGacmW>fLw3PWyv5Yt^1 zr-5U?-w)Uo)IZ*?Q%`21wkN54^k6Kf&|n05W}jF565|XgiAysp*^K45!IyQ|h^^G? z-S#CoQ~y%JE_Nx3lL-+gbO-S>@k`U zQp37JIJsCZP8dv6GHrfXI!&Phljqim$umCuu9DYVXb_f|uz9x>)GW|AXv>Mma?pw* za-M;gGK&4Ek2>|aIE&MU3nSFJ&v3#7)jL@P@okR=7L-_UQJxN3_|L+LAh_|z6_DRv zx)4oz7k0Sw3k@(xTsK?E;pW9h8L+ewAdvmc5=8lvY$SMp{3=foF%Jz9kJu-l-9e)B z>~vGUbmXZxh*t-Om4=sT+$Glpe|9%t9WNZ~CCo>gUylk(*DIhqCEzkRcpd2#U^545 zGx@u~kI(67$wgi5RL~1>z0#Fr$TBa`$H5Hz>g3p{x8Xd{qBug82#Dq{!Evi_CPS1& z&mscO8$SLx@o*+nMGRfNSkI530x>OKc&pXgr;&XIq6;PH-fd#QR7s?Q$G8Y`7Gjp= zR2h<>!@LNhuK4pH&g3*j7R@sSGWgtIYJxKTE_Fp$=C&2+2Q$|X(-)kM>OP8wn|-uU z`!H?SUplbz^JdsXCp{fxQQSS|GS`0qkA3&E zd)B+1II&B&&7{q$mC?X4dD^VIzq(vJDSK(y6mctRH+_EojQeneNZ?(v4A#vPa?JtW zx+H+-9;)&6Z@>Ds+uz%tyd2xZez`lipI>v&^Lyz$@v$eISvawH(rr9i@qBT$Ej_v^ zTTgYk{m(&VYwfCs45W%`4oSW+AfO8qdt+rMdk1G0V|yplKZ>UieA0ih7LaH=I#ERl zQa=t|fA@$czusskfsth^NhSE!2)ppS+LC3m$?css@uPPua1TNFZqZw@BiDJxrX|hv z4A>tjw@dfY2s^;&HLg`w(X|zZqOt)oC_6PJh={ne(9+R2h3%cD>Bf>eK{S>Dw32En zYfe#)bEbjK5-L?gf;%0H9Rp>>rOem9%sQnO*zEpuJb8y?H5o@}6jvol87gxjNb9Q( z)*-9_260#`TN3g4nxA$0*|Puyr%1ml_MkY>B*AT-v)Fq3{;I#FrpRly|Lhmm<98n~ z+o~{eu2!Rf$Y_pk6lIMJqwjv62xha9nCyb-JOWG=uJRo}V*tVe+1f0oO1APY78`+a zQZ1OYyS7)n`pn7+{g#;h+pO*SeOQTW2|GWFJfDvJu)Y!w8i&1LBqgs#51yEa3PMGq z==;QMK@$7FrZh3%Z;eYJgR2Px0MPzDrE!2X#a&FD)J$Dm{^*Xg{C3Uu#>mnR$-u$T z!Pvo!&Pdi&)`ZS*5WY?pDw=83#vRWpJ$Rjmm7U#zeRPX^Q`XSHWWzkd!06R9hQg@m zbV64MmD)L@S%_S)1r1r)`IIXxjAxh-dwyY`auHQ$eE>fKJ8J+dj0>O#D$N!F5h@IT zWd;|6ibGYBF6{ArY6ZSHJS8w{Q{=jx)oKeJ5lY9BFzr$x0JG~mxgMD(R9Ga`B{f8j zze>t)46aFp*cmxA1PSo3c4p|{@Lw~74B0=n?8Gs}BUbd#TiG^=9U)gFgo0tV%3&$p zfQZ@dwS|oOc~_f}Txn*d2`UwNq)$KXo_+Q%Yx{b>*J>fN;h!RL=j<>+m-(8mmKxX} zl~sACQmfaf&I`~jLScHxr{NYx3yVrZ(^Dd*A$^lV;TTU^6EB($S&rpzh>H=I+YWnZ z;U*wyU*p<{*Wdp|94-`8mEVFyE5=PX{^3!7YWOIYrTsHnU_eQFxIxT&*~VlRX_-8q zn5DdSIL5ms`UpE%FI*sr$(uXg*NoLnOtPBu!!+oRRb;98{4;WBWv*JG$x%n;hQId$|77?J(i(((((j$Y-x>T~ef^WcAf&zchfM!li~XJS_oCFF zq{qa+3-f1*>US>x9)Z!^;=mU7NUay01D(s15sngH-B9H56}RC8vp)\$]''') - prefix_re = compile_prefix_regex(nlp.Defaults.prefixes + ['-']) - suffix_re = compile_suffix_regex(nlp.Defaults.suffixes) - return Tokenizer( - nlp.vocab, - prefix_search=prefix_re.search, - suffix_search=suffix_re.search, - infix_finditer=infix_re.finditer, - ) - -def new_nlp(): - - nlp = spacy.blank('fr') - nlp.tokenizer = custom_tokenizer(nlp) - - return nlp -``` - -```python -nlp = new_nlp() -``` - -```python -# nlp.add_pipe('sentencizer') -nlp.add_pipe('matcher', config=dict(regex=dict(douleurs=['blème de locomotion', 'douleurs', 'IMV']))) -nlp.add_pipe('sections') -nlp.add_pipe('pollution') -``` - -```python -text = ( - "Le patient est admis pour des douleurs dans le bras droit, mais n'a pas de problème de locomotion. Test(et oui) " - "Historique d'AVC dans la famille. pourrait être un cas de rhume.\n" - "NBNbWbWbNbWbNBNbNbWbWbNBNbWbNbNbWbNBNbWbNbNBWbWbNbNbNBWbNbWbNbWBNbNbWbNbNBNbWbWbNbWBNbNbWbNBNbWbWbNb\n" - "IMV--deshabillé\n" - "Pourrait être un cas de rhume.\n" - "Motif :\n" - "-problème de locomotions==+test\n" - "Douleurs dans le bras droit." -) -``` - -```python -doc = nlp(text) -``` - -```python -doc.ents -``` - -```python -doc[19] -``` - -```python -doc._.sections -``` - -```python -doc._.clean_ -``` - -```python -doc[17]._.ascii_ -``` - -```python -doc._.clean_ -``` - -On peut tester l'extraction d'entité dans le texte nettoyé : - -```python -doc_clean = nlp(doc._.clean_) -``` - -```python -ent = doc_clean[64:68] -ent -``` - -Les deux textes ne sont plus alignés : - -```python -doc.text[ent.start_char:ent.end_char] -``` - -Mais la méthode `char_clean_span` permet de réaligner les deux représentations : - -```python -doc._.char_clean_span(ent.start_char, ent.end_char) -``` - -```python - -``` diff --git a/notebooks/utilities/brat.md b/notebooks/utilities/brat.md deleted file mode 100644 index e64321554..000000000 --- a/notebooks/utilities/brat.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -jupyter: - jupytext: - formats: ipynb,md - text_representation: - extension: .md - format_name: markdown - format_version: "1.3" - jupytext_version: 1.11.4 - kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -```python -%reload_ext autoreload -%autoreload 2 -``` - -```python -# Importation du "contexte", ie la bibliothèque sans installation -import context -``` - -```python -import spacy -``` - -```python -import pandas as pd -``` - -```python -# One-shot import of all declared spaCy components -from edsnlp.utils.brat import BratConnector -``` - -# BRAT connector - -```python -brat = BratConnector('../../data/section_dataset/') -``` - -```python -texts = brat.read_texts() -``` - -```python -texts.head() -``` - -```python -brat.read_brat_annotation('BMI_4406356.txt.txt') -``` - -```python -texts, annotations = brat.get_brat() -``` - -```python -annotations -``` - -```python -nlp = spacy.blank('fr') -``` - -```python -docs = brat.brat2docs(nlp) -``` - -```python -doc = docs[0] -``` - -```python -doc.ents -``` - -```python -doc.ents[0] -``` - -```python -annotations.head() -``` - -```python -brat = BratConnector('test') -``` - -```python -brat.docs2brat(docs) -``` - -```python - -``` diff --git a/notebooks/utilities/context.py b/notebooks/utilities/context.py deleted file mode 100644 index c26859b87..000000000 --- a/notebooks/utilities/context.py +++ /dev/null @@ -1,5 +0,0 @@ -import os -import sys - -REPO_PATH = os.path.abspath(os.path.join(os.path.dirname("__file__"), "..", "..")) -sys.path.insert(0, REPO_PATH)

ipqZvs9^^R%D)#}WukKNVPP9G(|0A4_ShA)46 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 From 954ea8a8b21422e4642b4246d929e0fba76bffe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Mon, 22 May 2023 14:02:31 +0000 Subject: [PATCH 03/11] Added use of table in Measurement pipe --- .../misc/measurements/measurements.py | 501 +++++++++++++++--- edsnlp/pipelines/misc/tables/tables.py | 2 +- 2 files changed, 436 insertions(+), 67 deletions(-) diff --git a/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/pipelines/misc/measurements/measurements.py index 78a6522ca..93630ac1b 100644 --- a/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/pipelines/misc/measurements/measurements.py @@ -18,7 +18,6 @@ __all__ = ["MeasurementsMatcher"] - AFTER_SNIPPET_LIMIT = 8 BEFORE_SNIPPET_LIMIT = 10 @@ -263,7 +262,7 @@ def __init__( "unit": str, }, ...], } - number_terms: Dict[str, List[str] + number_terms: Dict[str, List[str]] A mapping of numbers to their lexical variants value_range_terms: Dict[str, List[str] A mapping of range terms (=, <, >) to their lexical variants @@ -498,7 +497,37 @@ def make_pseudo_sentence( return pseudo, offsets - def get_matches(self, doc): + @classmethod + def combine_measure_pow10( + cls, + measure: float, + pow10_text: str, + ) -> float: + """ + Return a float based on the measure (float) and the power of + 10 extracted with regex (string) + + Parameters + ---------- + measure: float + pow10_text: str + + Returns + ------- + float + """ + pow10 = int( + re.fullmatch( + ( + r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|" + r"(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)" + ), + pow10_text, + ).group(1) + ) + return measure * 10**pow10 + + def get_matches(self, doc: Union[Doc, Span]): """ Extract and filter regex and phrase matches in the document to prepare the measurement extraction. @@ -506,7 +535,7 @@ def get_matches(self, doc): Parameters ---------- - doc: Doc + doc: Union[Doc, Span] Returns ------- @@ -531,21 +560,37 @@ def get_matches(self, doc): ] # Filter out measurement-related spans that overlap already matched - # entities (in doc.ents or doc.spans["dates"]) + # entities (in doc.ents or doc.spans["dates"] or doc.spans["tables"]) + # Tables are considered in a separate step # Note: we also include sentence ends tokens as 1-token spans in those matches - spans__keep__is_sent_end = filter_spans( - [ - # Tuples (span, keep = is measurement related, is sentence end) - *zip(doc.spans.get("dates", ()), repeat(False), repeat(False)), - *zip(regex_matches, repeat(True), repeat(False)), - *zip(non_unit_terms, repeat(True), repeat(False)), - *zip(units, repeat(True), repeat(False)), - *zip(doc.ents, repeat(False), repeat(False)), - *zip(sent_ends, repeat(True), repeat(True)), - ], - # filter entities to keep only the ... - sort_key=measurements_match_tuples_sort_key, - ) + if type(doc) == Doc: + spans__keep__is_sent_end = filter_spans( + [ + # Tuples (span, keep = is measurement related, is sentence end) + *zip(doc.spans.get("dates", ()), repeat(False), repeat(False)), + *zip(doc.spans.get("tables", ()), repeat(False), repeat(False)), + *zip(regex_matches, repeat(True), repeat(False)), + *zip(non_unit_terms, repeat(True), repeat(False)), + *zip(units, repeat(True), repeat(False)), + *zip(doc.ents, repeat(False), repeat(False)), + *zip(sent_ends, repeat(True), repeat(True)), + ], + # filter entities to keep only the ... + sort_key=measurements_match_tuples_sort_key, + ) + else: + spans__keep__is_sent_end = filter_spans( + [ + # Tuples (span, keep = is measurement related, is sentence end) + *zip(regex_matches, repeat(True), repeat(False)), + *zip(non_unit_terms, repeat(True), repeat(False)), + *zip(units, repeat(True), repeat(False)), + *zip(doc.ents, repeat(False), repeat(False)), + *zip(sent_ends, repeat(True), repeat(True)), + ], + # filter entities to keep only the ... + sort_key=measurements_match_tuples_sort_key, + ) # Remove non-measurement related spans (keep = False) and sort the matches matches_and_is_sentence_end: List[(Span, bool)] = sorted( @@ -559,9 +604,9 @@ def get_matches(self, doc): return matches_and_is_sentence_end, unit_label_hashes - def extract_measurements(self, doc: Doc): + def extract_measurements_from_doc(self, doc: Doc): """ - Extracts measure entities from the document + Extracts measure entities from the filtered document Parameters ---------- @@ -588,20 +633,6 @@ def get_matches_before(i): return yield i - j, ent - # Return a float based on the measure (float) and the power of - # 10 extracted with regex (string) - def combine_measure_pow10(measure, pow10_text): - pow10 = int( - re.fullmatch( - ( - r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|" - r"(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)" - ), - pow10_text, - ).group(1) - ) - return measure * 10**pow10 - # Make a pseudo sentence to query higher order patterns in the main loop # `offsets` is a mapping from matches indices (ie match n°i) to # char indices in the pseudo sentence @@ -650,7 +681,7 @@ def combine_measure_pow10(measure, pow10_text): pseudo_sent = pseudo[offsets[number_idx] + 1 : offsets[pow10_idx]] if re.fullmatch(r"[,o]*", pseudo_sent): pow10_text = pow10_ent.text - value = combine_measure_pow10(value, pow10_text) + value = self.combine_measure_pow10(value, pow10_text) except (AttributeError, StopIteration): pass @@ -754,7 +785,7 @@ def combine_measure_pow10(measure, pow10_text): and pseudo[offsets[unit_before_idx] - 1] == "p" ): pow10_text = matches[unit_before_idx - 1][0].text - value = combine_measure_pow10(value, pow10_text) + value = self.combine_measure_pow10(value, pow10_text) else: (unit_after_idx, unit_after_text) = next( (j, e) @@ -766,20 +797,6 @@ def combine_measure_pow10(measure, pow10_text): pseudo[offsets[number_idx] + 1 : offsets[unit_after_idx]], ): unit_norm = unit_after_text.label_ - # Check if there is a power of 10 between the measure and - # the unit without considering the one that we have already - # considered at the beginning of thos program - try: - (pow10_idx, pow10_ent) = next( - (j, e) - for j, e in get_matches_before(unit_after_idx) - if e.label == self.nlp.vocab.strings["pow10"] - ) - if pow10_idx > pseudo[offsets[number_idx] + 1]: - pow10_text = pow10_ent.text - value = combine_measure_pow10(value, pow10_text) - except (AttributeError, StopIteration): - pass except (AttributeError, StopIteration): pass @@ -792,18 +809,20 @@ def combine_measure_pow10(measure, pow10_text): continue # Compute the final entity - if unit_text and unit_text.end == number.start: - ent = doc[unit_text.start : number.end] - elif unit_text and unit_text.start == number.end: - ent = doc[number.start : unit_text.end] + if type(doc) == Doc: + if unit_text and unit_text.end == number.start: + ent = doc[unit_text.start : number.end] + elif unit_text and unit_text.start == number.end: + ent = doc[number.start : unit_text.end] + else: + ent = number else: - ent = number - - # Compute the dimensionality of the parsed unit - try: - dims = self.unit_registry.parse_unit(unit_norm)[0] - except KeyError: - continue + if unit_text and unit_text.end == number.start: + ent = doc[unit_text.start - doc.start : number.end - doc.start] + elif unit_text and unit_text.start == number.end: + ent = doc[number.start - doc.start : unit_text.end - doc.start] + else: + ent = number if self.all_measurements: ent._.value = SimpleMeasurement( @@ -813,12 +832,17 @@ def combine_measure_pow10(measure, pow10_text): else: # If the measure was not requested, dismiss it # Otherwise, relabel the entity and create the value attribute - if dims not in self.measure_names: + # Compute the dimensionality of the parsed unit + try: + dims = self.unit_registry.parse_unit(unit_norm)[0] + if dims not in self.measure_names: + continue + ent._.value = SimpleMeasurement( + value_range, value, unit_norm, self.unit_registry + ) + ent.label_ = self.measure_names[dims] + except KeyError: continue - ent._.value = SimpleMeasurement( - value_range, value, unit_norm, self.unit_registry - ) - ent.label_ = self.measure_names[dims] measurements.append(ent) @@ -827,6 +851,351 @@ def combine_measure_pow10(measure, pow10_text): return measurements + def extract_measurements_from_tables(self, doc: Doc): + """ + Extracts measure entities from the document tables + + Parameters + ---------- + doc: Doc + + Returns + ------- + List[Span] + """ + + tables = doc.spans.get("tables", None) + measurements = [] + + if not tables: + return [] + + def get_distance_between_columns(column1_key, column2_key): + return abs(keys.index(column1_key) - keys.index(column2_key)) + + for table in tables: + # Try to retrieve columns linked to values + # or columns linked to units + # or columns linked to powers of 10 + # And then iter through the value columns + # to recreate measurements + keys = list(table._.table.keys()) + + unit_column_keys = [] + value_column_keys = [] + pow10_column_keys = [] + # Table with measurements related labellisation + table_labeled = {key: [] for key in keys} + unit_label_hashes = set() + + for key, column in list(table._.table.items()): + # We link the column to values, powers of 10 or units + # if more than half of the cells contain the said object + + # Cell counters + n_unit = 0 + n_value = 0 + n_pow10 = 0 + + for term in column: + + matches_in_term, unit_label_hashes_in_term = self.get_matches(term) + unit_label_hashes = unit_label_hashes.union( + unit_label_hashes_in_term + ) + + measurement_matches = [] + is_unit = False + is_value = False + is_pow10 = False + + for match, _ in matches_in_term: + + if match.label in self.number_label_hashes: + is_value = True + measurement_matches.append(match) + elif match.label in unit_label_hashes_in_term: + is_unit = True + measurement_matches.append(match) + elif match.label == self.nlp.vocab.strings["pow10"]: + is_pow10 = True + measurement_matches.append(match) + elif match.label in self.value_range_label_hashes: + measurement_matches.append(match) + + if is_unit: + n_unit += 1 + if is_value: + n_value += 1 + if is_pow10: + n_pow10 += 1 + + table_labeled[key].append(measurement_matches) + + # Checking if half of the cells contain units, values + # or powers of 10 + if n_unit > len(column) / 2: + unit_column_keys.append(key) + if n_value > len(column) / 2: + value_column_keys.append(key) + if n_pow10 > len(column) / 2: + pow10_column_keys.append(key) + + # Iter through the value keys to create measurements + for value_column_key in value_column_keys: + + # If the table contains a unit column, + # try to pair the value to the unit of + # the nearest unit column + if len(unit_column_keys): + # Prevent same distance conflict + # For example is a table is organised as + # "header, unit1, value1, unit2, value2" + # value1 is at equal distance of unit1 and unit2 columns + # To solve this problem, we try to detect if we have a + # value - unit pattern or unit - value pattern by checking + # the first column that appears. + if keys.index(unit_column_keys[0]) > keys.index( + value_column_keys[0] + ): + measure_before_unit_in_table = True + else: + measure_before_unit_in_table = False + + # We only consider the nearest unit column when It + # is not a value column at the same time + # except if It is the column that we are considering + try: + unit_column_key = sorted( + [ + unit_column_key + for unit_column_key in unit_column_keys + if unit_column_key + not in [ + v + for v in value_column_keys + if v != value_column_key + ] + ], + key=lambda unit_column_key: get_distance_between_columns( + unit_column_key, value_column_key + ), + )[0 : min(2, len(unit_column_keys))][ + 0 * (not measure_before_unit_in_table) + - 1 * measure_before_unit_in_table + ] + except IndexError: + unit_column_key = value_column_key + else: + unit_column_key = value_column_key + + # If the table contains a power column, + # try to pair the value to the power of + # the nearest power column + if len(pow10_column_keys): + # Same distance conflict as for unit columns + if keys.index(pow10_column_keys[0]) > keys.index( + value_column_keys[0] + ): + measure_before_power_in_table = True + else: + measure_before_power_in_table = False + + try: + pow10_column_key = sorted( + [ + pow10_column_key + for pow10_column_key in pow10_column_keys + if pow10_column_key + not in [ + v + for v in value_column_keys + if v != value_column_key + ] + ], + key=lambda pow10_column_key: get_distance_between_columns( + pow10_column_key, value_column_key + ), + )[0 : min(2, len(pow10_column_keys))][ + 0 * (not measure_before_power_in_table) + - 1 * measure_before_power_in_table + ] + except IndexError: + pow10_column_key = value_column_key + else: + pow10_column_key = value_column_key + + # If unit column is the same as value column, extract + # measurement in this column with the + # extract_measurements_from_doc method + + if unit_column_key == value_column_key: + # Consider possible pow10 column + if pow10_column_key != value_column_key: + for term, pow10_list in zip( + table._.table[value_column_key], + table_labeled[pow10_column_key], + ): + measurements_part = self.extract_measurements_from_doc(term) + try: + pow10_text = [ + p.text + for p in pow10_list + if p.label == self.nlp.vocab.strings["pow10"] + ][0] + for measurement in measurements_part: + measurement._.value.value = ( + self.combine_measure_pow10( + measurement._.value.value, pow10_text + ) + ) + except IndexError: + pass + measurements += measurements_part + else: + for term in table._.table[value_column_key]: + measurements += self.extract_measurements_from_doc(term) + continue + + # If unit column is different from value column + # Iter through the value column to create the measurement + # Iter through the units and powers columns + # at the same time if they exist, else value column + for unit_list, value_list, pow10_list in zip( + table_labeled[unit_column_key], + table_labeled[value_column_key], + table_labeled[pow10_column_key], + ): + # Check if there is really a value + try: + ent = [ + v for v in value_list if v.label in self.number_label_hashes + ][0] + value = float( + ent.text.replace(" ", "").replace(",", ".").replace(" ", "") + ) + # Sometimes the value column contains a power. + # It may not be common enough to reach 50% + # of the cells, that's why + # It may not be labeled as pow10_column. + # Still, we should retrieve these powers. + try: + pow10_text = [ + p.text + for p in value_list + if p.label == self.nlp.vocab.strings["pow10"] + ][0] + value = self.combine_measure_pow10(value, pow10_text) + except IndexError: + pass + except (IndexError, ValueError): + continue + + # Check for value range terms + try: + value_range = [ + v_r.label_ + for v_r in value_list + if v_r.label in self.value_range_label_hashes + ][0] + except IndexError: + value_range = "=" + + # Check for units and powers in the unit column + # (for same reasons as described before) + # in units column + try: + unit_norm = [ + u.label_ for u in unit_list if u.label in unit_label_hashes + ][0] + # To avoid duplicates + if unit_column_key != value_column_key: + try: + pow10_text = [ + p.text + for p in unit_list + if p.label == self.nlp.vocab.strings["pow10"] + ][0] + value = self.combine_measure_pow10(value, pow10_text) + except IndexError: + pass + except IndexError: + unit_norm = "nounit" + + if unit_norm == "nounit": + # Try to retrieve a possible unit in the header + # of the value column + try: + unit_norm = [ + u.label_ + for u in self.extract_units( + list( + self.term_matcher( + self.nlp(str(value_column_key)), + as_spans=True, + ) + ) + ) + ][0] + except IndexError: + pass + + # Check for powers in power column + try: + if ( + pow10_column_key != value_column_key + and pow10_column_key != unit_column_key + ): + pow10_text = [ + p.text + for p in pow10_list + if p.label == self.nlp.vocab.strings["pow10"] + ][0] + value = self.combine_measure_pow10(value, pow10_text) + except IndexError: + pass + + if self.all_measurements: + ent._.value = SimpleMeasurement( + value_range, value, unit_norm, self.unit_registry + ) + ent.label_ = "eds.measurement" + else: + # If the measure was not requested, dismiss it + # Otherwise, relabel the entity and create the value attribute + # Compute the dimensionality of the parsed unit + try: + dims = self.unit_registry.parse_unit(unit_norm)[0] + if dims not in self.measure_names: + continue + ent._.value = SimpleMeasurement( + value_range, value, unit_norm, self.unit_registry + ) + ent.label_ = self.measure_names[dims] + except KeyError: + continue + + measurements.append(ent) + + return measurements + + def extract_measurements(self, doc: Doc): + """ + Extracts measure entities from the document + + Parameters + ---------- + doc: Doc + + Returns + ------- + List[Span] + """ + measurements = self.extract_measurements_from_doc(doc) + measurements += self.extract_measurements_from_tables(doc) + measurements = filter_spans(measurements) + return measurements + @classmethod def merge_adjacent_measurements(cls, measurements: List[Span]) -> List[Span]: """ diff --git a/edsnlp/pipelines/misc/tables/tables.py b/edsnlp/pipelines/misc/tables/tables.py index 792cdc7b8..d6e2ee1f7 100644 --- a/edsnlp/pipelines/misc/tables/tables.py +++ b/edsnlp/pipelines/misc/tables/tables.py @@ -54,7 +54,7 @@ def __init__( sep_pattern = patterns.sep self.regex_matcher = RegexMatcher(attr=attr, ignore_excluded=True) - self.regex_matcher.add("row", [tables_pattern]) + self.regex_matcher.add("table", [tables_pattern]) self.term_matcher = EDSPhraseMatcher(nlp.vocab, attr=attr, ignore_excluded=True) self.term_matcher.build_patterns( From fed2a02054a4b6e00693db1a6d99cb671472bcec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Tue, 23 May 2023 14:08:09 +0000 Subject: [PATCH 04/11] Booleans detections and others --- edsnlp/pipelines/misc/measurements/factory.py | 9 ++ .../misc/measurements/measurements.py | 151 ++++++++++++++---- .../pipelines/misc/measurements/patterns.py | 27 ++++ 3 files changed, 160 insertions(+), 27 deletions(-) diff --git a/edsnlp/pipelines/misc/measurements/factory.py b/edsnlp/pipelines/misc/measurements/factory.py index d6e5deff6..db63bf7de 100644 --- a/edsnlp/pipelines/misc/measurements/factory.py +++ b/edsnlp/pipelines/misc/measurements/factory.py @@ -21,6 +21,9 @@ stopwords_unitless=patterns.stopwords_unitless, stopwords_measure_unit=patterns.stopwords_measure_unit, measure_before_unit=False, + parse_doc=True, + parse_tables=True, + all_measurements=True, ) @@ -33,6 +36,9 @@ def create_component( units_config: Dict[str, UnitConfig], number_terms: Dict[str, List[str]], value_range_terms: Dict[str, List[str]], + all_measurements: bool, + parse_tables: bool, + parse_doc: bool, stopwords_unitless: List[str], stopwords_measure_unit: List[str], measure_before_unit: bool, @@ -45,6 +51,9 @@ def create_component( units_config=units_config, number_terms=number_terms, value_range_terms=value_range_terms, + all_measurements=all_measurements, + parse_tables=parse_tables, + parse_doc=parse_doc, unit_divisors=unit_divisors, measurements=measurements, stopwords_unitless=stopwords_unitless, diff --git a/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/pipelines/misc/measurements/measurements.py index 93630ac1b..cb8465823 100644 --- a/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/pipelines/misc/measurements/measurements.py @@ -9,7 +9,7 @@ import regex import spacy from spacy.tokens import Doc, Span -from typing_extensions import TypedDict +from typing_extensions import NotRequired, TypedDict from edsnlp.matchers.phrase import EDSPhraseMatcher from edsnlp.matchers.regex import RegexMatcher @@ -29,9 +29,15 @@ class UnitConfig(TypedDict): ui_decomposition: Dict[str, int] +class SimpleMeasurementConfigWithoutRegistry(TypedDict): + value_range: str + value: Union[float, int] + unit: str + + class UnitlessRange(TypedDict): - min: int - max: int + min: NotRequired[int] + max: NotRequired[int] unit: str @@ -46,9 +52,16 @@ class UnitlessPatternConfigWithName(TypedDict): name: str +class ValuelessPatternConfig(TypedDict): + terms: NotRequired[List[str]] + regex: NotRequired[List[str]] + measurement: SimpleMeasurementConfigWithoutRegistry + + class MeasureConfig(TypedDict): unit: str - unitless_patterns: List[UnitlessPatternConfig] + unitless_patterns: NotRequired[List[UnitlessPatternConfig]] + valueless_patterns: NotRequired[List[ValuelessPatternConfig]] class Measurement(abc.ABC): @@ -217,6 +230,9 @@ def __init__( units_config: Dict[str, UnitConfig], number_terms: Dict[str, List[str]], value_range_terms: Dict[str, List[str]], + all_measurements: bool = True, + parse_tables: bool = True, + parse_doc: bool = True, stopwords_unitless: List[str] = ("par", "sur", "de", "a", ":"), stopwords_measure_unit: List[str] = ("|", "¦", "…", "."), measure_before_unit: bool = False, @@ -248,7 +264,9 @@ def __init__( Each measure's configuration has the following shape: { "unit": str, # the unit of the measure (like "kg"), - "unitless_patterns": { # optional patterns to handle unitless cases + "unitless_patterns": List[ + # optional patterns to handle unitless cases + { "terms": List[str], # list of preceding terms used to trigger the measure # Mapping from ranges to unit to handle cases like @@ -262,10 +280,36 @@ def __init__( "unit": str, }, ...], } + ], + "valueless_patterns": List[ + # optional patterns to handle unmatched measures by + # this pipe. The measures are hardcoded with this option. + # It can be useful to detect measures such as "positive" + # or "negative" and store it as booleans. + { + "regex": List[str], + "terms": List[str], + "measurement": { + "value_range": str, + "value": Union[int, float], + "unit": str + } + } + ], number_terms: Dict[str, List[str]] A mapping of numbers to their lexical variants value_range_terms: Dict[str, List[str] A mapping of range terms (=, <, >) to their lexical variants + all_measurements: bool + Whether to keep all measurements or only the one specified in measurements. + If True, matched measurements not mentionned in measurements variable + will be labeled "eds.measurement", while the ones mentionned in + measurements variable will be labeled with the specified name. + parse_tables: bool + Whether to parse the tables of the doc detected with "eds.tables" pipe. + parse_doc: bool + Whether to parse the doc without the tables. If parse_tables and parse_doc + are both False, anything is parsed. stopwords_unitless: List[str] A list of stopwords that do not matter when placed between a unitless trigger and a number @@ -289,7 +333,6 @@ def __init__( ignore_excluded : bool Whether to exclude pollution patterns when matching in the text """ - self.all_measurements = True if measurements is None else False if measurements is None: measurements = common_measurements @@ -302,12 +345,17 @@ def __init__( self.regex_matcher = RegexMatcher(attr=attr, ignore_excluded=True) self.term_matcher = EDSPhraseMatcher(nlp.vocab, attr=attr, ignore_excluded=True) self.unitless_patterns: Dict[str, UnitlessPatternConfigWithName] = {} + self.valueless_patterns: Dict[str, SimpleMeasurement] = {} self.value_range_label_hashes: Set[int] = set() self.unit_part_label_hashes: Set[int] = set() self.unitless_label_hashes: Set[int] = set() + self.valueless_label_hashes: Set[int] = set() self.unit_followers: Dict[str, str] = {} self.measure_names: Dict[str, str] = {} self.measure_before_unit = measure_before_unit + self.all_measurements = all_measurements + self.parse_tables = parse_tables + self.parse_doc = parse_doc # INTERVALS self.regex_matcher.add( @@ -390,6 +438,25 @@ def __init__( ) self.unitless_label_hashes.add(nlp.vocab.strings[pattern_name]) self.unitless_patterns[pattern_name] = {"name": name, **pattern} + if "valueless_patterns" in measure_config: + for pattern in measure_config["valueless_patterns"]: + pattern_name = f"valueless_{len(self.valueless_patterns)}" + if "terms" in pattern: + self.term_matcher.build_patterns( + nlp, + terms={ + pattern_name: pattern["terms"], + }, + ) + if "regex" in pattern: + self.regex_matcher.add( + pattern_name, + pattern["regex"], + ) + self.valueless_label_hashes.add(nlp.vocab.strings[pattern_name]) + self.valueless_patterns[pattern_name] = SimpleMeasurement( + registry=self.unit_registry, **pattern["measurement"] + ) self.set_extensions() @@ -658,6 +725,14 @@ def get_matches_before(i): # Iterate through the number matches for number_idx, (number, is_sent_split) in enumerate(matches): if not is_sent_split and number.label not in self.number_label_hashes: + # Check if we have a valueless pattern + if number.label in self.valueless_label_hashes: + ent = number + ent._.value = self.valueless_patterns[number.label_] + ent.label_ = self.measure_names[ + self.unit_registry.parse_unit(ent._.value.unit)[0] + ] + measurements.append(ent) continue # Detect the measure value @@ -824,25 +899,23 @@ def get_matches_before(i): else: ent = number - if self.all_measurements: + # If the measure was not requested, dismiss it + # Otherwise, relabel the entity and create the value attribute + # Compute the dimensionality of the parsed unit + try: + dims = self.unit_registry.parse_unit(unit_norm)[0] ent._.value = SimpleMeasurement( value_range, value, unit_norm, self.unit_registry ) - ent.label_ = "eds.measurement" - else: - # If the measure was not requested, dismiss it - # Otherwise, relabel the entity and create the value attribute - # Compute the dimensionality of the parsed unit - try: - dims = self.unit_registry.parse_unit(unit_norm)[0] - if dims not in self.measure_names: + if dims not in self.measure_names: + if self.all_measurements: + ent.label_ = "eds.measurement" + else: continue - ent._.value = SimpleMeasurement( - value_range, value, unit_norm, self.unit_registry - ) + else: ent.label_ = self.measure_names[dims] - except KeyError: - continue + except KeyError: + continue measurements.append(ent) @@ -911,7 +984,10 @@ def get_distance_between_columns(column1_key, column2_key): for match, _ in matches_in_term: - if match.label in self.number_label_hashes: + if ( + match.label in self.number_label_hashes + or match.label in self.valueless_label_hashes + ): is_value = True measurement_matches.append(match) elif match.label in unit_label_hashes_in_term: @@ -1069,11 +1145,29 @@ def get_distance_between_columns(column1_key, column2_key): # Check if there is really a value try: ent = [ - v for v in value_list if v.label in self.number_label_hashes + v + for v in value_list + if v.label in self.number_label_hashes + or v.label in self.valueless_label_hashes ][0] - value = float( - ent.text.replace(" ", "").replace(",", ".").replace(" ", "") - ) + # Take the value linked to valueless pattern if + # we have a valueless pattern + if ent.label in self.valueless_label_hashes: + ent._.value = self.valueless_patterns[ent.label_] + ent.label_ = self.measure_names[ + self.unit_registry.parse_unit(ent._.value.unit)[0] + ] + measurements.append(ent) + continue + # Else try to parse the number + if ent.label_ == "number": + value = float( + ent.text.replace(" ", "") + .replace(",", ".") + .replace(" ", "") + ) + else: + value = float(ent.label_) # Sometimes the value column contains a power. # It may not be common enough to reach 50% # of the cells, that's why @@ -1191,8 +1285,11 @@ def extract_measurements(self, doc: Doc): ------- List[Span] """ - measurements = self.extract_measurements_from_doc(doc) - measurements += self.extract_measurements_from_tables(doc) + measurements = [] + if self.parse_doc: + measurements += self.extract_measurements_from_doc(doc) + if self.parse_tables: + measurements += self.extract_measurements_from_tables(doc) measurements = filter_spans(measurements) return measurements diff --git a/edsnlp/pipelines/misc/measurements/patterns.py b/edsnlp/pipelines/misc/measurements/patterns.py index 52a888fbc..cf34e2311 100644 --- a/edsnlp/pipelines/misc/measurements/patterns.py +++ b/edsnlp/pipelines/misc/measurements/patterns.py @@ -80,6 +80,27 @@ ], }, "eds.volume": {"unit": "m3", "unitless_patterns": []}, + "eds.bool": { + "unit": "bool", + "valueless_patterns": [ + { + "terms": ["positif", "positifs", "positive", "positives"], + "measurement": { + "value_range": "=", + "value": 1, + "unit": "bool", + }, + }, + { + "terms": ["negatif", "negatifs", "negative", "negatives"], + "measurement": { + "value_range": "=", + "value": 0, + "unit": "bool", + }, + }, + ], + }, } @@ -2350,4 +2371,10 @@ "followed_by": None, "ui_decomposition": {"mass": 1, "length": -1, "time": -2}, }, + "bool": { + "scale": 1, + "terms": [], + "followed_by": None, + "ui_decomposition": {"bool": 1}, + }, } From 07871ed4feca7dbbbd6f4dd578bdb460d5aa7d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Wed, 24 May 2023 14:50:40 +0000 Subject: [PATCH 05/11] Bug fixes, table pipe is more robust, Updated doc --- docs/pipelines/misc/measurements.md | 9 +- docs/pipelines/misc/tables.md | 105 +++++++++++++++++++++++ edsnlp/pipelines/misc/tables/factory.py | 6 +- edsnlp/pipelines/misc/tables/patterns.py | 4 +- edsnlp/pipelines/misc/tables/tables.py | 66 +++++++++++--- 5 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 docs/pipelines/misc/tables.md diff --git a/docs/pipelines/misc/measurements.md b/docs/pipelines/misc/measurements.md index a30041025..5aeb5a850 100644 --- a/docs/pipelines/misc/measurements.md +++ b/docs/pipelines/misc/measurements.md @@ -8,7 +8,7 @@ We use simple regular expressions to extract and normalize measurements, and use By default, the `eds.measurements` pipeline lets you match all measurements, i.e measurements in most units as well as unitless measurements. If a unit is not in our register, then you can add It manually. If not, the measurement will be matched without Its unit. -If you prefer to match specific measurements only, you can create your own measurement config. Nevertheless, some default measurements configs are already provided out of the box: +If you prefer matching specific measurements only, you can create your own measurement config anda set `all_measurements` parameter to `False`. Nevertheless, some default measurements configs are already provided out of the box: | Measurement name | Example | | ---------------- | ---------------------- | @@ -16,6 +16,7 @@ If you prefer to match specific measurements only, you can create your own measu | `eds.weight` | `12kg`, `1kg300` | | `eds.bmi` | `BMI: 24`, `24 kg.m-2` | | `eds.volume` | `2 cac`, `8ml` | +| `eds.bool` | `positive`, `negatif` | The normalized value can then be accessed via the `span._.value` attribute and converted on the fly to a desired unit (eg `span._.value.g_per_cl` or `span._.value.kg_per_m3` for a density). @@ -25,7 +26,7 @@ The measurements that can be extracted can have one or many of the following cha - Measurements with range indication (escpecially < or >) - Measurements with power -The measurement can be written in many coplex forms. Among them, this pipe can detect: +The measurement can be written in many complex forms. Among them, this pipe can detect: - Measurements with range indication, numerical value, power and units in many different orders and separated by customizable stop words - Composed units (eg `1m50`) - Measurement with "unitless patterns", i.e some textual information next to a numerical value which allows us to retrieve a unit even if It is not written (eg in the text `Height: 80`, this pipe will a detect the numlerical value `80`and match It to the unit `kg`) @@ -33,7 +34,9 @@ The measurement can be written in many coplex forms. Among them, this pipe can d ## Usage -The matched measurements are labelised with `eds.measurement` by default. However, if you are only creating your own measurement or using a predefined one, your measurements will be labeled with the name of this measurement (eg `eds.weight`). +This pipe works better with `eds.dates` and `eds.tables` pipe at the same time. These pipes let `eds.measurements` skip dates as measurements and make a specific matching for each table, benefitting of the structured data. + +The matched measurements are labeled with a default measurement name if available (eg `eds.size`), else `eds.measurement` if any measure is linked to the dimension of the measure's unit and if `all_measurements` is set to `True`. As said before, each matched measurement can be accessed via the `span._.value`. This gives you a `SimpleMeasurement` object with the following attributes : - `value_range` ("<", "=" or ">") diff --git a/docs/pipelines/misc/tables.md b/docs/pipelines/misc/tables.md new file mode 100644 index 000000000..0acc54f9d --- /dev/null +++ b/docs/pipelines/misc/tables.md @@ -0,0 +1,105 @@ +# Tables + +The `eds.tables` pipeline's role is to detect tables present in a medical document. +We use simple regular expressions to extract tables like text. + +## Usage + +This pipe lets you match different forms of tables. They can have a frame or not, rows can be spread on multiple consecutive lines (in case of a bad parsing for example)... You can also indicate the presence of headers with the `col_names` and `row_names` boolean parameters. + +Each matched table is returned as a `Span` object. You can then access to an equivalent dictionnary formatted table with `table` extension or use `to_pandas_table()` to get the equivalent pandas DataFrame. The key of the dictionnary is determined as folowed: +- If `col_names` is True, then, the dictionnary keys are the names of the columns (str). +- Elif `row_names` is True, then, the dictionnary keys are the names (str). +- Else the dictionnary keys are indexes of the columns (int). + +`to_pandas_table()` can be customised with `as_spans` parameter. If set to `True`, then the pandas dataframe will contain the cells as spans, else the pandas dataframe will contain the cells as raw strings. + +```python +import spacy + +nlp = spacy.blank("fr") +nlp.add_pipe("eds.normalizer") +nlp.add_pipe("eds.tables") + +text = """ +SERVICE +MEDECINE INTENSIVE – +REANIMATION +Réanimation / Surveillance Continue +Médicale + +COMPTE RENDU D'HOSPITALISATION du 05/06/2020 au 10/06/2020 +Madame DUPONT Marie, née le 16/05/1900, âgée de 20 ans, a été hospitalisée en réanimation du +05/06/1920 au 10/06/1920 pour intoxication médicamenteuse volontaire. + + +Examens complémentaires +Hématologie +Numération +Leucocytes ¦x10*9/L ¦4.97 ¦4.09-11 +Hématies ¦x10*12/L¦4.68 ¦4.53-5.79 +Hémoglobine ¦g/dL ¦14.8 ¦13.4-16.7 +Hématocrite ¦% ¦44.2 ¦39.2-48.6 +VGM ¦fL ¦94.4 + ¦79.6-94 +TCMH ¦pg ¦31.6 ¦27.3-32.8 +CCMH ¦g/dL ¦33.5 ¦32.4-36.3 +Plaquettes ¦x10*9/L ¦191 ¦172-398 +VMP ¦fL ¦11.5 + ¦7.4-10.8 + +Sur le plan neurologique : Devant la persistance d'une confusion à distance de l'intoxication au +... + +2/2Pat : |F | | |Intitulé RCP + +""" + +doc = nlp(text) + +# A table span +table = doc.spans["tables"][0] +# Leucocytes ¦x10*9/L ¦4.97 ¦4.09-11 +# Hématies ¦x10*12/L¦4.68 ¦4.53-5.79 +# Hémoglobine ¦g/dL ¦14.8 ¦13.4-16.7 +# Hématocrite ¦% ¦44.2 ¦39.2-48.6 +# VGM ¦fL ¦94.4 + ¦79.6-94 +# TCMH ¦pg ¦31.6 ¦27.3-32.8 +# CCMH ¦g/dL ¦33.5 ¦32.4-36.3 +# Plaquettes ¦x10*9/L ¦191 ¦172-398 +# VMP ¦fL ¦11.5 + ¦7.4-10.8 + +# Convert span to Pandas table +df = table._.to_pd_table(as_spans=False) +type(df) +# >> pandas.core.frame.DataFrame +``` +The pd DataFrame: +| | 0 | 1 | 2 | 3 | +| ---: | :---------- | :------- | :----- | :-------- | +| 0 | Leucocytes | x10*9/L | 4.97 | 4.09-11 | +| 1 | Hématies | x10*12/L | 4.68 | 4.53-5.79 | +| 2 | Hémoglobine | g/dL | 14.8 | 13.4-16.7 | +| 3 | Hématocrite | % | 44.2 | 39.2-48.6 | +| 4 | VGM | fL | 94.4 + | 79.6-94 | +| 5 | TCMH | pg | 31.6 | 27.3-32.8 | +| 6 | CCMH | g/dL | 33.5 | 32.4-36.3 | +| 7 | Plaquettes | x10*9/L | 191 | 172-398 | +| 8 | VMP | fL | 11.5 + | 7.4-10.8 | + +## Declared extensions + +The `eds.tables` pipeline declares two [spaCy extension](https://spacy.io/usage/processing-pipelines#custom-components-attributes) on the `Span` object. The first one is `to_pd_table()` method which returns a parsed pandas version of the table. The second one is `table` which contains the table stored as a dictionnary containing cells as `Span` objects. + +## Configuration + +The pipeline can be configured using the following parameters : + +| Parameter | Explanation | Default | +| ----------------- | ------------------------------------------------ | ---------------------- | +| `tables_pattern` | Pattern to identify table spans | `rf"(\b.*{sep}.*\n)+"` | +| `sep_pattern` | Pattern to identify column separation | `r"¦"` | +| `ignore_excluded` | Ignore excluded tokens | `True` | +| `attr` | spaCy attribute to match on, eg `NORM` or `TEXT` | `"TEXT"` | + +## Authors and citation + +The `eds.tables` pipeline was developed by AP-HP's Data Science team. diff --git a/edsnlp/pipelines/misc/tables/factory.py b/edsnlp/pipelines/misc/tables/factory.py index 801dea164..d796a56c5 100644 --- a/edsnlp/pipelines/misc/tables/factory.py +++ b/edsnlp/pipelines/misc/tables/factory.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Union +from typing import List, Optional from spacy.language import Language @@ -20,8 +20,8 @@ def create_component( nlp: Language, name: str, - tables_pattern: Optional[Dict[str, Union[List[str], str]]], - sep_pattern: Optional[str], + tables_pattern: Optional[List[str]], + sep_pattern: Optional[List[str]], attr: str, ignore_excluded: bool, col_names: Optional[bool] = False, diff --git a/edsnlp/pipelines/misc/tables/patterns.py b/edsnlp/pipelines/misc/tables/patterns.py index 7f29c7b4c..233bc47b6 100644 --- a/edsnlp/pipelines/misc/tables/patterns.py +++ b/edsnlp/pipelines/misc/tables/patterns.py @@ -1,2 +1,2 @@ -sep = r"¦" -regex = rf"(?:{sep}?(?:[^{sep}\n]*{sep})+[^{sep}\n]*{sep}?\n)+" +sep = [r"¦", r"|"] +regex = [r"(?:¦?(?:[^¦\n]*¦)+[^¦\n]*¦?\n)+", r"(?:\|?(?:[^\|\n]*\|)+[^\|\n]*\|?\n)+"] diff --git a/edsnlp/pipelines/misc/tables/tables.py b/edsnlp/pipelines/misc/tables/tables.py index d6e2ee1f7..8262b0bc0 100644 --- a/edsnlp/pipelines/misc/tables/tables.py +++ b/edsnlp/pipelines/misc/tables/tables.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Union +from typing import List, Optional import pandas as pd from spacy.language import Language @@ -19,10 +19,10 @@ class TablesMatcher: ---------- nlp : Language spaCy nlp pipeline to use for matching. - tables_pattern : Optional[str] - The regex pattern to identify tables. - sep_pattern : Optional[str] - The regex pattern to identify separators + tables_pattern : Optional[List[str]] + The regex patterns to identify tables. + sep_pattern : Optional[List[str]] + The regex patterns to identify separators in the detected tables col_names : Optional[bool] Whether the tables_pattern matches column names @@ -39,9 +39,9 @@ class TablesMatcher: def __init__( self, nlp: Language, - tables_pattern: Optional[str], - sep_pattern: Optional[str], - attr: Union[Dict[str, str], str], + tables_pattern: Optional[List[str]], + sep_pattern: Optional[List[str]], + attr: str, ignore_excluded: bool, col_names: Optional[bool] = False, row_names: Optional[bool] = False, @@ -54,7 +54,7 @@ def __init__( sep_pattern = patterns.sep self.regex_matcher = RegexMatcher(attr=attr, ignore_excluded=True) - self.regex_matcher.add("table", [tables_pattern]) + self.regex_matcher.add("table", tables_pattern) self.term_matcher = EDSPhraseMatcher(nlp.vocab, attr=attr, ignore_excluded=True) self.term_matcher.build_patterns( @@ -138,7 +138,53 @@ def get_tables(self, matches): if all(row[-1].start == row[-1].end for row in processed_table): processed_table = [row[:-1] for row in processed_table] - tables_list.append(processed_table) + # Check if all rows have the same dimension. + # If not, try to merge neighbour rows + # to find a new table + row_len = len(processed_table[0]) + if not all(len(row) == row_len for row in processed_table): + + # Method to find all possible lengths of the rows + def divisors(n): + result = set() + for i in range(1, int(n**0.5) + 1): + if n % i == 0: + result.add(i) + result.add(n // i) + return sorted(list(result)) + + if self.col_names: + n_rows = len(processed_table) - 1 + else: + n_rows = len(processed_table) + + for n_rows_to_merge in divisors(n_rows): + row_len = sum(len(row) for row in processed_table[:n_rows_to_merge]) + if all( + sum( + len(row) + for row in processed_table[ + i * n_rows_to_merge : (i + 1) * n_rows_to_merge + ] + ) + == row_len + for i in range(n_rows // n_rows_to_merge) + ): + processed_table = [ + [ + cell + for subrow in processed_table[ + i * n_rows_to_merge : (i + 1) * n_rows_to_merge + ] + for cell in subrow + ] + for i in range(n_rows // n_rows_to_merge) + ] + tables_list.append(processed_table) + break + continue + else: + tables_list.append(processed_table) # Convert to dictionnaries according to self.col_names # and self.row_names From 6902b0baa156ff5663ff08706f670a046fd40770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christel=20G=C3=A9rardin?= Date: Tue, 27 Jun 2023 08:12:00 +0000 Subject: [PATCH 06/11] Tables pipe modified with black --- edsnlp/pipelines/misc/tables/tables.py | 32 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/edsnlp/pipelines/misc/tables/tables.py b/edsnlp/pipelines/misc/tables/tables.py index 8262b0bc0..a6d29f9b6 100644 --- a/edsnlp/pipelines/misc/tables/tables.py +++ b/edsnlp/pipelines/misc/tables/tables.py @@ -153,6 +153,7 @@ def divisors(n): result.add(n // i) return sorted(list(result)) + # Do not count the column names when splitting the table if self.col_names: n_rows = len(processed_table) - 1 else: @@ -170,17 +171,26 @@ def divisors(n): == row_len for i in range(n_rows // n_rows_to_merge) ): - processed_table = [ - [ - cell - for subrow in processed_table[ - i * n_rows_to_merge : (i + 1) * n_rows_to_merge - ] - for cell in subrow - ] - for i in range(n_rows // n_rows_to_merge) - ] - tables_list.append(processed_table) + new_table = [] + for i in range(n_rows // n_rows_to_merge): + # Init new_row with the first subrow + new_row = processed_table[i * n_rows_to_merge] + for subrow in processed_table[ + i * n_rows_to_merge + 1 : (i + 1) * n_rows_to_merge + ]: + new_row = ( + new_row[:-1] + + [ + table[ + new_row[-1].start + - table.start : subrow[0].end + - table.start + ] + ] + + subrow[1:] + ) + new_table.append(new_row) + tables_list.append(new_table) break continue else: From ebeb79c06171d6a3b3381c30bb8bbd238717def2 Mon Sep 17 00:00:00 2001 From: Jungack Date: Tue, 27 Jun 2023 18:30:22 +0200 Subject: [PATCH 07/11] :goal_net: Improved measurements + Improved tables --- docs/pipelines/misc/measurements.md | 76 +- docs/pipelines/misc/tables.md | 105 + edsnlp/pipelines/factories.py | 1 + edsnlp/pipelines/misc/measurements/factory.py | 24 +- .../misc/measurements/measurements.py | 819 +++++- .../pipelines/misc/measurements/patterns.py | 2441 ++++++++++++++--- edsnlp/pipelines/misc/tables/__init__.py | 2 + edsnlp/pipelines/misc/tables/factory.py | 38 + edsnlp/pipelines/misc/tables/patterns.py | 2 + edsnlp/pipelines/misc/tables/tables.py | 268 ++ edsnlp/resources/verbs.csv.gz | Bin 200566 -> 200865 bytes 11 files changed, 3363 insertions(+), 413 deletions(-) create mode 100644 docs/pipelines/misc/tables.md create mode 100644 edsnlp/pipelines/misc/tables/__init__.py create mode 100644 edsnlp/pipelines/misc/tables/factory.py create mode 100644 edsnlp/pipelines/misc/tables/patterns.py create mode 100644 edsnlp/pipelines/misc/tables/tables.py diff --git a/docs/pipelines/misc/measurements.md b/docs/pipelines/misc/measurements.md index 4630f993c..5aeb5a850 100644 --- a/docs/pipelines/misc/measurements.md +++ b/docs/pipelines/misc/measurements.md @@ -1,21 +1,14 @@ # Measurements The `eds.measurements` pipeline's role is to detect and normalise numerical measurements within a medical document. -We use simple regular expressions to extract and normalize measurements, and use `Measurement` classes to store them. - -!!! warning - - The ``measurements`` pipeline is still in active development and has not been rigorously validated. - If you come across a measurement expression that goes undetected, please file an issue ! +We use simple regular expressions to extract and normalize measurements, and use `SimpleMeasurement` classes to store them. ## Scope -The `eds.measurements` pipeline can extract simple (eg `3cm`) measurements. -It can detect elliptic enumerations (eg `32, 33 et 34kg`) of measurements of the same type and split the measurements accordingly. +By default, the `eds.measurements` pipeline lets you match all measurements, i.e measurements in most units as well as unitless measurements. If a unit is not in our register, +then you can add It manually. If not, the measurement will be matched without Its unit. -The normalized value can then be accessed via the `span._.value` attribute and converted on the fly to a desired unit. - -The current pipeline annotates the following measurements out of the box: +If you prefer matching specific measurements only, you can create your own measurement config anda set `all_measurements` parameter to `False`. Nevertheless, some default measurements configs are already provided out of the box: | Measurement name | Example | | ---------------- | ---------------------- | @@ -23,9 +16,45 @@ The current pipeline annotates the following measurements out of the box: | `eds.weight` | `12kg`, `1kg300` | | `eds.bmi` | `BMI: 24`, `24 kg.m-2` | | `eds.volume` | `2 cac`, `8ml` | +| `eds.bool` | `positive`, `negatif` | + +The normalized value can then be accessed via the `span._.value` attribute and converted on the fly to a desired unit (eg `span._.value.g_per_cl` or `span._.value.kg_per_m3` for a density). + +The measurements that can be extracted can have one or many of the following characteristics: +- Unitless measurements +- Measurements with unit +- Measurements with range indication (escpecially < or >) +- Measurements with power + +The measurement can be written in many complex forms. Among them, this pipe can detect: +- Measurements with range indication, numerical value, power and units in many different orders and separated by customizable stop words +- Composed units (eg `1m50`) +- Measurement with "unitless patterns", i.e some textual information next to a numerical value which allows us to retrieve a unit even if It is not written (eg in the text `Height: 80`, this pipe will a detect the numlerical value `80`and match It to the unit `kg`) +- Elliptic enumerations (eg `32, 33 et 34mol`) of measurements of the same type and split the measurements accordingly ## Usage +This pipe works better with `eds.dates` and `eds.tables` pipe at the same time. These pipes let `eds.measurements` skip dates as measurements and make a specific matching for each table, benefitting of the structured data. + +The matched measurements are labeled with a default measurement name if available (eg `eds.size`), else `eds.measurement` if any measure is linked to the dimension of the measure's unit and if `all_measurements` is set to `True`. + +As said before, each matched measurement can be accessed via the `span._.value`. This gives you a `SimpleMeasurement` object with the following attributes : +- `value_range` ("<", "=" or ">") +- `value` +- `unit` +- `registry` (This attribute stores the entire unit config like the link between each unit, Its dimension like `length`, `quantity of matter`...) + +`SimpleMeasurement` objects are especially usefull when converting measurements to an other specified unit with the same dimension (eg densities stay densities). To do so, simply call your `SimpleMeasurement` followed by `.` + name of the usual unit abbreviation with `per` and `_` as separators (eg `object.kg_per_dm3`, `mol_per_l`, `g_per_cm2`). + +Moreover, for now, `SimpleMeasurement` objects can be manipulated with the following operations: +- compared with an other `SimpleMeasurement` object with the same dimension with automatic conversion (eg a density in kg_per_m3 and a density in g_per_l) +- summed with an other `SimpleMeasurement` object with the same dimension with automatic conversion +- substracted with an other `SimpleMeasurement` object with the same dimension with automatic conversion + +Note that for all operations listed above, different `value_range` attributes between two units do not matter: by default, the `value_range` of the first measurement is kept. + +Below is a complete example on a use case where we want to extract size, weigth and bmi measurements a simple text. + ```python import spacy @@ -77,7 +106,7 @@ str(measurements[4]._.value.kg_per_m2) ## Custom measurement -You can declare custom measurements by changing the patterns +You can declare custom measurements by changing the patterns. ```python import spacy @@ -114,21 +143,24 @@ nlp.add_pipe( ## Declared extensions The `eds.measurements` pipeline declares a single [spaCy extension](https://spacy.io/usage/processing-pipelines#custom-components-attributes) on the `Span` object, -the `value` attribute that is a `Measurement` instance. +the `value` attribute that is a `SimpleMeasurement` instance. ## Configuration The pipeline can be configured using the following parameters : -| Parameter | Explanation | Default | -| ----------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| `measurements` | A list or dict of the measurements to extract | `["eds.size", "eds.weight", "eds.angle"]` | -| `units_config` | A dict describing the units with lexical patterns, dimensions, scales, ... | ... | -| `number_terms` | A dict describing the textual forms of common numbers | ... | -| `stopwords` | A list of stopwords that do not matter when placed between a unitless trigger | ... | -| `unit_divisors` | A list of terms used to divide two units (like: m / s) | ... | -| `ignore_excluded` | Whether to ignore excluded tokens for matching | `False` | -| `attr` | spaCy attribute to match on, eg `NORM` or `TEXT` | `"NORM"` | +| Parameter | Explanation | Default | +| ------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `measurements` | A list or dict of the measurements to extract | `None` # Extract measurements from all units | +| `units_config` | A dict describing the units with lexical patterns, dimensions, scales, ... | ... # Config of mostly all commonly used units | +| `number_terms` | A dict describing the textual forms of common numbers | ... # Config of mostly all commonly used textual forms of common numbers | +| `value_range_terms` | A dict describing the textual forms of ranges ("<", "=" or ">") | ... # Config of mostly all commonly used range terms | +| `stopwords_unitless` | A list of stopwords that do not matter when placed between a unitless trigger | `["par", "sur", "de", "a", ":", ",", "et"]` | +| `stopwords_measure_unit` | A list of stopwords that do not matter when placed between a measure and a unit | `["|", "¦", "…", "."]` | +| `measure_before_unit` | A bool to tell if the numerical value is usually placed before the unit | `["par", "sur", "de", "a", ":", ",", "et"]` | +| `unit_divisors` | A list of terms used to divide two units (like: m / s) | `["/", "par"]` | +| `ignore_excluded` | Whether to ignore excluded tokens for matching | `False` | +| `attr` | spaCy attribute to match on, eg `NORM` or `TEXT` | `"NORM"` | ## Authors and citation diff --git a/docs/pipelines/misc/tables.md b/docs/pipelines/misc/tables.md new file mode 100644 index 000000000..0acc54f9d --- /dev/null +++ b/docs/pipelines/misc/tables.md @@ -0,0 +1,105 @@ +# Tables + +The `eds.tables` pipeline's role is to detect tables present in a medical document. +We use simple regular expressions to extract tables like text. + +## Usage + +This pipe lets you match different forms of tables. They can have a frame or not, rows can be spread on multiple consecutive lines (in case of a bad parsing for example)... You can also indicate the presence of headers with the `col_names` and `row_names` boolean parameters. + +Each matched table is returned as a `Span` object. You can then access to an equivalent dictionnary formatted table with `table` extension or use `to_pandas_table()` to get the equivalent pandas DataFrame. The key of the dictionnary is determined as folowed: +- If `col_names` is True, then, the dictionnary keys are the names of the columns (str). +- Elif `row_names` is True, then, the dictionnary keys are the names (str). +- Else the dictionnary keys are indexes of the columns (int). + +`to_pandas_table()` can be customised with `as_spans` parameter. If set to `True`, then the pandas dataframe will contain the cells as spans, else the pandas dataframe will contain the cells as raw strings. + +```python +import spacy + +nlp = spacy.blank("fr") +nlp.add_pipe("eds.normalizer") +nlp.add_pipe("eds.tables") + +text = """ +SERVICE +MEDECINE INTENSIVE – +REANIMATION +Réanimation / Surveillance Continue +Médicale + +COMPTE RENDU D'HOSPITALISATION du 05/06/2020 au 10/06/2020 +Madame DUPONT Marie, née le 16/05/1900, âgée de 20 ans, a été hospitalisée en réanimation du +05/06/1920 au 10/06/1920 pour intoxication médicamenteuse volontaire. + + +Examens complémentaires +Hématologie +Numération +Leucocytes ¦x10*9/L ¦4.97 ¦4.09-11 +Hématies ¦x10*12/L¦4.68 ¦4.53-5.79 +Hémoglobine ¦g/dL ¦14.8 ¦13.4-16.7 +Hématocrite ¦% ¦44.2 ¦39.2-48.6 +VGM ¦fL ¦94.4 + ¦79.6-94 +TCMH ¦pg ¦31.6 ¦27.3-32.8 +CCMH ¦g/dL ¦33.5 ¦32.4-36.3 +Plaquettes ¦x10*9/L ¦191 ¦172-398 +VMP ¦fL ¦11.5 + ¦7.4-10.8 + +Sur le plan neurologique : Devant la persistance d'une confusion à distance de l'intoxication au +... + +2/2Pat : |F | | |Intitulé RCP + +""" + +doc = nlp(text) + +# A table span +table = doc.spans["tables"][0] +# Leucocytes ¦x10*9/L ¦4.97 ¦4.09-11 +# Hématies ¦x10*12/L¦4.68 ¦4.53-5.79 +# Hémoglobine ¦g/dL ¦14.8 ¦13.4-16.7 +# Hématocrite ¦% ¦44.2 ¦39.2-48.6 +# VGM ¦fL ¦94.4 + ¦79.6-94 +# TCMH ¦pg ¦31.6 ¦27.3-32.8 +# CCMH ¦g/dL ¦33.5 ¦32.4-36.3 +# Plaquettes ¦x10*9/L ¦191 ¦172-398 +# VMP ¦fL ¦11.5 + ¦7.4-10.8 + +# Convert span to Pandas table +df = table._.to_pd_table(as_spans=False) +type(df) +# >> pandas.core.frame.DataFrame +``` +The pd DataFrame: +| | 0 | 1 | 2 | 3 | +| ---: | :---------- | :------- | :----- | :-------- | +| 0 | Leucocytes | x10*9/L | 4.97 | 4.09-11 | +| 1 | Hématies | x10*12/L | 4.68 | 4.53-5.79 | +| 2 | Hémoglobine | g/dL | 14.8 | 13.4-16.7 | +| 3 | Hématocrite | % | 44.2 | 39.2-48.6 | +| 4 | VGM | fL | 94.4 + | 79.6-94 | +| 5 | TCMH | pg | 31.6 | 27.3-32.8 | +| 6 | CCMH | g/dL | 33.5 | 32.4-36.3 | +| 7 | Plaquettes | x10*9/L | 191 | 172-398 | +| 8 | VMP | fL | 11.5 + | 7.4-10.8 | + +## Declared extensions + +The `eds.tables` pipeline declares two [spaCy extension](https://spacy.io/usage/processing-pipelines#custom-components-attributes) on the `Span` object. The first one is `to_pd_table()` method which returns a parsed pandas version of the table. The second one is `table` which contains the table stored as a dictionnary containing cells as `Span` objects. + +## Configuration + +The pipeline can be configured using the following parameters : + +| Parameter | Explanation | Default | +| ----------------- | ------------------------------------------------ | ---------------------- | +| `tables_pattern` | Pattern to identify table spans | `rf"(\b.*{sep}.*\n)+"` | +| `sep_pattern` | Pattern to identify column separation | `r"¦"` | +| `ignore_excluded` | Ignore excluded tokens | `True` | +| `attr` | spaCy attribute to match on, eg `NORM` or `TEXT` | `"TEXT"` | + +## Authors and citation + +The `eds.tables` pipeline was developed by AP-HP's Data Science team. diff --git a/edsnlp/pipelines/factories.py b/edsnlp/pipelines/factories.py index c3c226112..113716331 100644 --- a/edsnlp/pipelines/factories.py +++ b/edsnlp/pipelines/factories.py @@ -15,6 +15,7 @@ from .misc.measurements.factory import create_component as measurements from .misc.reason.factory import create_component as reason from .misc.sections.factory import create_component as sections +from .misc.tables.factory import create_component as tables from .ner.adicap.factory import create_component as adicap from .ner.cim10.factory import create_component as cim10 from .ner.covid.factory import create_component as covid diff --git a/edsnlp/pipelines/misc/measurements/factory.py b/edsnlp/pipelines/misc/measurements/factory.py index 2e9aefc07..db63bf7de 100644 --- a/edsnlp/pipelines/misc/measurements/factory.py +++ b/edsnlp/pipelines/misc/measurements/factory.py @@ -15,9 +15,15 @@ ignore_excluded=True, units_config=patterns.units_config, number_terms=patterns.number_terms, + value_range_terms=patterns.value_range_terms, unit_divisors=patterns.unit_divisors, measurements=None, - stopwords=patterns.stopwords, + stopwords_unitless=patterns.stopwords_unitless, + stopwords_measure_unit=patterns.stopwords_measure_unit, + measure_before_unit=False, + parse_doc=True, + parse_tables=True, + all_measurements=True, ) @@ -29,7 +35,13 @@ def create_component( measurements: Optional[Union[Dict[str, MeasureConfig], List[str]]], units_config: Dict[str, UnitConfig], number_terms: Dict[str, List[str]], - stopwords: List[str], + value_range_terms: Dict[str, List[str]], + all_measurements: bool, + parse_tables: bool, + parse_doc: bool, + stopwords_unitless: List[str], + stopwords_measure_unit: List[str], + measure_before_unit: bool, unit_divisors: List[str], ignore_excluded: bool, attr: str, @@ -38,9 +50,15 @@ def create_component( nlp, units_config=units_config, number_terms=number_terms, + value_range_terms=value_range_terms, + all_measurements=all_measurements, + parse_tables=parse_tables, + parse_doc=parse_doc, unit_divisors=unit_divisors, measurements=measurements, - stopwords=stopwords, + stopwords_unitless=stopwords_unitless, + stopwords_measure_unit=stopwords_measure_unit, + measure_before_unit=measure_before_unit, attr=attr, ignore_excluded=ignore_excluded, ) diff --git a/edsnlp/pipelines/misc/measurements/measurements.py b/edsnlp/pipelines/misc/measurements/measurements.py index a80b88169..cb8465823 100644 --- a/edsnlp/pipelines/misc/measurements/measurements.py +++ b/edsnlp/pipelines/misc/measurements/measurements.py @@ -9,7 +9,7 @@ import regex import spacy from spacy.tokens import Doc, Span -from typing_extensions import TypedDict +from typing_extensions import NotRequired, TypedDict from edsnlp.matchers.phrase import EDSPhraseMatcher from edsnlp.matchers.regex import RegexMatcher @@ -18,22 +18,26 @@ __all__ = ["MeasurementsMatcher"] - -AFTER_SNIPPET_LIMIT = 6 +AFTER_SNIPPET_LIMIT = 8 BEFORE_SNIPPET_LIMIT = 10 class UnitConfig(TypedDict): - dim: str - degree: int scale: float terms: List[str] followed_by: Optional[str] = None + ui_decomposition: Dict[str, int] + + +class SimpleMeasurementConfigWithoutRegistry(TypedDict): + value_range: str + value: Union[float, int] + unit: str class UnitlessRange(TypedDict): - min: int - max: int + min: NotRequired[int] + max: NotRequired[int] unit: str @@ -48,9 +52,16 @@ class UnitlessPatternConfigWithName(TypedDict): name: str +class ValuelessPatternConfig(TypedDict): + terms: NotRequired[List[str]] + regex: NotRequired[List[str]] + measurement: SimpleMeasurementConfigWithoutRegistry + + class MeasureConfig(TypedDict): unit: str - unitless_patterns: List[UnitlessPatternConfig] + unitless_patterns: NotRequired[List[UnitlessPatternConfig]] + valueless_patterns: NotRequired[List[ValuelessPatternConfig]] class Measurement(abc.ABC): @@ -81,13 +92,23 @@ def __getitem__(self, item) -> "SimpleMeasurement": class UnitRegistry: def __init__(self, config: Dict[str, UnitConfig]): + def generate_inverse_terms(unit_terms): + for unit_term in unit_terms: + yield "/" + unit_term + yield unit_term + "⁻¹" + yield unit_term + "-1" + self.config = {unicodedata.normalize("NFKC", k): v for k, v in config.items()} for unit, unit_config in list(self.config.items()): - if not unit.startswith("per_") and "per_" + unit not in unit_config: + if not unit.startswith("per_") and "per_" + unit not in self.config: self.config["per_" + unit] = { - "dim": unit_config["dim"], - "degree": -unit_config["degree"], "scale": 1 / unit_config["scale"], + "terms": list(generate_inverse_terms(unit_config["terms"])), + "followed_by": None, + "ui_decomposition": { + dim: -degree + for dim, degree in unit_config["ui_decomposition"].items() + }, } @lru_cache(maxsize=-1) @@ -96,13 +117,16 @@ def parse_unit(self, unit: str) -> Tuple[str, float]: scale = 1 for part in regex.split("(?) to their lexical variants + all_measurements: bool + Whether to keep all measurements or only the one specified in measurements. + If True, matched measurements not mentionned in measurements variable + will be labeled "eds.measurement", while the ones mentionned in + measurements variable will be labeled with the specified name. + parse_tables: bool + Whether to parse the tables of the doc detected with "eds.tables" pipe. + parse_doc: bool + Whether to parse the doc without the tables. If parse_tables and parse_doc + are both False, anything is parsed. + stopwords_unitless: List[str] A list of stopwords that do not matter when placed between a unitless - trigger - and a number + trigger and a number + stopwords_measure_unit: List[str] + A list of stopwords that do not matter when placed between a unit and + a number + These stopwords do not matter only in one of the following pattern: + unit - stopwords - measure or measure - stopwords - unit, + according to measure_before_unit parameter. + measure_before_unit: bool + Set It True if the measure is generally before the unit, False + in the other case. + This parameter will indicate if the stopwords in + stopwords_measure_unit should not matter in the unit-stopwords-measure + patterns only (False) or in the measure-stopwords- unit patterns + only (True) unit_divisors: List[str] A list of terms used to divide two units (like: m / s) attr : str @@ -242,10 +345,39 @@ def __init__( self.regex_matcher = RegexMatcher(attr=attr, ignore_excluded=True) self.term_matcher = EDSPhraseMatcher(nlp.vocab, attr=attr, ignore_excluded=True) self.unitless_patterns: Dict[str, UnitlessPatternConfigWithName] = {} + self.valueless_patterns: Dict[str, SimpleMeasurement] = {} + self.value_range_label_hashes: Set[int] = set() self.unit_part_label_hashes: Set[int] = set() self.unitless_label_hashes: Set[int] = set() + self.valueless_label_hashes: Set[int] = set() self.unit_followers: Dict[str, str] = {} self.measure_names: Dict[str, str] = {} + self.measure_before_unit = measure_before_unit + self.all_measurements = all_measurements + self.parse_tables = parse_tables + self.parse_doc = parse_doc + + # INTERVALS + self.regex_matcher.add( + "interval", + [r"-?\s*\d+(?:[.,]\d+)?\s*-\s*-?\s*\d+(?:[.,]\d+)?"], + ) + + # POWERS OF 10 + self.regex_matcher.add( + "pow10", + [ + ( + r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|" + r"(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)" + ), + ], + ) + + # MEASUREMENT VALUE RANGES + for value_range, terms in value_range_terms.items(): + self.term_matcher.build_patterns(nlp, {value_range: terms}) + self.value_range_label_hashes.add(nlp.vocab.strings[value_range]) # NUMBER PATTERNS self.regex_matcher.add( @@ -253,6 +385,7 @@ def __init__( [ r"(? Iterable[Span]: unit_label_hashes.add(units[-1].label) current = [] last = None - if len(current) > 0 or unit_part.label_ != "per": - current.append(unit_part) + current.append(unit_part) last = unit_part end = next( @@ -352,7 +517,6 @@ def extract_units(self, term_matches: Iterable[Span]) -> Iterable[Span]: unit = "_".join(part.label_ for part in current[: end + 1]) units.append(Span(doc, current[0].start, current[end].end, unit)) unit_label_hashes.add(units[-1].label) - return units @classmethod @@ -400,7 +564,37 @@ def make_pseudo_sentence( return pseudo, offsets - def get_matches(self, doc): + @classmethod + def combine_measure_pow10( + cls, + measure: float, + pow10_text: str, + ) -> float: + """ + Return a float based on the measure (float) and the power of + 10 extracted with regex (string) + + Parameters + ---------- + measure: float + pow10_text: str + + Returns + ------- + float + """ + pow10 = int( + re.fullmatch( + ( + r"(?:(?:\s*x\s*10\s*(?:\*{1,2}|\^)\s*)|" + r"(?:\s*\*\s*10\s*(?:\*{2}|\^)\s*))(-?\d+)" + ), + pow10_text, + ).group(1) + ) + return measure * 10**pow10 + + def get_matches(self, doc: Union[Doc, Span]): """ Extract and filter regex and phrase matches in the document to prepare the measurement extraction. @@ -408,7 +602,7 @@ def get_matches(self, doc): Parameters ---------- - doc: Doc + doc: Union[Doc, Span] Returns ------- @@ -433,21 +627,37 @@ def get_matches(self, doc): ] # Filter out measurement-related spans that overlap already matched - # entities (in doc.ents or doc.spans["dates"]) + # entities (in doc.ents or doc.spans["dates"] or doc.spans["tables"]) + # Tables are considered in a separate step # Note: we also include sentence ends tokens as 1-token spans in those matches - spans__keep__is_sent_end = filter_spans( - [ - # Tuples (span, keep = is measurement related, is sentence end) - *zip(doc.spans.get("dates", ()), repeat(False), repeat(False)), - *zip(regex_matches, repeat(True), repeat(False)), - *zip(non_unit_terms, repeat(True), repeat(False)), - *zip(units, repeat(True), repeat(False)), - *zip(doc.ents, repeat(False), repeat(False)), - *zip(sent_ends, repeat(True), repeat(True)), - ], - # filter entities to keep only the ... - sort_key=measurements_match_tuples_sort_key, - ) + if type(doc) == Doc: + spans__keep__is_sent_end = filter_spans( + [ + # Tuples (span, keep = is measurement related, is sentence end) + *zip(doc.spans.get("dates", ()), repeat(False), repeat(False)), + *zip(doc.spans.get("tables", ()), repeat(False), repeat(False)), + *zip(regex_matches, repeat(True), repeat(False)), + *zip(non_unit_terms, repeat(True), repeat(False)), + *zip(units, repeat(True), repeat(False)), + *zip(doc.ents, repeat(False), repeat(False)), + *zip(sent_ends, repeat(True), repeat(True)), + ], + # filter entities to keep only the ... + sort_key=measurements_match_tuples_sort_key, + ) + else: + spans__keep__is_sent_end = filter_spans( + [ + # Tuples (span, keep = is measurement related, is sentence end) + *zip(regex_matches, repeat(True), repeat(False)), + *zip(non_unit_terms, repeat(True), repeat(False)), + *zip(units, repeat(True), repeat(False)), + *zip(doc.ents, repeat(False), repeat(False)), + *zip(sent_ends, repeat(True), repeat(True)), + ], + # filter entities to keep only the ... + sort_key=measurements_match_tuples_sort_key, + ) # Remove non-measurement related spans (keep = False) and sort the matches matches_and_is_sentence_end: List[(Span, bool)] = sorted( @@ -461,9 +671,9 @@ def get_matches(self, doc): return matches_and_is_sentence_end, unit_label_hashes - def extract_measurements(self, doc: Doc): + def extract_measurements_from_doc(self, doc: Doc): """ - Extracts measure entities from the document + Extracts measure entities from the filtered document Parameters ---------- @@ -497,10 +707,15 @@ def get_matches_before(i): doc, matches, { - self.nlp.vocab.strings["stopword"]: ",", + self.nlp.vocab.strings["stopword"]: "o", + self.nlp.vocab.strings["interval"]: ",", + self.nlp.vocab.strings["stopword_unitless"]: ",", + self.nlp.vocab.strings["stopword_measure_unit"]: "s", self.nlp.vocab.strings["number"]: "n", + self.nlp.vocab.strings["pow10"]: "p", **{name: "u" for name in unit_label_hashes}, **{name: "n" for name in self.number_label_hashes}, + **{name: "r" for name in self.value_range_label_hashes}, }, ) @@ -510,6 +725,14 @@ def get_matches_before(i): # Iterate through the number matches for number_idx, (number, is_sent_split) in enumerate(matches): if not is_sent_split and number.label not in self.number_label_hashes: + # Check if we have a valueless pattern + if number.label in self.valueless_label_hashes: + ent = number + ent._.value = self.valueless_patterns[number.label_] + ent.label_ = self.measure_names[ + self.unit_registry.parse_unit(ent._.value.unit)[0] + ] + measurements.append(ent) continue # Detect the measure value @@ -523,6 +746,29 @@ def get_matches_before(i): except ValueError: continue + # Link It to Its adjacent power if available + try: + pow10_idx, pow10_ent = next( + (j, ent) + for j, ent in get_matches_after(number_idx) + if ent.label == self.nlp.vocab.strings["pow10"] + ) + pseudo_sent = pseudo[offsets[number_idx] + 1 : offsets[pow10_idx]] + if re.fullmatch(r"[,o]*", pseudo_sent): + pow10_text = pow10_ent.text + value = self.combine_measure_pow10(value, pow10_text) + except (AttributeError, StopIteration): + pass + + # Check if the measurement is an =, < or > measurement + try: + if pseudo[offsets[number_idx] - 1] == "r": + value_range = matches[number_idx - 1][0].label_ + else: + value_range = "=" + except (KeyError, AttributeError, IndexError): + value_range = "=" + unit_idx = unit_text = unit_norm = None # Find the closest unit after the number @@ -537,10 +783,11 @@ def get_matches_before(i): pass # Try to pair the number with this next unit if the two are only separated - # by numbers and separators alternatively (as in [1][,] [2] [and] [3] cm) + # by numbers (with or without powers of 10) and separators + # (as in [1][,] [2] [and] [3] cm) try: pseudo_sent = pseudo[offsets[number_idx] + 1 : offsets[unit_idx]] - if not re.fullmatch(r"(,n)*", pseudo_sent): + if not re.fullmatch(r"[,o]*p?([,o]n?p?)*", pseudo_sent): unit_text, unit_norm = None, None except TypeError: pass @@ -550,7 +797,10 @@ def get_matches_before(i): if unit_norm is None and number_idx - 1 in matched_unit_indices: try: unit_before = matches[number_idx - 1][0] - if unit_before.end == number.start: + if ( + unit_before.end == number.start + and pseudo[offsets[number_idx] - 2] == "n" + ): unit_norm = self.unit_followers[unit_before.label_] except (KeyError, AttributeError, IndexError): pass @@ -566,45 +816,107 @@ def get_matches_before(i): ) unit_norm = None if re.fullmatch( - r"[,n]*", + r"[,onr]*", pseudo[offsets[unitless_idx] + 1 : offsets[number_idx]], ): unitless_pattern = self.unitless_patterns[unitless_text.label_] unit_norm = next( scope["unit"] for scope in unitless_pattern["ranges"] - if ("min" not in scope or value >= scope["min"]) - and ("max" not in scope or value < scope["max"]) + if ( + "min" not in scope + or value >= scope["min"] + or value_range == ">" + ) + and ( + "max" not in scope + or value < scope["max"] + or value_range == "<" + ) ) except StopIteration: pass - # Otherwise, skip this number + # If no unit was matched, take the nearest unit only if + # It is separated by a stopword from stopwords_measure_unit and + # / or a value_range_term + # Take It before or after the measure according to if not unit_norm: - continue + try: + if not self.measure_before_unit: + (unit_before_idx, unit_before_text) = next( + (j, e) + for j, e in get_matches_before(number_idx) + if e.label in unit_label_hashes + ) + if re.fullmatch( + r"[sor]*", + pseudo[offsets[unit_before_idx] + 1 : offsets[number_idx]], + ): + unit_norm = unit_before_text.label_ + # Check if there is a power of 10 before the unit + if ( + offsets[unit_before_idx] >= 1 + and pseudo[offsets[unit_before_idx] - 1] == "p" + ): + pow10_text = matches[unit_before_idx - 1][0].text + value = self.combine_measure_pow10(value, pow10_text) + else: + (unit_after_idx, unit_after_text) = next( + (j, e) + for j, e in get_matches_after(number_idx) + if e.label in unit_label_hashes + ) + if re.fullmatch( + r"[sop]*", + pseudo[offsets[number_idx] + 1 : offsets[unit_after_idx]], + ): + unit_norm = unit_after_text.label_ + except (AttributeError, StopIteration): + pass + + # Otherwise, set the unit as no_unit if the value + # is not written with letters + if not unit_norm: + if number.label_ == "number": + unit_norm = "nounit" + else: + continue # Compute the final entity - if unit_text and unit_text.end == number.start: - ent = doc[unit_text.start : number.end] - elif unit_text and unit_text.start == number.end: - ent = doc[number.start : unit_text.end] + if type(doc) == Doc: + if unit_text and unit_text.end == number.start: + ent = doc[unit_text.start : number.end] + elif unit_text and unit_text.start == number.end: + ent = doc[number.start : unit_text.end] + else: + ent = number else: - ent = number + if unit_text and unit_text.end == number.start: + ent = doc[unit_text.start - doc.start : number.end - doc.start] + elif unit_text and unit_text.start == number.end: + ent = doc[number.start - doc.start : unit_text.end - doc.start] + else: + ent = number + # If the measure was not requested, dismiss it + # Otherwise, relabel the entity and create the value attribute # Compute the dimensionality of the parsed unit try: dims = self.unit_registry.parse_unit(unit_norm)[0] + ent._.value = SimpleMeasurement( + value_range, value, unit_norm, self.unit_registry + ) + if dims not in self.measure_names: + if self.all_measurements: + ent.label_ = "eds.measurement" + else: + continue + else: + ent.label_ = self.measure_names[dims] except KeyError: continue - # If the measure was not requested, dismiss it - # Otherwise, relabel the entity and create the value attribute - if dims not in self.measure_names: - continue - - ent._.value = SimpleMeasurement(value, unit_norm, self.unit_registry) - ent.label_ = self.measure_names[dims] - measurements.append(ent) if unit_idx is not None: @@ -612,6 +924,375 @@ def get_matches_before(i): return measurements + def extract_measurements_from_tables(self, doc: Doc): + """ + Extracts measure entities from the document tables + + Parameters + ---------- + doc: Doc + + Returns + ------- + List[Span] + """ + + tables = doc.spans.get("tables", None) + measurements = [] + + if not tables: + return [] + + def get_distance_between_columns(column1_key, column2_key): + return abs(keys.index(column1_key) - keys.index(column2_key)) + + for table in tables: + # Try to retrieve columns linked to values + # or columns linked to units + # or columns linked to powers of 10 + # And then iter through the value columns + # to recreate measurements + keys = list(table._.table.keys()) + + unit_column_keys = [] + value_column_keys = [] + pow10_column_keys = [] + # Table with measurements related labellisation + table_labeled = {key: [] for key in keys} + unit_label_hashes = set() + + for key, column in list(table._.table.items()): + # We link the column to values, powers of 10 or units + # if more than half of the cells contain the said object + + # Cell counters + n_unit = 0 + n_value = 0 + n_pow10 = 0 + + for term in column: + + matches_in_term, unit_label_hashes_in_term = self.get_matches(term) + unit_label_hashes = unit_label_hashes.union( + unit_label_hashes_in_term + ) + + measurement_matches = [] + is_unit = False + is_value = False + is_pow10 = False + + for match, _ in matches_in_term: + + if ( + match.label in self.number_label_hashes + or match.label in self.valueless_label_hashes + ): + is_value = True + measurement_matches.append(match) + elif match.label in unit_label_hashes_in_term: + is_unit = True + measurement_matches.append(match) + elif match.label == self.nlp.vocab.strings["pow10"]: + is_pow10 = True + measurement_matches.append(match) + elif match.label in self.value_range_label_hashes: + measurement_matches.append(match) + + if is_unit: + n_unit += 1 + if is_value: + n_value += 1 + if is_pow10: + n_pow10 += 1 + + table_labeled[key].append(measurement_matches) + + # Checking if half of the cells contain units, values + # or powers of 10 + if n_unit > len(column) / 2: + unit_column_keys.append(key) + if n_value > len(column) / 2: + value_column_keys.append(key) + if n_pow10 > len(column) / 2: + pow10_column_keys.append(key) + + # Iter through the value keys to create measurements + for value_column_key in value_column_keys: + + # If the table contains a unit column, + # try to pair the value to the unit of + # the nearest unit column + if len(unit_column_keys): + # Prevent same distance conflict + # For example is a table is organised as + # "header, unit1, value1, unit2, value2" + # value1 is at equal distance of unit1 and unit2 columns + # To solve this problem, we try to detect if we have a + # value - unit pattern or unit - value pattern by checking + # the first column that appears. + if keys.index(unit_column_keys[0]) > keys.index( + value_column_keys[0] + ): + measure_before_unit_in_table = True + else: + measure_before_unit_in_table = False + + # We only consider the nearest unit column when It + # is not a value column at the same time + # except if It is the column that we are considering + try: + unit_column_key = sorted( + [ + unit_column_key + for unit_column_key in unit_column_keys + if unit_column_key + not in [ + v + for v in value_column_keys + if v != value_column_key + ] + ], + key=lambda unit_column_key: get_distance_between_columns( + unit_column_key, value_column_key + ), + )[0 : min(2, len(unit_column_keys))][ + 0 * (not measure_before_unit_in_table) + - 1 * measure_before_unit_in_table + ] + except IndexError: + unit_column_key = value_column_key + else: + unit_column_key = value_column_key + + # If the table contains a power column, + # try to pair the value to the power of + # the nearest power column + if len(pow10_column_keys): + # Same distance conflict as for unit columns + if keys.index(pow10_column_keys[0]) > keys.index( + value_column_keys[0] + ): + measure_before_power_in_table = True + else: + measure_before_power_in_table = False + + try: + pow10_column_key = sorted( + [ + pow10_column_key + for pow10_column_key in pow10_column_keys + if pow10_column_key + not in [ + v + for v in value_column_keys + if v != value_column_key + ] + ], + key=lambda pow10_column_key: get_distance_between_columns( + pow10_column_key, value_column_key + ), + )[0 : min(2, len(pow10_column_keys))][ + 0 * (not measure_before_power_in_table) + - 1 * measure_before_power_in_table + ] + except IndexError: + pow10_column_key = value_column_key + else: + pow10_column_key = value_column_key + + # If unit column is the same as value column, extract + # measurement in this column with the + # extract_measurements_from_doc method + + if unit_column_key == value_column_key: + # Consider possible pow10 column + if pow10_column_key != value_column_key: + for term, pow10_list in zip( + table._.table[value_column_key], + table_labeled[pow10_column_key], + ): + measurements_part = self.extract_measurements_from_doc(term) + try: + pow10_text = [ + p.text + for p in pow10_list + if p.label == self.nlp.vocab.strings["pow10"] + ][0] + for measurement in measurements_part: + measurement._.value.value = ( + self.combine_measure_pow10( + measurement._.value.value, pow10_text + ) + ) + except IndexError: + pass + measurements += measurements_part + else: + for term in table._.table[value_column_key]: + measurements += self.extract_measurements_from_doc(term) + continue + + # If unit column is different from value column + # Iter through the value column to create the measurement + # Iter through the units and powers columns + # at the same time if they exist, else value column + for unit_list, value_list, pow10_list in zip( + table_labeled[unit_column_key], + table_labeled[value_column_key], + table_labeled[pow10_column_key], + ): + # Check if there is really a value + try: + ent = [ + v + for v in value_list + if v.label in self.number_label_hashes + or v.label in self.valueless_label_hashes + ][0] + # Take the value linked to valueless pattern if + # we have a valueless pattern + if ent.label in self.valueless_label_hashes: + ent._.value = self.valueless_patterns[ent.label_] + ent.label_ = self.measure_names[ + self.unit_registry.parse_unit(ent._.value.unit)[0] + ] + measurements.append(ent) + continue + # Else try to parse the number + if ent.label_ == "number": + value = float( + ent.text.replace(" ", "") + .replace(",", ".") + .replace(" ", "") + ) + else: + value = float(ent.label_) + # Sometimes the value column contains a power. + # It may not be common enough to reach 50% + # of the cells, that's why + # It may not be labeled as pow10_column. + # Still, we should retrieve these powers. + try: + pow10_text = [ + p.text + for p in value_list + if p.label == self.nlp.vocab.strings["pow10"] + ][0] + value = self.combine_measure_pow10(value, pow10_text) + except IndexError: + pass + except (IndexError, ValueError): + continue + + # Check for value range terms + try: + value_range = [ + v_r.label_ + for v_r in value_list + if v_r.label in self.value_range_label_hashes + ][0] + except IndexError: + value_range = "=" + + # Check for units and powers in the unit column + # (for same reasons as described before) + # in units column + try: + unit_norm = [ + u.label_ for u in unit_list if u.label in unit_label_hashes + ][0] + # To avoid duplicates + if unit_column_key != value_column_key: + try: + pow10_text = [ + p.text + for p in unit_list + if p.label == self.nlp.vocab.strings["pow10"] + ][0] + value = self.combine_measure_pow10(value, pow10_text) + except IndexError: + pass + except IndexError: + unit_norm = "nounit" + + if unit_norm == "nounit": + # Try to retrieve a possible unit in the header + # of the value column + try: + unit_norm = [ + u.label_ + for u in self.extract_units( + list( + self.term_matcher( + self.nlp(str(value_column_key)), + as_spans=True, + ) + ) + ) + ][0] + except IndexError: + pass + + # Check for powers in power column + try: + if ( + pow10_column_key != value_column_key + and pow10_column_key != unit_column_key + ): + pow10_text = [ + p.text + for p in pow10_list + if p.label == self.nlp.vocab.strings["pow10"] + ][0] + value = self.combine_measure_pow10(value, pow10_text) + except IndexError: + pass + + if self.all_measurements: + ent._.value = SimpleMeasurement( + value_range, value, unit_norm, self.unit_registry + ) + ent.label_ = "eds.measurement" + else: + # If the measure was not requested, dismiss it + # Otherwise, relabel the entity and create the value attribute + # Compute the dimensionality of the parsed unit + try: + dims = self.unit_registry.parse_unit(unit_norm)[0] + if dims not in self.measure_names: + continue + ent._.value = SimpleMeasurement( + value_range, value, unit_norm, self.unit_registry + ) + ent.label_ = self.measure_names[dims] + except KeyError: + continue + + measurements.append(ent) + + return measurements + + def extract_measurements(self, doc: Doc): + """ + Extracts measure entities from the document + + Parameters + ---------- + doc: Doc + + Returns + ------- + List[Span] + """ + measurements = [] + if self.parse_doc: + measurements += self.extract_measurements_from_doc(doc) + if self.parse_tables: + measurements += self.extract_measurements_from_tables(doc) + measurements = filter_spans(measurements) + return measurements + @classmethod def merge_adjacent_measurements(cls, measurements: List[Span]) -> List[Span]: """ diff --git a/edsnlp/pipelines/misc/measurements/patterns.py b/edsnlp/pipelines/misc/measurements/patterns.py index b2f0830a8..cf34e2311 100644 --- a/edsnlp/pipelines/misc/measurements/patterns.py +++ b/edsnlp/pipelines/misc/measurements/patterns.py @@ -31,12 +31,109 @@ "1000": ["mille", "milles"], } + +value_range_terms = { + "<": ["<", "<=", "inferieure a", "inferieur a", "inf a", "inf"], + ">": [">", ">=", "superieure a", "superieur a", "sup a", "sup"], +} + + +common_measurements = { + "eds.weight": { + "unit": "kg", + "unitless_patterns": [ + { + "terms": ["poids", "poid", "pese", "pesant", "pesait", "pesent"], + "ranges": [ + {"min": 0, "max": 200, "unit": "kg"}, + {"min": 200, "unit": "g"}, + ], + } + ], + }, + "eds.size": { + "unit": "m", + "unitless_patterns": [ + { + "terms": [ + "mesure", + "taille", + "mesurant", + "mesurent", + "mesurait", + "mesuree", + "hauteur", + "largeur", + "longueur", + ], + "ranges": [ + {"min": 0, "max": 3, "unit": "m"}, + {"min": 3, "unit": "cm"}, + ], + } + ], + }, + "eds.bmi": { + "unit": "kg_per_m2", + "unitless_patterns": [ + {"terms": ["imc", "bmi"], "ranges": [{"unit": "kg_per_m2"}]} + ], + }, + "eds.volume": {"unit": "m3", "unitless_patterns": []}, + "eds.bool": { + "unit": "bool", + "valueless_patterns": [ + { + "terms": ["positif", "positifs", "positive", "positives"], + "measurement": { + "value_range": "=", + "value": 1, + "unit": "bool", + }, + }, + { + "terms": ["negatif", "negatifs", "negative", "negatives"], + "measurement": { + "value_range": "=", + "value": 0, + "unit": "bool", + }, + }, + ], + }, +} + + +unit_divisors = ["/", "par"] + + +stopwords_unitless = ["par", "sur", "de", "a", ":", ",", "et"] + + +stopwords_measure_unit = ["|", "¦", "…", "."] + + units_config = { - # Lengths - "µm": { - "dim": "length", - "degree": 1, - "scale": 1e-4, + "fm": { + "scale": 1e-15, + "terms": ["femtometre", "femtometres", "femto-metre", "femto-metres", "fm"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "pm": { + "scale": 1e-12, + "terms": ["picometre", "picometres", "pico-metre", "pico-metres", "pm"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "nm": { + "scale": 1e-09, + "terms": ["nanometre", "nanometres", "nano-metre", "nano-metres", "nm"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "μm": { + "scale": 1e-06, "terms": [ "micrometre", "micrometres", @@ -46,40 +143,106 @@ "um", ], "followed_by": None, + "ui_decomposition": {"length": 1}, }, "mm": { - "dim": "length", - "degree": 1, - "scale": 1e-1, + "scale": 0.001, "terms": ["millimetre", "millimetres", "milimetre", "milimetres", "mm"], "followed_by": None, + "ui_decomposition": {"length": 1}, }, "cm": { - "dim": "length", - "degree": 1, - "scale": 1e0, + "scale": 0.01, "terms": ["centimetre", "centimetres", "cm"], "followed_by": None, + "ui_decomposition": {"length": 1}, }, "dm": { - "dim": "length", - "degree": 1, - "scale": 1e1, + "scale": 0.1, "terms": ["decimetre", "decimetres", "dm"], "followed_by": None, + "ui_decomposition": {"length": 1}, }, "m": { - "dim": "length", - "degree": 1, - "scale": 1e2, + "scale": 1.0, "terms": ["metre", "metres", "m"], "followed_by": "cm", + "ui_decomposition": {"length": 1}, + }, + "dam": { + "scale": 10.0, + "terms": ["decametre", "decametres", "dam"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "hm": { + "scale": 100.0, + "terms": ["hectometre", "hectometres", "hm"], + "followed_by": None, + "ui_decomposition": {"length": 1}, + }, + "km": { + "scale": 1000.0, + "terms": ["kilometre", "kilometres", "km"], + "followed_by": "m", + "ui_decomposition": {"length": 1}, + }, + "fg": { + "scale": 1e-18, + "terms": [ + "femtogramme", + "femtogrammes", + "femto-gramme", + "femto-grammes", + "fgr", + "fg", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "pg": { + "scale": 1e-15, + "terms": [ + "picogramme", + "picogrammes", + "pico-gramme", + "pico-grammes", + "pgr", + "pg", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "ng": { + "scale": 1e-12, + "terms": [ + "nanogramme", + "nanogrammes", + "nano-gramme", + "nano-grammes", + "ngr", + "ng", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "µg": { + "scale": 1e-9, + "terms": [ + "microgramme", + "microgrammes", + "micro-gramme", + "micro-grammes", + "µgr", + "µg", + "ugr", + "ug", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1}, }, - # Weights "mg": { - "dim": "mass", - "degree": 1, - "scale": 1e0, + "scale": 1e-6, "terms": [ "milligramme", "miligramme", @@ -89,489 +252,2129 @@ "mg", ], "followed_by": None, + "ui_decomposition": {"mass": 1}, }, "cg": { - "dim": "mass", - "degree": 1, - "scale": 1e1, + "scale": 1e-5, "terms": ["centigramme", "centigrammes", "cg", "cgr"], "followed_by": None, + "ui_decomposition": {"mass": 1}, }, "dg": { - "dim": "mass", - "degree": 1, - "scale": 1e2, + "scale": 1e-4, "terms": ["decigramme", "decigrammes", "dgr", "dg"], "followed_by": None, + "ui_decomposition": {"mass": 1}, }, "g": { - "dim": "mass", - "degree": 1, - "scale": 1e3, + "scale": 0.001, "terms": ["gramme", "grammes", "gr", "g"], "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "dag": { + "scale": 0.01, + "terms": ["decagramme", "decagrammes", "dagr", "dag"], + "followed_by": None, + "ui_decomposition": {"mass": 1}, + }, + "hg": { + "scale": 0.1, + "terms": ["hectogramme", "hectogrammes", "hgr", "hg"], + "followed_by": None, + "ui_decomposition": {"mass": 1}, }, "kg": { - "dim": "mass", - "degree": 1, - "scale": 1e6, + "scale": 1.0, "terms": ["kilo", "kilogramme", "kilogrammes", "kgr", "kg"], "followed_by": "g", + "ui_decomposition": {"mass": 1}, + }, + "fs": { + "scale": 1e-15, + "terms": [ + "femtoseconde", + "femtosecondes", + "femto-seconde", + "femto-secondes", + "fs", + ], + "followed_by": None, + "ui_decomposition": {"time": 1}, + }, + "ps": { + "scale": 1e-12, + "terms": ["picoseconde", "picosecondes", "pico-seconde", "pico-secondes", "ps"], + "followed_by": None, + "ui_decomposition": {"time": 1}, + }, + "ns": { + "scale": 1e-09, + "terms": ["nanoseconde", "nanosecondes", "nano-seconde", "nano-secondes", "ns"], + "followed_by": None, + "ui_decomposition": {"time": 1}, + }, + "µs": { + "scale": 1e-06, + "terms": [ + "microseconde", + "microsecondes", + "micro-seconde", + "micro-secondes", + "µs", + "us", + ], + "followed_by": None, + "ui_decomposition": {"time": 1}, + }, + "ms": { + "scale": 0.001, + "terms": ["milliseconde", "millisecondes", "miliseconde", "milisecondes", "ms"], + "followed_by": None, + "ui_decomposition": {"time": 1}, }, - # Durations - "second": { - "dim": "time", - "degree": 1, + "s": { "scale": 1, "terms": ["seconde", "secondes", "s"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, - "minute": { - "dim": "time", - "degree": 1, + "min": { "scale": 60, "terms": ["mn", "min", "minute", "minutes"], "followed_by": "second", + "ui_decomposition": {"time": 1}, }, - "hour": { - "dim": "time", - "degree": 1, + "h": { "scale": 3600, - "terms": ["heure", "h"], + "terms": ["heure", "heures", "h"], "followed_by": "minute", + "ui_decomposition": {"time": 1}, }, "day": { - "dim": "time", - "degree": 1, - "scale": 3600 * 1, + "scale": 3600, "terms": ["jour", "jours", "j"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, "month": { - "dim": "time", - "degree": 1, - "scale": 3600 * 30.4167, + "scale": 109500.12, "terms": ["mois"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, "week": { - "dim": "time", - "degree": 1, - "scale": 3600 * 7, + "scale": 25200, "terms": ["semaine", "semaines"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, "year": { - "dim": "time", - "degree": 1, - "scale": 3600 * 365.25, + "scale": 1314900.0, "terms": ["an", "année", "ans", "années"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, - # Angle "arc-second": { - "dim": "time", - "degree": 1, - "scale": 2 / 60.0, + "scale": 0.03333333333333333, "terms": ['"', "''"], "followed_by": None, + "ui_decomposition": {"time": 1}, }, "arc-minute": { - "dim": "time", - "degree": 1, "scale": 2, "terms": ["'"], "followed_by": "arc-second", + "ui_decomposition": {"time": 1}, }, "degree": { - "dim": "time", - "degree": 1, "scale": 120, "terms": ["degre", "°", "deg"], "followed_by": "arc-minute", + "ui_decomposition": {"time": 1}, }, - # Temperature "celcius": { - "dim": "temperature", - "degree": 1, "scale": 1, "terms": ["°C", "° celcius", "celcius"], "followed_by": None, + "ui_decomposition": {"temperature": 1}, + }, + "fl": { + "scale": 1e-18, + "terms": ["femtolitre", "femtolitres", "femto-litre", "femto-litres", "fl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "pl": { + "scale": 1e-15, + "terms": ["picolitre", "picolitres", "pico-litre", "pico-litres", "pl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "nl": { + "scale": 1e-12, + "terms": ["nanolitre", "nanolitres", "nano-litre", "nano-litres", "nl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "µl": { + "scale": 1e-09, + "terms": [ + "microlitre", + "microlitres", + "micro-litre", + "micro-litres", + "µl", + "ul", + ], + "followed_by": None, + "ui_decomposition": {"length": 3}, }, - # Volumes "ml": { - "dim": "length", - "degree": 3, - "scale": 1e0, + "scale": 1e-06, "terms": ["mililitre", "millilitre", "mililitres", "millilitres", "ml"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "cl": { - "dim": "length", - "degree": 3, - "scale": 1e1, + "scale": 1e-05, "terms": ["centilitre", "centilitres", "cl"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "dl": { - "dim": "length", - "degree": 3, - "scale": 1e2, + "scale": 0.0001, "terms": ["decilitre", "decilitres", "dl"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "l": { - "dim": "length", - "degree": 3, - "scale": 1e3, - "terms": ["litre", "litres", "l", "dm3"], + "scale": 0.001, + "terms": ["litre", "litres", "l"], "followed_by": "ml", + "ui_decomposition": {"length": 3}, + }, + "dal": { + "scale": 0.01, + "terms": ["decalitre", "decalitres", "dal"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "hl": { + "scale": 0.1, + "terms": ["hectolitre", "hectolitres", "hl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "kl": { + "scale": 1.0, + "terms": ["kilolitre", "kilolitres", "kl"], + "followed_by": None, + "ui_decomposition": {"length": 3}, }, "cac": { - "dim": "length", - "degree": 3, - "scale": 5e-3, + "scale": 5e-09, "terms": ["cac", "c.a.c", "cuillere à café", "cuillères à café"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "goutte": { - "dim": "length", - "degree": 3, - "scale": 5e-5, + "scale": 5e-11, "terms": ["gt", "goutte"], "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "fm3": { + "scale": 1e-45, + "terms": ["fm3", "fm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "pm3": { + "scale": 1e-36, + "terms": ["pm3", "pm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "nm3": { + "scale": 1e-27, + "terms": ["nm3", "nm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "µm3": { + "scale": 1e-18, + "terms": ["um3", "um³", "µm3", "µm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, }, "mm3": { - "dim": "length", - "degree": 3, - "scale": 1e-3, + "scale": 1e-09, "terms": ["mm3", "mm³"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "cm3": { - "dim": "length", - "degree": 3, - "scale": 1e0, + "scale": 1e-06, "terms": ["cm3", "cm³", "cc"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "dm3": { - "dim": "length", - "degree": 3, - "scale": 1e3, + "scale": 0.001, "terms": ["dm3", "dm³"], "followed_by": None, + "ui_decomposition": {"length": 3}, }, "m3": { - "dim": "length", - "degree": 3, - "scale": 1e6, + "scale": 1, "terms": ["m3", "m³"], "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "dam3": { + "scale": 1000.0, + "terms": ["dam3", "dam³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "hm3": { + "scale": 1000000.0, + "terms": ["hm3", "hm³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "km3": { + "scale": 1000000000.0, + "terms": ["km3", "km³"], + "followed_by": None, + "ui_decomposition": {"length": 3}, + }, + "fm2": { + "scale": 1e-30, + "terms": ["fm2", "fm²"], + "followed_by": None, + "ui_decomposition": {"length": 2}, + }, + "pm2": { + "scale": 1e-24, + "terms": ["fm2", "fm²"], + "followed_by": None, + "ui_decomposition": {"length": 2}, }, - # Surfaces - "µm2": { - "dim": "length", - "degree": 2, - "scale": 1e-8, - "terms": ["µm2", "µm²"], + "nm2": { + "scale": 1e-18, + "terms": ["nm2", "nm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, + }, + "μm2": { + "scale": 1e-12, + "terms": ["µm2", "µm²", "um2", "um²"], + "followed_by": None, + "ui_decomposition": {"length": 2}, }, "mm2": { - "dim": "length", - "degree": 2, - "scale": 1e-2, + "scale": 1e-06, "terms": ["mm2", "mm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, "cm2": { - "dim": "length", - "degree": 2, - "scale": 1e0, + "scale": 0.0001, "terms": ["cm2", "cm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, "dm2": { - "dim": "length", - "degree": 2, - "scale": 1e2, + "scale": 0.01, "terms": ["dm2", "dm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, "m2": { - "dim": "length", - "degree": 2, - "scale": 1e4, + "scale": 1.0, "terms": ["m2", "m²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, - # International units - "mui": { - "dim": "ui", - "degree": 1, - "scale": 1e0, - "terms": ["mui", "m ui"], + "dam2": { + "scale": 100.0, + "terms": ["dam2", "dam²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, - "dui": { - "dim": "ui", - "degree": 1, - "scale": 1e1, - "terms": ["dui", "d ui"], + "hm2": { + "scale": 10000.0, + "terms": ["hm2", "hm²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, - "cui": { - "dim": "ui", - "degree": 1, - "scale": 1e2, - "terms": ["cui", "c ui"], + "km2": { + "scale": 1000000.0, + "terms": ["km2", "km²"], "followed_by": None, + "ui_decomposition": {"length": 2}, }, - "ui": { - "dim": "ui", - "degree": 1, - "scale": 1e3, - "terms": ["ui"], + "fui": { + "scale": 1e-15, + "terms": ["fui", "f ui", "fu", "f u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - # Inverse - "per_µm": { - "dim": "length", - "degree": -1, - "scale": 1e4, - "terms": ["µm-1"], + "pui": { + "scale": 1e-12, + "terms": ["pui", "p ui", "pu", "p u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_mm": { - "dim": "length", - "degree": -1, - "scale": 1e1, - "terms": ["mm-1"], + "nui": { + "scale": 1e-09, + "terms": ["nui", "n ui", "nu", "n u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_cm": { - "dim": "length", - "degree": -1, - "scale": 1e0, - "terms": ["cm-1"], + "µui": { + "scale": 1e-06, + "terms": ["µui", "µ ui", "uui", "u ui", "µu", "µ u", "uu", "u u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_dm": { - "dim": "length", - "degree": -1, - "scale": 1e-1, - "terms": ["dm-1"], + "mui": { + "scale": 0.001, + "terms": ["mui", "m ui", "mu", "m u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_m": { - "dim": "length", - "degree": -1, - "scale": 1e-3, - "terms": ["m-1"], + "cui": { + "scale": 0.01, + "terms": ["cui", "c ui", "cu", "c u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_mg": { - "dim": "mass", - "degree": -1, - "scale": 1e-0, - "terms": ["mgr-1", "mg-1", "mgr⁻¹", "mg⁻¹"], + "dui": { + "scale": 0.1, + "terms": ["dui", "d ui", "du", "d u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_cg": { - "dim": "mass", - "degree": -1, - "scale": 1e-1, - "terms": ["cg-1", "cgr-1", "cg⁻¹", "cgr⁻¹"], + "ui": { + "scale": 1.0, + "terms": ["ui", "u"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_dg": { - "dim": "mass", - "degree": -1, - "scale": 1e-2, - "terms": ["dgr-1", "dg-1", "dgr⁻¹", "dg⁻¹"], + "daui": { + "scale": 10.0, + "terms": ["daui", "dau"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_g": { - "dim": "mass", - "degree": -1, - "scale": 1e-3, - "terms": ["gr-1", "g-1", "gr⁻¹", "g⁻¹"], + "hui": { + "scale": 100.0, + "terms": ["hui", "hu"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_kg": { - "dim": "mass", - "degree": -1, - "scale": 1e-6, - "terms": ["kgr-1", "kg-1", "kgr⁻¹", "kg⁻¹"], + "kui": { + "scale": 1000.0, + "terms": ["kui", "ku"], "followed_by": None, + "ui_decomposition": {"ui": 1}, }, - "per_ml": { - "dim": "length", - "degree": -3, - "scale": 1e-0, - "terms": ["ml-1", "ml⁻¹"], + "fmol": { + "scale": 1e-15, + "terms": ["fmol", "f mol", "fmole", "f mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_cl": { - "dim": "length", - "degree": -3, - "scale": 1e-1, - "terms": ["cl-1", "cl⁻¹"], + "pmol": { + "scale": 1e-12, + "terms": ["pmol", "p mol", "pmole", "p mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_dl": { - "dim": "length", - "degree": -3, - "scale": 1e-2, - "terms": ["dl-1", "dl⁻¹"], + "nmol": { + "scale": 1e-09, + "terms": ["nmol", "n mol", "nmole", "n mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_l": { - "dim": "length", - "degree": -3, - "scale": 1e-3, - "terms": ["l-1", "l⁻¹"], + "µmol": { + "scale": 1e-06, + "terms": ["µmol", "µ mol", "umole", "u mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_mm3": { - "dim": "length", - "degree": -3, - "scale": 1e3, - "terms": ["mm-3", "mm⁻³"], + "mmol": { + "scale": 0.001, + "terms": ["mmol", "m mol", "mmole", "m mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_cm3": { - "dim": "length", - "degree": -3, - "scale": 1e-0, - "terms": ["cm-3", "cm⁻³", "cc-1", "cc⁻¹"], + "cmol": { + "scale": 0.01, + "terms": ["cmol", "c mol", "cmole", "c mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_dm3": { - "dim": "length", - "degree": -3, - "scale": 1e-3, - "terms": ["dm-3", "dm⁻³"], + "dmol": { + "scale": 0.1, + "terms": ["dmol", "d mol", "dmole", "d mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_m3": { - "dim": "length", - "degree": -3, - "scale": 1e-6, - "terms": ["m-3", "m⁻³"], + "mol": { + "scale": 1.0, + "terms": ["mol", "mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_mui": { - "dim": "ui", - "degree": -1, - "scale": 1e-0, - "terms": ["mui-1", "mui⁻¹"], + "damol": { + "scale": 10.0, + "terms": ["damol", "da mol", "damole", "da mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_dui": { - "dim": "ui", - "degree": -1, - "scale": 1e-1, - "terms": ["dui-1", "dui⁻¹"], + "hmol": { + "scale": 100.0, + "terms": ["hmol", "h mol", "hmole", "h mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_cui": { - "dim": "ui", - "degree": -1, - "scale": 1e-2, - "terms": ["cui-1", "cui⁻¹"], + "kmol": { + "scale": 1000.0, + "terms": ["kmol", "k mol", "kmole", "k mole"], "followed_by": None, + "ui_decomposition": {"nsubstance": 1}, }, - "per_ui": { - "dim": "ui", - "degree": -1, - "scale": 1e-3, - "terms": ["ui-1", "ui⁻¹"], + "per_fm": { + "scale": 1.0e15, + "terms": [ + "/femtometre", + "femtometre⁻¹", + "femtometre-1", + "/femtometres", + "femtometres⁻¹", + "femtometres-1", + "/femto-metre", + "femto-metre⁻¹", + "femto-metre-1", + "/femto-metres", + "femto-metres⁻¹", + "femto-metres-1", + "/fm", + "fm⁻¹", + "fm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - # Surfaces - "per_µm2": { - "dim": "length", - "degree": -2, - "scale": 1e8, - "terms": ["µm-2", "µm⁻²"], + "per_pm": { + "scale": 1.0e12, + "terms": [ + "/picometre", + "picometre⁻¹", + "picometre-1", + "/picometres", + "picometres⁻¹", + "picometres-1", + "/pico-metre", + "pico-metre⁻¹", + "pico-metre-1", + "/pico-metres", + "pico-metres⁻¹", + "pico-metres-1", + "/pm", + "pm⁻¹", + "pm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "per_mm2": { - "dim": "length", - "degree": -2, - "scale": 1e2, - "terms": ["mm-2", "mm⁻²"], + "per_nm": { + "scale": 1.0e9, + "terms": [ + "/nanometre", + "nanometre⁻¹", + "nanometre-1", + "/nanometres", + "nanometres⁻¹", + "nanometres-1", + "/nano-metre", + "nano-metre⁻¹", + "nano-metre-1", + "/nano-metres", + "nano-metres⁻¹", + "nano-metres-1", + "/nm", + "nm⁻¹", + "nm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "per_cm2": { - "dim": "length", - "degree": -2, - "scale": 1e-0, - "terms": ["cm-2", "cm⁻²"], + "per_μm": { + "scale": 1.0e6, + "terms": [ + "/micrometre", + "micrometre⁻¹", + "micrometre-1", + "/micrometres", + "micrometres⁻¹", + "micrometres-1", + "/micro-metre", + "micro-metre⁻¹", + "micro-metre-1", + "/micrometres", + "micrometres⁻¹", + "micrometres-1", + "/µm", + "µm⁻¹", + "µm-1", + "/um", + "um⁻¹", + "um-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "per_dm2": { - "dim": "length", - "degree": -2, - "scale": 1e-2, - "terms": ["dm-2", "dm⁻²"], + "per_mm": { + "scale": 1000.0, + "terms": [ + "/millimetre", + "millimetre⁻¹", + "millimetre-1", + "/millimetres", + "millimetres⁻¹", + "millimetres-1", + "/milimetre", + "milimetre⁻¹", + "milimetre-1", + "/milimetres", + "milimetres⁻¹", + "milimetres-1", + "/mm", + "mm⁻¹", + "mm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "per_m2": { - "dim": "length", - "degree": -2, - "scale": 1e-4, - "terms": ["m-2", "m⁻²"], + "per_cm": { + "scale": 100.0, + "terms": [ + "/centimetre", + "centimetre⁻¹", + "centimetre-1", + "/centimetres", + "centimetres⁻¹", + "centimetres-1", + "/cm", + "cm⁻¹", + "cm-1", + ], "followed_by": None, + "ui_decomposition": {"length": -1}, }, -} - - -common_measurements = { - "eds.weight": { - "unit": "kg", - "unitless_patterns": [ - { - "terms": ["poids", "poid", "pese", "pesant", "pesait", "pesent"], - "ranges": [ - {"min": 0, "max": 200, "unit": "kg"}, - {"min": 200, "unit": "g"}, - ], - } + "per_dm": { + "scale": 10.0, + "terms": [ + "/decimetre", + "decimetre⁻¹", + "decimetre-1", + "/decimetres", + "decimetres⁻¹", + "decimetres-1", + "/dm", + "dm⁻¹", + "dm-1", ], + "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "eds.size": { - "unit": "m", - "unitless_patterns": [ - { - "terms": [ - "mesure", - "taille", - "mesurant", - "mesurent", - "mesurait", - "mesuree", - "hauteur", - "largeur", - "longueur", - ], - "ranges": [ - {"min": 0, "max": 3, "unit": "m"}, - {"min": 3, "unit": "cm"}, - ], - } + "per_m": { + "scale": 1.0, + "terms": [ + "/metre", + "metre⁻¹", + "metre-1", + "/metres", + "metres⁻¹", + "metres-1", + "/m", + "m⁻¹", + "m-1", ], + "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "eds.bmi": { - "unit": "kg_per_m2", - "unitless_patterns": [ - {"terms": ["imc", "bmi"], "ranges": [{"unit": "kg_per_m2"}]} + "per_dam": { + "scale": 0.1, + "terms": [ + "/decametre", + "decametre⁻¹", + "decametre-1", + "/decametres", + "decametres⁻¹", + "decametres-1", + "/dam", + "dam⁻¹", + "dam-1", ], + "followed_by": None, + "ui_decomposition": {"length": -1}, }, - "eds.volume": {"unit": "m3", "unitless_patterns": []}, -} - -unit_divisors = ["/", "par"] - -stopwords = ["par", "sur", "de", "a", ":", ",", "et"] + "per_hm": { + "scale": 0.01, + "terms": [ + "/hectometre", + "hectometre⁻¹", + "hectometre-1", + "/hectometres", + "hectometres⁻¹", + "hectometres-1", + "/hm", + "hm⁻¹", + "hm-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -1}, + }, + "per_km": { + "scale": 0.001, + "terms": [ + "/kilometre", + "kilometre⁻¹", + "kilometre-1", + "/kilometres", + "kilometres⁻¹", + "kilometres-1", + "/km", + "km⁻¹", + "km-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -1}, + }, + "per_fg": { + "scale": 1.0e18, + "terms": [ + "/femtogramme", + "femtogramme⁻¹", + "femtogramme-1", + "/femtogrammes", + "femtogrammes⁻¹", + "femtogrammes-1", + "/femto-gramme", + "femto-gramme⁻¹", + "femto-gramme-1", + "/femto-grammes", + "femto-grammes⁻¹", + "femto-grammes-1", + "/fgr", + "fgr⁻¹", + "fgr-1", + "/fg", + "fg⁻¹", + "fg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_pg": { + "scale": 1.0e15, + "terms": [ + "/picogramme", + "picogramme⁻¹", + "picogramme-1", + "/picogrammes", + "picogrammes⁻¹", + "picogrammes-1", + "/pico-gramme", + "pico-gramme⁻¹", + "pico-gramme-1", + "/pico-grammes", + "pico-grammes⁻¹", + "pico-grammes-1", + "/pgr", + "pgr⁻¹", + "pgr-1", + "/pg", + "pg⁻¹", + "pg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_ng": { + "scale": 1.0e12, + "terms": [ + "/nanogramme", + "nanogramme⁻¹", + "nanogramme-1", + "/nanogrammes", + "nanogrammes⁻¹", + "nanogrammes-1", + "/nano-gramme", + "nano-gramme⁻¹", + "nano-gramme-1", + "/nano-grammes", + "nano-grammes⁻¹", + "nano-grammes-1", + "/ngr", + "ngr⁻¹", + "ngr-1", + "/ng", + "ng⁻¹", + "ng-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_µg": { + "scale": 1.0e9, + "terms": [ + "/microgramme", + "microgramme⁻¹", + "microgramme-1", + "/microgrammes", + "microgrammes⁻¹", + "microgrammes-1", + "/micro-gramme", + "micro-gramme⁻¹", + "micro-gramme-1", + "/micro-grammes", + "micro-grammes⁻¹", + "micro-grammes-1", + "/µgr", + "µgr⁻¹", + "µgr-1", + "/µg", + "µg⁻¹", + "µg-1", + "/ugr", + "ugr⁻¹", + "ugr-1", + "/ug", + "ug⁻¹", + "ug-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_mg": { + "scale": 1.0e6, + "terms": [ + "/milligramme", + "milligramme⁻¹", + "milligramme-1", + "/miligramme", + "miligramme⁻¹", + "miligramme-1", + "/milligrammes", + "milligrammes⁻¹", + "milligrammes-1", + "/miligrammes", + "miligrammes⁻¹", + "miligrammes-1", + "/mgr", + "mgr⁻¹", + "mgr-1", + "/mg", + "mg⁻¹", + "mg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_cg": { + "scale": 1.0e5, + "terms": [ + "/centigramme", + "centigramme⁻¹", + "centigramme-1", + "/centigrammes", + "centigrammes⁻¹", + "centigrammes-1", + "/cg", + "cg⁻¹", + "cg-1", + "/cgr", + "cgr⁻¹", + "cgr-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_dg": { + "scale": 1.0e4, + "terms": [ + "/decigramme", + "decigramme⁻¹", + "decigramme-1", + "/decigrammes", + "decigrammes⁻¹", + "decigrammes-1", + "/dgr", + "dgr⁻¹", + "dgr-1", + "/dg", + "dg⁻¹", + "dg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_g": { + "scale": 1000.0, + "terms": [ + "/gramme", + "gramme⁻¹", + "gramme-1", + "/grammes", + "grammes⁻¹", + "grammes-1", + "/gr", + "gr⁻¹", + "gr-1", + "/g", + "g⁻¹", + "g-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_dag": { + "scale": 100.0, + "terms": [ + "/decagramme", + "decagramme⁻¹", + "decagramme-1", + "/decagrammes", + "decagrammes⁻¹", + "decagrammes-1", + "/dagr", + "dagr⁻¹", + "dagr-1", + "/dag", + "dag⁻¹", + "dag-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_hg": { + "scale": 10.0, + "terms": [ + "/hectogramme", + "hectogramme⁻¹", + "hectogramme-1", + "/hectogrammes", + "hectogrammes⁻¹", + "hectogrammes-1", + "/hgr", + "hgr⁻¹", + "hgr-1", + "/hg", + "hg⁻¹", + "hg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_kg": { + "scale": 1.0, + "terms": [ + "/kilo", + "kilo⁻¹", + "kilo-1", + "/kilogramme", + "kilogramme⁻¹", + "kilogramme-1", + "/kilogrammes", + "kilogrammes⁻¹", + "kilogrammes-1", + "/kgr", + "kgr⁻¹", + "kgr-1", + "/kg", + "kg⁻¹", + "kg-1", + ], + "followed_by": None, + "ui_decomposition": {"mass": -1}, + }, + "per_fs": { + "scale": 1.0e15, + "terms": [ + "/femtoseconde", + "femtoseconde⁻¹", + "femtoseconde-1", + "/femtosecondes", + "femtosecondes⁻¹", + "femtosecondes-1", + "/femto-seconde", + "femto-seconde⁻¹", + "femto-seconde-1", + "/femto-secondes", + "femto-secondes⁻¹", + "femto-secondes-1", + "/fs", + "fs⁻¹", + "fs-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_ps": { + "scale": 1.0e12, + "terms": [ + "/picoseconde", + "picoseconde⁻¹", + "picoseconde-1", + "/picosecondes", + "picosecondes⁻¹", + "picosecondes-1", + "/pico-seconde", + "pico-seconde⁻¹", + "pico-seconde-1", + "/pico-secondes", + "pico-secondes⁻¹", + "pico-secondes-1", + "/ps", + "ps⁻¹", + "ps-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_ns": { + "scale": 1.0e9, + "terms": [ + "/nanoseconde", + "nanoseconde⁻¹", + "nanoseconde-1", + "/nanosecondes", + "nanosecondes⁻¹", + "nanosecondes-1", + "/nano-seconde", + "nano-seconde⁻¹", + "nano-seconde-1", + "/nano-secondes", + "nano-secondes⁻¹", + "nano-secondes-1", + "/ns", + "ns⁻¹", + "ns-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_µs": { + "scale": 1.0e6, + "terms": [ + "/microseconde", + "microseconde⁻¹", + "microseconde-1", + "/microsecondes", + "microsecondes⁻¹", + "microsecondes-1", + "/micro-seconde", + "micro-seconde⁻¹", + "micro-seconde-1", + "/micro-secondes", + "micro-secondes⁻¹", + "micro-secondes-1", + "/µs", + "µs⁻¹", + "µs-1", + "/us", + "us⁻¹", + "us-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_ms": { + "scale": 1000.0, + "terms": [ + "/milliseconde", + "milliseconde⁻¹", + "milliseconde-1", + "/millisecondes", + "millisecondes⁻¹", + "millisecondes-1", + "/miliseconde", + "miliseconde⁻¹", + "miliseconde-1", + "/milisecondes", + "milisecondes⁻¹", + "milisecondes-1", + "/ms", + "ms⁻¹", + "ms-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_s": { + "scale": 1.0, + "terms": [ + "/seconde", + "seconde⁻¹", + "seconde-1", + "/secondes", + "secondes⁻¹", + "secondes-1", + "/s", + "s⁻¹", + "s-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_min": { + "scale": 0.016666666666666666, + "terms": [ + "/mn", + "mn⁻¹", + "mn-1", + "/min", + "min⁻¹", + "min-1", + "/minute", + "minute⁻¹", + "minute-1", + "/minutes", + "minutes⁻¹", + "minutes-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_h": { + "scale": 0.0002777777777777778, + "terms": [ + "/heure", + "heure⁻¹", + "heure-1", + "/heures", + "heures⁻¹", + "heures-1", + "/h", + "h⁻¹", + "h-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_day": { + "scale": 0.0002777777777777778, + "terms": [ + "/jour", + "jour⁻¹", + "jour-1", + "/jours", + "jours⁻¹", + "jours-1", + "/j", + "j⁻¹", + "j-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_month": { + "scale": 9.132410083203562e-06, + "terms": ["/mois", "mois⁻¹", "mois-1"], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_week": { + "scale": 3.968253968253968e-05, + "terms": [ + "/semaine", + "semaine⁻¹", + "semaine-1", + "/semaines", + "semaines⁻¹", + "semaines-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_year": { + "scale": 7.605141075366948e-07, + "terms": [ + "/an", + "an⁻¹", + "an-1", + "/année", + "année⁻¹", + "année-1", + "/ans", + "ans⁻¹", + "ans-1", + "/années", + "années⁻¹", + "années-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_arc-second": { + "scale": 30.0, + "terms": ['/"', '"⁻¹', '"-1', "/''", "''⁻¹", "''-1"], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_arc-minute": { + "scale": 0.5, + "terms": ["/'", "'⁻¹", "'-1"], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_degree": { + "scale": 0.008333333333333333, + "terms": [ + "/degre", + "degre⁻¹", + "degre-1", + "/°", + "°⁻¹", + "°-1", + "/deg", + "deg⁻¹", + "deg-1", + ], + "followed_by": None, + "ui_decomposition": {"time": -1}, + }, + "per_celcius": { + "scale": 1.0, + "terms": [ + "/°C", + "°C⁻¹", + "°C-1", + "/° celcius", + "° celcius⁻¹", + "° celcius-1", + "/celcius", + "celcius⁻¹", + "celcius-1", + ], + "followed_by": None, + "ui_decomposition": {"temperature": -1}, + }, + "per_fl": { + "scale": 1.0e18, + "terms": [ + "/femtolitre", + "femtolitre⁻¹", + "femtolitre-1", + "/femtolitres", + "femtolitres⁻¹", + "femtolitres-1", + "/femto-litre", + "femto-litre⁻¹", + "femto-litre-1", + "/femto-litres", + "femto-litres⁻¹", + "femto-litres-1", + "/fl", + "fl⁻¹", + "fl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_pl": { + "scale": 1.0e15, + "terms": [ + "/picolitre", + "picolitre⁻¹", + "picolitre-1", + "/picolitres", + "picolitres⁻¹", + "picolitres-1", + "/pico-litre", + "pico-litre⁻¹", + "pico-litre-1", + "/pico-litres", + "pico-litres⁻¹", + "pico-litres-1", + "/pl", + "pl⁻¹", + "pl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_nl": { + "scale": 1.0e12, + "terms": [ + "/nanolitre", + "nanolitre⁻¹", + "nanolitre-1", + "/nanolitres", + "nanolitres⁻¹", + "nanolitres-1", + "/nano-litre", + "nano-litre⁻¹", + "nano-litre-1", + "/nano-litres", + "nano-litres⁻¹", + "nano-litres-1", + "/nl", + "nl⁻¹", + "nl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_µl": { + "scale": 1.0e9, + "terms": [ + "/microlitre", + "microlitre⁻¹", + "microlitre-1", + "/microlitres", + "microlitres⁻¹", + "microlitres-1", + "/micro-litre", + "micro-litre⁻¹", + "micro-litre-1", + "/micro-litres", + "micro-litres⁻¹", + "micro-litres-1", + "/µl", + "µl⁻¹", + "µl-1", + "/ul", + "ul⁻¹", + "ul-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_ml": { + "scale": 1.0e6, + "terms": [ + "/mililitre", + "mililitre⁻¹", + "mililitre-1", + "/millilitre", + "millilitre⁻¹", + "millilitre-1", + "/mililitres", + "mililitres⁻¹", + "mililitres-1", + "/millilitres", + "millilitres⁻¹", + "millilitres-1", + "/ml", + "ml⁻¹", + "ml-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_cl": { + "scale": 1.0e5, + "terms": [ + "/centilitre", + "centilitre⁻¹", + "centilitre-1", + "/centilitres", + "centilitres⁻¹", + "centilitres-1", + "/cl", + "cl⁻¹", + "cl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_dl": { + "scale": 1.0e4, + "terms": [ + "/decilitre", + "decilitre⁻¹", + "decilitre-1", + "/decilitres", + "decilitres⁻¹", + "decilitres-1", + "/dl", + "dl⁻¹", + "dl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_l": { + "scale": 1000.0, + "terms": [ + "/litre", + "litre⁻¹", + "litre-1", + "/litres", + "litres⁻¹", + "litres-1", + "/l", + "l⁻¹", + "l-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_dal": { + "scale": 100.0, + "terms": [ + "/decalitre", + "decalitre⁻¹", + "decalitre-1", + "/decalitres", + "decalitres⁻¹", + "decalitres-1", + "/dal", + "dal⁻¹", + "dal-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_hl": { + "scale": 10.0, + "terms": [ + "/hectolitre", + "hectolitre⁻¹", + "hectolitre-1", + "/hectolitres", + "hectolitres⁻¹", + "hectolitres-1", + "/hl", + "hl⁻¹", + "hl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_kl": { + "scale": 1.0, + "terms": [ + "/kilolitre", + "kilolitre⁻¹", + "kilolitre-1", + "/kilolitres", + "kilolitres⁻¹", + "kilolitres-1", + "/kl", + "kl⁻¹", + "kl-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_cac": { + "scale": 200000000.0, + "terms": [ + "/cac", + "cac⁻¹", + "cac-1", + "/c.a.c", + "c.a.c⁻¹", + "c.a.c-1", + "/cuillere à café", + "cuillere à café⁻¹", + "cuillere à café-1", + "/cuillères à café", + "cuillères à café⁻¹", + "cuillères à café-1", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_goutte": { + "scale": 20000000000.0, + "terms": ["/gt", "gt⁻¹", "gt-1", "/goutte", "goutte⁻¹", "goutte-1"], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_fm3": { + "scale": 1.0e45, + "terms": [ + "/fm3", + "fm3⁻¹", + "fm3-1", + "/fm³", + "fm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_pm3": { + "scale": 1.0e36, + "terms": [ + "/pm3", + "pm3⁻¹", + "pm3-1", + "/pm³", + "pm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_nm3": { + "scale": 1.0e27, + "terms": [ + "/nm3", + "nm3⁻¹", + "nm3-1", + "/nm³", + "nm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_µm3": { + "scale": 1.0e18, + "terms": [ + "/um3", + "um3⁻¹", + "um3-1", + "/um³", + "um⁻³", + "/µm3", + "µm3⁻¹", + "µm3-1", + "/µm³", + "µm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_mm3": { + "scale": 1.0e9, + "terms": [ + "/mm3", + "mm3⁻¹", + "mm3-1", + "/mm³", + "mm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_cm3": { + "scale": 1.0e6, + "terms": ["/cm3", "cm3⁻¹", "cm3-1", "/cm³", "cm⁻³", "/cc", "cc⁻¹", "cc-1"], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_dm3": { + "scale": 1000.0, + "terms": [ + "/dm3", + "dm3⁻¹", + "dm3-1", + "/dm³", + "dm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_m3": { + "scale": 1.0, + "terms": [ + "/m3", + "m3⁻¹", + "m3-1", + "/m³", + "m⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_dam3": { + "scale": 0.001, + "terms": [ + "/dam3", + "dam3⁻¹", + "dam3-1", + "/dam³", + "dam⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_hm3": { + "scale": 1e-06, + "terms": [ + "/hm3", + "hm3⁻¹", + "hm3-1", + "/hm³", + "hm⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_km3": { + "scale": 1e-09, + "terms": [ + "/km3", + "km3⁻¹", + "km3-1", + "/km³", + "km⁻³", + ], + "followed_by": None, + "ui_decomposition": {"length": -3}, + }, + "per_fm2": { + "scale": 1.0 + 30, + "terms": [ + "/fm2", + "fm2⁻¹", + "fm2-1", + "/fm²", + "fm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_pm2": { + "scale": 1.0e24, + "terms": [ + "/fm2", + "fm2⁻¹", + "fm2-1", + "/fm²", + "fm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_nm2": { + "scale": 1.0e18, + "terms": [ + "/nm2", + "nm2⁻¹", + "nm2-1", + "/nm²", + "nm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_μm2": { + "scale": 1.0e12, + "terms": [ + "/µm2", + "µm2⁻¹", + "µm2-1", + "/µm²", + "µm⁻²", + "/um2", + "um2⁻¹", + "um2-1", + "/um²", + "um⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_mm2": { + "scale": 1.0e6, + "terms": [ + "/mm2", + "mm2⁻¹", + "mm2-1", + "/mm²", + "mm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_cm2": { + "scale": 1.0e4, + "terms": [ + "/cm2", + "cm2⁻¹", + "cm2-1", + "/cm²", + "cm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_dm2": { + "scale": 100.0, + "terms": [ + "/dm2", + "dm2⁻¹", + "dm2-1", + "/dm²", + "dm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_m2": { + "scale": 1.0, + "terms": [ + "/m2", + "m2⁻¹", + "m2-1", + "/m²", + "m⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_dam2": { + "scale": 0.01, + "terms": [ + "/dam2", + "dam2⁻¹", + "dam2-1", + "/dam²", + "dam⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_hm2": { + "scale": 1.0e-4, + "terms": [ + "/hm2", + "hm2⁻¹", + "hm2-1", + "/hm²", + "hm⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_km2": { + "scale": 1e-06, + "terms": [ + "/km2", + "km2⁻¹", + "km2-1", + "/km²", + "km⁻²", + ], + "followed_by": None, + "ui_decomposition": {"length": -2}, + }, + "per_fui": { + "scale": 1.0e15, + "terms": [ + "/fui", + "fui⁻¹", + "fui-1", + "/f ui", + "f ui⁻¹", + "f ui-1", + "/fu", + "fu⁻¹", + "fu-1", + "/f u", + "f u⁻¹", + "f u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_pui": { + "scale": 1.0e12, + "terms": [ + "/pui", + "pui⁻¹", + "pui-1", + "/p ui", + "p ui⁻¹", + "p ui-1", + "/pu", + "pu⁻¹", + "pu-1", + "/p u", + "p u⁻¹", + "p u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_nui": { + "scale": 1.0e9, + "terms": [ + "/nui", + "nui⁻¹", + "nui-1", + "/n ui", + "n ui⁻¹", + "n ui-1", + "/nu", + "nu⁻¹", + "nu-1", + "/n u", + "n u⁻¹", + "n u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_µui": { + "scale": 1.0e6, + "terms": [ + "/µui", + "µui⁻¹", + "µui-1", + "/µ ui", + "µ ui⁻¹", + "µ ui-1", + "/uui", + "uui⁻¹", + "uui-1", + "/u ui", + "u ui⁻¹", + "u ui-1", + "/µu", + "µu⁻¹", + "µu-1", + "/µ u", + "µ u⁻¹", + "µ u-1", + "/uu", + "uu⁻¹", + "uu-1", + "/u u", + "u u⁻¹", + "u u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_mui": { + "scale": 1000.0, + "terms": [ + "/mui", + "mui⁻¹", + "mui-1", + "/m ui", + "m ui⁻¹", + "m ui-1", + "/mu", + "mu⁻¹", + "mu-1", + "/m u", + "m u⁻¹", + "m u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_cui": { + "scale": 100.0, + "terms": [ + "/cui", + "cui⁻¹", + "cui-1", + "/c ui", + "c ui⁻¹", + "c ui-1", + "/cu", + "cu⁻¹", + "cu-1", + "/c u", + "c u⁻¹", + "c u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_dui": { + "scale": 10.0, + "terms": [ + "/dui", + "dui⁻¹", + "dui-1", + "/d ui", + "d ui⁻¹", + "d ui-1", + "/du", + "du⁻¹", + "du-1", + "/d u", + "d u⁻¹", + "d u-1", + ], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_ui": { + "scale": 1.0, + "terms": ["/ui", "ui⁻¹", "ui-1", "/u", "u⁻¹", "u-1"], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_daui": { + "scale": 0.1, + "terms": ["/daui", "daui⁻¹", "daui-1", "/dau", "dau⁻¹", "dau-1"], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_hui": { + "scale": 0.01, + "terms": ["/hui", "hui⁻¹", "hui-1", "/hu", "hu⁻¹", "hu-1"], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_kui": { + "scale": 0.001, + "terms": ["/kui", "kui⁻¹", "kui-1", "/ku", "ku⁻¹", "ku-1"], + "followed_by": None, + "ui_decomposition": {"ui": -1}, + }, + "per_fmol": { + "scale": 1.0e15, + "terms": [ + "/fmol", + "fmol⁻¹", + "fmol-1", + "/f mol", + "f mol⁻¹", + "f mol-1", + "/fmole", + "fmole⁻¹", + "fmole-1", + "/f mole", + "f mole⁻¹", + "f mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_pmol": { + "scale": 1.0e12, + "terms": [ + "/pmol", + "pmol⁻¹", + "pmol-1", + "/p mol", + "p mol⁻¹", + "p mol-1", + "/pmole", + "pmole⁻¹", + "pmole-1", + "/p mole", + "p mole⁻¹", + "p mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_nmol": { + "scale": 1.0e9, + "terms": [ + "/nmol", + "nmol⁻¹", + "nmol-1", + "/n mol", + "n mol⁻¹", + "n mol-1", + "/nmole", + "nmole⁻¹", + "nmole-1", + "/n mole", + "n mole⁻¹", + "n mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_µmol": { + "scale": 1.0e6, + "terms": [ + "/µmol", + "µmol⁻¹", + "µmol-1", + "/µ mol", + "µ mol⁻¹", + "µ mol-1", + "/umole", + "umole⁻¹", + "umole-1", + "/u mole", + "u mole⁻¹", + "u mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_mmol": { + "scale": 1000.0, + "terms": [ + "/mmol", + "mmol⁻¹", + "mmol-1", + "/m mol", + "m mol⁻¹", + "m mol-1", + "/mmole", + "mmole⁻¹", + "mmole-1", + "/m mole", + "m mole⁻¹", + "m mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_cmol": { + "scale": 100.0, + "terms": [ + "/cmol", + "cmol⁻¹", + "cmol-1", + "/c mol", + "c mol⁻¹", + "c mol-1", + "/cmole", + "cmole⁻¹", + "cmole-1", + "/c mole", + "c mole⁻¹", + "c mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_dmol": { + "scale": 10.0, + "terms": [ + "/dmol", + "dmol⁻¹", + "dmol-1", + "/d mol", + "d mol⁻¹", + "d mol-1", + "/dmole", + "dmole⁻¹", + "dmole-1", + "/d mole", + "d mole⁻¹", + "d mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_mol": { + "scale": 1.0, + "terms": ["/mol", "mol⁻¹", "mol-1", "/mole", "mole⁻¹", "mole-1"], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_damol": { + "scale": 0.1, + "terms": [ + "/damol", + "damol⁻¹", + "damol-1", + "/da mol", + "da mol⁻¹", + "da mol-1", + "/damole", + "damole⁻¹", + "damole-1", + "/da mole", + "da mole⁻¹", + "da mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_hmol": { + "scale": 0.01, + "terms": [ + "/hmol", + "hmol⁻¹", + "hmol-1", + "/h mol", + "h mol⁻¹", + "h mol-1", + "/hmole", + "hmole⁻¹", + "hmole-1", + "/h mole", + "h mole⁻¹", + "h mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "per_kmol": { + "scale": 0.001, + "terms": [ + "/kmol", + "kmol⁻¹", + "kmol-1", + "/k mol", + "k mol⁻¹", + "k mol-1", + "/kmole", + "kmole⁻¹", + "kmole-1", + "/k mole", + "k mole⁻¹", + "k mole-1", + ], + "followed_by": None, + "ui_decomposition": {"nsubstance": -1}, + }, + "nounit": { + "scale": 1, + "terms": [], + "followed_by": None, + "ui_decomposition": {"nounit": 1}, + }, + "percent": { + "scale": 0.01, + "terms": [ + "%", + "pourcent", + "pourcents", + ], + "followed_by": None, + "ui_decomposition": {"nounit": 1}, + }, + "permille": { + "scale": 0.001, + "terms": [ + "‰", + "pourmille", + "pour mille", + "pourmilles", + "pour milles", + ], + "followed_by": None, + "ui_decomposition": {"nounit": 1}, + }, + "mmhg": { + "scale": 133.3224, + "terms": [ + "mmhg", + "torr", + ], + "followed_by": None, + "ui_decomposition": {"mass": 1, "length": -1, "time": -2}, + }, + "bool": { + "scale": 1, + "terms": [], + "followed_by": None, + "ui_decomposition": {"bool": 1}, + }, +} diff --git a/edsnlp/pipelines/misc/tables/__init__.py b/edsnlp/pipelines/misc/tables/__init__.py new file mode 100644 index 000000000..861ef7628 --- /dev/null +++ b/edsnlp/pipelines/misc/tables/__init__.py @@ -0,0 +1,2 @@ +from .patterns import regex, sep +from .tables import TablesMatcher diff --git a/edsnlp/pipelines/misc/tables/factory.py b/edsnlp/pipelines/misc/tables/factory.py new file mode 100644 index 000000000..d796a56c5 --- /dev/null +++ b/edsnlp/pipelines/misc/tables/factory.py @@ -0,0 +1,38 @@ +from typing import List, Optional + +from spacy.language import Language + +from edsnlp.pipelines.misc.tables import TablesMatcher +from edsnlp.utils.deprecation import deprecated_factory + +DEFAULT_CONFIG = dict( + tables_pattern=None, + sep_pattern=None, + attr="TEXT", + ignore_excluded=True, + col_names=False, + row_names=False, +) + + +@deprecated_factory("tables", "eds.tables", default_config=DEFAULT_CONFIG) +@Language.factory("eds.tables", default_config=DEFAULT_CONFIG) +def create_component( + nlp: Language, + name: str, + tables_pattern: Optional[List[str]], + sep_pattern: Optional[List[str]], + attr: str, + ignore_excluded: bool, + col_names: Optional[bool] = False, + row_names: Optional[bool] = False, +): + return TablesMatcher( + nlp, + tables_pattern=tables_pattern, + sep_pattern=sep_pattern, + attr=attr, + ignore_excluded=ignore_excluded, + col_names=col_names, + row_names=row_names, + ) diff --git a/edsnlp/pipelines/misc/tables/patterns.py b/edsnlp/pipelines/misc/tables/patterns.py new file mode 100644 index 000000000..233bc47b6 --- /dev/null +++ b/edsnlp/pipelines/misc/tables/patterns.py @@ -0,0 +1,2 @@ +sep = [r"¦", r"|"] +regex = [r"(?:¦?(?:[^¦\n]*¦)+[^¦\n]*¦?\n)+", r"(?:\|?(?:[^\|\n]*\|)+[^\|\n]*\|?\n)+"] diff --git a/edsnlp/pipelines/misc/tables/tables.py b/edsnlp/pipelines/misc/tables/tables.py new file mode 100644 index 000000000..a6d29f9b6 --- /dev/null +++ b/edsnlp/pipelines/misc/tables/tables.py @@ -0,0 +1,268 @@ +from typing import List, Optional + +import pandas as pd +from spacy.language import Language +from spacy.tokens import Doc, Span + +from edsnlp.matchers.phrase import EDSPhraseMatcher +from edsnlp.matchers.regex import RegexMatcher +from edsnlp.pipelines.misc.tables import patterns + + +class TablesMatcher: + """ + Pipeline to identify the Tables. + + It adds the key `tables` to doc.spans. + + Parameters + ---------- + nlp : Language + spaCy nlp pipeline to use for matching. + tables_pattern : Optional[List[str]] + The regex patterns to identify tables. + sep_pattern : Optional[List[str]] + The regex patterns to identify separators + in the detected tables + col_names : Optional[bool] + Whether the tables_pattern matches column names + row_names : Optional[bool] + Whether the table_pattern matches row names + attr : str + spaCy's attribute to use: + a string with the value "TEXT" or "NORM", or a dict with + the key 'term_attr'. We can also add a key for each regex. + ignore_excluded : bool + Whether to skip excluded tokens. + """ + + def __init__( + self, + nlp: Language, + tables_pattern: Optional[List[str]], + sep_pattern: Optional[List[str]], + attr: str, + ignore_excluded: bool, + col_names: Optional[bool] = False, + row_names: Optional[bool] = False, + ): + + if tables_pattern is None: + tables_pattern = patterns.regex + + if sep_pattern is None: + sep_pattern = patterns.sep + + self.regex_matcher = RegexMatcher(attr=attr, ignore_excluded=True) + self.regex_matcher.add("table", tables_pattern) + + self.term_matcher = EDSPhraseMatcher(nlp.vocab, attr=attr, ignore_excluded=True) + self.term_matcher.build_patterns( + nlp, + { + "eol_pattern": "\n", + "sep_pattern": sep_pattern, + }, + ) + + self.col_names = col_names + self.row_names = row_names + + if not Span.has_extension("to_pd_table"): + Span.set_extension("to_pd_table", method=self.to_pd_table) + + self.set_extensions() + + @classmethod + def set_extensions(cls) -> None: + """ + Set extensions for the tables pipeline. + """ + + if not Span.has_extension("table"): + Span.set_extension("table", default=None) + + def get_tables(self, matches): + """ + Convert spans of tables to dictionnaries + + Parameters + ---------- + matches : List[Span] + + Returns + ------- + List[Span] + """ + + # Dictionnaries linked to each table + # Has the following format : + # List[Dict[Union[str, int], List[Span]]] + # - List of dictionnaries containing the tables. Keys are + # column names (str) if col_names is set to True, else row + # names (str) if row_names is set to True, else index of + # column (int) + tables_list = [] + + # Returned list + tables = [] + + # Iter through matches to consider each table individually + for table in matches: + # We store each row in a list and store each of hese lists + # in processed_table for post processing + # considering the self.col_names and self.row_names var + processed_table = [] + delimiters = [ + delimiter + for delimiter in self.term_matcher(table, as_spans=True) + if delimiter.start >= table.start and delimiter.end <= table.end + ] + + last = table.start + row = [] + # Parse the table to match each cell thanks to delimiters + for delimiter in delimiters: + row.append(table[last - table.start : delimiter.start - table.start]) + last = delimiter.end + + # End the actual row if there is an end of line + if delimiter.label_ == "eol_pattern": + processed_table.append(row) + row = [] + + # Remove first or last column in case the separator pattern is + # also used in the raw table to draw the outlines + if all(row[0].start == row[0].end for row in processed_table): + processed_table = [row[1:] for row in processed_table] + if all(row[-1].start == row[-1].end for row in processed_table): + processed_table = [row[:-1] for row in processed_table] + + # Check if all rows have the same dimension. + # If not, try to merge neighbour rows + # to find a new table + row_len = len(processed_table[0]) + if not all(len(row) == row_len for row in processed_table): + + # Method to find all possible lengths of the rows + def divisors(n): + result = set() + for i in range(1, int(n**0.5) + 1): + if n % i == 0: + result.add(i) + result.add(n // i) + return sorted(list(result)) + + # Do not count the column names when splitting the table + if self.col_names: + n_rows = len(processed_table) - 1 + else: + n_rows = len(processed_table) + + for n_rows_to_merge in divisors(n_rows): + row_len = sum(len(row) for row in processed_table[:n_rows_to_merge]) + if all( + sum( + len(row) + for row in processed_table[ + i * n_rows_to_merge : (i + 1) * n_rows_to_merge + ] + ) + == row_len + for i in range(n_rows // n_rows_to_merge) + ): + new_table = [] + for i in range(n_rows // n_rows_to_merge): + # Init new_row with the first subrow + new_row = processed_table[i * n_rows_to_merge] + for subrow in processed_table[ + i * n_rows_to_merge + 1 : (i + 1) * n_rows_to_merge + ]: + new_row = ( + new_row[:-1] + + [ + table[ + new_row[-1].start + - table.start : subrow[0].end + - table.start + ] + ] + + subrow[1:] + ) + new_table.append(new_row) + tables_list.append(new_table) + break + continue + else: + tables_list.append(processed_table) + + # Convert to dictionnaries according to self.col_names + # and self.row_names + if self.col_names: + for table_index in range(len(tables_list)): + tables_list[table_index] = { + tables_list[table_index][0][column_index].text: [ + tables_list[table_index][row_index][column_index] + for row_index in range(1, len(tables_list[table_index])) + ] + for column_index in range(len(tables_list[table_index][0])) + } + elif self.row_names: + for table_index in range(len(tables_list)): + tables_list[table_index] = { + tables_list[table_index][row_index][0].text: [ + tables_list[table_index][row_index][column_index] + for column_index in range(1, len(tables_list[table_index][0])) + ] + for row_index in range(len(tables_list[table_index])) + } + else: + for table_index in range(len(tables_list)): + tables_list[table_index] = { + column_index: [ + tables_list[table_index][row_index][column_index] + for row_index in range(len(tables_list[table_index])) + ] + for column_index in range(len(tables_list[table_index][0])) + } + + for i in range(len(matches)): + ent = matches[i] + ent._.table = tables_list[i] + tables.append(ent) + + return tables + + def __call__(self, doc: Doc) -> Doc: + """ + Find spans that contain tables + + Parameters + ---------- + doc : Doc + + Returns + ------- + Doc + """ + matches = list(self.regex_matcher(doc, as_spans=True)) + tables = self.get_tables(matches) + doc.spans["tables"] = tables + + return doc + + def to_pd_table(self, span, as_spans=True) -> pd.DataFrame: + """ + Return pandas DataFrame + """ + if as_spans: + table = span._.table + else: + table = { + key: [str(cell) for cell in data] + for key, data in list(span._.table.items()) + } + if self.row_names: + return pd.DataFrame.from_dict(table, orient="index") + else: + return pd.DataFrame.from_dict(table) diff --git a/edsnlp/resources/verbs.csv.gz b/edsnlp/resources/verbs.csv.gz index b05fb4eeffde8d7c4d8403757b12d2445c925f9c..370d9d3c1a5eb3fbcc6088761d60ac0fadacddfc 100644 GIT binary patch literal 200865 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≧szBZ^-2E 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