Nexus: update PWscf analyzer - #6196
Conversation
# Conflicts: # nexus/nexus/pwscf_analyzer.py
|
What are the plans to document the analyzer? |
|
I plan to make a "Working with PWscf" manual section with analyzer use as a subsection. I can add a first version after this PR if desired. I also have a few expanding PRs to add on to this (code is done, just rolling out a portion at a time to aid review). Let me know your preference. |
# Conflicts: # nexus/nexus/examples/qmcpack/rsqmc_misc/excited/band.py # nexus/nexus/examples/quantum_espresso/relax_Ge_T_vs_kpoints/relax_vs_kpoints_example.py # nexus/nexus/pwscf_analyzer.py
brockdyer03
left a comment
There was a problem hiding this comment.
Another partial review since I don't have enough time to comb through the LLM's code. As I expected, there are very similar problems in this PR as in #6194.
Things like using obj for class attributes inherently precludes automatic documentation and makes this code harder to maintain.
As a general remark, I want to stress that more flexible code does not always mean the code is more robust.
| # ``the spin up/dw Fermi energies are 5.1 5.2 ev``. Missing eV units, three | ||
| # energies, ``highest occupied level``, and prose merely mentioning Fermi | ||
| # energy fail. | ||
| fermi_energies_pattern = ( |
There was a problem hiding this comment.
Only used in one place, move inline.
| for line in lines: | ||
| if not has_cell and line.strip().startswith('CELL_PARAMETERS'): | ||
| has_cell = True | ||
| if not has_bfgs and 'BFGS Geometry Optimization' in line: |
There was a problem hiding this comment.
Use elif, avoids checking for each thing on lines that may have already had the preceding thing.
There was a problem hiding this comment.
The independent flags are intentionally cumulative
There was a problem hiding this comment.
That does not affect my comment. If a line has one of the strings then it does not have the other strings on it as well, so it should continue to the next line and not check the line for the other things.
| self.read_energies(lines) | ||
| self.read_pressure(lines) | ||
| self.read_stress(lines) | ||
| self.read_forces(lines) |
There was a problem hiding this comment.
Why do we read forces for scf but not structure, when clearly the forces rely on being able to parse a structure?
There was a problem hiding this comment.
The structure is not changing from the already known input structure
|
|
||
| def read_band_edges(self): | ||
| """Add VBM and CBM energies to a parsed bands object.""" | ||
| bands = self.bands |
There was a problem hiding this comment.
Why alias this? Just call self.bands
There was a problem hiding this comment.
Local variable is clearer to read
| unit.append(coordinates) | ||
| if not valid: | ||
| continue | ||
| self.kpoints_cart = np.array(cart,dtype=float) |
There was a problem hiding this comment.
Again, why are we storing these as their own objects instead of in a Structure? Then we don't need to store both cartesian and unit coordinate versions since Structure can do the conversion on its own.
There was a problem hiding this comment.
Same reason as above
| aforces = [] | ||
| j = i+1 | ||
| while j<len(lines): | ||
| match = re.search(atomic_force_pattern,lines[j]) |
There was a problem hiding this comment.
Why are we using regex for something that can be done with a simple string split?
|
Comments addressed. Final changes made. |
brockdyer03
left a comment
There was a problem hiding this comment.
A lot of problems and changes.
I am unsure of this code in its entirety. There are so many small problems and minute details that it would take ages to find everything.
| float(value.lower().replace('d','e')) | ||
| for value in values | ||
| ) | ||
| if len(fermi_energies)>0: |
There was a problem hiding this comment.
As far as I know, all successful QE calculations should have a Fermi energy printed. If this has zero, it should log an error/warning.
| fermi_energies = [] | ||
| for line in lines: | ||
| if 'Fermi energ' in line: | ||
| match = re.search(fermi_energies_pattern,line) |
There was a problem hiding this comment.
No need for a regex here. The lines are printed as
the Fermi energy is 10.1198 ev
A simple str.split() will work just fine.
| if value is not None: | ||
| energy = value | ||
| if energy is not None: | ||
| self.E = energy |
There was a problem hiding this comment.
The implicit else part of this branch makes this is a silent failure. Add logging of some sort.
| return time | ||
| #end def pwscf_time | ||
| calculation = 'scf' | ||
| self.calculation = calculation |
There was a problem hiding this comment.
It looks like this defaults to scf if a known mode is not found. If QE adds any more run modes (not sure what that would be) this will silently assume scf, which is bad practice.
| # Match the Fermi-energy result without collecting unrelated numbers earlier | ||
| # on the line. Real singular forms include ``the Fermi energy is 10.1198 ev`` | ||
| # and ``the Fermi energy = -3.22772442 eV``; spin-polarized output may report | ||
| # ``the spin up/dw Fermi energies are 5.1 5.2 ev``. Missing eV units, three |
There was a problem hiding this comment.
I see no example files with the following formatting:
the spin up/dw Fermi energies are 5.1 5.2 ev
Why are we accepting that?
| conf.axes = axes | ||
| i+=3 | ||
| else: | ||
| conf = None |
| ) | ||
|
|
||
|
|
||
| def parse_float(text): |
There was a problem hiding this comment.
This has the opportunity to fail silently every single time it is called, absolutely bad form. We should know if something is a float or not, and if this just fails silently then we are opening up the rest of the code to huge problems later on.
| if len(aforces)>0 and (nat is None or len(aforces)==nat): | ||
| forces.append(aforces) | ||
| if len(forces)>0: | ||
| self.forces = np.array(forces,dtype=float) |
| ] | ||
| unit.append(coordinates) | ||
| if not valid: | ||
| continue |
There was a problem hiding this comment.
There are a lot of problems I am noticing in this parser.
The most egregious is the countless situations where a silent failure can occur. I've marked some of the more obvious ones, but there are just so many it's hard to even find them all.
The next problem is the copious use of obj. There are many, many situations where a data structure is created with obj that requires documenting it elsewhere. The code should largely be self-documenting, and this certainly is not. I let the obj slide in the RMG analyzer because I was, at the time, unaware of how much of a burden it would be.
Another issue is that every read_ function sets the internal variable instead of returning it. This makes the code extremely difficult, if not impossible, to unit test. Ideally the read_ functions would be static methods unless it is absolutely required that they have access to self, which many of them do not need.
I haven't even gotten to PwscfAnalyzer yet, so after discussion of the comments on PwscfOutData, I will review that next.
There was a problem hiding this comment.
The silent failures are not what you think. They are generally part of the design, i.e. read what is available and don't crash on parse. The physical quantities that are not available will return "None" when queried via the public/user-facing API. The exceptions raised on those relate to truly invalid requests on the part of the user; not failures in the code itself. The behavior of the read_* functions is intentional within this framework (parse/store/query).
I will remind you that obj is part of the Nexus codebase and in no way invalidates code committed within a PR.
This PR generalizes and hardens the PWscf analyzer class. Capabilities are limited to scf, nscf, and relax runs.
Many real input/output files from PWscf are included. These are there to support testing here and for the roll-forward.
GPT-5.6 Sol was used