-
Notifications
You must be signed in to change notification settings - Fork 101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Abinit workflows: static, non-scf and relaxation. #183
Conversation
Hi @utf and @gpetretto, here is the new PR for abinit (previous discussions available here: #81 ). I think now things are fine. I disabled "Allow edits and access to secrets by maintainers", as I think this is the reason why things went sideways. I do not see how to "request" a review above so I'm leaving it this way. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #183 +/- ##
==========================================
- Coverage 77.03% 74.53% -2.50%
==========================================
Files 122 128 +6
Lines 9108 9888 +780
Branches 1429 1493 +64
==========================================
+ Hits 7016 7370 +354
- Misses 1663 2072 +409
- Partials 429 446 +17
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are some more comments. I suppose that some of them could be addressed in a future PR.
src/atomate2/abinit/jobs/base.py
Outdated
# Deal with extra_abivars. | ||
# First take extra_abivars from the input_set_generator of the current maker. | ||
# Update with those from the input_set_generator of the previous maker. | ||
# Update with the extra_abivars from the **params if provided. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems a point where it would be very easy to get the priority order wrong for a user. If I pass an explicit input_set_generator
and I have set extra_abivars
there, I probably would not expect them to be overwritten.
Are all the options needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean that you would remove the last option ? (i.e. possibility to update extra_abivars in the **params)
src/atomate2/abinit/schemas/core.py
Outdated
"""Definition of task document about an Abinit Job.""" | ||
|
||
state: Status = Field(None, description="State of this job.") | ||
run_number: int = Field(None, description="Run number of this job.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could this be replaced by the job index?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still needed? Is this not contained in the history? On that note, do you want to store the history in the task document.
src/atomate2/abinit/schemas/core.py
Outdated
energy: float = Field(None, description="Final energy of the calculation.") | ||
|
||
|
||
class AbinitTaskDocument(StructureMetadata): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No ouputs stored here? Will we revise this later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, related to #145
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to store some outputs here so these workflows could be used? If you follow the VASP task document roughly that would mean it could be a reasonable template to use for a general task document.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just bumping the above comment.
Hi @davidwaroquiers @gpetretto What is the current status/plan for this PR? |
On my side you can review it. I would favor pushing it asap, pointing out that the abinit workflows are still in experimental development and make smaller PRs afterwards. We can open a series of issues related to this PR and deal with it in subsequent PRs. What do you think? |
My biggest concern with the current PR is how the input sets work. They are quite complicated and I wonder if they could be simplified quite a bit. The goal for the new VASP input sets in atomate2 was to make it so that:
With this in mind, I think the input set generators in this PR could be simplified by:
To give you an example of what I mean, the current @dataclass
class GroundStateSetGenerator(AbinitInputGenerator):
"""Common class for ground-state generators."""
calc_type: str = "ground_state"
kppa: Optional[float] = None
ecut: Optional[float] = None
pawecutdg: Optional[float] = None
nband: Optional[int] = None
accuracy: str = "normal"
spin_mode: str = "polarized"
smearing: str = "fermi_dirac:0.1 eV"
charge: float = 0.0
scf_algorithm: Optional[str] = None
shifts: Union[str, tuple] = "Monkhorst-Pack"
restart_from_deps: tuple = (f"{SCF}|{RELAX}|{MOLECULAR_DYNAMICS}:WFK|DEN",)
@dataclass
class StaticSetGenerator(GroundStateSetGenerator):
"""Class to generate Abinit static input sets."""
calc_type: str = "static"
def get_abinit_input(
self,
structure=None,
pseudos=None,
prev_outputs=None,
kppa=GroundStateSetGenerator.kppa,
ecut=GroundStateSetGenerator.ecut,
pawecutdg=GroundStateSetGenerator.pawecutdg,
nband=GroundStateSetGenerator.nband,
accuracy=GroundStateSetGenerator.accuracy,
spin_mode=GroundStateSetGenerator.spin_mode,
smearing=GroundStateSetGenerator.smearing,
charge=GroundStateSetGenerator.charge,
scf_algorithm=GroundStateSetGenerator.scf_algorithm,
shifts=GroundStateSetGenerator.shifts,
):
"""Get AbinitInput object for static calculation."""
if structure is None:
raise RuntimeError("Structure is mandatory for StaticSet generation.")
if prev_outputs is not None:
raise RuntimeError(
"Previous outputs not allowed for StaticSetGenerator. "
"To restart from a previous static or otherwise scf "
"(e.g. relaxation) calculation, use restart_from argument of "
"get_input_set method instead."
)
inp = scf_input(
structure=structure,
pseudos=pseudos,
kppa=kppa,
ecut=ecut,
pawecutdg=pawecutdg,
nband=nband,
accuracy=accuracy,
spin_mode=spin_mode,
smearing=smearing,
charge=charge,
scf_algorithm=scf_algorithm,
shift_mode=self._get_shift_mode(shifts),
)
self._set_shifts_kpoints(inp, structure, kppa, shifts)
return inp
def on_restart(self, abinit_input):
"""Perform updates of AbinitInput upon restart.
In this case, a static calculation can be started from a relaxation one.
The relaxation-like variables need to be removed from the AbinitInput.
"""
# Always remove relaxation-like variables so that if we make the SCF job
# starting from a previous relaxation or molecular dynamics job, the
# structure will indeed be static.
abinit_input.pop_vars(["ionmov", "optcell", "ntime"]) This could be refactored to: @dataclass
class StaticSetGenerator(AbinitInputGenerator):
"""Common class for ground-state generators."""
calc_type: str = "static"
factory: Callable = scf_input
restart_from_deps: tuple = (f"{SCF}|{RELAX}|{MOLECULAR_DYNAMICS}:WFK|DEN",)
factory_kwargs : dict = field(
default_factory=lambda: {
"accuracy": "normal",
"spin_mode": "polarized",
"smearing": "fermi_dirac:0.1 eV",
"charge": 0.0,
"shifts": "Monkhorst-Pack",
}
)
user_abinit_settings : dict = field(
default_factory=lambda: {
"ionmov": None,
"optcell": None,
"ntime": None,
}
) A few points on this:
Similarly, for @dataclass
class RelaxSetGenerator(GroundStateSetGenerator):
"""Class to generate Abinit relaxation input sets."""
calc_type: str = "relaxation"
relax_cell: bool = True
tolmxf: float = 5.0e-5
def get_abinit_input(
self,
structure=None,
pseudos=None,
prev_outputs=None,
kppa=GroundStateSetGenerator.kppa,
ecut=GroundStateSetGenerator.ecut,
pawecutdg=GroundStateSetGenerator.pawecutdg,
nband=GroundStateSetGenerator.nband,
accuracy=GroundStateSetGenerator.accuracy,
spin_mode=GroundStateSetGenerator.spin_mode,
smearing=GroundStateSetGenerator.smearing,
charge=GroundStateSetGenerator.charge,
scf_algorithm=GroundStateSetGenerator.scf_algorithm,
shifts=GroundStateSetGenerator.shifts,
relax_cell=relax_cell,
tolmxf=tolmxf,
**kwargs,
):
if structure is None:
raise RuntimeError("Structure is mandatory for RelaxSet generation.")
if prev_outputs is not None:
raise RuntimeError(
"Previous outputs not allowed for RelaxSetGenerator. "
"To restart from a previous static or otherwise scf "
"(e.g. relaxation) calculation, use restart_from argument of "
"get_input_set method instead."
)
if kwargs.get("atoms_constraints", None) is not None:
raise NotImplementedError("Atoms constraints not implemented.")
ind = 1 if relax_cell else 0
relax_input = ion_ioncell_relax_input(
structure,
pseudos=pseudos,
kppa=kppa,
nband=nband,
ecut=ecut,
pawecutdg=pawecutdg,
accuracy=accuracy,
spin_mode=spin_mode,
smearing=smearing,
charge=charge,
scf_algorithm=scf_algorithm,
shift_mode=self._get_shift_mode(shifts),
)[ind]
relax_input["tolmxf"] = tolmxf
self._set_shifts_kpoints(relax_input, structure, kppa, shifts)
return relax_input Could go to something like: @dataclass
class RelaxSetGenerator(GroundStateSetGenerator):
"""Class to generate Abinit relaxation input sets."""
calc_type: str = "relaxation"
factory: Callable = ion_ioncell_relax_input
relax_cell: bool = True
factory_kwargs : dict = field(
default_factory=lambda: {
"accuracy": "normal",
"spin_mode": "polarized",
"smearing": "fermi_dirac:0.1 eV",
"charge": 0.0,
"shifts": "Monkhorst-Pack",
}
)
user_abinit_settings: dict = field(lambda: {"tolmxf": 5.0e-5})
structure_required: bool = field(True, init=False)
allow_from_prev: bool = field(False, init=False)
def get_abinit_input(
self, structure=None, pseudos=None, prev_outputs=None,
):
input_set = super().get_abinit_input(structure=structure, pseudos=psuedos, prev_outputs=prev_outputs)
ind = 1 if self.relax_cell else 0
return input_set[ind] Some notes on this input set:
For the Non scf maker, I'm actually quite unhappy with how I implemented it in atomate2. I actually think it makes sense to split the inputset generators into two, one for uniform and one for line mode since they are actually different settings that you need. Your current code is this: @dataclass
class NonSCFSetGenerator(AbinitInputGenerator):
"""Class to generate Abinit non-SCF input sets."""
calc_type: str = "nscf"
nbands_factor: float = 1.2
accuracy: str = "normal"
ndivsm: int = 15
kppa: float = 1000
shifts: Union[str, tuple] = "Monkhorst-Pack"
dos_method: Optional[str] = None
projection: str = "l"
mode: str = "line"
restart_from_deps: tuple = (f"{NSCF}:WFK",)
prev_outputs_deps: tuple = (f"{SCF}:DEN",)
def __post_init__(self):
"""Ensure mode is set correctly."""
self.mode = self.mode.lower()
supported_modes = ("line", "uniform")
if self.mode not in supported_modes:
raise ValueError(f"Supported modes are: {', '.join(supported_modes)}")
def get_abinit_input(
self,
structure=None,
pseudos=None,
prev_outputs=None,
nbands_factor=nbands_factor,
accuracy=accuracy,
ndivsm=ndivsm,
kppa=kppa,
shifts=shifts,
dos_method=dos_method,
projection=projection,
mode=mode,
):
"""Get AbinitInput object for Non-SCF calculation."""
if prev_outputs is None:
raise RuntimeError(
"No previous_outputs. Cannot perform non-SCF calculation."
)
if len(prev_outputs) != 1:
raise RuntimeError(
"Should have exactly one previous output (an SCF calculation)."
)
prev_output = prev_outputs[0]
previous_abinit_input = load_abinit_input(prev_output)
previous_structure = get_final_structure(prev_output)
previous_abinit_input.set_structure(previous_structure)
if structure is not None:
if structure != previous_structure:
raise RuntimeError(
"Structure is provided in non-SCF input set generator but "
"is not the same as the one from the previous (SCF) input set."
)
nband = previous_abinit_input.get(
"nband",
previous_abinit_input.structure.num_valence_electrons(
previous_abinit_input.pseudos
),
)
nband = int(np.ceil(nband * nbands_factor))
if mode == "line":
return ebands_from_gsinput(
gs_input=previous_abinit_input,
nband=nband,
ndivsm=ndivsm,
accuracy=accuracy,
)
elif mode == "uniform":
if dos_method:
uniform_input = dos_from_gsinput(
gs_input=previous_abinit_input,
kppa=kppa,
nband=nband,
accuracy=accuracy,
dos_method=dos_method,
projection=projection,
shift_mode=self._get_shift_mode(shifts),
)
else:
uniform_input = nscf_from_gsinput(
gs_input=previous_abinit_input,
kppa=kppa,
nband=nband,
accuracy=accuracy,
shift_mode=self._get_shift_mode(shifts),
)
self._set_shifts_kpoints(uniform_input, structure, kppa, shifts)
return uniform_input
else:
raise RuntimeError(
f"'{self.mode}' is wrong mode for {self.__class__.__name__}."
) I wonder if it could look like: @dataclass
class LineNonSCFSetGenerator(AbinitInputGenerator):
"""Class to generate Abinit non-SCF input sets."""
calc_type: str = "nscf"
factory: Callable = ebands_from_gsinput
restart_from_deps: tuple = (f"{NSCF}:WFK",)
prev_outputs_deps: tuple = (f"{SCF}:DEN",)
nbands_factor: float = 1.2
factory_kwargs : dict = field(
default_factory=lambda: {
"accuracy": "normal",
"ndivsm": 15,
"kppa": 1000,
"shifts": "Monkhorst-Pack",
}
)
def get_abinit_input(
self,
structure=None,
pseudos=None,
prev_outputs=None,
):
"""Get AbinitInput object for Non-SCF calculation."""
self.factor_kwargs["nband"] = _get_nband(prev_outputs, nbands_factor=self.nbands_factor)
return super().get_abinit_input(structure=structure, psuedos=psuedos, prev_outputs=prev_outputs)
def _get_nband(prev_outputs, nbands_factor=1.):
prev_output = prev_outputs[0]
previous_abinit_input = load_abinit_input(prev_output)
nband = previous_abinit_input.get(
"nband",
previous_abinit_input.structure.num_valence_electrons(
previous_abinit_input.pseudos
),
)
return int(np.ceil(nband * nbands_factor)) In some sense, part of the difficulty is that the input set factory methods in abipy are a little bumpy in that their inputs/outputs are quite variable. So I guess a lot of what we have to do in atomate2 is smooth over those bumps. The issue is that because of the factories, if a user wants to customise the factory kwargs, they will have to go to abipy to see what settings are available for that factory method. And by wrapping it in the atomate2 generator, we almost have a nested factory which is complicated. One thing would be to effectively rewrite the abipy factories directly in atomate2 but this would be a lot of work and so probably not recommended. Also, I realise it is much easier for me to write pseudo code without having to deal with the practicalities of the code, so I realise it might not be possible to get it this clean. But I think some standardisation will definitely help with the usability. |
The other things which I'm not sure about are the |
Abinit workflows
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @davidwaroquiers @gpetretto, this is looking excellent! Mostly only cosmetic changes.
One big remaining thing is the task document. Currently no outputs are stored which is a big limitation. What do you think about following the template of the VASP schemas? The CP2K schema is almost exactly the same as VASP so I think if you stay along those lines we should be fine to abstract it out at a later point.
src/atomate2/abinit/jobs/base.py
Outdated
run_abinit_kwargs: dict = field(default_factory=dict) | ||
|
||
# class variables | ||
CRITICAL_EVENTS: ClassVar[Sequence[str]] = () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think it makes sense to the objects themselves.
src/atomate2/abinit/jobs/base.py
Outdated
prev_outputs: str | Path | list[str] | None = None, | ||
restart_from: str | Path | list[str] | None = None, | ||
history: JobHistory | None = None, | ||
) -> jobflow.Flow | jobflow.Job: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this ever return a Flow?
src/atomate2/abinit/jobs/base.py
Outdated
return Response( | ||
output=task_document, | ||
stop_children=True, | ||
stop_jobflow=True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why stop Jobflow? I think this should just be stop children as there may be other parts of the workflow that you want to keep running.
output=task_document, | ||
stop_children=True, | ||
stop_jobflow=True, | ||
stored_data={"error": unconverged_error}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any updated thoughts on this?
src/atomate2/abinit/jobs/core.py
Outdated
# TODO: add the possibility to tune the RelaxInputGenerator options | ||
# in this class method. | ||
return cls( | ||
# input_set_generator=RelaxSetGenerator(relax_cell=True, *args, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line seems like it should provide full control. Any reason it is commented out?
src/atomate2/abinit/schemas/core.py
Outdated
"""Definition of task document about an Abinit Job.""" | ||
|
||
state: Status = Field(None, description="State of this job.") | ||
run_number: int = Field(None, description="Run number of this job.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still needed? Is this not contained in the history? On that note, do you want to store the history in the task document.
src/atomate2/abinit/sets/base.py
Outdated
restart_from_deps: | ||
Defines the files that needs to be linked from previous calculations in | ||
case of restart. The format is a tuple where each element is a list of | ||
"|" separated runelevels (as defined in the AbinitInput object) followed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"|" separated runelevels (as defined in the AbinitInput object) followed | |
"|" separated run levels (as defined in the AbinitInput object) followed |
src/atomate2/abinit/sets/base.py
Outdated
Defines the files that needs to be linked from previous calculations and | ||
are required for the execution of the current calculation. | ||
The format is a tuple where each element is a list of "|" separated | ||
runelevels (as defined in the AbinitInput object) followed by a colon and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
runelevels (as defined in the AbinitInput object) followed by a colon and | |
run levels (as defined in the AbinitInput object) followed by a colon and |
input_index | ||
The index to be used to select the AbinitInput in case a factory | ||
returns a MultiDataset. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove extra empty space.
src/atomate2/abinit/sets/core.py
Outdated
calc_type: str = "static" | ||
factory: Callable = scf_input | ||
restart_from_deps: tuple = field( | ||
default_factory=lambda: tuple(GS_RESTART_FROM_DEPS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for the default_factory stuff here and in other generators where you define restart_from_deps
. That's because tuples are immutable anyway, so you can just definitely this as:
restart_from_deps: tuple = (GS_RESTART_FROM_DEPS, )
Hi @davidwaroquiers @gpetretto a quick catch-up with this PR: |
…it_workflows pull trunk main
Abinit workflows
@utf This should be ready to be reviewed (and hopefully merged). @VicTrqt and @gpetretto did a great work of tackling the different issues, comments and suggestions. It took also some more time due to versions compatibility issues between pymatgen abipy and other packages. The workflow needs approval for the testing. |
Hi @davidwaroquiers, thanks very much for this. I've looked over and it looks ready to merge. The last thing I want to ask is about moving the inputsets to pymatgen. Do you see any barrier to this? |
Hi @utf , Great, thanks a lot for checking this huge PR :) Actually for the input sets, there is indeed an issue in doing that. The input sets use abipy's functionalities and abipy depend on pymatgen. So if we move the input sets to pymatgen, there will be a circular dependency problem. We already thought about this with @gpetretto and one option could be to move it to abipy, which could somehow make sense. Another possibility could be to make an add-on to pymatgen for the abinit input sets ? Not sure if this 1/ would work for the circular dependency and 2/ would actually really make sense. What do you think ? |
Thanks for the quick response. Personally, I think moving them to abipy could make sense? That way other abipy users could make use of the input sets too. |
Yup we will go that way. Hopefully we have this done by next week (and a new abipy release is also made) |
Hi @utf, I have implemented the required changes to move the input sets to abipy. However, since we hope to add a few more workflow before the atomate2 paper is finalised, this means that we would need to rely on making new releases of abipy each time we add a new workflow. Since the time for the paper is relatively strict, we thought that it may be worth merging this as it is and move the input sets after we have finalised more workflows. What do you think? |
Sounds good to me! Thanks everyone involved in this PR. Excited to have it in atomate2! |
I can't merge at the moment:
|
(@gpetretto is there a chance for another (harmonic) phonon workflow? :D) |
I resolved the conflicts. You should be able to merge as soon as David merges my PR to his branch davidwaroquiers#93
We will try to make a phonopy workflow based on the common one. Probably the DFPT will take longer. |
@gpetretto That would be awesome. |
Resolve conflicts
Head branch was pushed to by a user without write access
Hi @utf I just merged Guido's resolution of conflicts, this PR just needs approval for the tests to run now and hopefully you are able to merge afterwards. Thanks a lot! |
* Abinit workflows: static, non-scf and relaxation. * Various changes based on GP's comments. * Removed usage of pmg_serialize. * Fixed issues with changes in current atomate2. * Fixed linting. * More linting. * More modifications + refactored tests to functions only. * change logic of input set generator. update tests * update abipy version * fix lint * more lint * PR review start * linting * linting+pytest * Start schemas like Aims/CP2K * Start schemas like Aims/CP2K * abinit schemas * Optional * test print files * remove schemas/core from import * outdata files * remove if TYPE_CHECKING Vector3D * gsr abinit version * abinit_objects to None for the time being * trajectory * debugging * functioning abinit workflows schemas * debug * switch critical_events into objects instead of str * fix how to check for unconverged states * remove Molecule * remove unused PrevOutput + use atomate2.utils.datetime + factory Callable jobflow fix * linting * remove critical_events from calculation * remove commented code * linting + pass tests * linting --------- Co-authored-by: Guido Petretto <[email protected]> Co-authored-by: Victor Trinquet <[email protected]>
Summary
Include a summary of major changes in bullet points:
Additional dependencies introduced (if any)
TODO