import numpy as np
from logging_config import get_logger
logger = get_logger(__name__)
[docs]class SymmetryMask:
# Up to now only abelian groups
product_tables = {
'ci': {
('Ag' , 'Ag'): 'Ag',
('Ag' , 'Au'): 'Au',
('Au' , 'Ag'): 'Au',
('Au' , 'Au'): 'Ag'
},
'cs': {
("A'", "A'"): "A'",
("A'", "A''"): "A''",
("A''", "A'"): "A''",
("A''", "A''"): "A'"
},
'c2': {
('A' , 'A'): 'A',
('A' , 'B'): 'B',
('B' , 'A'): 'B',
('B' , 'B'): 'A'
},
'c2h': {
('Ag', 'Ag'): 'Ag',
('Ag', 'Bg'): 'Bg',
('Ag', 'Au'): 'Au',
('Ag', 'Bu'): 'Bu',
('Bg', 'Ag'): 'Bg',
('Bg', 'Bg'): 'Ag',
('Bg', 'Au'): 'Bu',
('Bg', 'Bu'): 'Au',
('Au', 'Ag'): 'Au',
('Au', 'Bg'): 'Bu',
('Au', 'Au'): 'Ag',
('Au', 'Bu'): 'Bg',
('Bu', 'Ag'): 'Bu',
('Bu', 'Bg'): 'Au',
('Bu', 'Au'): 'Bg',
('Bu', 'Bu'): 'Ag',
},
'c2v': {
('A1', 'A1'): 'A1',
('A1', 'A2'): 'A2',
('A1', 'B1'): 'B1',
('A1', 'B2'): 'B2',
('A2', 'A1'): 'A2',
('A2', 'A2'): 'A1',
('A2', 'B1'): 'B2',
('A2', 'B2'): 'B1',
('B1', 'A1'): 'B1',
('B1', 'A2'): 'B2',
('B1', 'B1'): 'A1',
('B1', 'B2'): 'A2',
('B2', 'A1'): 'B2',
('B2', 'A2'): 'B1',
('B2', 'B1'): 'A2',
('B2', 'B2'): 'A1'
},
'd2': {
('A', 'A'): 'A',
('A', 'B1'): 'B1',
('A', 'B2'): 'B2',
('A', 'B3'): 'B3',
('B1', 'A'): 'B1',
('B1', 'B1'): 'A',
('B1', 'B2'): 'B3',
('B1', 'B3'): 'B2',
('B2', 'A'): 'B2',
('B2', 'B1'): 'B3',
('B2', 'B2'): 'A',
('B2', 'B3'): 'B1',
('B3', 'A'): 'B3',
('B3', 'B1'): 'B2',
('B3', 'B2'): 'B1',
('B3', 'B3'): 'A',
},
'd2h': {
('Ag', 'Ag'): 'Ag',
('Ag', 'B1g'): 'B1g',
('Ag', 'B2g'): 'B2g',
('Ag', 'B3g'): 'B3g',
('Ag', 'Au'): 'Au',
('Ag', 'B1u'): 'B1u',
('Ag', 'B2u'): 'B2u',
('Ag', 'B3u'): 'B3u',
('B1g', 'Ag'): 'B1g',
('B1g', 'B1g'): 'Ag',
('B1g', 'B2g'): 'B3g',
('B1g', 'B3g'): 'B2g',
('B1g', 'Au'): 'B1u',
('B1g', 'B1u'): 'Au',
('B1g', 'B2u'): 'B3u',
('B1g', 'B3u'): 'B2u',
('B2g', 'Ag'): 'B2g',
('B2g', 'B1g'): 'B3g',
('B2g', 'B2g'): 'Ag',
('B2g', 'B3g'): 'B1g',
('B2g', 'Au'): 'B2u',
('B2g', 'B1u'): 'B3u',
('B2g', 'B2u'): 'Au',
('B2g', 'B3u'): 'B1u',
('B3g', 'Ag'): 'B3g',
('B3g', 'B1g'): 'B2g',
('B3g', 'B2g'): 'B1g',
('B3g', 'B3g'): 'Ag',
('B3g', 'Au'): 'B3u',
('B3g', 'B1u'): 'B2u',
('B3g', 'B2u'): 'B1u',
('B3g', 'B3u'): 'Au',
('Au', 'Ag'): 'Au',
('Au', 'B1g'): 'B1u',
('Au', 'B2g'): 'B2u',
('Au', 'B3g'): 'B3u',
('Au', 'Au'): 'Ag',
('Au', 'B1u'): 'B1g',
('Au', 'B2u'): 'B2g',
('Au', 'B3u'): 'B3g',
('B1u', 'Ag'): 'B1u',
('B1u', 'B1g'): 'Au',
('B1u', 'B2g'): 'B3u',
('B1u', 'B3g'): 'B2u',
('B1u', 'Au'): 'B1g',
('B1u', 'B1u'): 'Ag',
('B1u', 'B2u'): 'B3g',
('B1u', 'B3u'): 'B2g',
('B2u', 'Ag'): 'B2u',
('B2u', 'B1g'): 'B3u',
('B2u', 'B2g'): 'Au',
('B2u', 'B3g'): 'B1u',
('B2u', 'Au'): 'B2g',
('B2u', 'B1u'): 'B3g',
('B2u', 'B2u'): 'Ag',
('B2u', 'B3u'): 'B1g',
('B3u', 'Ag'): 'B3u',
('B3u', 'B1g'): 'B2u',
('B3u', 'B2g'): 'B1u',
('B3u', 'B3g'): 'Au',
('B3u', 'Au'): 'B3g',
('B3u', 'B1u'): 'B2g',
('B3u', 'B2u'): 'B1g',
('B3u', 'B3u'): 'Ag',
}
}
[docs] @classmethod
def get_product_table(cls, sym_point_group: str) -> dict:
"""
Get the product table for a given symmetry point group.
Parameters
----------
sym_point_group : str
The symmetry point group.
Returns
-------
dict
The product table for the given symmetry point group.
Raises
------
ValueError
If the symmetry point group is unknown.
"""
sym_point_group = sym_point_group.lower()
if sym_point_group not in cls.product_tables:
raise ValueError(f"Unknown symmetry point group: {sym_point_group}")
return cls.product_tables[sym_point_group]
[docs] @classmethod
def get_operation_result(cls, sym_point_group: str, state1: str, state2: str) -> str:
"""
Get the result of a symmetry operation for two states.
Parameters
----------
sym_point_group : str
The symmetry point group.
state1 : str
The first state.
state2 : str
The second state.
Returns
-------
str
The result of the symmetry operation.
Raises
------
ValueError
If the operation result is not found.
"""
table = cls.get_product_table(sym_point_group)
state1, state2 = state1.upper(), state2.upper()
if (state1, state2) not in table:
raise ValueError(f"Operation result not found for states: {state1}, {state2}")
return table[(state1, state2)]
[docs] @classmethod
def get_total_sym_irrep(cls, VCSystem) -> str:
"""
Get the totally symmetric irreducible representation for a given symmetry point group.
Parameters
----------
VCSystem : str or object
The symmetry point group or an object with a 'symmetry_point_group' attribute.
Returns
-------
str
The totally symmetric irreducible representation.
Raises
------
ValueError
If VCSystem is not a string or an object with a 'symmetry_point_group' attribute.
"""
# if isinstance(VCSystem, str):
# sym_point_group = VCSystem
if hasattr(VCSystem, 'symmetry_point_group'):
sym_point_group = VCSystem.symmetry_point_group
else:
logger.error("VCSystem must be a string or an object with a 'symmetry_point_group' attribute")
raise ValueError("VCSystem must be a string or an object with a 'symmetry_point_group' attribute")
table = cls.get_product_table(sym_point_group)
for (state1, state2), result in table.items():
if state1 == state2 and state1 == result:
return result
return next(iter(table.values()))
[docs] @classmethod
def create_symmetry_matrix(cls, VCSystem) -> np.ndarray:
"""
Create a symmetry matrix based on the given parameters.
Parameters
----------
VCSystem : object
An object containing the necessary attributes:
- symmetry_point_group: str, the symmetry point group.
- number_normal_modes: int, number of normal modes.
- number_states: int, number of states.
- symmetry_modes: list, list of symmetry modes.
- symmetry_states: list, list of symmetry states.
- coupling_with_gs: bool, whether to decouple the ground state.
- totally_sym_irrep: str, the totally symmetric irreducible representation.
- JT_effects: list of dict, each dictionary specifies a JT effect with keys:
* 'mode': int, index of the normal mode where the JT effect occurs.
* 'state_pairs': list of tuples/lists, each containing two integers (state indices).
* 'types': list of str, JT effect types (e.g., 'Exe', 'Exb') corresponding to each state pair.
* 'active' (optional): bool, indicating if the effect is actively optimized (default is True).
* 'source' (optional): int, if inactive, the mode index from which parameters should be copied.
Returns
-------
np.ndarray
The symmetry matrix.
Raises
------
AttributeError
If VCSystem is missing an expected attribute.
ValueError
If the provided JT_effects do not meet the expected format.
TypeError
If mode or state indices are not integers.
IndexError
If mode or state indices are out of bounds.
"""
try:
# Extract required attributes from VCSystem
sym_point_group = VCSystem.symmetry_point_group
nnormal_modes = VCSystem.number_normal_modes
nstates = VCSystem.number_states
symmodes = [mode.upper() for mode in VCSystem.symmetry_modes]
symstates = [state.upper() for state in VCSystem.symmetry_states]
decoupled_gs = VCSystem.coupling_with_gs
total_sym_irrep = VCSystem.totally_sym_irrep.upper()
# Use the new JT_effects attribute (if provided)
jt_effects = VCSystem.JT_effects or []
# Initialize the symmetry matrix
symmetry_matrix = np.zeros((nnormal_modes, nstates, nstates), dtype=np.float64)
# Fill the matrix based on symmetry operations
for nmode, sym_nmode in enumerate(symmodes):
for i, state_i in enumerate(symstates):
inter_sym = cls.get_operation_result(sym_point_group, state_i, sym_nmode)
for j, state_j in enumerate(symstates):
product = cls.get_operation_result(sym_point_group, inter_sym, state_j)
if sym_nmode == total_sym_irrep:
# Symmetric mode handling (specific to LVC)
symmetry_matrix[nmode, i, j] = 0.0
if i == j:
symmetry_matrix[nmode, i, j] = 1.0 # kappa
elif product == total_sym_irrep:
if not decoupled_gs and (i == 0 or j == 0):
# Decouple the ground state
symmetry_matrix[nmode, i, j] = 0.0
else:
symmetry_matrix[nmode, i, j] = 1.0
else:
symmetry_matrix[nmode, i, j] = 0.0
# Handle Jahn-Teller effects using the new JT_effects structure
if jt_effects:
logger.info("Jahn-Teller effect detected.")
logger.info("JT_effects: %s, number of effects: %d", jt_effects, len(jt_effects))
for effect in jt_effects:
# Extract JT effect information from the dictionary
mode_index = effect.get("mode")
state_pairs = effect.get("state_pairs")
types = effect.get("types")
active = effect.get("active", True) # Default to active if not specified
# Validate the effect's mode index
if not isinstance(mode_index, int):
raise TypeError(f"JT effect 'mode' must be an integer, got {type(mode_index)}.")
if not (0 <= mode_index < len(symmodes)):
raise IndexError(f"JT effect mode index {mode_index} is out of bounds for modes list of length {len(symmodes)}.")
# Validate that state_pairs and types are lists and have the same length
if not isinstance(state_pairs, list):
raise TypeError("JT effect 'state_pairs' must be a list.")
if not isinstance(types, list):
raise TypeError("JT effect 'types' must be a list.")
if len(state_pairs) != len(types):
raise ValueError("In a JT effect, the number of 'state_pairs' must equal the number of 'types'.")
# Process each state pair within this JT effect
for pair_idx, pair in enumerate(state_pairs):
if not isinstance(pair, (list, tuple)):
raise TypeError(f"State pair at index {pair_idx} in JT_effect must be a list or tuple, got {type(pair)}.")
if len(pair) != 2:
raise ValueError(f"State pair at index {pair_idx} in JT_effect must contain exactly two elements, got {len(pair)} elements.")
state_a, state_b = pair
# Validate that state indices are integers and within bounds
if not isinstance(state_a, int) or not isinstance(state_b, int):
raise TypeError(f"State indices in JT_effect pair at index {pair_idx} must be integers, got {type(state_a)} and {type(state_b)}.")
n_states = symmetry_matrix[mode_index].shape[0]
if not (0 <= state_a < n_states):
raise IndexError(f"State index {state_a} is out of bounds for mode {mode_index} with shape {symmetry_matrix[mode_index].shape}.")
if not (0 <= state_b < n_states):
raise IndexError(f"State index {state_b} is out of bounds for mode {mode_index} with shape {symmetry_matrix[mode_index].shape}.")
# Depending on whether the effect is active, assign the JT-related coupling
if active:
# Active JT effect: set off-diagonal coupling for the first state pair
symmetry_matrix[mode_index][state_a, state_b] = 2.0
symmetry_matrix[mode_index][state_b, state_a] = 2.0
else:
# Inactive JT effect: copy parameters by setting diagonal coupling elements
symmetry_matrix[mode_index][state_a, state_a] = 2.0
symmetry_matrix[mode_index][state_b, state_b] = 2.0
return symmetry_matrix
except AttributeError as e:
logger.error("VCSystem is missing an expected attribute: %s", e)
raise
except Exception as e:
logger.exception("An error occurred while creating the symmetry matrix: %s", e)
raise