Skip to content
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

STM settings as warg to the to_stm method of the PartialCharge class #158

Merged
merged 68 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
faccb92
started to implement partial charges.
MichaelWolloch Feb 13, 2024
7de89b0
Merge remote-tracking branch 'origin/master' into partial_charges_and…
MichaelWolloch Feb 13, 2024
d244700
Merge remote-tracking branch 'origin/master' into partial_charges_and…
MichaelWolloch Feb 13, 2024
75de62d
Small typo fixed in the schema, added 'partial_charge' to the __init_…
MichaelWolloch Feb 26, 2024
5abfa06
Merge branch 'vasp-dev:master' into partial_charges_and_STM
MichaelWolloch Feb 28, 2024
9455876
first version of partial_charge class. Still no tests.
MichaelWolloch Feb 28, 2024
862b83d
running black and isort
MichaelWolloch Feb 28, 2024
4da95c3
Cleaned up the STM_data class.
MichaelWolloch Mar 1, 2024
2ce734b
Adding the unit tests for real now...
MichaelWolloch Mar 1, 2024
8081576
Slight cleanup, added docstrings, and a couple more tests.
MichaelWolloch Mar 5, 2024
6a10e4f
changed current units to nA and constant current no outputs correct h…
MichaelWolloch Mar 7, 2024
9daf282
Fixed test for constant-current after changing the input units.
MichaelWolloch Mar 11, 2024
f58354f
Add basic Contour class
martin-schlipf Mar 12, 2024
ca664a9
Add Contour to Graph infrastructure
martin-schlipf Mar 12, 2024
19e5490
Implement basic rectangular heatmap
martin-schlipf Mar 12, 2024
993dd31
Implement supercell for Heatmap
martin-schlipf Mar 12, 2024
58485af
Add label from Contour to plotly figure
martin-schlipf Mar 12, 2024
11c8a9f
Hide axes if only Contour are present
martin-schlipf Mar 12, 2024
d3169ef
Implement interpolation
martin-schlipf Mar 12, 2024
f1ad337
Put schema and dataclass in alphabetical order.
MichaelWolloch Mar 12, 2024
2bf6fd9
Removed old plotting and implemented graph.Graph.
MichaelWolloch Mar 12, 2024
7d706c4
Implement drawing the unit cell
martin-schlipf Mar 12, 2024
ccda056
Merge remote-tracking branch 'origin/stm' into partial_charges_and_STM
MichaelWolloch Mar 12, 2024
4a530f3
Fix transpose of data w.r.t. plotly
martin-schlipf Mar 13, 2024
0cba8d8
Make plot look prettier by using same scale for axes
martin-schlipf Mar 13, 2024
9155c6c
Merge remote-tracking branch 'origin/stm' into partial_charges_and_STM
MichaelWolloch Mar 13, 2024
d00d5e7
Fix plotting supercell with interpolation
martin-schlipf Mar 13, 2024
be5f96e
Choose different default colorscale
martin-schlipf Mar 13, 2024
63d9297
Merge remote-tracking branch 'origin/stm' into partial_charges_and_STM
MichaelWolloch Mar 13, 2024
3416766
Fixed bug with passing also z component of first two lattice vectors …
MichaelWolloch Mar 13, 2024
43a7cf2
fixed test as well
MichaelWolloch Mar 13, 2024
2d9b77f
removed testing functions
MichaelWolloch Mar 13, 2024
6d7e759
Add documentation to Contour class
martin-schlipf Mar 14, 2024
780f775
Introduce a base class to define the API Graph uses
martin-schlipf Mar 14, 2024
fe87bb9
Add test combining Contour and Series
martin-schlipf Mar 14, 2024
4a8602a
Merge remote-tracking branch 'origin/stm' into partial_charges_and_STM
MichaelWolloch Mar 14, 2024
dac38b9
set the height of contour plots to 500 to make STM pics larger
MichaelWolloch Mar 14, 2024
ff143a0
Fix formatting
martin-schlipf Mar 14, 2024
920974e
Rename mode to selection for consistency with other classes
martin-schlipf Mar 14, 2024
d0274d9
Remove undocumented/untested squeeze option from read
martin-schlipf Mar 14, 2024
b0eb93c
Make STM settings immutable
martin-schlipf Mar 14, 2024
ea15e4b
Use select Tree for selection in partial charge
martin-schlipf Mar 14, 2024
7781c08
Rename to_array -> to_numpy and use selection instead of spin
martin-schlipf Mar 14, 2024
161b99f
Refactor to make code more compact
martin-schlipf Mar 14, 2024
c72cbdd
Refactor partial charge
martin-schlipf Mar 15, 2024
3ad5197
Refactor spline construction
martin-schlipf Mar 15, 2024
a33a05e
Refactor test
martin-schlipf Mar 15, 2024
bfac8ea
Fix data access decorators
martin-schlipf Mar 15, 2024
3614343
Capitalize STM
martin-schlipf Mar 15, 2024
ee6ff90
Update dependencies for scipy
martin-schlipf Mar 15, 2024
78f3c3f
Make scipy optional
martin-schlipf Mar 15, 2024
516bfdb
corrected format label for constant current scans.
MichaelWolloch Mar 20, 2024
40660e2
now printing current in nA. Had forget to change that in the tests.
MichaelWolloch Apr 12, 2024
4288ad1
Changed the implementation so that also non-orthogonal z-axis are all…
MichaelWolloch May 15, 2024
eef83b0
Merge remote-tracking branch 'origin/master' into partial_charges_and…
MichaelWolloch May 16, 2024
e56f041
fixes of merge conflicts
MichaelWolloch May 16, 2024
5e476b7
fixed lattice plane passing to contour and updated tests accordingly
MichaelWolloch May 16, 2024
bdc8f4a
updated cc height difference to be measured in Angstrom; relative to …
MichaelWolloch May 16, 2024
fce96d0
fixed poetry.lock that I apparently broke at some point
MichaelWolloch May 16, 2024
4789c31
Using keyword only arguments for tip_height, current, and supercell a…
MichaelWolloch May 17, 2024
57f266a
Remove commented out old version of getting STM plane
MichaelWolloch May 17, 2024
668c64f
made STM_settings a keyword argument to the to_stm method, so the set…
MichaelWolloch May 21, 2024
be306bc
re-added the stm_settings property
MichaelWolloch May 24, 2024
e897f24
small cleanup
MichaelWolloch Jun 18, 2024
9e288cb
fixed mutability of stm settings and tests. apparently broke constant…
MichaelWolloch Jun 18, 2024
3be4afb
formatting
MichaelWolloch Jun 18, 2024
b8b9dee
formatting
MichaelWolloch Jun 18, 2024
28507fc
Prevent calling optional scipy in conftest.py
martin-schlipf Jun 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 26 additions & 19 deletions src/py4vasp/calculation/_partial_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def to_stm(
tip_height: float = 2.0,
current: float = 1.0,
supercell: Union[int, np.array] = 2,
stm_settings: STM_settings = STM_settings(),
) -> Graph:
"""Generate STM image data from the partial charge density.

Expand All @@ -129,6 +130,9 @@ def to_stm(
Only used in "constant_current" mode.
supercell : int | np.array
The supercell to be used for plotting the STM. The default is 2.
stm_settings : STM_settings
Settings for the STM simulation concerning smoothening parameters
and interpolation. The default is STM_settings().

Returns
-------
Expand All @@ -143,8 +147,9 @@ def to_stm(
if index > 0:
message = "Selecting more than one STM is not implemented."
raise exception.NotImplemented(message)
contour = self._make_contour(selection, tip_height, current)
contour = self._make_contour(selection, tip_height, current, stm_settings)
contour.supercell = self._parse_supercell(supercell)
contour.settings = stm_settings
return Graph(series=contour, title=contour.label)

def _parse_supercell(self, supercell):
Expand All @@ -156,16 +161,18 @@ def _parse_supercell(self, supercell):
The supercell is used to multiply the x and y directions of the lattice."""
raise exception.IncorrectUsage(message)

def _make_contour(self, selection, tip_height, current):
def _make_contour(self, selection, tip_height, current, stm_settings):
self._raise_error_if_tip_too_far_away(tip_height)
mode = self._parse_mode(selection)
spin = self._parse_spin(selection)
self._raise_error_if_selection_not_understood(selection, mode, spin)
smoothed_charge = self._get_stm_data(spin)
smoothed_charge = self._get_stm_data(spin, stm_settings)
if mode == "constant_height" or mode is None:
return self._constant_height_stm(smoothed_charge, tip_height, spin)
return self._constant_height_stm(
smoothed_charge, tip_height, spin, stm_settings
)
current = current * 1e-09 # convert nA to A
return self._constant_current_stm(smoothed_charge, current, spin)
return self._constant_current_stm(smoothed_charge, current, spin, stm_settings)

def _parse_mode(self, selection):
for mode, aliases in _STM_MODES.items():
Expand All @@ -185,14 +192,14 @@ def _raise_error_if_selection_not_understood(self, selection, mode, spin):
message = f"STM mode '{selection}' was parsed as mode='{mode}' and spin='{spin}' which could not be used. Please use 'constant_height' or 'constant_current' as mode and 'up', 'down', or 'total' as spin."
raise exception.IncorrectUsage(message)

def _constant_current_stm(self, smoothed_charge, current, spin):
def _constant_current_stm(self, smoothed_charge, current, spin, stm_settings):
z_start = _min_of_z_charge(
self._get_stm_data(spin),
sigma=self.stm_settings.sigma_z,
truncate=self.stm_settings.truncate,
self._get_stm_data(spin, stm_settings),
sigma=stm_settings.sigma_z,
truncate=stm_settings.truncate,
)
grid = self.grid()
z_step = 1 / self.stm_settings.interpolation_factor
z_step = 1 / stm_settings.interpolation_factor
# roll smoothed charge so that we are not bothered by the boundary of the
# unit cell if the slab is not centered. z_start is now the first index
smoothed_charge = np.roll(smoothed_charge, -z_start, axis=2)
Expand All @@ -205,9 +212,9 @@ def _constant_current_stm(self, smoothed_charge, current, spin):
label = f"STM of {topology} for {spin_label} at constant current={current*1e9:.2f} nA"
return Contour(data=scan, lattice=self._get_stm_plane(), label=label)

def _constant_height_stm(self, smoothed_charge, tip_height, spin):
def _constant_height_stm(self, smoothed_charge, tip_height, spin, stm_settings):
zz = self._z_index_for_height(tip_height + self._get_highest_z_coord())
height_scan = smoothed_charge[:, :, zz] * self.stm_settings.enhancement_factor
height_scan = smoothed_charge[:, :, zz] * stm_settings.enhancement_factor
spin_label = "both spin channels" if spin == "total" else f"spin {spin}"
topology = self._topology()
label = f"STM of {topology} for {spin_label} at constant height={float(tip_height):.2f} Angstrom"
Expand Down Expand Up @@ -250,27 +257,27 @@ def _raise_error_if_tip_too_far_away(self, tip_height):
You would be sampling the bottom of your slab, which is not supported."""
raise exception.IncorrectUsage(message)

def _get_stm_data(self, spin):
def _get_stm_data(self, spin, stm_settings):
if 0 not in self.bands() or 0 not in self.kpoints():
massage = """Simulated STM images are only supported for non-separated bands and k-points.
Please set LSEPK and LSEPB to .FALSE. in the INCAR file."""
raise exception.NotImplemented(massage)
chg = self._correct_units(self.to_numpy(spin, band=0, kpoint=0))
return self._smooth_stm_data(chg)
return self._smooth_stm_data(chg, stm_settings)

def _correct_units(self, charge_data):
grid_volume = np.prod(self.grid())
cell_volume = self._structure.volume()
return charge_data / (grid_volume * cell_volume)

def _smooth_stm_data(self, data):
def _smooth_stm_data(self, data, stm_settings):
sigma = (
self.stm_settings.sigma_xy,
self.stm_settings.sigma_xy,
self.stm_settings.sigma_z,
stm_settings.sigma_xy,
stm_settings.sigma_xy,
stm_settings.sigma_z,
)
return ndimage.gaussian_filter(
data, sigma=sigma, truncate=self.stm_settings.truncate, mode="wrap"
data, sigma=sigma, truncate=stm_settings.truncate, mode="wrap"
)

def _get_stm_plane(self):
Expand Down
77 changes: 62 additions & 15 deletions tests/calculation/test_partial_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from py4vasp import calculation
from py4vasp._util.slicing import plane
from py4vasp.calculation._partial_charge import STM_settings
from py4vasp.exception import IncorrectUsage, NoData, NotImplemented


Expand Down Expand Up @@ -102,7 +103,7 @@ def make_reference_partial_charge(raw_data, selection):
return parchg


def test_read(PartialCharge, Assert):
def test_read(PartialCharge, Assert, not_core):
actual = PartialCharge.read()
expected = PartialCharge.ref
Assert.allclose(actual["bands"], expected.bands)
Expand All @@ -113,31 +114,31 @@ def test_read(PartialCharge, Assert):
Assert.same_structure(actual["structure"], expected.structure.read())


def test_topology(PartialCharge):
def test_topology(PartialCharge, not_core):
actual = PartialCharge._topology()
expected = str(PartialCharge.ref.structure._topology())
assert actual == expected


def test_bands(PartialCharge, Assert):
def test_bands(PartialCharge, Assert, not_core):
actual = PartialCharge.bands()
expected = PartialCharge.ref.bands
Assert.allclose(actual, expected)


def test_kpoints(PartialCharge, Assert):
def test_kpoints(PartialCharge, Assert, not_core):
actual = PartialCharge.kpoints()
expected = PartialCharge.ref.kpoints
Assert.allclose(actual, expected)


def test_grid(PartialCharge, Assert):
def test_grid(PartialCharge, Assert, not_core):
actual = PartialCharge.grid()
expected = PartialCharge.ref.grid
Assert.allclose(actual, expected)


def test_non_split_to_numpy(PolarizedNonSplitPartialCharge, Assert):
def test_non_split_to_numpy(PolarizedNonSplitPartialCharge, Assert, not_core):
actual = PolarizedNonSplitPartialCharge.to_numpy("total")
expected = PolarizedNonSplitPartialCharge.ref.partial_charge
Assert.allclose(actual, expected[0, 0, 0].T)
Expand All @@ -149,7 +150,7 @@ def test_non_split_to_numpy(PolarizedNonSplitPartialCharge, Assert):
Assert.allclose(actual, 0.5 * (expected[0, 0, 0].T - expected[0, 0, 1].T))


def test_split_to_numpy(PolarizedAllSplitPartialCharge, Assert):
def test_split_to_numpy(PolarizedAllSplitPartialCharge, Assert, not_core):
bands = PolarizedAllSplitPartialCharge.ref.bands
kpoints = PolarizedAllSplitPartialCharge.ref.kpoints
for band_index, band in enumerate(bands):
Expand All @@ -175,28 +176,30 @@ def test_split_to_numpy(PolarizedAllSplitPartialCharge, Assert):
assert msg in str(excinfo.value)


def test_non_polarized_to_numpy(NonSplitPartialCharge, spin, Assert):
def test_non_polarized_to_numpy(NonSplitPartialCharge, spin, Assert, not_core):
actual = NonSplitPartialCharge.to_numpy(selection=spin)
expected = NonSplitPartialCharge.ref.partial_charge
Assert.allclose(actual, np.asarray(expected).T[:, :, :, 0, 0, 0])


def test_split_bands_to_numpy(NonPolarizedBandSplitPartialCharge, spin, Assert):
def test_split_bands_to_numpy(
NonPolarizedBandSplitPartialCharge, spin, Assert, not_core
):
bands = NonPolarizedBandSplitPartialCharge.ref.bands
for band_index, band in enumerate(bands):
actual = NonPolarizedBandSplitPartialCharge.to_numpy(spin, band=band)
expected = NonPolarizedBandSplitPartialCharge.ref.partial_charge
Assert.allclose(actual, np.asarray(expected).T[:, :, :, 0, band_index, 0])


def test_to_stm_split(PolarizedAllSplitPartialCharge):
def test_to_stm_split(PolarizedAllSplitPartialCharge, not_core):
msg = "set LSEPK and LSEPB to .FALSE. in the INCAR file."
with pytest.raises(NotImplemented) as excinfo:
PolarizedAllSplitPartialCharge.to_stm(selection="constant_current")
assert msg in str(excinfo.value)


def test_to_stm_nonsplit_tip_to_high(NonSplitPartialCharge):
def test_to_stm_nonsplit_tip_to_high(NonSplitPartialCharge, not_core):
actual = NonSplitPartialCharge
tip_height = 8.4
error = f"""The tip position at {tip_height:.2f} is above half of the
Expand All @@ -208,27 +211,28 @@ def test_to_stm_nonsplit_tip_to_high(NonSplitPartialCharge):

def test_to_stm_nonsplit_not_orthogonal_no_vacuum(
PolarizedNonSplitPartialChargeSr2TiO4,
not_core,
):
msg = "The vacuum region in your cell is too small for STM simulations."
with pytest.raises(IncorrectUsage) as excinfo:
PolarizedNonSplitPartialChargeSr2TiO4.to_stm()
assert msg in str(excinfo.value)


def test_to_stm_wrong_spin_nonsplit(PolarizedNonSplitPartialCharge):
def test_to_stm_wrong_spin_nonsplit(PolarizedNonSplitPartialCharge, not_core):
msg = "'up', 'down', or 'total'"
with pytest.raises(IncorrectUsage) as excinfo:
PolarizedNonSplitPartialCharge.to_stm(selection="all")
assert msg in str(excinfo.value)


def test_to_stm_wrong_mode(PolarizedNonSplitPartialCharge):
def test_to_stm_wrong_mode(PolarizedNonSplitPartialCharge, not_core):
with pytest.raises(IncorrectUsage) as excinfo:
PolarizedNonSplitPartialCharge.to_stm(selection="stm")
assert "STM mode" in str(excinfo.value)


def test_wrong_vacuum_direction(NonSplitPartialChargeNi_100):
def test_wrong_vacuum_direction(NonSplitPartialChargeNi_100, not_core):
msg = """The vacuum region in your cell is not located along
the third lattice vector."""
with pytest.raises(NotImplemented) as excinfo:
Expand Down Expand Up @@ -311,7 +315,7 @@ def test_to_stm_nonsplit_constant_current_non_ortho(
assert f"{current:.2f}" in actual.title


def test_stm_default_settings(PolarizedNonSplitPartialCharge):
def test_stm_settings(PolarizedNonSplitPartialCharge, not_core):
actual = dataclasses.asdict(PolarizedNonSplitPartialCharge.stm_settings)
defaults = {
"sigma_xy": 4.0,
Expand All @@ -321,6 +325,49 @@ def test_stm_default_settings(PolarizedNonSplitPartialCharge):
"interpolation_factor": 10,
}
assert actual == defaults
modified = STM_settings(
sigma_xy=2.0,
sigma_z=2.0,
truncate=1.0,
enhancement_factor=500,
interpolation_factor=5,
)
graph = PolarizedNonSplitPartialCharge.to_stm(stm_settings=modified)
assert graph.series.settings == modified


def test_smoothening_change(PolarizedNonSplitPartialCharge, not_core):
mod_settings = STM_settings(sigma_xy=2.0, sigma_z=2.0, truncate=1.0)
data = PolarizedNonSplitPartialCharge.to_numpy("total", band=0, kpoint=0)
default_smoothed_density = PolarizedNonSplitPartialCharge._smooth_stm_data(
data=data, stm_settings=STM_settings()
)
new_smoothed_density = PolarizedNonSplitPartialCharge._smooth_stm_data(
data=data, stm_settings=mod_settings
)
assert not np.allclose(default_smoothed_density, new_smoothed_density)


def test_enhancement_setting_change(PolarizedNonSplitPartialCharge, Assert, not_core):
enhance_settings = STM_settings(
enhancement_factor=STM_settings().enhancement_factor / 2.0
)
graph_def = PolarizedNonSplitPartialCharge.to_stm("constant_height")
graph_less_enhanced = PolarizedNonSplitPartialCharge.to_stm(
"constant_height", stm_settings=enhance_settings
)
Assert.allclose(graph_def.series.data, graph_less_enhanced.series.data * 2)


def test_interpolation_setting_change(PolarizedNonSplitPartialCharge, not_core):
interp_settings = STM_settings(
interpolation_factor=STM_settings().interpolation_factor / 4.0
)
graph_def = PolarizedNonSplitPartialCharge.to_stm("constant_current", current=1)
graph_less_interp_points = PolarizedNonSplitPartialCharge.to_stm(
"constant_current", current=1, stm_settings=interp_settings
)
assert not np.allclose(graph_def.series.data, graph_less_interp_points.series.data)


def test_factory_methods(raw_data, check_factory_methods):
Expand Down
44 changes: 27 additions & 17 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from numpy.testing import assert_array_almost_equal_nulp

from py4vasp import exception, raw
from py4vasp._util import import_

stats = import_.optional("scipy.stats")

number_steps = 4
number_atoms = 7
Expand All @@ -26,26 +29,25 @@


@pytest.fixture(scope="session")
def is_core():
def only_core():
if not _is_core():
pytest.skip("This test checks py4vasp-core functionality not used by py4vasp.")


@pytest.fixture(scope="session")
def not_core():
if _is_core():
pytest.skip("This test requires features not present in py4vasp-core.")


def _is_core():
try:
importlib.metadata.distribution("py4vasp-core")
return True
except importlib.metadata.PackageNotFoundError:
return False


@pytest.fixture()
def only_core(is_core):
if not is_core:
pytest.skip("This test checks py4vasp-core functionality not used by py4vasp.")


@pytest.fixture()
def not_core(is_core):
if is_core:
pytest.skip("This test requires features not present in py4vasp-core.")


class _Assert:
@staticmethod
def allclose(actual, desired, tolerance=1):
Expand Down Expand Up @@ -710,14 +712,22 @@ def _partial_charge(selection):
else:
spin_dimension = 1
grid = raw.VaspData(tuple(reversed(grid_dim)))
random_charge = raw.VaspData(
np.random.rand(len(kpoints), len(bands), spin_dimension, *grid_dim)
)
gaussian_charge = np.zeros((len(kpoints), len(bands), spin_dimension, *grid_dim))
if not _is_core():
cov = grid_dim[0] / 10 # standard deviation
z = np.arange(grid_dim[0]) # z range
for gy in range(grid_dim[1]):
for gx in range(grid_dim[2]):
m = int(grid_dim[0] / 2) + gy / 10 + gx / 10
val = stats.multivariate_normal(mean=m, cov=cov).pdf(z)
# Fill the gaussian_charge array
gaussian_charge[:, :, :, :, gy, gx] = val
gaussian_charge = raw.VaspData(gaussian_charge)
return raw.PartialCharge(
structure=structure,
bands=bands,
kpoints=kpoints,
partial_charge=random_charge,
partial_charge=gaussian_charge,
grid=grid,
)

Expand Down