11import urllib
22from functools import partial
33from pathlib import Path
4- from typing import ClassVar , Optional , Union , Callable , Sequence
4+ from typing import Optional , Union , Callable , Sequence
55
66import numpy as np
77from parsl .app .app import python_app
2626from psiflow .utils .io import dump_json
2727
2828
29- @psiflow .serializable
29+ apply_threads = python_app (_apply , executors = ["default_threads" ])
30+ apply_htex = python_app (_apply , executors = ["default_htex" ])
31+ apply_modelevaluation = python_app (_apply , executors = ["ModelEvaluation" ])
32+
33+
3034class Hamiltonian (Computable ):
31- # TODO: app is actually an instance variable, but serialization complains..
32- outputs : ClassVar [tuple ] = ("energy" , "forces" , "stress" )
35+ outputs : tuple = ("energy" , "forces" , "stress" )
3336 batch_size = 1000
34- app : ClassVar [ Callable ]
35- function_name : ClassVar [ str ]
37+ app : Callable
38+ function_name : str
3639
3740 def compute (
3841 self ,
@@ -44,12 +47,7 @@ def compute(
4447 outputs = tuple (self .__class__ .outputs )
4548 if batch_size == - 1 :
4649 batch_size = self .__class__ .batch_size
47- return compute (
48- arg ,
49- self .app ,
50- outputs_ = outputs ,
51- batch_size = batch_size ,
52- )
50+ return compute (arg , self .get_app (), outputs_ = outputs , batch_size = batch_size )
5351
5452 def __eq__ (self , hamiltonian : "Hamiltonian" ) -> bool :
5553 raise NotImplementedError
@@ -80,32 +78,13 @@ def serialize_function(self, **kwargs) -> DataFuture:
8078 ).outputs [0 ]
8179
8280 def parameters (self ) -> dict :
83- return {}
84-
85-
86- @psiflow .serializable
87- class Zero (Hamiltonian ):
88-
89- def __init__ (self ):
90- apply_zero = python_app (_apply , executors = ["default_threads" ])
91- self .app = partial (apply_zero , function_cls = ZeroFunction )
92-
93- def __eq__ (self , hamiltonian : Hamiltonian ) -> bool :
94- if type (hamiltonian ) is Zero :
95- return True
96- return False
97-
98- def __mul__ (self , a : float ) -> "Zero" :
99- return Zero ()
100-
101- def __add__ (self , hamiltonian : Hamiltonian ) -> Hamiltonian :
102- # (Zero + Hamiltonian) is different from (Hamiltonian + Zero)
103- return hamiltonian
81+ raise NotImplementedError
10482
105- __rmul__ = __mul__ # handle float * Zero
83+ def get_app (self ) -> Callable :
84+ raise NotImplementedError
10685
10786
108- @psiflow .serializable
87+ @psiflow .register_serializable
10988class MixtureHamiltonian (Hamiltonian ):
11089 hamiltonians : list [Hamiltonian ]
11190 coefficients : list [float ]
@@ -127,10 +106,9 @@ def compute( # override compute for efficient batching
127106 ) -> Union [list [AppFuture ], AppFuture ]:
128107 if outputs is None :
129108 outputs = list (self .__class__ .outputs )
130- apply_apps = [h .app for h in self .hamiltonians ]
109+ apply_apps = [h .get_app () for h in self .hamiltonians ]
131110 reduce_func = partial (
132- aggregate_multiple ,
133- coefficients = np .array (self .coefficients ),
111+ aggregate_multiple , coefficients = np .array (self .coefficients )
134112 )
135113 return compute (
136114 arg ,
@@ -231,25 +209,47 @@ def serialize(self, **kwargs) -> list[DataFuture]:
231209 return [h .serialize_function (** kwargs ) for h in self .hamiltonians ]
232210
233211
234- @psiflow .serializable
212+ @psiflow .register_serializable
213+ class Zero (Hamiltonian ):
214+ function_name : str = "ZeroFunction"
215+
216+ def __init__ (self ):
217+ pass
218+
219+ def get_app (self ) -> Callable :
220+ return partial (apply_threads , function_cls = ZeroFunction )
221+
222+ def __eq__ (self , hamiltonian : Hamiltonian ) -> bool :
223+ if type (hamiltonian ) is Zero :
224+ return True
225+ return False
226+
227+ def __mul__ (self , a : float ) -> "Zero" :
228+ return Zero ()
229+
230+ def __add__ (self , hamiltonian : Hamiltonian ) -> Hamiltonian :
231+ # (Zero + Hamiltonian) is different from (Hamiltonian + Zero)
232+ return hamiltonian
233+
234+ __rmul__ = __mul__ # handle float * Zero
235+
236+
237+ @psiflow .register_serializable
235238class EinsteinCrystal (Hamiltonian ):
236- reference_geometry : Union [Geometry , AppFuture ]
239+ # TODO: logic not consistent depending on Geometry | AppFuture
240+ reference_geometry : Geometry | AppFuture
237241 force_constant : float
238- function_name : ClassVar [ str ] = "EinsteinCrystalFunction"
242+ function_name : str = "EinsteinCrystalFunction"
239243
240- def __init__ (
241- self , geometry : Union [Geometry , AppFuture [Geometry ]], force_constant : float
242- ):
244+ def __init__ (self , geometry : Union [Geometry , AppFuture ], force_constant : float ):
243245 super ().__init__ ()
244246 self .reference_geometry = copy_app_future (geometry )
245247 self .force_constant = force_constant
246248 self .external = None # needed
247- self ._create_apps ()
248249
249- def _create_apps (self ):
250- apply_app = python_app (_apply , executors = ["default_threads" ])
251- self .app = partial (
252- apply_app , function_cls = EinsteinCrystalFunction , ** self .parameters ()
250+ def get_app (self ) -> Callable :
251+ return partial (
252+ apply_threads , function_cls = EinsteinCrystalFunction , ** self .parameters ()
253253 )
254254
255255 def parameters (self ) -> dict :
@@ -269,34 +269,27 @@ def __eq__(self, hamiltonian: Hamiltonian) -> bool:
269269 return True
270270
271271
272- @psiflow .serializable
272+ @psiflow .register_serializable
273273class PlumedHamiltonian (Hamiltonian ):
274274 plumed_input : str # TODO: or future?
275275 external : Optional [psiflow ._DataFuture ]
276- function_name : ClassVar [ str ] = "PlumedFunction"
276+ function_name : str = "PlumedFunction"
277277
278278 def __init__ (
279279 self ,
280280 plumed_input : str ,
281281 external : Union [None , str , Path , File , DataFuture ] = None ,
282282 ):
283283 super ().__init__ ()
284-
285284 self .plumed_input = remove_comments_printflush (plumed_input )
286285 if type (external ) in [str , Path ]:
287286 external = File (str (external ))
288287 if external is not None :
289288 assert external .filepath in self .plumed_input
290289 self .external = external
291- self ._create_apps ()
292290
293- def _create_apps (self ):
294- apply_app = python_app (_apply , executors = ["default_htex" ])
295- self .app = partial (
296- apply_app ,
297- function_cls = PlumedFunction ,
298- ** self .parameters (),
299- )
291+ def get_app (self ) -> Callable :
292+ return partial (apply_htex , function_cls = PlumedFunction , ** self .parameters ())
300293
301294 def parameters (self ) -> dict :
302295 if self .external is not None : # ensure parameters depends on self.external
@@ -314,28 +307,24 @@ def __eq__(self, other: Hamiltonian) -> bool:
314307 return True
315308
316309
317- @psiflow .serializable
310+ @psiflow .register_serializable
318311class Harmonic (Hamiltonian ):
319- reference_geometry : Union [ Geometry , AppFuture [ Geometry ]]
320- hessian : Union [ np .ndarray , AppFuture [ np . ndarray ]]
321- function_name : ClassVar [ str ] = "HarmonicFunction"
312+ reference_geometry : Geometry | AppFuture
313+ hessian : np .ndarray | AppFuture
314+ function_name : str = "HarmonicFunction"
322315
323316 def __init__ (
324317 self ,
325- reference_geometry : Union [ Geometry , AppFuture [ Geometry ]] ,
326- hessian : Union [ np .ndarray , AppFuture [ np . ndarray ]] ,
318+ reference_geometry : Geometry | AppFuture ,
319+ hessian : np .ndarray | AppFuture ,
327320 ):
328321 # TODO: why not copy_app_future(geometry) like others?
329322 self .reference_geometry = reference_geometry
330323 self .hessian = hessian
331- self ._create_apps ()
332324
333- def _create_apps (self ):
334- apply_app = python_app (_apply , executors = ["default_threads" ])
335- self .app = partial (
336- apply_app ,
337- function_cls = HarmonicFunction ,
338- ** self .parameters (),
325+ def get_app (self ) -> Callable :
326+ return partial (
327+ apply_threads , function_cls = HarmonicFunction , ** self .parameters ()
339328 )
340329
341330 def parameters (self ) -> dict :
@@ -367,30 +356,26 @@ def __eq__(self, hamiltonian: Hamiltonian) -> bool:
367356 return True
368357
369358
370- @psiflow .serializable
359+ @psiflow .register_serializable
371360class D3Hamiltonian (Hamiltonian ):
372361 method : str
373362 damping : str
374- function_name : ClassVar [ str ] = "DispersionFunction"
363+ function_name : str = "DispersionFunction"
375364
376365 def __init__ (self , method : str , damping : str = "d3bj" ):
377366 self .method , self .damping = method , damping
378- self ._create_apps ()
379367
380- def _create_apps (self ):
381- # TODO: does this make sense? GPU settings are useless for example
368+ def get_app (self ) -> Callable :
369+ # execution-side parameters of function are not included in self.parameters()
382370 evaluation = psiflow .context ().definitions ["ModelEvaluation" ]
383- apply_app = python_app (_apply , executors = ["ModelEvaluation" ])
384371 resources = evaluation .wq_resources (1 )
385- resources .pop ("gpus" , None )
386-
387- # execution-side parameters of function are not included in self.parameters()
388- self .app = partial (
389- apply_app ,
372+ resources .pop ("gpus" , None ) # do not request GPU
373+ return partial (
374+ apply_modelevaluation ,
390375 function_cls = DispersionFunction ,
391376 parsl_resource_specification = resources ,
392377 ** self .parameters (),
393- num_threads = resources .get ("cores" , 1 ), # TODO: sloppy
378+ num_threads = resources .get ("cores" ),
394379 )
395380
396381 def parameters (self ) -> dict :
@@ -406,11 +391,11 @@ def __eq__(self, hamiltonian: Hamiltonian) -> bool:
406391 return True
407392
408393
409- @psiflow .serializable
394+ @psiflow .register_serializable
410395class MACEHamiltonian (Hamiltonian ):
411396 external : psiflow ._DataFuture
412397 atomic_energies : dict [str , float ]
413- function_name : ClassVar [ str ] = "MACEFunction"
398+ function_name : str = "MACEFunction"
414399
415400 def __init__ (
416401 self ,
@@ -422,16 +407,13 @@ def __init__(
422407 self .external = File (external )
423408 else :
424409 self .external = external
425- self ._create_apps ()
426410
427- def _create_apps (self ):
411+ def get_app (self ) -> Callable :
412+ # execution-side parameters of function are not included in self.parameters()
428413 evaluation = psiflow .context ().definitions ["ModelEvaluation" ]
429- apply_app = python_app (_apply , executors = ["ModelEvaluation" ])
430414 resources = evaluation .wq_resources (1 )
431-
432- # execution-side parameters of function are not included in self.parameters()
433- self .app = partial (
434- apply_app ,
415+ return partial (
416+ apply_modelevaluation ,
435417 function_cls = MACEFunction ,
436418 parsl_resource_specification = resources ,
437419 ** self .parameters (),
@@ -469,7 +451,7 @@ def __eq__(self, hamiltonian: Hamiltonian) -> bool:
469451 # TODO: the methods below are outdated..
470452
471453 @classmethod
472- def mace_mp0 (cls , size : str = "small" ) -> ' MACEHamiltonian' :
454+ def mace_mp0 (cls , size : str = "small" ) -> " MACEHamiltonian" :
473455 urls = dict (
474456 small = "https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/2023-12-10-mace-128-L0_energy_epoch-249.model" , # 2023-12-10-mace-128-L0_energy_epoch-249.model
475457 large = "https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0/2023-12-03-mace-128-L1_epoch-199.model" ,
@@ -483,7 +465,7 @@ def mace_mp0(cls, size: str = "small") -> 'MACEHamiltonian':
483465 return cls (parsl_file , {})
484466
485467 @classmethod
486- def mace_cc (cls ) -> ' MACEHamiltonian' :
468+ def mace_cc (cls ) -> " MACEHamiltonian" :
487469 url = "https://github.com/molmod/psiflow/raw/main/examples/data/ani500k_cc_cpu.model"
488470 parsl_file = psiflow .context ().new_file ("mace_mp_" , ".pth" )
489471 urllib .request .urlretrieve (
0 commit comments