Skip to content

Ensemble: opt-in linear-memory q-space mode, plus four fixes on the Fourier path - #428

Open
SorBalda wants to merge 2 commits into
SSCHAcode:masterfrom
SorBalda:qspace-linear
Open

Ensemble: opt-in linear-memory q-space mode, plus four fixes on the Fourier path#428
SorBalda wants to merge 2 commits into
SSCHAcode:masterfrom
SorBalda:qspace-linear

Conversation

@SorBalda

@SorBalda SorBalda commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Ensemble: opt-in linear-memory q-space mode, plus four fixes on the Fourier path

Part of a three-repository series that makes the q-space TDSCHA path linear in
memory instead of quadratic. The four bug fixes at the end are independent of
the new flag and stand on their own.

Ensemble(..., qspace_light=True)

Opt-in, default False: nothing changes unless it is asked for. In light mode
the dense (3N, 3N) supercell polarization vectors are never materialized — not
even at construction — and reweighting goes through update_weights_fourier,
which works per q point. This is what makes the q-space TDSCHA path linear in
the supercell size; it needs DiagonalizeSupercell(q_only=True) from the
CellConstructor PR above.

Because the polarization vectors do not exist in this mode, the routines that
genuinely need them (get_free_energy, get_free_energy_hessian_dev) now fail
with an explicit message naming the flag, instead of an AttributeError on
None raised deep inside CellConstructor.

refresh_qspace_caches_from_real_space() is public

After a real-space update_weights the q-space caches are stale. The consumer
(tdscha) had to call a private method and then reach into Ensemble.__dict__ to
raise the coherence marker. The public method raises the marker itself, so the
invariant cannot drift and no caller touches private state. The private name
remains as a deprecated alias.

Four bugs on the Fourier path

These are independent of qspace_light and fire on any ensemble:

  • init(): self.N = self.structures assigned the list itself instead of
    its length, so the following [None] * self.N and
    np.tile(..., (self.N,1,1)) could not work once that branch was taken.
  • Fourier sscha_energies used forces_qspace (the ab initio forces) where
    it needs sscha_forces_qspace. The same expression is written correctly in
    update_weights_fourier; the two sites now agree.
  • init_from_structures() left the Fourier energies in Ry/Bohr (missing
    BOHR_TO_ANGSTROM) and with the wrong overall sign — again unlike the
    equivalent block elsewhere in the same file.

update_weights_fourier now also rejects, instead of silently mixing, a new
dynamical matrix whose q points are reordered or whose unit cell differs: the
Fourier update contracts dynq and u_disps_qspace index by index, and
r_lat/q_grid are fixed at generation time.


Testing. tests/test_qspace_light/ (11 tests) checks that light and standard
ensembles agree and that the light path refuses what it cannot do. The full
suite passes with the merge of current master.

…ourier path

Opt-in flag Ensemble(..., qspace_light=True), default False, so nothing
changes unless it is asked for. In light mode the dense (3N, 3N) supercell
polarization vectors are never materialized -- not even at construction --
and reweighting goes through update_weights_fourier, which works per q point.
This is what makes the q-space TDSCHA path linear in memory instead of
quadratic; it needs CellConstructor's DiagonalizeSupercell(q_only=True).
Because the polarization vectors do not exist in this mode, the routines that
genuinely need them (get_free_energy, get_free_energy_hessian_dev) now fail
with an explicit message instead of an AttributeError on None deep inside
CellConstructor.

refresh_qspace_caches_from_real_space() is public. After a real-space
update_weights the q-space caches are stale, and the consumer (tdscha) had to
call a private method and then reach into Ensemble.__dict__ to raise the
coherence marker. The public method raises the marker itself, so the
invariant cannot drift and no caller needs private state. The private name
remains as a deprecated alias.

Four bugs fixed on the Fourier path, all independent of the flag above:

* init(): "self.N = self.structures" assigned the list itself instead of its
  length, so the following [None] * self.N and np.tile(..., (self.N,1,1))
  could not work once that branch was taken.
* the Fourier sscha_energies used forces_qspace (the ab initio forces) where
  it needs sscha_forces_qspace. The same expression is written correctly in
  update_weights_fourier; the two sites now agree.
* init_from_structures() left the Fourier energies in Ry/Bohr (missing
  BOHR_TO_ANGSTROM) and with the wrong overall sign, again unlike the
  equivalent block elsewhere in the file.

update_weights_fourier also rejects, instead of silently mixing, a new
dynamical matrix whose q points are reordered or whose unit cell differs: the
Fourier update contracts dynq and u_disps_qspace index by index, and r_lat /
q_grid are fixed at generation.

Tested by tests/test_qspace_light (11 tests), which check that light and
standard ensembles agree and that the light path refuses what it cannot do.
…peError

The light mode calls DiagonalizeSupercell(q_only=True), which only newer
CellConstructor releases provide. Against an older one the flag failed with
'DiagonalizeSupercell() got an unexpected keyword argument q_only', raised
from inside __setattr__ and pointing at neither the flag nor the requirement.

A module-level probe now reports the capability, so the constructor raises an
explanatory error, and tests/test_qspace_light skips instead of failing when
the installed CellConstructor cannot support it. Nothing else changes: the
default path never touches q_only.

@mesonepigreco mesonepigreco left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The architecture of this PR is actually terrible. Is an hack, not a releasable code.
The API of Ensemble MUST BE STABLE, regardless of the version of other packages like cellconstructor.
What can be done is to use a top-module flag that check wether cellconstructor is at least version 1.7, and raises an error of incompatibility otherwise (CC new version must also be flagged as 1.7, we will merge cellconstructor PR first, so after that point, this version can see the 1.7 CC version and correctly install and work).

Then, all hacks of hassatr dict , and so must be avoided at all costs!!!
Moreover, the get_free_energy module does not need at all the sc vectors, as it might work also with q space polarization. This will allow it to run faster. The only problem is in the get_free_energy_hessian_dev, at which they could be computed and initialized live at the beginning of that function. Therefore, the q_only can become the only implemented way in the ensemble and everything is much cleaner (so remove all the flags). We must avoid this branching of kind of ensembles. Only one way should be implemented. Moreover, we can remove the supercell pathways, as this was obsolete since version 1.4, at 1.7 we could make the julia path the only working environment (as there is a massive difference in both performances and memory allocation).

Moreover, some of the edits are on dead code. We could probably remove dead code if identified.

Comment thread Modules/Ensemble.py
# only newer CellConstructor releases provide. Probe it once, so that asking
# for the flag against an older one gives an explanatory error instead of a
# TypeError raised from inside __setattr__.
import inspect as _inspect

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other. Forbidden to use inspect. instead constrain version to all 1.7

Eventually, you can inspect if cellconstructor version is < 1.7 and raise an exception. The PR of cellconstructor and this one needs to move the version to 1.7.

Comment thread Modules/Ensemble.py
self.current_pols = self.pols_0.copy()
self.w_q_current = self.w_q_0.copy()
self.pols_q_current = self.pols_q_0.copy()
if self.__dict__.get("qspace_light", False):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be avoided at any cost! Use standard
if not self.qspace_light:

If it raises an error, then the code architecture is implemented wrongly. We cannot accept these code inside the main branch.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moreover this is very hacky. If you need to initialize something, do it at module init level! Moreover, Ensemble API must be stable and cannot depend on version of other libraries installed. So every element must be defined in the init for all path. Allocation is a different thing and can be done in a separate allocate() method that performs allocations and initialization (maybe called by the init). This is the clean way to implement things.

Comment thread Modules/Ensemble.py
self.w_q_current = self.w_q_0.copy()
self.pols_q_current = self.pols_q_0.copy()
if self.__dict__.get("qspace_light", False):
if not _CC_HAS_Q_ONLY:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here a version control on cellconstructor is sufficient. Maybe use a global flag on the top of the module for version rather for having individual attributes.

Comment thread Modules/Ensemble.py
# The caches have been rebuilt from scratch: force the q-space
# consumers (QSpaceLanczos) to re-derive them from the real-space
# arrays rather than trusting a stale provenance marker.
self.__dict__["_last_weight_update_fourier"] = False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO! Super hacky! This needs to be completely rewritten

Comment thread Modules/Ensemble.py
self.__dict__["_last_weight_update_fourier"] = False


def _refresh_qspace_caches_from_real_space(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it deprecated and introduced in this same PR? clean it

Comment thread Modules/Ensemble.py

w_original = self.w_0.copy()
pols_original = self.pols_0.copy()
if not self.__dict__.get("qspace_light", False):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again!

Comment thread Modules/Ensemble.py
# Exclude translations
if not self.ignore_small_w:
trans_original = super_struct0.get_asr_modes(pols_original)
if self.__dict__.get("qspace_light", False):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again

Comment thread Modules/Ensemble.py

if not self.ignore_small_w:
trans_mask = super_structure.get_asr_modes(pols)
if self.__dict__.get("qspace_light", False):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And again!

Comment thread Modules/Ensemble.py
Since it is the most time consuming part, it can be safely avoided.
"""

if getattr(self, "qspace_light", False):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO!!!!!

Comment thread Modules/Ensemble.py
#print( "Time elapsed to update weights the sscha energies, forces and displacements:", t1 - t3, "s")
else:
self.current_dyn = new_dynamical_matrix.Copy()
self.__dict__["_last_weight_update_fourier"] = False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants