Skip to content

SymEntry

SymEntry

SymEntry(entry: int, sym_map: list[str] = None, **kwargs)

Stores information for a particular symmetry definition which derives from a standard definition of a global symmetry type and provides access to operators and attributes which allow symmetric manipulation.

Parameters:

  • entry (int) –

    The entry integer which uniquely identifies this instance information

  • sym_map (list[str], default: None ) –

    The mapping of individual groups to construct this instance

  • **kwargs
Source code in symdesign/utils/SymEntry.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def __init__(self, entry: int, sym_map: list[str] = None, **kwargs):
    """Construct the instance

    Args:
        entry: The entry integer which uniquely identifies this instance information
        sym_map: The mapping of individual groups to construct this instance
        **kwargs:
    """
    try:
        self.group_info, result_info = parsed_symmetry_combinations[entry]
        # group_info, result_info = parsed_symmetry_combinations[entry]
        # returns
        #  {'group1': [self.int_dof_group1, self.rot_set_group1, self.ref_frame_tx_dof1],
        #   'group2': [self.int_dof_group2, self.rot_set_group2, self.ref_frame_tx_dof2],
        #   ...},
        #  [point_group_symmetry, resulting_symmetry, dimension, unit_cell, tot_dof, cycle_size]
    except KeyError:
        raise ValueError(
            f"Invalid symmetry entry '{entry}'. Supported values are Nanohedra entries: "
            f'{1}-{len(nanohedra_symmetry_combinations)} and custom entries: '
            f'{", ".join(map(str, custom_entries))}')

    try:  # To unpack the result_info. This will fail if a CRYST1 record placeholder
        self.point_group_symmetry, self.resulting_symmetry, self.dimension, self.cell_lengths, self.cell_angles, \
            self.total_dof, self.cycle_size = result_info
    except ValueError:  # Not enough values to unpack, probably a CRYST token
        # Todo - Crystallographic symmetry could coincide with group symmetry...
        self.group_info = [('C1', [['r:<1,1,1,h,i,a>', 't:<j,k,b>'], 1, None])]  # Assume for now that the groups are C1
        # group_info = [('C1', [['r:<1,1,1,h,i,a>', 't:<j,k,b>'], 1, None])]  # Assume for now that the groups are C1
        self.point_group_symmetry = None
        # self.resulting_symmetry = kwargs.get('resulting_symmetry', None)
        if 'space_group' in kwargs:
            self.resulting_symmetry = kwargs['space_group']
        elif sym_map is None:
            # self.resulting_symmetry = None
            raise utils.SymmetryInputError(
                f"Can't create a {self.__class__.__name__} without passing 'space_group' or 'sym_map'")
        else:
            self.resulting_symmetry, *_ = sym_map
        self.dimension = 2 if self.resulting_symmetry in utils.symmetry.layer_group_cryst1_fmt_dict else 3
        self.cell_lengths = self.cell_angles = None
        self.total_dof = self.cycle_size = 0

    self._int_dof_groups, self._setting_matrices, self._setting_matrices_numbers, self._ref_frame_tx_dof, \
        self.__external_dof = [], [], [], [], []
    self.number = entry
    self.entry_groups = [group_name for group_name, group_params in self.group_info if group_name is not None]
    # Check if this entry ever has the same symmetry
    self._same_symmetry = len(self.entry_groups) != len(set(self.entry_groups))
    # group1, group2, *extra = entry_groups
    if sym_map is None:  # Assume standard SymEntry
        # Assumes 2 component symmetry. index with only 2 options
        self.sym_map = [self.resulting_symmetry] + self.entry_groups
        groups = self.entry_groups
    else:  # Requires full specification of all symmetry groups
        # Clean any missing groups then remove the result, pass the groups
        result, *groups = self.sym_map = [sym for sym in sym_map if sym is not None]

    # Solve the group information for each passed symmetry
    self.groups = []
    for idx, group in enumerate(groups, 1):
        self.append_group(group)
        # self.groups = []
        # for idx, group in enumerate(groups, 1):
        #     self.append_group(group)
        #     # if group not in valid_symmetries:
        #     #     if group is None:
        #     #         # Todo
        #     #         #  Need to refactor symmetry_combinations for any number of elements
        #     #         continue
        #     #     else:  # Recurse to see if it is yet another symmetry specification
        #     #         # raise ValueError(
        #     #         logger.warning(
        #     #             f"The symmetry group '{group}' specified at index '{idx}' isn't a valid sub-symmetry. "
        #     #             f"Trying to correct by applying another {self.__class__.__name__}()")
        #     #         raise NotImplementedError()
        #     #
        #     # if group not in entry_groups:
        #     #     # This is probably a sub-symmetry of one of the groups. Is it allowed?
        #     #     if not symmetry_groups_are_allowed_in_entry(groups, *entry_groups, result=self.resulting_symmetry):
        #     #                                                 # group1=group1, group2=group2):
        #     #         viable_groups = [group for group in entry_groups if group is not None]
        #     #         raise utils.SymmetryInputError(
        #     #             f"The symmetry group '{group}' isn't an allowed sub-symmetry of the result "
        #     #             f'{self.resulting_symmetry}, or the group(s) {", ".join(viable_groups)}')
        #     # self.groups.append(group)

    # def add_group():
    #     # Todo
    #     #  Can the accuracy of this creation method be guaranteed with the usage of the same symmetry
    #     #  operator and different orientations? Think T33
    #     self._int_dof_groups.append(int_dof)
    #     self._setting_matrices.append(setting_matrices[set_mat_number])
    #     self._setting_matrices_numbers.append(set_mat_number)
    #     if ext_dof is None:
    #         self._ref_frame_tx_dof.append(ext_dof)
    #         self.__external_dof.append(construct_uc_matrix(('0', '0', '0')))
    #     else:
    #         ref_frame_tx_dof = ext_dof.split(',')
    #         self._ref_frame_tx_dof.append(ref_frame_tx_dof)
    #         if group_idx <= 2:
    #             # This isn't possible with more than 2 groups unless the groups is tethered to existing
    #             self.__external_dof.append(construct_uc_matrix(ref_frame_tx_dof))
    #         else:
    #             if ref_frame_tx_dof:
    #                 raise utils.SymmetryInputError(
    #                     f"Can't create {self.__class__.__name__} with external degrees of freedom and > 2 groups")
    #
    # for group_idx, group_symmetry in enumerate(self.groups, 1):
    #     if isinstance(group_symmetry, SymEntry):
    #         group_symmetry = group_symmetry.resulting_symmetry
    #     # for entry_group_symmetry, (int_dof, set_mat_number, ext_dof) in self.group_info:
    #     for entry_group_symmetry, (int_dof, set_mat_number, ext_dof) in group_info:
    #         if group_symmetry == entry_group_symmetry:
    #             add_group()
    #             break
    #     else:  # None was found for this group_symmetry
    #         # raise utils.SymmetryInputError(
    #         logger.critical(
    #             f"Trying to assign the group '{group_symmetry}' at index {group_idx} to "
    #             f"{self.__class__.__name__}.number={self.number}")
    #         # See if the group is a sub-symmetry of a known group
    #         # for entry_group_symmetry, (int_dof, set_mat_number, ext_dof) in self.group_info:
    #         for entry_group_symmetry, (int_dof, set_mat_number, ext_dof) in group_info:
    #             entry_sub_groups = sub_symmetries.get(entry_group_symmetry, [None])
    #             if group_symmetry in entry_sub_groups:
    #                 add_group()
    #                 break
    #         else:
    #             raise utils.SymmetryInputError(
    #                 f"Assignment of the group '{group_symmetry}' failed")

    # Check construction is valid
    if self.point_group_symmetry not in valid_symmetries:
        if not self.number == 0:  # Anything besides CRYST entry
            raise utils.SymmetryInputError(
                f'Invalid point group symmetry {self.point_group_symmetry}')
    try:
        if self.dimension == 0:
            self._expand_matrices = point_group_symmetry_operators[self.resulting_symmetry]
        elif self.dimension in [2, 3]:
            self._expand_matrices, expand_translations = space_group_symmetry_operators[self.resulting_symmetry]
        else:
            raise utils.SymmetryInputError(
                'Invalid symmetry entry. Supported dimensions are 0, 2, and 3')
    except KeyError:
        raise utils.SymmetryInputError(
            f"The symmetry result '{self.resulting_symmetry}' isn't allowed as there aren't group operators "
            "available for it")

    if self.cell_lengths:
        self.unit_cell = (self.cell_lengths, self.cell_angles)
    else:
        self.unit_cell = None

number_of_operations property

number_of_operations: int

The number of symmetric copies in the full symmetric system

group_subunit_numbers property

group_subunit_numbers: list[int]

Returns the number of subunits for each symmetry group

specification property

specification: str

Return the specification for the instance. Ex: RESULT:{SUBSYMMETRY1}{SUBSYMMETRY2}... -> (T:{C3}{C3})

simple_specification property

simple_specification: str

Return the simple specification for the instance. Ex: 'RESULTSUBSYMMETRY1SUBSYMMETRY2... -> (T33)

uc_specification property

uc_specification: tuple[tuple[str] | None, tuple[int] | None]

The external dof and angle parameters which constitute a viable lattice

uc_dimensions property writable

uc_dimensions: tuple[float, float, float, float, float, float] | None

The unit cell dimensions for the lattice specified by lengths a, b, c and angles alpha, beta, gamma

Returns:

  • tuple[float, float, float, float, float, float] | None

    length a, length b, length c, angle alpha, angle beta, angle gamma

rotation_range1 property

rotation_range1: float

Return the rotation range according the first symmetry group operator

rotation_range2 property

rotation_range2: float

Return the rotation range according the second symmetry group operator

rotation_range3 property

rotation_range3: float

Return the rotation range according the third symmetry group operator

number_dof_rotation property

number_dof_rotation: int

Return the number of internal rotational degrees of freedom

is_internal_rot1 property

is_internal_rot1: bool

Whether there are rotational degrees of freedom for group 1

is_internal_rot2 property

is_internal_rot2: bool

Whether there are rotational degrees of freedom for group 2

number_dof_translation property

number_dof_translation: int

Return the number of internal translational degrees of freedom

is_internal_tx1 property

is_internal_tx1: bool

Whether there are internal translational degrees of freedom for group 1

is_internal_tx2 property

is_internal_tx2: bool

Whether there are internal translational degrees of freedom for group 2

number_dof_external property

number_dof_external: int

Return the number of external degrees of freedom

external_dof property

external_dof: ndarray

Return the total external degrees of freedom as a number DOF externalx3 array

external_dofs property

external_dofs: list[ndarray]

Return the 3x3 external degrees of freedom for component1

external_dof1 property

external_dof1: ndarray

Return the 3x3 external degrees of freedom for component1

external_dof2 property

external_dof2: ndarray

Return the 3x3 external degrees of freedom for component2

degeneracy_matrices1 property

degeneracy_matrices1: ndarray

Returns the (number of degeneracies, 3, 3) degeneracy matrices for component1

degeneracy_matrices2 property

degeneracy_matrices2: ndarray

Returns the (number of degeneracies, 3, 3) degeneracy matrices for component2

cryst_record property writable

cryst_record: str | None

Get the CRYST1 record associated with this SymEntry

from_cryst classmethod

from_cryst(space_group: str, **kwargs)

Create a SymEntry from a specified symmetry in Hermann-Mauguin notation and the unit-cell dimensions

Source code in symdesign/utils/SymEntry.py
347
348
349
350
@classmethod
def from_cryst(cls, space_group: str, **kwargs):  # uc_dimensions: Iterable[float],
    """Create a SymEntry from a specified symmetry in Hermann-Mauguin notation and the unit-cell dimensions"""
    return cls(0, space_group=space_group, **kwargs)

append_group

append_group(group: str)

Add an additional symmetry group to the SymEntry

Source code in symdesign/utils/SymEntry.py
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
def append_group(self, group: str):
    """Add an additional symmetry group to the SymEntry"""
    if group not in valid_symmetries:
        if group is None:
            # Todo
            #  Need to refactor symmetry_combinations for any number of elements
            return
        else:  # Recurse to see if it is yet another symmetry specification
            # raise ValueError(
            logger.warning(
                f"The symmetry group '{group}' at index {len(self.groups)} isn't a valid sub-symmetry. "
                f"Trying to correct by applying another {self.__class__.__name__}()")
            raise NotImplementedError()

    if group not in self.entry_groups:
        # This is probably a sub-symmetry of one of the groups. Is it allowed?
        if not symmetry_groups_are_allowed_in_entry([group], *self.entry_groups, result=self.resulting_symmetry):
            # group1=group1, group2=group2):
            viable_groups = [group for group in self.entry_groups if group is not None]
            raise utils.SymmetryInputError(
                f"The symmetry group '{group}' isn't an allowed sub-symmetry of the result "
                f'{self.resulting_symmetry}, or the group(s) {", ".join(viable_groups)}')
    self.groups.append(group)

    def add_group() -> bool:
        if self._same_symmetry:
            if int_dof in self._int_dof_groups:
                return False
        self._int_dof_groups.append(int_dof)
        self._setting_matrices.append(setting_matrices[set_mat_number])
        self._setting_matrices_numbers.append(set_mat_number)
        if ext_dof is None:
            self._ref_frame_tx_dof.append(ext_dof)
            self.__external_dof.append(construct_uc_matrix(('0', '0', '0')))
        else:
            ref_frame_tx_dof = ext_dof.split(',')
            self._ref_frame_tx_dof.append(ref_frame_tx_dof)
            if len(self.groups) <= 2:
                # This isn't possible with more than 2 groups unless the groups is tethered to existing
                self.__external_dof.append(construct_uc_matrix(ref_frame_tx_dof))
            else:
                if ref_frame_tx_dof:
                    raise utils.SymmetryInputError(
                        f"Can't create {self.__class__.__name__} with external degrees of freedom and > 2 groups")

        return True

    if isinstance(group, SymEntry):
        group = group.resulting_symmetry
    for entry_group_symmetry, (int_dof, set_mat_number, ext_dof) in self.group_info:
        if group == entry_group_symmetry:
            # Try to add the group. If it doesn't work, then proceed
            if add_group():
                break
    else:  # None was found for this group
        # raise utils.SymmetryInputError(
        logger.critical(
            f"Trying to assign the group '{group}' at index {len(self.groups)} to "
            f"{self.__class__.__name__}.number={self.number}")
        # See if the group is a sub-symmetry of a known group
        for entry_group_symmetry, (int_dof, set_mat_number, ext_dof) in self.group_info:
            entry_sub_groups = sub_symmetries.get(entry_group_symmetry, [None])
            if group in entry_sub_groups:
                add_group()
                break
        else:
            raise utils.SymmetryInputError(
                f"Assignment of the group '{group}' failed")

is_token

is_token() -> bool

Is the SymEntry utilizing a provided CRYST1 record

Source code in symdesign/utils/SymEntry.py
843
844
845
def is_token(self) -> bool:
    """Is the SymEntry utilizing a provided CRYST1 record"""
    return self.number == 0

needs_cryst_record

needs_cryst_record() -> bool

Is the SymEntry utilizing a provided CRYST1 record

Source code in symdesign/utils/SymEntry.py
847
848
849
850
def needs_cryst_record(self) -> bool:
    """Is the SymEntry utilizing a provided CRYST1 record"""
    # If .number is 0, then definitely yes. Otherwise, need to check if one is already set
    return self.dimension > 0 and self.uc_dimensions is None

get_uc_dimensions

get_uc_dimensions(optimal_shift_vec: ndarray) -> ndarray | None

Return an array with the three unit cell lengths and three angles [20, 20, 20, 90, 90, 90] by combining UC basis vectors with component translation degrees of freedom

Parameters:

  • optimal_shift_vec (ndarray) –

    An Nx3 array where N is the number of shift instances and 3 is number of possible external degrees of freedom (even if they are not utilized)

Returns: The unit cell dimensions for each optimal shift vector passed

Source code in symdesign/utils/SymEntry.py
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
def get_uc_dimensions(self, optimal_shift_vec: np.ndarray) -> np.ndarray | None:
    """Return an array with the three unit cell lengths and three angles [20, 20, 20, 90, 90, 90] by combining UC
    basis vectors with component translation degrees of freedom

    Args:
        optimal_shift_vec: An Nx3 array where N is the number of shift instances
            and 3 is number of possible external degrees of freedom (even if they are not utilized)
    Returns:
        The unit cell dimensions for each optimal shift vector passed
    """
    if self.unit_cell is None:
        return None
    # for entry 6 - self.cell_lengths is ('4*e', '4*e', '4*e')
    # construct_uc_matrix() = [[4, 4, 4], [0, 0, 0], [0, 0, 0]]
    uc_mat = construct_uc_matrix(self.cell_lengths) * optimal_shift_vec[:, :, None]
    # [:, :, None] <- expands axis so multiplication is accurate. eg. [[[1.], [0.], [0.]], [[0.], [0.], [0.]]]
    lengths = np.abs(uc_mat.sum(axis=-2))
    #               (^).sum(axis=-2) = [4, 4, 4]
    if len(self.cell_lengths) == 2:
        lengths[:, 2] = 1.

    if len(self.cell_angles) == 1:
        angles = [90., 90., float(self.cell_angles[0])]
    else:
        angles = [0., 0., 0.]  # Initialize incase there are < 1 self.cell_angles
        for idx, string_angle in enumerate(self.cell_angles):
            angles[idx] = float(string_angle)

    # return np.concatenate(lengths, np.tile(angles, len(lengths)))
    return np.hstack((lengths, np.tile(angles, len(lengths)).reshape(-1, 3)))

get_optimal_shift_from_uc_dimensions

get_optimal_shift_from_uc_dimensions(a: float, b: float, c: float, *angles: list) -> ndarray | None

Return the optimal shifts provided unit cell dimensions and the external translation degrees of freedom

Parameters:

  • a (float) –

    The unit cell parameter for the lattice dimension 'a'

  • b (float) –

    The unit cell parameter for the lattice dimension 'b'

  • c (float) –

    The unit cell parameter for the lattice dimension 'c'

  • angles (list, default: () ) –

    The unit cell parameters for the lattice angles alpha, beta, gamma. Not utilized!

Returns: The optimal shifts in each direction a, b, and c if they are allowed

Source code in symdesign/utils/SymEntry.py
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
def get_optimal_shift_from_uc_dimensions(self, a: float, b: float, c: float, *angles: list) -> np.ndarray | None:
    """Return the optimal shifts provided unit cell dimensions and the external translation degrees of freedom

    Args:
        a: The unit cell parameter for the lattice dimension 'a'
        b: The unit cell parameter for the lattice dimension 'b'
        c: The unit cell parameter for the lattice dimension 'c'
        angles: The unit cell parameters for the lattice angles alpha, beta, gamma. Not utilized!
    Returns:
        The optimal shifts in each direction a, b, and c if they are allowed
    """
    if self.unit_cell is None:
        return None
    # uc_mat = construct_uc_matrix(string_lengths) * optimal_shift_vec[:, :, None]  # <- expands axis so mult accurate
    uc_mat = construct_uc_matrix(self.cell_lengths)
    # To reverse the values from the incoming a, b, and c, divide by the uc_matrix_constraints
    # given the matrix should only ever have one value in each column (max) a sum over the column should produce the
    # desired vector to calculate the optimal shift.
    # There is a possibility of returning inf when we divide 0 by a value so ignore this warning
    with warnings.catch_warnings():
        # Cause all warnings to always be ignored
        warnings.simplefilter('ignore')
        external_translation_shifts = [a, b, c] / np.abs(uc_mat.sum(axis=-2))
        # Replace any inf with zero
        external_translation_shifts = np.nan_to_num(external_translation_shifts, copy=False, posinf=0., neginf=0.)

    if len(self.cell_lengths) == 2:
        external_translation_shifts[2] = 1.

    return external_translation_shifts

sdf_lookup

sdf_lookup() -> AnyStr

Locate the proper symmetry definition file depending on the specified symmetry

Returns:

  • AnyStr

    The location of the symmetry definition file on disk

Source code in symdesign/utils/SymEntry.py
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
def sdf_lookup(self) -> AnyStr:
    """Locate the proper symmetry definition file depending on the specified symmetry

    Returns:
        The location of the symmetry definition file on disk
    """
    if self.dimension > 0:
        return os.path.join(putils.symmetry_def_files, 'C1.sym')

    symmetry = self.simple_specification
    for file, ext in map(os.path.splitext, os.listdir(putils.symmetry_def_files)):
        if symmetry == file:
            return os.path.join(putils.symmetry_def_files, file + ext)

    symmetry = self.resulting_symmetry
    for file, ext in map(os.path.splitext, os.listdir(putils.symmetry_def_files)):
        if symmetry == file:
            return os.path.join(putils.symmetry_def_files, file + ext)

    raise FileNotFoundError(
        f"Couldn't locate symmetry definition file at '{putils.symmetry_def_files}' for {self.__class__.__name__} "
        f"{self.number}")

log_parameters

log_parameters()

Log the SymEntry Parameters

Source code in symdesign/utils/SymEntry.py
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
def log_parameters(self):
    """Log the SymEntry Parameters"""
    #                pdb1_path, pdb2_path, master_outdir
    # log.info('NANOHEDRA PROJECT INFORMATION')
    # log.info(f'Oligomer 1 Input: {pdb1_path}')
    # log.info(f'Oligomer 2 Input: {pdb2_path}')
    # log.info(f'Master Output Directory: {master_outdir}\n')
    logger.info('SYMMETRY COMBINATION MATERIAL INFORMATION')
    logger.info(f'Nanohedra Entry Number: {self.number}')
    logger.info(f'Oligomer 1 Point Group Symmetry: {self.group1}')
    logger.info(f'Oligomer 2 Point Group Symmetry: {self.group2}')
    logger.info(f'SCM Point Group Symmetry: {self.point_group_symmetry}')
    # logger.debug(f'Oligomer 1 Internal ROT DOF: {self.is_internal_rot1}')
    # logger.debug(f'Oligomer 2 Internal ROT DOF: {self.is_internal_rot2}')
    # logger.debug(f'Oligomer 1 Internal Tx DOF: {self.is_internal_tx1}')
    # logger.debug(f'Oligomer 2 Internal Tx DOF: {self.is_internal_tx2}')
    # Todo textwrap.textwrapper() prettify these matrices
    logger.debug(f'Oligomer 1 Setting Matrix: {self.setting_matrix1.tolist()}')
    logger.debug(f'Oligomer 2 Setting Matrix: {self.setting_matrix2.tolist()}')
    ext_tx_dof1, ext_tx_dof2, *_ = self.ref_frame_tx_dof
    logger.debug(f'Oligomer 1 Reference Frame Tx DOF: {ext_tx_dof1}')
    logger.debug(f'Oligomer 2 Reference Frame Tx DOF: {ext_tx_dof2}')
    logger.info(f'Resulting SCM Symmetry: {self.resulting_symmetry}')
    logger.info(f'SCM Dimension: {self.dimension}')
    logger.info(f'SCM Unit Cell Specification: {self.uc_specification}\n')
    # rot_step_deg1, rot_step_deg2 = get_rotation_step(self, rot_step_deg1, rot_step_deg2, initial=True, log=logger)
    logger.info('ROTATIONAL SAMPLING INFORMATION')
    logger.info(f'Oligomer 1 ROT Sampling Range: '
                f'{self.rotation_range1 if self.is_internal_rot1 else None}')
    logger.info('Oligomer 2 ROT Sampling Range: '
                f'{self.rotation_range2 if self.is_internal_rot2 else None}')
    # logger.info('Oligomer 1 ROT Sampling Step: '
    #             f'{rot_step_deg1 if self.is_internal_rot1 else None}')
    # logger.info('Oligomer 2 ROT Sampling Step: '
    #             f'{rot_step_deg2 if self.is_internal_rot2 else None}\n')
    # Get Degeneracy Matrices
    # logger.info('Searching For Possible Degeneracies')
    if self.degeneracy_matrices1 is None:
        logger.info('No Degeneracies Found for Oligomer 1')
    elif len(self.degeneracy_matrices1) == 1:
        logger.info('1 Degeneracy Found for Oligomer 1')
    else:
        logger.info(f'{len(self.degeneracy_matrices1)} Degeneracies Found for Oligomer 1')
    if self.degeneracy_matrices2 is None:
        logger.info('No Degeneracies Found for Oligomer 2\n')
    elif len(self.degeneracy_matrices2) == 1:
        logger.info('1 Degeneracy Found for Oligomer 2\n')
    else:
        logger.info(f'{len(self.degeneracy_matrices2)} Degeneracies Found for Oligomer 2\n')

SymEntryFactory

SymEntryFactory(**kwargs)

Return a SymEntry instance by calling the Factory instance with the SymEntry entry number and symmetry map (sym_map)

Handles creation and allotment to other processes by saving expensive memory load of multiple instances and allocating a shared pointer to the SymEntry

Source code in symdesign/utils/SymEntry.py
1104
1105
def __init__(self, **kwargs):
    self._entries = {}

__call__

__call__(entry: int, sym_map: list[str] = None, **kwargs) -> SymEntry

Return the specified SymEntry object singleton

Parameters:

  • entry (int) –

    The entry number

  • sym_map (list[str], default: None ) –

    The particular mapping of the symmetric groups

Returns: The instance of the specified SymEntry

Source code in symdesign/utils/SymEntry.py
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
def __call__(self, entry: int, sym_map: list[str] = None, **kwargs) -> SymEntry:
    """Return the specified SymEntry object singleton

    Args:
        entry: The entry number
        sym_map: The particular mapping of the symmetric groups
    Returns:
        The instance of the specified SymEntry
    """
    if sym_map is None:
        sym_map_string = 'None'
    else:
        sym_map_string = '|'.join('None' if sym is None else sym for sym in sym_map)

    if entry == 0:
        # Don't add this to the self._entries
        return CrystSymEntry(sym_map=sym_map, **kwargs)

    entry_key = f'{entry}|{sym_map_string}'
    symmetry = self._entries.get(entry_key)
    if symmetry:
        return symmetry
    else:
        self._entries[entry_key] = sym_entry = SymEntry(entry, sym_map=sym_map, **kwargs)
        return sym_entry

get

get(entry: int, sym_map: list[str] = None, **kwargs) -> SymEntry

Return the specified SymEntry object singleton

Parameters:

  • entry (int) –

    The entry number

  • sym_map (list[str], default: None ) –

    The particular mapping of the symmetric groups

Returns: The instance of the specified SymEntry

Source code in symdesign/utils/SymEntry.py
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
def get(self, entry: int, sym_map: list[str] = None, **kwargs) -> SymEntry:
    """Return the specified SymEntry object singleton

    Args:
        entry: The entry number
        sym_map: The particular mapping of the symmetric groups
    Returns:
        The instance of the specified SymEntry
    """
    return self.__call__(entry, sym_map, **kwargs)

construct_uc_matrix

construct_uc_matrix(string_vector: Iterable[str]) -> ndarray

Calculate a matrix specifying the degrees of freedom in each dimension of the unit cell

Parameters:

  • string_vector (Iterable[str]) –

    The string vector as parsed from the symmetry combination table

Returns:

  • ndarray

    Float array with shape (3, 3) the values to specify unit cell dimensions from basis vector constraints

Source code in symdesign/utils/SymEntry.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
def construct_uc_matrix(string_vector: Iterable[str]) -> np.ndarray:
    """Calculate a matrix specifying the degrees of freedom in each dimension of the unit cell

    Args:
        string_vector: The string vector as parsed from the symmetry combination table

    Returns:
        Float array with shape (3, 3) the values to specify unit cell dimensions from basis vector constraints
    """
    string_position = {'e': 0, 'f': 1, 'g': 2}
    variable_matrix = np.zeros((3, 3))  # default is float
    for col_idx, string in enumerate(string_vector):  # ex ['4*e', 'f', '0']
        if string[-1] != '0':
            row_idx = string_position[string[-1]]
            variable_matrix[row_idx][col_idx] = float(string.split('*')[0]) if '*' in string else 1.

            if '-' in string:
                variable_matrix[row_idx][col_idx] *= -1

    # for entry 6 - unit cell string_vector is ['4*e', '4*e', '4*e']
    #  [[4, 4, 4], [0, 0, 0], [0, 0, 0]]
    #  component1 string vector is ['0', 'e', '0']
    #   [[0, 1, 0], [0, 0, 0], [0, 0, 0]]
    #  component2 string vector is ['0', '0', '0']
    #   [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
    # for entry 85 - string_vector is ['4*e', '4*f', '4*g']
    #  [[4, 0, 0], [0, 4, 0], [0, 0, 4]]
    #  component1 string vector is ['0', '0', '0']
    #   [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
    #  component2 string vector is ['e', 'f', 'g']
    #   [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
    return variable_matrix

get_rot_matrices

get_rot_matrices(step_deg: int | float, axis: str = 'z', rot_range_deg: int | float = 360) -> ndarray | None

Return a group of rotation matrices to rotate coordinates about a specified axis in set step increments

Parameters:

  • step_deg (int | float) –

    The number of degrees for each rotation step

  • axis (str, default: 'z' ) –

    The axis about which to rotate

  • rot_range_deg (int | float, default: 360 ) –

    The range with which rotation is possible

Returns: The rotation matrices with shape (rotations, 3, 3)

Source code in symdesign/utils/SymEntry.py
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
def get_rot_matrices(step_deg: int | float, axis: str = 'z', rot_range_deg: int | float = 360) -> np.ndarray | None:
    """Return a group of rotation matrices to rotate coordinates about a specified axis in set step increments

    Args:
        step_deg: The number of degrees for each rotation step
        axis: The axis about which to rotate
        rot_range_deg: The range with which rotation is possible
    Returns:
        The rotation matrices with shape (rotations, 3, 3)
    """
    if rot_range_deg == 0:
        return None

    # Todo use scipy.Rotation to create these!
    rot_matrices = []
    axis = axis.lower()
    if axis == 'x':
        for step in range(0, int(rot_range_deg//step_deg)):
            rad = math.radians(step * step_deg)
            rot_matrices.append([[1., 0., 0.], [0., math.cos(rad), -math.sin(rad)], [0., math.sin(rad), math.cos(rad)]])
    elif axis == 'y':
        for step in range(0, int(rot_range_deg//step_deg)):
            rad = math.radians(step * step_deg)
            rot_matrices.append([[math.cos(rad), 0., math.sin(rad)], [0., 1., 0.], [-math.sin(rad), 0., math.cos(rad)]])
    elif axis == 'z':
        for step in range(0, int(rot_range_deg//step_deg)):
            rad = math.radians(step * step_deg)
            rot_matrices.append([[math.cos(rad), -math.sin(rad), 0.], [math.sin(rad), math.cos(rad), 0.], [0., 0., 1.]])
    else:
        raise ValueError(f"Axis '{axis}' isn't supported")

    return np.array(rot_matrices)

make_rotations_degenerate

make_rotations_degenerate(rotations: ndarray | list[ndarray] | list[list[list[float]]] = None, degeneracies: ndarray | list[ndarray] | list[list[list[float]]] = None) -> ndarray

From a set of degeneracy matrices and a set of rotation matrices, produce the complete combination of the specified transformations

Parameters:

  • rotations (ndarray | list[ndarray] | list[list[list[float]]], default: None ) –

    A group of rotations with shape (rotations, 3, 3)

  • degeneracies (ndarray | list[ndarray] | list[list[list[float]]], default: None ) –

    A group of degeneracies with shape (degeneracies, 3, 3)

Returns: The matrices resulting from the multiplication of each rotation by each degeneracy. Product has length = (rotations x degeneracies, 3, 3) where the first 3x3 array on axis 0 is the identity

Source code in symdesign/utils/SymEntry.py
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
def make_rotations_degenerate(rotations: np.ndarray | list[np.ndarray] | list[list[list[float]]] = None,
                              degeneracies: np.ndarray | list[np.ndarray] | list[list[list[float]]] = None) \
        -> np.ndarray:
    """From a set of degeneracy matrices and a set of rotation matrices, produce the complete combination of the
    specified transformations

    Args:
        rotations: A group of rotations with shape (rotations, 3, 3)
        degeneracies: A group of degeneracies with shape (degeneracies, 3, 3)
    Returns:
        The matrices resulting from the multiplication of each rotation by each degeneracy.
            Product has length = (rotations x degeneracies, 3, 3) where the first 3x3 array on axis 0 is the identity
    """
    if rotations is None:
        rotations = identity_matrix[None, :, :]  # Expand to shape (1, 3, 3)
    elif np.all(identity_matrix == rotations[0]):
        pass  # This is correct
    else:
        logger.warning(f'{make_rotations_degenerate.__name__}: The argument "rotations" is missing an identity '
                       'matrix which is recommended to produce the correct matrices. Adding now')
        rotations = [identity_matrix] + list(rotations)

    if degeneracies is None:
        degeneracies = identity_matrix[None, :, :]  # Expand to shape (1, 3, 3)
    elif np.all(identity_matrix == degeneracies[0]):
        pass  # This is correct
    else:
        logger.warning(f'{make_rotations_degenerate.__name__}: The argument "degeneracies" is missing an identity '
                       'matrix which is recommended to produce the correct matrices. Adding now')
        degeneracies = [identity_matrix] + list(degeneracies)

    return np.concatenate([np.matmul(rotations, degen_mat) for degen_mat in degeneracies])

parse_symmetry_specification

parse_symmetry_specification(specification: str) -> list[str]

Parse the typical symmetry specification string with format RESULT:{SUBSYMMETRY1}{SUBSYMMETRY2}... to a list

Parameters:

  • specification (str) –

    The specification string

Returns: The parsed string with each member split into a list - ['RESULT', 'SUBSYMMETRY1', 'SUBSYMMETRY2', ...]

Source code in symdesign/utils/SymEntry.py
1339
1340
1341
1342
1343
1344
1345
1346
1347
def parse_symmetry_specification(specification: str) -> list[str]:
    """Parse the typical symmetry specification string with format RESULT:{SUBSYMMETRY1}{SUBSYMMETRY2}... to a list

    Args:
        specification: The specification string
    Returns:
        The parsed string with each member split into a list - ['RESULT', 'SUBSYMMETRY1', 'SUBSYMMETRY2', ...]
    """
    return [split.strip('}:') for split in specification.split('{')]

parse_symmetry_to_sym_entry

parse_symmetry_to_sym_entry(sym_entry_number: int = None, symmetry: str = None, sym_map: list[str] = None) -> SymEntry | None

Take a symmetry specified in a number of ways and return the symmetry parameters in a SymEntry instance

Parameters:

  • sym_entry_number (int, default: None ) –

    The integer corresponding to the desired SymEntry

  • symmetry (str, default: None ) –

    The symmetry specified by a string

  • sym_map (list[str], default: None ) –

    A symmetry map where each successive entry is the corresponding symmetry group number for the structure

Returns: The SymEntry instance or None if parsing failed

Source code in symdesign/utils/SymEntry.py
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
def parse_symmetry_to_sym_entry(sym_entry_number: int = None, symmetry: str = None, sym_map: list[str] = None) -> \
        SymEntry | None:
    """Take a symmetry specified in a number of ways and return the symmetry parameters in a SymEntry instance

    Args:
        sym_entry_number: The integer corresponding to the desired SymEntry
        symmetry: The symmetry specified by a string
        sym_map: A symmetry map where each successive entry is the corresponding symmetry group number for the structure
    Returns:
        The SymEntry instance or None if parsing failed
    """
    if sym_map is None:  # Find sym_map from symmetry
        if symmetry is not None:
            symmetry = symmetry.strip()
            if symmetry in space_group_symmetry_operators:  # space_group_symmetry_operators in Hermann-Mauguin notation
                # Only have the resulting symmetry, set it and then solve by lookup_sym_entry_by_symmetry_combination()
                sym_map = [symmetry]
            elif len(symmetry) > 3:
                if ':{' in symmetry:  # Symmetry specification of typical type result:{subsymmetry}{}...
                    sym_map = parse_symmetry_specification(symmetry)
                elif CRYST in symmetry.upper():  # This is crystal specification
                    return None  # Have to set SymEntry up after parsing cryst records
                else:  # This is some Rosetta based symmetry?
                    sym_str1, sym_str2, sym_str3, *_ = symmetry
                    sym_map = f'{sym_str1} C{sym_str2} C{sym_str3}'.split()
                    logger.error(f"Symmetry specification '{symmetry}' isn't understood, trying to solve anyway\n\n")
            elif symmetry in valid_symmetries:
                # logger.debug(f'{parse_symmetry_to_sym_entry.__name__}: The functionality of passing symmetry as '
                #              f"{symmetry} hasn't been tested thoroughly yet")
                # Specify as [result, entity1, None as there are no other entities]
                # If the symmetry is specified as C2 and the structure is A2B2, then this may fail
                sym_map = [symmetry, symmetry, None]
            elif len(symmetry) == 3 and symmetry[1].isdigit() and symmetry[2].isdigit():  # like I32, O43 format
                sym_map = [*symmetry]
            else:  # C35
                raise ValueError(
                    f"{symmetry} isn't a supported symmetry... {highest_point_group_msg}")
        elif sym_entry_number is not None:
            return symmetry_factory.get(sym_entry_number)
        else:
            raise utils.SymmetryInputError(
                f"{parse_symmetry_to_sym_entry.__name__}: Can't initialize without 'symmetry' or 'sym_map'")

    if sym_entry_number is None:
        try:  # To lookup in the all_sym_entry_dict
            sym_entry_number = utils.dictionary_lookup(all_sym_entry_dict, sym_map)
            if not isinstance(sym_entry_number, int):
                raise TypeError
        except (KeyError, TypeError):
            # The prescribed symmetry is a point, plane, or space group that isn't in Nanohedra symmetry combinations.
            # Try to load a custom input
            sym_entry_number = lookup_sym_entry_by_symmetry_combination(*sym_map)

    return symmetry_factory.get(sym_entry_number, sym_map=sym_map)

sdf_lookup

sdf_lookup(symmetry: str = None) -> AnyStr

From the set of possible point groups, locate the proper symmetry definition file depending on the specified symmetry. If none is specified, a C1 symmetry will be returned (this doesn't make sense but is completely viable)

Parameters:

  • symmetry (str, default: None ) –

    Can be a valid_point_group, or None

Returns: The location of the symmetry definition file on disk

Source code in symdesign/utils/SymEntry.py
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
def sdf_lookup(symmetry: str = None) -> AnyStr:
    """From the set of possible point groups, locate the proper symmetry definition file depending on the specified
    symmetry. If none is specified, a C1 symmetry will be returned (this doesn't make sense but is completely viable)

    Args:
        symmetry: Can be a valid_point_group, or None
    Returns:
        The location of the symmetry definition file on disk
    """
    if not symmetry or symmetry.upper() == 'C1':
        return os.path.join(putils.symmetry_def_files, 'C1.sym')
    else:
        symmetry = symmetry.upper()

    for file, ext in map(os.path.splitext, os.listdir(putils.symmetry_def_files)):
        if symmetry == file:
            return os.path.join(putils.symmetry_def_files, file + ext)

    raise FileNotFoundError(
        f"For symmetry: {symmetry}, couldn't locate correct symmetry definition file at '{putils.symmetry_def_files}'")

symmetry_groups_are_allowed_in_entry

symmetry_groups_are_allowed_in_entry(symmetry_operators: Iterable[str], *groups: Iterable[str], result: str = None, entry_number: int = None) -> bool

Check if the provided symmetry operators are allowed in a SymEntry

Parameters:

  • symmetry_operators (Iterable[str]) –

    The symmetry operators of interest

  • groups (Iterable[str], default: () ) –

    The groups provided in the symmetry

  • result (str, default: None ) –

    The resulting symmetry

  • entry_number (int, default: None ) –

    The SymEntry number of interest

Returns: True if the symmetry operators are valid, False otherwise

Source code in symdesign/utils/SymEntry.py
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
def symmetry_groups_are_allowed_in_entry(symmetry_operators: Iterable[str], *groups: Iterable[str], result: str = None,
                                         entry_number: int = None) -> bool:
    """Check if the provided symmetry operators are allowed in a SymEntry

    Args:
        symmetry_operators: The symmetry operators of interest
        groups: The groups provided in the symmetry
        result: The resulting symmetry
        entry_number: The SymEntry number of interest
    Returns:
        True if the symmetry operators are valid, False otherwise
    """
    if result is not None:
        # if group1 is None and group2 is None:
        if not groups:
            raise ValueError(
                f"When using the argument 'result', must provide at least 1 group. Got {groups}")
    elif entry_number is not None:
        entry = symmetry_combinations.get(entry_number)
        if entry is None:
            raise utils.SymmetryInputError(
                f"The entry number {entry_number} isn't an available {SymEntry.__name__}")

        group1, _, _, _, group2, _, _, _, _, result, *_ = entry
        groups = (group1, group2)  # Todo modify for more than 2
    else:
        raise ValueError(
            'Must provide entry_number, or the result and *groups arguments. None were provided')

    # Find all sub_symmetries that are viable in the component group members
    for group in groups:
        group_members = sub_symmetries.get(group, [None])
        for sym_operator in symmetry_operators:
            if sym_operator in [result, *groups]:
                continue
            elif sym_operator in group_members:
                continue
            else:
                return False

    return True  # Assume correct unless proven incorrect

get_int_dof

get_int_dof(*groups: Iterable[str]) -> list[tuple[int, int], ...]

Usage int_dof1, int_dof2, *_ = get_int_dof(int_dof_group1, int_dof_group2)

Source code in symdesign/utils/SymEntry.py
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
def get_int_dof(*groups: Iterable[str]) -> list[tuple[int, int], ...]:
    """Usage
    int_dof1, int_dof2, *_ = get_int_dof(int_dof_group1, int_dof_group2)
    """
    group_int_dofs = []
    for group_int_dof in groups:
        int_rot = int_tx = 0
        for int_dof in group_int_dof:
            if int_dof.startswith('r'):
                int_rot = 1
            if int_dof.startswith('t'):
                int_tx = 1
        group_int_dofs.append((int_rot, int_tx))

    return group_int_dofs

lookup_sym_entry_by_symmetry_combination

lookup_sym_entry_by_symmetry_combination(result: str, *symmetry_operators: str) -> int

Given the resulting symmetry and the symmetry operators for each Entity, solve for the SymEntry

Parameters:

  • result (str) –

    The global symmetry

  • symmetry_operators (str, default: () ) –

    Additional operators which specify sub-symmetric systems in the larger result

Returns: The entry number of the SymEntry

Source code in symdesign/utils/SymEntry.py
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
def lookup_sym_entry_by_symmetry_combination(result: str, *symmetry_operators: str) -> int:
    """Given the resulting symmetry and the symmetry operators for each Entity, solve for the SymEntry

    Args:
        result: The global symmetry
        symmetry_operators: Additional operators which specify sub-symmetric systems in the larger result
    Returns:
        The entry number of the SymEntry
    """

    # def print_matching_entries(entries):
    #     if not entries:
    #         return
    #     print_query_header()
    #     for _entry in entries:
    #         _group1, _int_dof_group1, _, _ref_frame_tx_dof_group1, _group2, _int_dof_group2, _, \
    #             _ref_frame_tx_dof_group2, _, _result, dimension, *_ = symmetry_combinations[_entry]
    #         int_dof1, int_dof2, *_ = get_int_dof(_int_dof_group1, _int_dof_group2)
    #         print(query_output_format_string.format(
    #             _entry, result, dimension,
    #             _group1, *int_dof1, str(_ref_frame_tx_dof_group1),
    #             _group2, *int_dof2, str(_ref_frame_tx_dof_group2)))
    def specification_string(result, symmetry_operators):
        return f'{result}:{{{"}{".join(symmetry_operators)}}}'

    def report_multiple_solutions(entries: list[int]):
        # entries = sorted(entries)
        # print(f'\033[1mFound specified symmetries matching including {", ".join(map(str, entries))}\033[0m')
        # print(f'\033[1mFound specified symmetries matching\033[0m')
        print_matching_entries(specification_string(result, symmetry_operators), entries)
        print(repeat_with_sym_entry)

    result = str(result)
    result_entries = []
    matching_entries = []
    for entry_number, entry in symmetry_combinations.items():
        group1, _, _, _, group2, _, _, _, _, resulting_symmetry, *_ = entry
        if resulting_symmetry == result:
            result_entries.append(entry_number)

            if symmetry_operators and \
                    symmetry_groups_are_allowed_in_entry(symmetry_operators, entry_number=entry_number):
                matching_entries.append(entry_number)  # Todo include the groups?

    if matching_entries:
        if len(matching_entries) != 1:
            # Try to solve
            # good_matches: dict[int, list[str]] = defaultdict(list)
            good_matches: dict[int, list[str]] = {entry_number: [None, None] for entry_number in matching_entries}
            for entry_number in matching_entries:
                group1, _, _, _, group2, _, _, _, _, resulting_symmetry, *_ = symmetry_combinations[entry_number]
                match_tuple = good_matches[entry_number]
                for sym_op in symmetry_operators:
                    if sym_op == group1 and match_tuple[0] is None:
                        match_tuple[0] = sym_op
                    elif sym_op == group2 and match_tuple[1] is None:
                        match_tuple[1] = sym_op
            # logger.debug(f'good matches: {good_matches}')

            # max_ops = 0
            exact_matches = []
            for entry_number, ops in good_matches.items():
                number_ops = sum(False if op is None else True for op in ops)
                if number_ops == len(symmetry_operators):
                    exact_matches.append(entry_number)
                # elif number_ops > max_ops:
                #     max_ops = number_ops

            if exact_matches:
                if len(exact_matches) == 1:
                    matching_entries = exact_matches
                else:  # Still equal, report bad
                    report_multiple_solutions(exact_matches)
                    sys.exit(1)
                    # -------- TERMINATE --------
            else:  # symmetry_operations are 3 or greater. Get the highest symmetry
                all_matches = []
                for entry_number, matching_ops in good_matches.items():
                    if all(matching_ops):
                        all_matches.append(entry_number)

                if all_matches:
                    if len(all_matches) == 1:
                        matching_entries = all_matches
                    else:  # Still equal, report bad
                        report_multiple_solutions(exact_matches)
                        sys.exit(1)
                        # -------- TERMINATE --------
                else:  # None match all, this must be 2 or more sub-symmetries
                    # max_symmetry_number = 0
                    # symmetry_number_to_entries = defaultdict(list)
                    # for entry_number, matching_ops in good_matches.items():
                    #     if len(matching_ops) == max_ops:
                    #         total_symmetry_number = sum([valid_subunit_number[op] for op in matching_ops])
                    #         symmetry_number_to_entries[total_symmetry_number].append(entry_number)
                    #         if total_symmetry_number > max_symmetry_number:
                    #             max_symmetry_number = total_symmetry_number
                    #
                    # exact_matches = symmetry_number_to_entries[max_symmetry_number]
                    # if len(exact_matches) == 1:
                    #     matching_entries = exact_matches
                    # else:  # Still equal, report bad
                    # print('non-exact matches')
                    print_matching_entries(specification_string(result, symmetry_operators), exact_matches)
                    # report_multiple_solutions(exact_matches)
                    sys.exit(1)
                    # -------- TERMINATE --------
    elif symmetry_operators:
        if result in space_group_symmetry_operators:  # space_group_symmetry_operators in Hermann-Mauguin notation
            matching_entries = [0]  # 0 = CrystSymEntry
        else:
            raise ValueError(
                f"The specified symmetries '{', '.join(symmetry_operators)}' couldn't be coerced to make the resulting "
                f"symmetry='{result}'. Try to reformat your symmetry specification if this is the result of a typo to "
                'include only symmetries that are group members of the resulting symmetry such as '
                f'{", ".join(all_sym_entry_dict.get(result, {}).keys())}\nUse the format {example_symmetry_specification} '
                'during your specification')
    else:  # No symmetry_operators
        if result_entries:
            report_multiple_solutions(result_entries)
            sys.exit()
        else:  # no matches
            raise ValueError(
                f"The resulting symmetry {result} didn't match any possible symmetry_combinations. You are likely "
                'requesting a symmetry that is outside of the parameterized SymEntry entries. If this is a '
                '\033[1mchiral\033[0m plane/space group, modify the function '
                f'{lookup_sym_entry_by_symmetry_combination.__name__} to use '
                f'non-Nanohedra compatible chiral space_group_symmetry_operators. {highest_point_group_msg}')

    logger.debug(f'Found matching SymEntry.number {matching_entries[0]}')
    return matching_entries[0]

print_matching_entries

print_matching_entries(match_string, matching_entries: Iterable[int])

Report the relevant information from passed SymEntry entry numbers

Parameters:

  • match_string

    The string inserted into "All entries found matching {match_string}:"

  • matching_entries (Iterable[int]) –

    The matching entry numbers

Returns: None

Source code in symdesign/utils/SymEntry.py
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
def print_matching_entries(match_string, matching_entries: Iterable[int]):
    """Report the relevant information from passed SymEntry entry numbers

    Args:
        match_string: The string inserted into "All entries found matching {match_string}:"
        matching_entries: The matching entry numbers
    Returns:
        None
    """
    if not matching_entries:
        print(f'\033[1mNo entries found matching {match_string}\033[0m\n')
        return
    else:
        matching_entries = sorted(matching_entries)

    print(f'\033[1mAll entries found matching {match_string}:\033[0m')
    print_query_header()
    for entry in matching_entries:
        group1, int_dof_group1, _, ref_frame_tx_dof_group1, group2, int_dof_group2, _, \
            ref_frame_tx_dof_group2, result_point_group, result, dimension, cell_lengths, cell_angles, tot_dof, \
            ring_size = symmetry_combinations[entry]
        int_dof1, int_dof2, *_ = get_int_dof(int_dof_group1, int_dof_group2)
        # print(entry, result, dimension,
        #       group1, *int_dof1, ref_frame_tx_dof_group1,
        #       group2, *int_dof2, ref_frame_tx_dof_group2, tot_dof, ring_size)
        if ref_frame_tx_dof_group1 is None:
            ref_frame_tx_dof_group1 = '0,0,0'
        if ref_frame_tx_dof_group2 is None:
            ref_frame_tx_dof_group2 = '0,0,0'
        print(query_output_format_string.format(
                entry, result, dimension,
                group1, *int_dof1, f'<{ref_frame_tx_dof_group1}>',
                str(group2), *int_dof2, f'<{ref_frame_tx_dof_group2}>', tot_dof, ring_size,
                True if entry in nanohedra_symmetry_combinations else False))

query

query(mode: query_modes_literal, *additional_mode_args, nanohedra: bool = True)

Perform a query of the symmetry combinations

Parameters:

  • mode (query_modes_literal) –

    The type of query to perform. Viable options are: 'all-entries', 'combination', 'counterpart', 'dimension', and 'result'

  • *additional_mode_args

    Additional query args required

  • nanohedra (bool, default: True ) –

    True if only Nanohedra docking symmetries should be queried

Returns: None

Source code in symdesign/utils/SymEntry.py
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
def query(mode: query_modes_literal, *additional_mode_args, nanohedra: bool = True):
    """Perform a query of the symmetry combinations

    Args:
        mode: The type of query to perform. Viable options are:
            'all-entries', 'combination', 'counterpart', 'dimension', and 'result'
        *additional_mode_args: Additional query args required
        nanohedra: True if only Nanohedra docking symmetries should be queried
    Returns:
        None
    """
    if nanohedra:
        symmetry_combinations_of_interest = nanohedra_symmetry_combinations
    else:
        symmetry_combinations_of_interest = symmetry_combinations

    matching_entries = []
    if mode == 'all-entries':
        match_string = mode
        # all_entries()
        # def all_entries():
        matching_entries.extend(symmetry_combinations_of_interest.keys())
    else:
        if not additional_mode_args:
            # raise ValueError(
            #     f"Can't query with mode '{mode}' without additional arguments")
            instructions = defaultdict(str, {
                'combination': 'Provide multiple symmetry groups from the possible groups '
                               f'{", ".join(valid_symmetries)}\n'})
            mode_instructions = instructions[mode]
            more_info_prompt = f"For the query mode '{mode}', more information is needed\n" \
                               f"{mode_instructions}What {mode} is requested?"
            additional_mode_args = utils.query.format_input(more_info_prompt)

        if mode == 'combination':
            combination = additional_mode_args
            match_string = f"{mode} {''.join(f'{{{group}}}' for group in combination)}"
            # query_combination(*additional_mode_args)
            # def query_combination(*combination):
            for entry_number, entry in symmetry_combinations_of_interest.items():
                group1, _, _, _, group2, *_ = entry
                if combination == (group1, group2) or combination == (group2, group1):
                    matching_entries.append(entry_number)
        elif mode == 'result':
            result, *_ = additional_mode_args
            match_string = f'{mode}={result}'
            # query_result(result)
            # def query_result(desired_result: str):
            for entry_number, entry in symmetry_combinations_of_interest.items():
                _, _, _, _, _, _, _, _, _, entry_result, *_ = entry
                if result == entry_result:
                    matching_entries.append(entry_number)
        elif mode == 'group':
            group, *_ = additional_mode_args
            match_string = f'{mode}={group}'
            # query_counterpart(counterpart)
            # def query_counterpart(group: str):
            for entry_number, entry in symmetry_combinations_of_interest.items():
                group1, _, _, _, group2, *_ = entry
                if group in (group1, group2):
                    matching_entries.append(entry_number)

        elif mode == 'dimension':
            dim, *_ = additional_mode_args
            match_string = f'{mode}={dim}'
            # dimension(dim)
            # def dimension(dim: int):
            try:
                dim = int(dim)
            except ValueError:
                pass

            if dim in [0, 2, 3]:
                for entry_number, entry in symmetry_combinations_of_interest.items():
                    _, _, _, _, _, _, _, _, _, _, dimension, *_ = entry
                    if dimension == dim:
                        matching_entries.append(entry_number)
            else:
                print(f"Dimension '{dim}' isn't supported. Valid dimensions are: 0, 2 or 3")
                sys.exit()
        else:
            raise ValueError(
                f"The mode '{mode}' isn't available")

    # Report those found
    # print(matching_entries)
    print_matching_entries(match_string, matching_entries)