Skip to content

Commit e72b2c1

Browse files
yury-lysogorskiyYury Lysogorskiy
andauthored
- bugfix reduce_elements functionality when elements are not ordered lexicographically (#19)
- add _configure_keras_backend to the top-most module - bump version to 0.5.5 Co-authored-by: Yury Lysogorskiy <[email protected]>
1 parent 4a9af77 commit e72b2c1

File tree

13 files changed

+232
-46
lines changed

13 files changed

+232
-46
lines changed

docs/gracemaker/faq.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
## Resolving the `TypeError: 'NoneType' object is not callable` in TensorFlow Callbacks
2+
3+
If you encounter a `TypeError: 'NoneType' object is not callable` error, typically after the first epoch, the traceback will look similar to this:
4+
```python
5+
...
6+
if self.monitor_op(current, self.best):
7+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8+
TypeError: 'NoneType' object is not callable
9+
```
10+
This issue often occurs due to a change in how TensorFlow/Keras handles callbacks in newer versions.
11+
To resolve this, ensure that you set the following environment variable before running `gracemaker`:
12+
```bash
13+
export TF_USE_LEGACY_KERAS=1
14+
```
15+
16+
Setting this variable forces the use of the legacy Keras backend, which resolves compatibility conflicts with certain callback implementations.
17+
18+
---
19+
120
## How to Continue a Current Fit?
221

322
- Run `gracemaker -r` in the folder of the original fit to restart from the previous best-test-loss checkpoint.

docs/gracemaker/inputfile.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ seed: 42
77
cutoff: 6
88
99
# cutoff_dict: {Mo: 4, MoNb: 3, W: 5, Ta*: 7 } ## Defining cutoff for each bond type separately, used by certain models
10-
## possible defaults: DEFAULT_CUTOFF_1L, DEFAULT_CUTOFF_2L
10+
## possible defaults: DEFAULT_CUTOFF_1L, DEFAULT_CUTOFF_2L, CUTOFF_2L
1111
1212
######################
1313
## DATA ##
@@ -23,6 +23,11 @@ data:
2323
# save_dataset: False # default is True
2424
# stress_units: eV/A3 # eV/A3 (default) or GPa or kbar or -kbar
2525
# max_workers: 6 # for parallel data builder
26+
27+
## Extra input/reference DataBuilder/s required for model
28+
# extra_components: {
29+
# MagMomDataBuilder: {},
30+
# }
2631
2732
######################
2833
## POTENTIAL ##
@@ -86,12 +91,13 @@ fit:
8691
maxiter: 500 # Max number of optimization epochs
8792
optimizer: Adam
8893
# Optimization with Adam: good for large number of parameters, first-order method
89-
opt_params: { learning_rate: 0.01, use_ema: True, ema_momentum: 0.99, weight_decay: 1.e-20, clipnorm: 1.0}
94+
opt_params: { learning_rate: 0.008, use_ema: True, ema_momentum: 0.99, weight_decay: 1.e-20, clipnorm: 1.0}
9095
# reset_optimizer: True # reset optimizer state, after being loaded from checkpoint
9196
# reset_epoch_and_step: False # reset epoch and step internal counters (stored in checkpoint)
9297
scheduler: cosine_decay # scheduler for learning-rate reduction during training
9398
# available options are: reduce_on_plateau, cosine_decay, linear_decay, exponential_decay
94-
scheduler_params: {"warmup_epochs": 2, "cold_learning_rate": 0.1, "minimal_learning_rate": 0.05}
99+
scheduler_params: {"minimal_learning_rate": 0.0001}
100+
#scheduler_params: {"warmup_epochs": 2, "cold_learning_rate": 0.1, "minimal_learning_rate": 0.05}
95101
# If :warmup_epochs: > 0, begin optimization with :cold_learning_rate: and reach :opt_params::learning_rate:
96102
# within :warmup_epochs: (can be < 1). Else, begin optimization with :opt_params::learning_rate: and decay down to
97103
# minimum_learning_rate within :maxiter: epochs

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "tensorpotential"
7-
version = "0.5.4"
7+
version = "0.5.5"
88
authors = [
99
{ name = "Anton Bochkarev", email = "[email protected]" },
1010
{ name = "Yury Lysogorskiy", email = "[email protected]" },

tensorpotential/__init__.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
1+
import os
2+
import sys
3+
import warnings
4+
5+
6+
def _configure_keras_backend(verbose=True):
7+
"""
8+
Sets TF_USE_LEGACY_KERAS=1 and informs the user.
9+
Must be run before 'import tensorflow'.
10+
"""
11+
target_val = "1"
12+
env_key = "TF_USE_LEGACY_KERAS"
13+
14+
existing_val = os.environ.get(env_key)
15+
16+
# CRITICAL CHECK: Is TensorFlow already loaded?
17+
if 'tensorflow' in sys.modules and existing_val!=target_val:
18+
warnings.warn(
19+
f"TensorFlow was imported before {__name__} could set {env_key}={target_val}. "
20+
"The flag may be ignored. Please import this package first or continue at your own risk.",
21+
RuntimeWarning,
22+
stacklevel=2
23+
)
24+
return
25+
26+
if existing_val is None or existing_val=='':
27+
# It is missing, set it and inform.
28+
os.environ[env_key] = target_val
29+
if verbose:
30+
msg = f"[{__name__}] Info: Environment variable {env_key} is automatically set to '{target_val}'."
31+
print(msg)
32+
elif existing_val not in [target_val, 'true']:
33+
if verbose:
34+
msg = f"[{__name__}] Warning: Environment variable {env_key} is already set to '{existing_val}', but tensorpotential requires '{target_val}'. Do it at your own risk"
35+
print(msg)
36+
37+
# Run immediately on import
38+
_configure_keras_backend(verbose=True)
39+
140
from tensorpotential.tensorpot import TensorPotential
241
from tensorpotential.tpmodel import TPModel
342
from tensorpotential.loss import LossFunction, L2Loss

tensorpotential/calculator/asecalculator.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,38 @@ def __init__(
382382
)
383383
self.data_builders = [self.geom_data_builder]
384384
if constants.ATOMIC_MAGMOM in self.data_keys:
385-
from tensorpotential.experimental.mag.databuilder import MagMomDataBuilder
385+
try:
386+
from tensorpotential.experimental.mag.databuilder import (
387+
MagMomDataBuilder,
388+
)
389+
except ModuleNotFoundError:
390+
raise ImportError(
391+
"TensorPotential.experimental.mag.databuilder not found"
392+
)
386393

387394
self.data_builders.append(MagMomDataBuilder())
395+
if constants.ATOMIC_POS in self.data_keys:
396+
try:
397+
from tensorpotential.experimental.gen_tensor.databuilder import (
398+
PositionsDataBuilder,
399+
)
400+
except ModuleNotFoundError:
401+
raise ImportError(
402+
"TensorPotential.experimental.gen_tensor.databuilder not found"
403+
)
404+
405+
self.data_builders.append(PositionsDataBuilder(cutoff=self.cutoff))
406+
if constants.CELL_VECTORS in self.data_keys:
407+
try:
408+
from tensorpotential.experimental.gen_tensor.databuilder import (
409+
CellDataBuilder,
410+
)
411+
except ModuleNotFoundError:
412+
raise ImportError(
413+
"TensorPotential.experimental.gen_tensor.databuilder not found"
414+
)
415+
416+
self.data_builders.append(CellDataBuilder(cutoff=self.cutoff))
388417

389418
self.padding_manager = PaddingManager(
390419
data_builders=self.data_builders,

tensorpotential/cli/train.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
LRSchedulerFactory,
2626
CustomReduceLROnPlateau,
2727
)
28+
from tensorpotential.utils import NumpyEncoder
2829

2930
LEGACY_SCHEDULER_PARAMS = "learning_rate_reduction"
3031
SCHEDULER_PARAMS = "scheduler_params"
@@ -41,7 +42,7 @@ def dump_metrics(filename, metrics):
4142
directory = os.path.dirname(filename)
4243
if not os.path.exists(directory):
4344
os.makedirs(directory, exist_ok=True)
44-
json_repr = json.dumps(metrics)
45+
json_repr = json.dumps(metrics, cls=NumpyEncoder)
4546
with open(filename, "at") as f:
4647
print("-", json_repr, file=f)
4748

tensorpotential/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
N_STRUCTURES_BATCH_REAL: Final[str] = "batch_total_num_structures"
1717
N_STRUCTURES_BATCH_TOTAL: Final[str] = "n_struct_total"
1818

19+
CELL_VECTORS: Final[str] = "cell_vectors"
20+
21+
ATOMIC_POS: Final[str] = "atomic_positions"
22+
1923
ATOMIC_MU_I: Final[str] = "atomic_mu_i"
2024
ATOMIC_MAGMOM: Final[str] = "atomic_magmom"
2125
ATOMS_TO_STRUCTURE_MAP: Final[str] = "map_atoms_to_structure"

tensorpotential/data/databuilder.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
from tensorpotential import constants
2323
from tensorpotential.data.process_df import ENERGY_CORRECTED_COL, FORCES_COL, STRESS_COL
24-
from tensorpotential.utils import process_cutoff_dict
24+
from tensorpotential.utils import process_cutoff_dict, enforce_pbc
25+
2526

2627
# from ase.neighborlist import neighbor_list as nl
2728

@@ -45,19 +46,6 @@ def f():
4546
thread.start()
4647

4748

48-
def enforce_pbc(atoms, cutoff):
49-
"""Enforce periodic boundary conditions for a given cutoff."""
50-
pos = atoms.get_positions()
51-
if (atoms.get_pbc() == 0).all():
52-
max_d = np.max(np.linalg.norm(pos - pos[0], axis=1))
53-
cell = np.eye(3) * ((max_d + cutoff) * 2)
54-
atoms.set_cell(cell)
55-
atoms.center()
56-
atoms.set_pbc(True)
57-
58-
return atoms
59-
60-
6149
def transparent_iterator(iterator, *arg, **kwarg):
6250
return iterator
6351

tensorpotential/instructions/compute.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -976,12 +976,21 @@ def frwrd(self, input_data: dict, training=False):
976976
)
977977

978978
def get_index_to_select(self, elements_to_select):
979+
# 1. Create a dictionary mapping {symbol: index} for fast O(1) lookup.
980+
# We decode the bytes to string here to match the format of elements_to_select.
981+
symbol_to_index_map = {
982+
sym.decode(): idx
983+
for idx, sym in zip(self.element_map_index.numpy(), self.element_map_symbols.numpy())
984+
}
985+
986+
# 2. Iterate through the input list (elements_to_select) to preserve its order.
979987
index_to_select = []
980-
for ei, es in zip(
981-
self.element_map_index.numpy(), self.element_map_symbols.numpy()
982-
):
983-
if es.decode() in elements_to_select:
984-
index_to_select.append(ei)
988+
for element in elements_to_select:
989+
# Only append if the element exists in our map
990+
if element in symbol_to_index_map:
991+
index_to_select.append(symbol_to_index_map[element])
992+
else:
993+
raise ValueError(f"Element {element} not found in the map ({symbol_to_index_map}).")
985994

986995
return tf.constant(index_to_select, dtype=tf.int32)
987996

tensorpotential/scripts/grace_predict.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
tqdm.pandas()
2525

26+
NUMBER_ASSERT_ERRORS_SHOWN = 3
27+
2628

2729
def set_magmom(at, magmoms):
2830
magmoms = np.array(magmoms)
@@ -38,15 +40,25 @@ def set_magmom(at, magmoms):
3840
return at
3941

4042

41-
def predict(row, calc):
43+
def predict(row, calc, raise_errors):
4244
at = row["ase_atoms"].copy()
4345
if "mag_mom" in row:
4446
at = set_magmom(at, row["mag_mom"])
4547
at.calc = calc
46-
e = at.get_potential_energy()
47-
f = at.get_forces()
48-
s = at.get_stress()
49-
return {"energy": e, "forces": f, "stress": s}
48+
try:
49+
e = at.get_potential_energy()
50+
f = at.get_forces()
51+
s = at.get_stress()
52+
return {"energy": e, "forces": f, "stress": s}
53+
except AssertionError as e:
54+
if raise_errors:
55+
raise e
56+
global NUMBER_ASSERT_ERRORS_SHOWN
57+
if NUMBER_ASSERT_ERRORS_SHOWN > 0:
58+
print("Error: ", e)
59+
NUMBER_ASSERT_ERRORS_SHOWN -= 1
60+
print("No more errors will be shown.")
61+
return {}
5062

5163

5264
def main(args=None):
@@ -79,11 +91,21 @@ def main(args=None):
7991
dest="output",
8092
)
8193

94+
parser.add_argument(
95+
"-e",
96+
"--raise-errors",
97+
help="Whether to NOT ignore errors and stop the program.",
98+
action="store_true",
99+
default=False,
100+
dest="raise_errors",
101+
)
102+
82103
args_parse = parser.parse_args(args)
83104

84105
model_path = os.path.abspath(args_parse.model_path)
85106
dataset_file = args_parse.dataset_file
86107
output_file = args_parse.output
108+
raise_errors = args_parse.raise_errors
87109

88110
logger.info(f"Loading model from: {model_path}")
89111
calc = TPCalculator(
@@ -98,11 +120,10 @@ def main(args=None):
98120

99121
logger.info(f"Starting prediction")
100122

101-
df["prediction"] = df.progress_apply(predict, axis=1, args=(calc,))
102-
df["energy_predicted"] = df["prediction"].map(lambda x: x["energy"])
103-
df["forces_predicted"] = df["prediction"].map(lambda x: x["forces"])
104-
df["stress_predicted"] = df["prediction"].map(lambda x: x["stress"])
105-
# df = df.drop(columns=["ase_atoms", "prediction"])
123+
df["prediction"] = df.progress_apply(predict, axis=1, args=(calc, raise_errors))
124+
df["energy_predicted"] = df["prediction"].map(lambda x: x.get("energy"))
125+
df["forces_predicted"] = df["prediction"].map(lambda x: x.get("forces"))
126+
df["stress_predicted"] = df["prediction"].map(lambda x: x.get("stress"))
106127

107128
logger.info(f"Saving dataset to {output_file}")
108129
df.drop(columns=["ase_atoms", "prediction"]).to_pickle(

0 commit comments

Comments
 (0)