decimer 2.7.0__tar.gz → 2.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/Efficient_Net_encoder.py +0 -2
  2. decimer-2.8.0/DECIMER/__init__.py +31 -0
  3. decimer-2.8.0/DECIMER/config.py +148 -0
  4. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/decimer.py +49 -8
  5. decimer-2.8.0/DECIMER/pre_process.py +243 -0
  6. decimer-2.8.0/DECIMER/utils.py +143 -0
  7. decimer-2.8.0/PKG-INFO +221 -0
  8. decimer-2.8.0/README.md +180 -0
  9. decimer-2.8.0/decimer.egg-info/PKG-INFO +221 -0
  10. {decimer-2.7.0 → decimer-2.8.0}/decimer.egg-info/SOURCES.txt +4 -1
  11. {decimer-2.7.0 → decimer-2.8.0}/decimer.egg-info/requires.txt +1 -1
  12. decimer-2.8.0/pyproject.toml +3 -0
  13. {decimer-2.7.0 → decimer-2.8.0}/setup.py +7 -10
  14. decimer-2.8.0/tests/test_functions.py +30 -0
  15. decimer-2.7.0/DECIMER/__init__.py +0 -29
  16. decimer-2.7.0/DECIMER/config.py +0 -381
  17. decimer-2.7.0/DECIMER/utils.py +0 -97
  18. decimer-2.7.0/PKG-INFO +0 -145
  19. decimer-2.7.0/README.md +0 -121
  20. decimer-2.7.0/decimer.egg-info/PKG-INFO +0 -145
  21. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/DECIMER_EfficinetNetV2_Transfomer_Trainer.py +0 -0
  22. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/Predictor_usingCheckpoints.py +0 -0
  23. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/Repack_model.py +0 -0
  24. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/Transformer_decoder.py +0 -0
  25. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/Utils/Create_TFrecord_From_pickles.py +0 -0
  26. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/Utils/Create_image_tokenizer.py +0 -0
  27. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/efficientnetv2/effnetv2_configs.py +0 -0
  28. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/efficientnetv2/effnetv2_model.py +0 -0
  29. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/efficientnetv2/hparams.py +0 -0
  30. {decimer-2.7.0 → decimer-2.8.0}/DECIMER/efficientnetv2/utils.py +0 -0
  31. {decimer-2.7.0 → decimer-2.8.0}/LICENSE +0 -0
  32. {decimer-2.7.0 → decimer-2.8.0}/decimer.egg-info/dependency_links.txt +0 -0
  33. {decimer-2.7.0 → decimer-2.8.0}/decimer.egg-info/entry_points.txt +0 -0
  34. {decimer-2.7.0 → decimer-2.8.0}/decimer.egg-info/top_level.txt +0 -0
  35. {decimer-2.7.0 → decimer-2.8.0}/setup.cfg +0 -0
@@ -1,8 +1,6 @@
1
1
  # EfficientNet-V2 config
2
2
  import tensorflow as tf
3
3
 
4
- import DECIMER.efficientnetv2
5
- from DECIMER.efficientnetv2 import effnetv2_configs
6
4
  from DECIMER.efficientnetv2 import effnetv2_model
7
5
 
8
6
  BATCH_SIZE_DEBUG = 2
@@ -0,0 +1,31 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ DECIMER V2.7.2 Python Package
4
+ =============================
5
+
6
+ Deep lEarning for Chemical ImagE Recognition (DECIMER) project
7
+ was launched to address the Optical Chemical Structure Recognition (OCSR)
8
+ problem using deep learning based methods,
9
+ providing an automated open-source software solution.
10
+
11
+ Typical usage example::
12
+
13
+ from decimer import predict_SMILES
14
+
15
+ # Chemical depiction to SMILES translation
16
+ image_path = "path/to/imagefile"
17
+ SMILES = predict_SMILES(image_path)
18
+ print(SMILES)
19
+
20
+ For comments, bug reports, or feature ideas,
21
+ please raise an issue on the GitHub repository.
22
+ """
23
+
24
+ __version__ = "2.7.2"
25
+
26
+ __all__ = [
27
+ "DECIMER",
28
+ ]
29
+
30
+
31
+ from .decimer import predict_SMILES
@@ -0,0 +1,148 @@
1
+ import tensorflow as tf
2
+
3
+ import DECIMER.Efficient_Net_encoder as Efficient_Net_encoder
4
+ import DECIMER.Transformer_decoder as Transformer_decoder
5
+
6
+ TARGET_DTYPE = tf.float32
7
+
8
+
9
+ class Config:
10
+ """Configuration class."""
11
+
12
+ def __init__(
13
+ self,
14
+ ):
15
+ self.encoder_config = {}
16
+ self.transformer_config = {}
17
+ self.lr_config = {}
18
+
19
+ def initialize_encoder_config(
20
+ self,
21
+ image_embedding_dim,
22
+ preprocessing_fn,
23
+ backbone_fn,
24
+ image_shape,
25
+ do_permute=False,
26
+ pretrained_weights=None,
27
+ ):
28
+ """This functions initializes the Efficient-Net V2 encoder with user
29
+ defined configurations.
30
+
31
+ Args:
32
+ image_embedding_dim (int): Embedding dimension of the input image
33
+ preprocessing_fn (method): Efficient Net preprocessing function for input image
34
+ backbone_fn (method): Calls Efficient-Net V2 as backbone for encoder
35
+ image_shape (int): Shape of the input image
36
+ do_permute (bool, optional): . Defaults to False.
37
+ pretrained_weights (keras weights, optional): Use pretrained efficient net weights or not. Defaults to None.
38
+ """
39
+ self.encoder_config = dict(
40
+ image_embedding_dim=image_embedding_dim,
41
+ preprocessing_fn=preprocessing_fn,
42
+ backbone_fn=backbone_fn,
43
+ image_shape=image_shape,
44
+ do_permute=do_permute,
45
+ pretrained_weights=pretrained_weights,
46
+ )
47
+
48
+ def initialize_transformer_config(
49
+ self,
50
+ vocab_len,
51
+ max_len,
52
+ n_transformer_layers,
53
+ transformer_d_dff,
54
+ transformer_n_heads,
55
+ image_embedding_dim,
56
+ rate=0.1,
57
+ ):
58
+ """This functions initializes the Transformer model as decoder with
59
+ user defined configurations.
60
+
61
+ Args:
62
+ vocab_len (int): Total number of words in the input vocabulary
63
+ max_len (int): Maximum length of the string found on the training dataset
64
+ n_transformer_layers (int): Number of layers present in the transformer model
65
+ transformer_d_dff (int): Transformer feed forward upwards projection size
66
+ transformer_n_heads (int): Number of heads present in the transformer model
67
+ image_embedding_dim (int): Total number of dimension the image gets embedded
68
+ dropout_rate (float, optional): Fraction of the input units to drop. Defaults to 0.1.
69
+ """
70
+ self.transformer_config = dict(
71
+ num_layers=n_transformer_layers,
72
+ d_model=image_embedding_dim,
73
+ num_heads=transformer_n_heads,
74
+ dff=transformer_d_dff,
75
+ target_vocab_size=vocab_len,
76
+ max_len=max_len,
77
+ rate=0.1,
78
+ )
79
+
80
+ def initialize_lr_config(self, warm_steps, n_epochs):
81
+ """This function sets the configuration to initialize learning rate.
82
+
83
+ Args:
84
+ warm_steps (int): Number of steps The learning rate is increased
85
+ n_epochs (int): Number of epochs
86
+ """
87
+ self.lr_config = dict(
88
+ warm_steps=warm_steps,
89
+ n_epochs=n_epochs,
90
+ )
91
+
92
+
93
+ class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
94
+ """Custom schedule for learning rate used during training.
95
+
96
+ Args:
97
+ tf (_type_): keras learning rate schedule
98
+ """
99
+
100
+ def __init__(self, d_model, warmup_steps=4000):
101
+ super(CustomSchedule, self).__init__()
102
+
103
+ self.d_model = d_model
104
+ self.d_model = tf.cast(self.d_model, tf.float32)
105
+
106
+ self.warmup_steps = warmup_steps
107
+
108
+ def __call__(self, step):
109
+ arg1 = tf.math.rsqrt(step)
110
+ arg2 = step * (self.warmup_steps**-1.5)
111
+
112
+ return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)
113
+
114
+
115
+ def prepare_models(encoder_config, transformer_config, replica_batch_size, verbose=0):
116
+ """This function is used to initiate the Encoder and the Transformer with
117
+ appropriate configs set by the user. After initiating the models this
118
+ function returns the Encoder,Transformer and the optimizer.
119
+
120
+ Args:
121
+ encoder_config ([type]): Encoder configuration set by user in the config class.
122
+ transformer_config ([type]): Transformer configuration set by user in the config class.
123
+ replica_batch_size ([type]): Per replica batch size set by user(during distributed training).
124
+ verbose (int, optional): Defaults to 0.
125
+
126
+ Returns:
127
+ [type]: Optimizer, Encoder model and the Transformer
128
+ """
129
+
130
+ # Instantiate an optimizer
131
+ optimizer = tf.keras.optimizers.Adam(learning_rate=0.00051)
132
+
133
+ # Instantiate the encoder model
134
+
135
+ encoder = Efficient_Net_encoder.Encoder(**encoder_config)
136
+
137
+ # Instantiate the decoder model
138
+ transformer = Transformer_decoder.Decoder(**transformer_config)
139
+
140
+ # Show the model architectures and plot the learning rate
141
+ if verbose:
142
+ print("\nEncoder model\n")
143
+ print(encoder.summary())
144
+
145
+ print("\nTransformer model\n")
146
+ print(transformer.summary())
147
+
148
+ return optimizer, encoder, transformer
@@ -5,10 +5,11 @@ import sys
5
5
  from typing import List
6
6
  from typing import Tuple
7
7
 
8
+ import numpy as np
8
9
  import pystow
9
10
  import tensorflow as tf
10
11
 
11
- import DECIMER.config as config
12
+ import DECIMER.pre_process as pre_process
12
13
  import DECIMER.utils as utils
13
14
 
14
15
  # Silence tensorflow model loading warnings.
@@ -17,6 +18,46 @@ logging.getLogger("absl").setLevel("ERROR")
17
18
  # Silence tensorflow errors - not recommended if your model is not working properly.
18
19
  os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
19
20
 
21
+
22
+ # ============================================================================
23
+ # Keras 2 to Keras 3 Compatibility Layer
24
+ # ============================================================================
25
+ class KerasCompatibilityUnpickler(pickle.Unpickler):
26
+ """Custom unpickler to handle Keras 2.x tokenizers in Keras 3 environment.
27
+
28
+ This handles the module path changes between Keras 2 and Keras 3:
29
+ - keras.preprocessing.text -> tensorflow.keras.preprocessing.text
30
+ """
31
+
32
+ def find_class(self, module, name):
33
+ # Redirect old Keras 2.x imports to TensorFlow Keras
34
+ if module.startswith("keras."):
35
+ module = module.replace("keras.", "tensorflow.keras.")
36
+ return super().find_class(module, name)
37
+
38
+
39
+ def load_tokenizer(tokenizer_path: str) -> object:
40
+ """Load tokenizer with Keras 2/3 compatibility.
41
+
42
+ Args:
43
+ tokenizer_path (str): Path to the pickled tokenizer file
44
+
45
+ Returns:
46
+ tokenizer: Loaded tokenizer object
47
+ """
48
+ try:
49
+ # Try standard pickle load first (for Keras 3 tokenizers)
50
+ with open(tokenizer_path, "rb") as f:
51
+ return pickle.load(f)
52
+ except ModuleNotFoundError as e:
53
+ if "keras.preprocessing" in str(e):
54
+ # Use compatibility unpickler for Keras 2.x tokenizers
55
+ with open(tokenizer_path, "rb") as f:
56
+ return KerasCompatibilityUnpickler(f).load()
57
+ else:
58
+ raise
59
+
60
+
20
61
  # Set the absolute path
21
62
  HERE = os.path.dirname(os.path.abspath(__file__))
22
63
 
@@ -58,7 +99,7 @@ def get_models(model_urls: dict):
58
99
  tokenizer_path = os.path.join(
59
100
  model_paths["DECIMER"], "assets", "tokenizer_SMILES.pkl"
60
101
  )
61
- tokenizer = pickle.load(open(tokenizer_path, "rb"))
102
+ tokenizer = load_tokenizer(tokenizer_path)
62
103
 
63
104
  # Load DECIMER models
64
105
  DECIMER_V2 = tf.saved_model.load(model_paths["DECIMER"])
@@ -122,19 +163,19 @@ def detokenize_output_add_confidence(
122
163
 
123
164
 
124
165
  def predict_SMILES(
125
- image_path: str, confidence: bool = False, hand_drawn: bool = False
166
+ image_input: [str, np.ndarray], confidence: bool = False, hand_drawn: bool = False
126
167
  ) -> str:
127
168
  """Predicts SMILES representation of a molecule depicted in the given image.
128
169
 
129
170
  Args:
130
- image_path (str): Path of chemical structure depiction image
131
- confidence (bool): Flag to indicate whether to return confidence values along with SMILES prediction
132
- hand_drawn (bool): Flag to indicate whether the molecule in the image is hand-drawn
171
+ image_input (str or np.ndarray): Path of chemical structure depiction image or a numpy array representing the image.
172
+ confidence (bool): Flag to indicate whether to return confidence values along with SMILES prediction.
173
+ hand_drawn (bool): Flag to indicate whether the molecule in the image is hand-drawn.
133
174
 
134
175
  Returns:
135
- str: SMILES representation of the molecule in the input image, optionally with confidence values
176
+ str: SMILES representation of the molecule in the input image, optionally with confidence values.
136
177
  """
137
- chemical_structure = config.decode_image(image_path)
178
+ chemical_structure = pre_process.decode_image(image_input)
138
179
 
139
180
  model = DECIMER_Hand_drawn if hand_drawn else DECIMER_V2
140
181
  predicted_tokens, confidence_values = model(tf.constant(chemical_structure))
@@ -0,0 +1,243 @@
1
+ import io
2
+ import cv2
3
+ import efficientnet.tfkeras as efn
4
+ import tensorflow as tf
5
+ import numpy as np
6
+
7
+ from PIL import Image
8
+ from PIL import ImageEnhance
9
+ from pillow_heif import register_heif_opener
10
+ from typing import Union
11
+
12
+
13
+ def resize_byratio(image):
14
+ """This function takes a Pillow Image object and will resize the image by
15
+ 512 x 512 To upscale or to downscale the image LANCZOS resampling method is
16
+ used.
17
+
18
+ with the new pillow version the antialias is turned on when using LANCZOS.
19
+ Args: PIL.Image
20
+ Returns: PIL.Image
21
+ """
22
+ maxwidth = 512
23
+ ratio = maxwidth / max(image.width, image.height)
24
+ new_size = int((float(image.width) * ratio)), int((float(image.height) * ratio))
25
+ resized_image = image.resize(new_size, resample=Image.LANCZOS)
26
+ return resized_image
27
+
28
+
29
+ def central_square_image(image):
30
+ """This function takes a Pillow Image object and will add white padding so
31
+ that the image has a square shape with the width/height of the longest side
32
+ of the original image.
33
+
34
+ Args: PIL.Image
35
+ Returns: PIL.Image
36
+ """
37
+ max_wh = int(1.2 * max(image.size))
38
+ # If the new image is smaller than 299x299, then let's paste it into an empty image
39
+ # of that size instead of distorting it later while resizing.
40
+ if max_wh < 512:
41
+ max_wh = 512
42
+ new_im = Image.new(image.mode, (max_wh, max_wh), "white")
43
+ paste_pos = (
44
+ int((new_im.size[0] - image.size[0]) / 2),
45
+ int((new_im.size[1] - image.size[1]) / 2),
46
+ )
47
+ new_im.paste(image, paste_pos)
48
+ return new_im
49
+
50
+
51
+ def delete_empty_borders(image):
52
+ """This function takes a Pillow Image object, converts it to grayscale and
53
+ deletes white space at the borders.
54
+
55
+ Args: PIL.Image
56
+ Returns: PIL.Image
57
+ """
58
+ image = np.asarray(image.convert("L"))
59
+ mask = image > 200
60
+ rows = np.flatnonzero((~mask).sum(axis=1))
61
+ cols = np.flatnonzero((~mask).sum(axis=0))
62
+ crop = image[rows.min() : rows.max() + 1, cols.min() : cols.max() + 1]
63
+ return Image.fromarray(crop)
64
+
65
+
66
+ def PIL_im_to_BytesIO(image):
67
+ """
68
+ Convert pillow image to io.BytesIO object
69
+ Args: PIL.Image
70
+ Returns: io.BytesIO object with the image data
71
+ """
72
+ output = io.BytesIO()
73
+ image.save(output, format="PNG")
74
+ return output
75
+
76
+
77
+ def HEIF_to_pillow(image_path: str):
78
+ """
79
+ Converts Apple's HEIF format to useful pillow object
80
+ Returns: image_path (str): path of input image
81
+ Returns: PIL.Image
82
+ """
83
+ register_heif_opener()
84
+
85
+ heif_file = Image.open(image_path).convert("RGBA")
86
+ return heif_file
87
+
88
+
89
+ def remove_transparent(image: Union[str, np.ndarray]) -> Image.Image:
90
+ """
91
+ Removes the transparent layer from a PNG image with an alpha channel.
92
+
93
+ Args:
94
+ image (Union[str, np.ndarray]): Path of the input image or a numpy array representing the image.
95
+
96
+ Returns:
97
+ PIL.Image.Image: The image with transparency removed.
98
+ """
99
+
100
+ def process_image(png: Image.Image) -> Image.Image:
101
+ """
102
+ Helper function to remove transparency from a single image.
103
+
104
+ Args:
105
+ png (PIL.Image.Image): The input PIL image with transparency.
106
+
107
+ Returns:
108
+ PIL.Image.Image: The image with transparency removed.
109
+ """
110
+ background = Image.new("RGBA", png.size, (255, 255, 255))
111
+ alpha_composite = Image.alpha_composite(background, png)
112
+ return alpha_composite
113
+
114
+ def handle_image_path(image_path: str) -> Image.Image:
115
+ """
116
+ Helper function to handle image paths.
117
+
118
+ Args:
119
+ image_path (str): The path to the input image.
120
+
121
+ Returns:
122
+ PIL.Image.Image: The image with transparency removed.
123
+ """
124
+ try:
125
+ png = Image.open(image_path).convert("RGBA")
126
+ except Exception as e:
127
+ if type(e).__name__ == "UnidentifiedImageError":
128
+ png = HEIF_to_pillow(image_path)
129
+ else:
130
+ print(e)
131
+ raise Exception
132
+ return process_image(png)
133
+
134
+ def handle_numpy_array(array: np.ndarray) -> Image.Image:
135
+ """
136
+ Helper function to handle a numpy array.
137
+
138
+ Args:
139
+ array (np.ndarray): The numpy array representing the image.
140
+
141
+ Returns:
142
+ PIL.Image.Image: The image with transparency removed.
143
+ """
144
+ png = Image.fromarray(array).convert("RGBA")
145
+ return process_image(png)
146
+
147
+ # Check if input is a numpy array
148
+ if isinstance(image, np.ndarray):
149
+ return handle_numpy_array(array=image)
150
+
151
+ return handle_image_path(image_path=image)
152
+
153
+
154
+ def get_bnw_image(image):
155
+ """
156
+ converts images to black and white
157
+ Args: PIL.Image
158
+ Returns: PIL.Image
159
+ """
160
+
161
+ im_np = np.asarray(image)
162
+ grayscale = cv2.cvtColor(im_np, cv2.COLOR_BGR2GRAY)
163
+ # (thresh, im_bw) = cv2.threshold(grayscale, 128, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
164
+ im_pil = Image.fromarray(grayscale)
165
+ enhancer = ImageEnhance.Contrast(im_pil)
166
+ im_output = enhancer.enhance(1.8)
167
+ return im_output
168
+
169
+
170
+ def increase_contrast(image):
171
+ """This function increases the contrast of an image input.
172
+
173
+ Args: PIL.Image
174
+ Returns: PIL.Image
175
+ """
176
+
177
+ # Get brightness range - cast to int to avoid uint8 overflow
178
+ min_val = int(np.min(image))
179
+ max_val = int(np.max(image))
180
+
181
+ # Use LUT to convert image values
182
+ LUT = np.zeros(256, dtype=np.uint8)
183
+ LUT[min_val : max_val + 1] = np.linspace(
184
+ start=0, stop=255, num=(max_val - min_val) + 1, endpoint=True, dtype=np.uint8
185
+ )
186
+
187
+ # Apply LUT and return image
188
+ return Image.fromarray(LUT[image])
189
+
190
+
191
+ def get_resize(image):
192
+ """This function used to decide how to resize a given image without losing
193
+ much information.
194
+
195
+ Args: PIL.Image
196
+ Returns: PIL.Image
197
+ """
198
+ width, height = image.size
199
+
200
+ # Checks the image size and resizes using the LANCOS image resampling
201
+ if (width == height) and (width < 512):
202
+ image = image.resize((512, 512), resample=Image.LANCZOS)
203
+ elif width >= 512 or height >= 512:
204
+ return image
205
+ else:
206
+ image = resize_byratio(image)
207
+
208
+ return image
209
+
210
+
211
+ def increase_brightness(image):
212
+ """This function adjusts the brightness of the given image.
213
+
214
+ Args: PIL.Image
215
+ Returns: PIL.Image
216
+ """
217
+ enhancer = ImageEnhance.Brightness(image)
218
+ image = enhancer.enhance(1.6)
219
+ return image
220
+
221
+
222
+ def decode_image(image_path: Union[str, np.ndarray]):
223
+ """Loads an image and preprocesses the input image in several steps to get
224
+ the image ready for DECIMER input.
225
+
226
+ Args:
227
+ image_path (Union[str, np.ndarray]): path of input image or numpy array representing the image.
228
+
229
+ Returns:
230
+ Processed image
231
+ """
232
+ img = remove_transparent(image_path)
233
+ img = increase_contrast(img)
234
+ img = get_bnw_image(img)
235
+ img = get_resize(img)
236
+ img = delete_empty_borders(img)
237
+ img = central_square_image(img)
238
+ img = increase_brightness(img)
239
+ img = PIL_im_to_BytesIO(img)
240
+ img = tf.image.decode_png(img.getvalue(), channels=3)
241
+ img = tf.image.resize(img, (512, 512), method="gaussian", antialias=True)
242
+ img = efn.preprocess_input(img)
243
+ return img
@@ -0,0 +1,143 @@
1
+ import os
2
+ import re
3
+ import shutil
4
+
5
+ import pystow
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ pattern = "R([0-9]*)|X([0-9]*)|Y([0-9]*)|Z([0-9]*)"
10
+ add_space_re = r"^(\W+)|(\W+)$"
11
+
12
+
13
+ def split_and_modify_atoms(SMILES):
14
+ splitted_SMILES = list(SMILES)
15
+ modified_SMILES = re.sub(r"\s+(?=[a-z])", "", " ".join(map(str, splitted_SMILES)))
16
+ return modified_SMILES
17
+
18
+
19
+ def replacer(match):
20
+ part = match.group(0)
21
+ text = (
22
+ part.replace("1", "!")
23
+ .replace("2", "$")
24
+ .replace("3", "^")
25
+ .replace("4", "<")
26
+ .replace("5", ">")
27
+ .replace("6", "?")
28
+ .replace("7", "£")
29
+ .replace("8", "¢")
30
+ .replace("9", "€")
31
+ .replace("0", "§")
32
+ )
33
+ return text
34
+
35
+
36
+ def add_space(match_):
37
+ if match_.group(1) is not None:
38
+ return "{} ".format(match_.group(1))
39
+ else:
40
+ return " {}".format(match_.group(2))
41
+
42
+
43
+ def encoder(SMILES):
44
+ replaced_SMILES = re.sub(pattern, replacer, SMILES)
45
+ splitted_SMILES = split_and_modify_atoms(replaced_SMILES)
46
+ modified_SMILES = " ".join(
47
+ [re.sub(add_space_re, add_space, word) for word in splitted_SMILES.split()]
48
+ )
49
+ return modified_SMILES
50
+
51
+
52
+ def decoder(predictions):
53
+ modified = (
54
+ predictions.replace("!", "1")
55
+ .replace("$", "2")
56
+ .replace("^", "3")
57
+ .replace("<", "4")
58
+ .replace(">", "5")
59
+ .replace("?", "6")
60
+ .replace("£", "7")
61
+ .replace("¢", "8")
62
+ .replace("€", "9")
63
+ .replace("§", "0")
64
+ )
65
+ return modified
66
+
67
+
68
+ # Downloads the model and unzips the file downloaded, if the model is not present on the working directory.
69
+ def download_trained_weights(model_url: str, model_path: str, verbose=1):
70
+ """This function downloads the trained models and tokenizers to a default
71
+ location. After downloading the zipped file the function unzips the file
72
+ automatically. If the model exists on the default location this function
73
+ will not work.
74
+
75
+ Args:
76
+ model_url (str): trained model url for downloading.
77
+ model_path (str): model default path to download.
78
+
79
+ Returns:
80
+ path (str): downloaded model.
81
+ """
82
+ # Download trained models
83
+ if verbose > 0:
84
+ print("Downloading trained model to " + str(model_path))
85
+ model_path = pystow.ensure("DECIMER-V2", url=model_url)
86
+ if verbose > 0:
87
+ print(model_path)
88
+ print("... done downloading trained model!")
89
+
90
+ with zipfile.ZipFile(model_path.as_posix(), "r") as zip_ref:
91
+ zip_ref.extractall(model_path.parent.as_posix())
92
+
93
+ # Delete zipfile after downloading
94
+ if Path(model_path).exists():
95
+ Path(model_path).unlink()
96
+
97
+
98
+ def ensure_models(default_path: str, model_urls: dict) -> dict:
99
+ """Function to ensure models are present locally.
100
+
101
+ Convenient function to ensure model downloads before usage.
102
+ Models are re-downloaded if the URL changes (e.g., new Zenodo record).
103
+
104
+ Args:
105
+ default_path (str): Default path for model data
106
+ model_urls (dict): Dictionary containing model names as keys and their corresponding URLs as values
107
+
108
+ Returns:
109
+ dict: A dictionary containing model names as keys and their local paths as values
110
+ """
111
+ model_paths = {}
112
+
113
+ for model_name, model_url in model_urls.items():
114
+ model_path = os.path.join(default_path, f"{model_name}_model")
115
+ saved_model_file = os.path.join(model_path, "saved_model.pb")
116
+ version_file = os.path.join(model_path, ".model_url")
117
+
118
+ # Check if model needs to be downloaded:
119
+ # 1. saved_model.pb doesn't exist, OR
120
+ # 2. The URL has changed (model was updated)
121
+ needs_download = not os.path.exists(saved_model_file)
122
+
123
+ if not needs_download and os.path.exists(version_file):
124
+ with open(version_file, "r") as f:
125
+ cached_url = f.read().strip()
126
+ if cached_url != model_url:
127
+ needs_download = True
128
+ print(f"Model {model_name} has been updated, re-downloading...")
129
+
130
+ if needs_download:
131
+ # Clean up incomplete/corrupted model directory if it exists
132
+ if os.path.exists(model_path):
133
+ shutil.rmtree(model_path)
134
+ download_trained_weights(model_url, default_path)
135
+
136
+ # Store the URL used for this download
137
+ os.makedirs(model_path, exist_ok=True)
138
+ with open(version_file, "w") as f:
139
+ f.write(model_url)
140
+
141
+ model_paths[model_name] = model_path
142
+
143
+ return model_paths