decimer 2.7.1__tar.gz → 2.8.1__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 (36) hide show
  1. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/Efficient_Net_encoder.py +0 -2
  2. decimer-2.8.1/DECIMER/__init__.py +38 -0
  3. decimer-2.8.1/DECIMER/config.py +148 -0
  4. decimer-2.8.1/DECIMER/decimer.py +313 -0
  5. decimer-2.7.1/DECIMER/config.py → decimer-2.8.1/DECIMER/pre_process.py +8 -189
  6. decimer-2.8.1/DECIMER/utils.py +143 -0
  7. decimer-2.8.1/PKG-INFO +249 -0
  8. decimer-2.8.1/README.md +208 -0
  9. decimer-2.8.1/decimer.egg-info/PKG-INFO +249 -0
  10. {decimer-2.7.1 → decimer-2.8.1}/decimer.egg-info/SOURCES.txt +5 -1
  11. {decimer-2.7.1 → decimer-2.8.1}/decimer.egg-info/requires.txt +1 -1
  12. decimer-2.8.1/pyproject.toml +3 -0
  13. {decimer-2.7.1 → decimer-2.8.1}/setup.py +7 -10
  14. decimer-2.8.1/tests/test_functions.py +30 -0
  15. decimer-2.8.1/tests/test_lazy_models.py +317 -0
  16. decimer-2.7.1/DECIMER/__init__.py +0 -29
  17. decimer-2.7.1/DECIMER/decimer.py +0 -172
  18. decimer-2.7.1/DECIMER/utils.py +0 -97
  19. decimer-2.7.1/PKG-INFO +0 -195
  20. decimer-2.7.1/README.md +0 -171
  21. decimer-2.7.1/decimer.egg-info/PKG-INFO +0 -195
  22. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/DECIMER_EfficinetNetV2_Transfomer_Trainer.py +0 -0
  23. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/Predictor_usingCheckpoints.py +0 -0
  24. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/Repack_model.py +0 -0
  25. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/Transformer_decoder.py +0 -0
  26. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/Utils/Create_TFrecord_From_pickles.py +0 -0
  27. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/Utils/Create_image_tokenizer.py +0 -0
  28. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/efficientnetv2/effnetv2_configs.py +0 -0
  29. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/efficientnetv2/effnetv2_model.py +0 -0
  30. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/efficientnetv2/hparams.py +0 -0
  31. {decimer-2.7.1 → decimer-2.8.1}/DECIMER/efficientnetv2/utils.py +0 -0
  32. {decimer-2.7.1 → decimer-2.8.1}/LICENSE +0 -0
  33. {decimer-2.7.1 → decimer-2.8.1}/decimer.egg-info/dependency_links.txt +0 -0
  34. {decimer-2.7.1 → decimer-2.8.1}/decimer.egg-info/entry_points.txt +0 -0
  35. {decimer-2.7.1 → decimer-2.8.1}/decimer.egg-info/top_level.txt +0 -0
  36. {decimer-2.7.1 → decimer-2.8.1}/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,38 @@
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
+ Importing DECIMER does not download or load any model. The models are downloaded
21
+ (if they are missing) and loaded when predict_SMILES() is first called, or earlier
22
+ by calling ``load_models()``. They are stored in ``$PYSTOW_HOME/DECIMER-V2/``
23
+ (``~/.data/DECIMER-V2/`` by default); a directory that already holds them is used
24
+ without any network access.
25
+
26
+ For comments, bug reports, or feature ideas,
27
+ please raise an issue on the GitHub repository.
28
+ """
29
+
30
+ __version__ = "2.8.1"
31
+
32
+ __all__ = [
33
+ "DECIMER",
34
+ ]
35
+
36
+
37
+ from .decimer import load_models
38
+ 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
@@ -0,0 +1,313 @@
1
+ import logging
2
+ import os
3
+ import pickle
4
+ import sys
5
+ import threading
6
+ from typing import List
7
+ from typing import Tuple
8
+
9
+ import numpy as np
10
+ import pystow
11
+ import tensorflow as tf
12
+
13
+ import DECIMER.pre_process as pre_process
14
+ import DECIMER.utils as utils
15
+
16
+ # Silence tensorflow model loading warnings.
17
+ logging.getLogger("absl").setLevel("ERROR")
18
+
19
+ # Silence tensorflow errors - not recommended if your model is not working properly.
20
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
21
+
22
+
23
+ # ============================================================================
24
+ # Keras 2 to Keras 3 Compatibility Layer
25
+ # ============================================================================
26
+ class KerasCompatibilityUnpickler(pickle.Unpickler):
27
+ """Custom unpickler to handle Keras 2.x tokenizers in Keras 3 environment.
28
+
29
+ This handles the module path changes between Keras 2 and Keras 3:
30
+ - keras.preprocessing.text -> tensorflow.keras.preprocessing.text
31
+ """
32
+
33
+ def find_class(self, module, name):
34
+ # Redirect old Keras 2.x imports to TensorFlow Keras
35
+ if module.startswith("keras."):
36
+ module = module.replace("keras.", "tensorflow.keras.")
37
+ return super().find_class(module, name)
38
+
39
+
40
+ def load_tokenizer(tokenizer_path: str) -> object:
41
+ """Load tokenizer with Keras 2/3 compatibility.
42
+
43
+ Args:
44
+ tokenizer_path (str): Path to the pickled tokenizer file
45
+
46
+ Returns:
47
+ tokenizer: Loaded tokenizer object
48
+ """
49
+ try:
50
+ # Try standard pickle load first (for Keras 3 tokenizers)
51
+ with open(tokenizer_path, "rb") as f:
52
+ return pickle.load(f)
53
+ except ModuleNotFoundError as e:
54
+ if "keras.preprocessing" in str(e):
55
+ # Use compatibility unpickler for Keras 2.x tokenizers
56
+ with open(tokenizer_path, "rb") as f:
57
+ return KerasCompatibilityUnpickler(f).load()
58
+ else:
59
+ raise
60
+
61
+
62
+ # Set the absolute path
63
+ HERE = os.path.dirname(os.path.abspath(__file__))
64
+
65
+ # Set model to run on default GPU and allow memory to grow as much as needed.
66
+ # This allows us to run multiple instances of inference in the same GPU.
67
+ os.environ["CUDA_VISIBLE_DEVICES"] = "0"
68
+ gpus = tf.config.experimental.list_physical_devices("GPU")
69
+ for gpu in gpus:
70
+ tf.config.experimental.set_memory_growth(gpu, True)
71
+
72
+ # The models are NOT downloaded or loaded when this module is imported. They are
73
+ # downloaded (if missing) and loaded the first time they are needed, either by the
74
+ # first predict_SMILES() call or by an explicit load_models(), and cached afterwards.
75
+ # They live in $PYSTOW_HOME/DECIMER-V2/ (PYSTOW_HOME defaults to ~/.data), read when
76
+ # the models are first needed. A directory that already holds them is used as it is,
77
+ # without any network access.
78
+ model_urls = {
79
+ "DECIMER": "https://zenodo.org/record/8300489/files/models.zip",
80
+ "DECIMER_HandDrawn": "https://zenodo.org/records/10781330/files/DECIMER_HandDrawn_model.zip",
81
+ }
82
+
83
+
84
+ def _get_default_path():
85
+ """Directory that holds (or will hold) the downloaded models.
86
+
87
+ Looked up when the models are first needed, so that PYSTOW_HOME can still be set
88
+ after DECIMER was imported. Nothing is created here.
89
+ """
90
+ return pystow.join("DECIMER-V2", ensure_exists=False)
91
+
92
+
93
+ def get_models(model_urls: dict):
94
+ """Download and load models from the provided URLs.
95
+
96
+ This function downloads models from the provided URLs to a default location,
97
+ then loads tokenizers and TensorFlow saved models. It loads everything at once
98
+ and does not touch the cached models of predict_SMILES(); most callers want
99
+ load_models() instead.
100
+
101
+ Args:
102
+ model_urls (dict): A dictionary containing model names as keys and their corresponding URLs as values.
103
+
104
+ Returns:
105
+ tuple: A tuple containing loaded tokenizer and TensorFlow saved models.
106
+ - tokenizer (object): Tokenizer for DECIMER model.
107
+ - DECIMER_V2 (tf.saved_model): TensorFlow saved model for DECIMER.
108
+ - DECIMER_Hand_drawn (tf.saved_model): TensorFlow saved model for DECIMER HandDrawn.
109
+ """
110
+ # Download models to a default location
111
+ model_paths = utils.ensure_models(
112
+ default_path=_get_default_path(), model_urls=model_urls
113
+ )
114
+
115
+ # Load tokenizers
116
+ tokenizer_path = os.path.join(
117
+ model_paths["DECIMER"], "assets", "tokenizer_SMILES.pkl"
118
+ )
119
+ tokenizer = load_tokenizer(tokenizer_path)
120
+
121
+ # Load DECIMER models
122
+ DECIMER_V2 = tf.saved_model.load(model_paths["DECIMER"])
123
+ DECIMER_Hand_drawn = tf.saved_model.load(model_paths["DECIMER_HandDrawn"])
124
+
125
+ return tokenizer, DECIMER_V2, DECIMER_Hand_drawn
126
+
127
+
128
+ def _load_tokenizer():
129
+ """Ensure the printed-structure model files are present and load its tokenizer.
130
+
131
+ The tokenizer of the printed-structure model decodes the output of both models.
132
+ """
133
+ model_paths = utils.ensure_models(
134
+ default_path=_get_default_path(),
135
+ model_urls={"DECIMER": model_urls["DECIMER"]},
136
+ )
137
+ return load_tokenizer(
138
+ os.path.join(model_paths["DECIMER"], "assets", "tokenizer_SMILES.pkl")
139
+ )
140
+
141
+
142
+ def _load_saved_model(model_name: str):
143
+ """Ensure the files of one model are present, then load its SavedModel."""
144
+ model_paths = utils.ensure_models(
145
+ default_path=_get_default_path(),
146
+ model_urls={model_name: model_urls[model_name]},
147
+ )
148
+ return tf.saved_model.load(model_paths[model_name])
149
+
150
+
151
+ # Module-level names that are filled on first use. Once loaded, an object is stored
152
+ # in this module's globals under the same name that decimer 2.8.0 and older bound at
153
+ # import, so `from DECIMER.decimer import tokenizer, DECIMER_V2` keeps working.
154
+ _lazy_loaders = {
155
+ "tokenizer": _load_tokenizer,
156
+ "DECIMER_V2": lambda: _load_saved_model("DECIMER"),
157
+ "DECIMER_Hand_drawn": lambda: _load_saved_model("DECIMER_HandDrawn"),
158
+ }
159
+ _load_lock = threading.RLock()
160
+
161
+
162
+ def _lazy(name: str):
163
+ """Return tokenizer, DECIMER_V2 or DECIMER_Hand_drawn, loading it on first use.
164
+
165
+ Downloads the files first if they are missing. The lock makes concurrent first
166
+ calls download and load once. A failed download or load is not cached, so the
167
+ next call tries again.
168
+ """
169
+ try:
170
+ return globals()[name]
171
+ except KeyError:
172
+ pass
173
+ with _load_lock:
174
+ if name not in globals():
175
+ globals()[name] = _lazy_loaders[name]()
176
+ return globals()[name]
177
+
178
+
179
+ def __getattr__(name: str):
180
+ """Load tokenizer, DECIMER_V2 and DECIMER_Hand_drawn on first attribute access."""
181
+ if name == "default_path":
182
+ return _get_default_path()
183
+ if name in _lazy_loaders:
184
+ return _lazy(name)
185
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
186
+
187
+
188
+ def load_models(printed: bool = True, hand_drawn: bool = False) -> None:
189
+ """Download (if missing) and load the models now, instead of at the first prediction.
190
+
191
+ Call this once at start-up if the first predict_SMILES() call should not pay for
192
+ the download and the loading, for example in a server or when building an image.
193
+ Models that are already loaded are not loaded again. The tokenizer, which both
194
+ models need, is always loaded.
195
+
196
+ Args:
197
+ printed (bool): Load the model for printed chemical structures (the default
198
+ model of predict_SMILES).
199
+ hand_drawn (bool): Also load the hand-drawn model. load_models(hand_drawn=True)
200
+ loads everything that decimer 2.8.0 and older loaded on import.
201
+ """
202
+ with _load_lock:
203
+ _lazy("tokenizer")
204
+ if printed:
205
+ _lazy("DECIMER_V2")
206
+ if hand_drawn:
207
+ _lazy("DECIMER_Hand_drawn")
208
+
209
+
210
+ def detokenize_output(predicted_array: int) -> str:
211
+ """This function takes the predicted tokens from the DECIMER model and
212
+ returns the decoded SMILES string.
213
+
214
+ Args:
215
+ predicted_array (int): Predicted tokens from DECIMER
216
+
217
+ Returns:
218
+ (str): SMILES representation of the molecule
219
+ """
220
+ index_word = _lazy("tokenizer").index_word
221
+ outputs = [index_word[i] for i in predicted_array[0].numpy()]
222
+ prediction = (
223
+ "".join([str(elem) for elem in outputs])
224
+ .replace("<start>", "")
225
+ .replace("<end>", "")
226
+ )
227
+ return prediction
228
+
229
+
230
+ def detokenize_output_add_confidence(
231
+ predicted_array: tf.Tensor,
232
+ confidence_array: tf.Tensor,
233
+ ) -> List[Tuple[str, float]]:
234
+ """This function takes the predicted array of tokens as well as the
235
+ confidence values returned by the Transformer Decoder and returns a list of
236
+ tuples that contain each token of the predicted SMILES string and the
237
+ confidence value.
238
+
239
+ Args:
240
+ predicted_array (tf.Tensor): Transformer Decoder output array (predicted tokens)
241
+
242
+ Returns:
243
+ str: SMILES string
244
+ """
245
+ index_word = _lazy("tokenizer").index_word
246
+ prediction_with_confidence = [
247
+ (
248
+ index_word[predicted_array[0].numpy()[i]],
249
+ confidence_array[i].numpy(),
250
+ )
251
+ for i in range(len(confidence_array))
252
+ ]
253
+ # remove start and end tokens
254
+ prediction_with_confidence_ = prediction_with_confidence[1:-1]
255
+
256
+ decoded_prediction_with_confidence = list(
257
+ [(utils.decoder(tok), conf) for tok, conf in prediction_with_confidence_]
258
+ )
259
+
260
+ return decoded_prediction_with_confidence
261
+
262
+
263
+ def predict_SMILES(
264
+ image_input: [str, np.ndarray], confidence: bool = False, hand_drawn: bool = False
265
+ ) -> str:
266
+ """Predicts SMILES representation of a molecule depicted in the given image.
267
+
268
+ Args:
269
+ image_input (str or np.ndarray): Path of chemical structure depiction image or a numpy array representing the image.
270
+ confidence (bool): Flag to indicate whether to return confidence values along with SMILES prediction.
271
+ hand_drawn (bool): Flag to indicate whether the molecule in the image is hand-drawn.
272
+
273
+ Returns:
274
+ str: SMILES representation of the molecule in the input image, optionally with confidence values.
275
+ """
276
+ chemical_structure = pre_process.decode_image(image_input)
277
+
278
+ # The first call downloads the models that are missing and loads the ones it needs
279
+ # (the tokenizer, and the printed or the hand-drawn model); later calls reuse them.
280
+ _lazy("tokenizer")
281
+ model = _lazy("DECIMER_Hand_drawn" if hand_drawn else "DECIMER_V2")
282
+ predicted_tokens, confidence_values = model(tf.constant(chemical_structure))
283
+
284
+ predicted_SMILES = utils.decoder(detokenize_output(predicted_tokens))
285
+
286
+ if confidence:
287
+ predicted_SMILES_with_confidence = detokenize_output_add_confidence(
288
+ predicted_tokens, confidence_values
289
+ )
290
+ return predicted_SMILES, predicted_SMILES_with_confidence
291
+
292
+ return predicted_SMILES
293
+
294
+
295
+ def main():
296
+ """This function take the path of the image as user input and returns the
297
+ predicted SMILES as output in CLI.
298
+
299
+ Agrs:
300
+ str: image_path
301
+
302
+ Returns:
303
+ str: predicted SMILES
304
+ """
305
+ if len(sys.argv) != 2:
306
+ print("Usage: {} $image_path".format(sys.argv[0]))
307
+ else:
308
+ SMILES = predict_SMILES(sys.argv[1])
309
+ print(SMILES)
310
+
311
+
312
+ if __name__ == "__main__":
313
+ main()