def test_invert(self): for xyz in ['-y,-x,-z+1/4', 'y,-x,z+3/4', 'y,x,-z', 'y+1/2,x,-z+1/3']: op = gemmi.Op(xyz) self.assertEqual(op * op.inverse(), 'x,y,z') self.assertEqual(op.inverse().inverse(), op) op = gemmi.Op("-y+z,x+z,-x+y+z") # det=3 self.assertRaises(RuntimeError, op.inverse)
def test_combine(self): a = gemmi.Op('x+1/3,z,-y') self.assertEqual(a.combine(a).triplet(), 'x+2/3,-y,-z') self.assertEqual('x,-y,z' * gemmi.Op('-x,-y,z'), '-x,y,z') a = gemmi.Op('-y+1/4,x+3/4,z+1/4') b = gemmi.Op('-x+1/2,y,-z') self.assertEqual((a * b).triplet(), '-y+1/4,-x+1/4,-z+1/4') c = '-y,-z,-x' self.assertNotEqual(b * c, c * b) self.assertEqual((a * c).triplet(), 'z+1/4,-y+3/4,-x+1/4') self.assertEqual(b * c, gemmi.Op('y+1/2,-z,x')) self.assertEqual(c * b, '-y,z,x+1/2')
def verify_hall_symbol(entry): if not gemmi: return hall_ops = gemmi.symops_from_hall(entry['hall']) assert len(hall_ops.sym_ops) == len(entry['symops']) assert len(hall_ops.cen_ops) == len(entry['cenops']) # centering vectors are exactly the same assert (set(gemmi.Op().translated(tr) for tr in hall_ops.cen_ops) == set(gemmi.Op(e) for e in entry['cenops'])), entry # symops differ in some cases but are the same modulo centering vectors given = set(gemmi.Op(s) * c for s in entry['symops'] for c in entry['cenops']) assert given == set(hall_ops), entry
def test_table(self): for sg in gemmi.spacegroup_table(): if sg.ccp4 != 0: self.assertEqual(sg.ccp4 % 1000, sg.number) if sg.operations().is_centric(): self.assertEqual(sg.laue_str(), sg.point_group_hm()) else: self.assertNotEqual(sg.laue_str(), sg.point_group_hm()) if sgtbx: hall = sg.hall.encode() cctbx_sg = sgtbx.space_group(hall) cctbx_info = sgtbx.space_group_info(group=cctbx_sg) self.assertEqual(sg.is_reference_setting(), cctbx_info.is_reference_setting()) #to_ref = cctbx_info.change_of_basis_op_to_reference_setting() #from_ref = '%s' % cob_to_ref.inverse().c() c2p_sg = gemmi.Op(cctbx_sg.z2p_op().c().inverse().as_xyz()) self.assertEqual(sg.centred_to_primitive(), c2p_sg) ops = gemmi.get_spacegroup_reference_setting(sg.number).operations() ops.change_basis_forward(sg.basisop) self.assertEqual(ops, sg.operations()) itb = gemmi.spacegroup_table_itb() if sgtbx: for s in sgtbx.space_group_symbol_iterator(): self.assertEqual(s.hall().strip(), next(itb).hall) with self.assertRaises(StopIteration): next(itb)
def parse_chunk(lines): return { 'number': int(lines[0]), 'hall': lines[1].strip(), 'xhm': lines[2].strip(), 'symops': [gemmi.Op(line) for line in lines[3:]] }
def test_invert(self): for xyz in ['-y,-x,-z+1/4', 'y,-x,z+3/4', 'y,x,-z', 'y+1/2,x,-z+1/3']: op = gemmi.Op(xyz) self.assertEqual(op * op.inverse(), 'x,y,z') self.assertEqual(op.inverse().inverse(), op) # test change-of-basis op between hexagonal and trigonal settings op = gemmi.Op("-y+z,x+z,-x+y+z") # det=3 self.assertEqual(op.det_rot(), 3 * gemmi.Op.DEN**3) inv = op.inverse() self.assertEqual(inv * op, 'x,y,z') self.assertEqual(op * inv, 'x,y,z') expected_inv = '-1/3*x+2/3*y-1/3*z,-2/3*x+1/3*y+1/3*z,1/3*x+1/3*y+1/3*z' self.assertEqual(inv.triplet(), expected_inv) self.assertEqual(gemmi.Op(expected_inv), inv) op = gemmi.Op('1/2*x+1/2*y,-1/2*x+1/2*y,z') self.assertEqual(op.inverse().triplet(), 'x-y,x+y,z')
def _spgr(self) -> gemmi.SpaceGroup: if self.symmops: symm_ops = self.symmops else: symm_ops = self.symmops_from_spgr return gemmi.find_spacegroup_by_ops( gemmi.GroupOps([gemmi.Op(o) for o in symm_ops]))
def test_groupops(self): gops = gemmi.GroupOps([gemmi.Op(t) for t in ['x, y, z', 'x, -y, z+1/2', 'x+1/2, y+1/2, z', 'x+1/2, -y+1/2, z+1/2']]) self.assertEqual(gops.find_centering(), 'C') self.assertEqual(len(gops), 4) self.assertEqual(gemmi.find_spacegroup_by_ops(gops).hm, 'C 1 c 1')
def test_triplet_roundtrip(self): singles = list(CANONICAL_SINGLES.keys()) for i in range(4): items = [random.choice(singles) for j in range(3)] triplet = ','.join(items) op = gemmi.parse_triplet(triplet) self.assertEqual(op.triplet(), triplet) self.assertEqual(gemmi.Op(' x , - y, + z ').triplet(), 'x,-y,z')
def change_basis(self, name_a, name_b, basisop_triplet): basisop = gemmi.Op(basisop_triplet) a = gemmi.find_spacegroup_by_name(name_a) b = gemmi.find_spacegroup_by_name(name_b) ops = a.operations() ops.change_basis(basisop) self.assertEqual(ops, b.operations()) ops.change_basis(basisop.inverse()) self.assertEqual(ops, a.operations())
def test_invert(self): for xyz in ['-y,-x,-z+1/4', 'y,-x,z+3/4', 'y,x,-z', 'y+1/2,x,-z+1/3']: op = gemmi.Op(xyz) self.assertEqual(op * op.inverse(), 'x,y,z') self.assertEqual(op.inverse().inverse(), op) # test change-of-basis op between hexagonal and trigonal settings op = gemmi.Op("-y+z,x+z,-x+y+z") # det=3 self.assertEqual(op.det_rot(), 3 * gemmi.Op.DEN**3) inv = op.inverse() self.assertEqual(inv * op, 'x,y,z') self.assertEqual(op * inv, 'x,y,z') expected_inv = '-x/3+2/3*y-z/3,-2/3*x+y/3+z/3,x/3+y/3+z/3' self.assertEqual(inv.triplet(), expected_inv) self.assertEqual(gemmi.Op(expected_inv), inv) op = gemmi.Op('1/2*x+1/2*y,-1/2*x+1/2*y,z') self.assertEqual(op.inverse().triplet(), 'x-y,x+y,z') # check also alternative writing op2 = gemmi.Op('x/2+y/2,-a/2+k/2,z') self.assertEqual(op, op2)
def main(): syminfo = parse_syminfo(sys.argv[1]) seen_nums = set() for entry in syminfo: ccp4 = entry['ccp4'] hall = entry['hall'] basisop = entry['basisop'] num = entry['number'] xhm = entry['xhm'] if num not in seen_nums: seen_nums.add(num) assert ccp4 == num if basisop != 'x,y,z': print(num, xhm, basisop) assert ccp4 == 0 or ccp4 % 1000 == num if ccp4 == num: if basisop != 'x,y,z': pass # print(ccp4, basisop) if basisop != 'x,y,z': if '(%s)' % basisop not in hall: print('Hall symbol "%s" w/o basisop: %s' % (hall, basisop)) hall_ops = gemmi.symops_from_hall(hall) assert len(hall_ops.cen_ops) == len(entry['cenops']) assert (set(gemmi.Op().translated(tr) for tr in hall_ops.cen_ops) == set(entry['cenops'])) assert len(hall_ops.sym_ops) == len(entry['symops']) # symops differ in about dozen cases but are the same modulo # centering vectors generated = set(hall_ops) given = set(s * c for s in entry['symops'] for c in entry['cenops']) assert generated == given, entry print('OK. %d entries.' % len(syminfo)) hall_ref = read_ref() xhm_set = set() for d in syminfo: xhm = d['xhm'] if xhm and xhm in xhm_set: print('dup xHM:', xhm) xhm_set.add(xhm) ref = hall_ref.get(xhm) if ref is not None: assert d['number'] == ref[0] hall1 = d['hall'] hall2 = ref[1] sym1 = gemmi.symops_from_hall(hall1) sym2 = gemmi.symops_from_hall(hall2) assert set(sym1) == set(sym2), (hall1, hall2) else: print('extra:', xhm, ' (%d)' % d['number'], d['hall']) missing = set(hall_ref.keys()) - xhm_set for d in sorted(missing, key=lambda m: hall_ref[m][0]): print('missing:', d, ' (%d)' % hall_ref[d][0])
def test_change_of_basis(self): uc = gemmi.UnitCell(20, 30, 39, 73, 93, 99) op = gemmi.Op('y-x/2,-2/3*z+2/3*y,3*x') uc2 = uc.changed_basis_backward(op, set_images=False) # compare with result from cctbx: # from cctbx import sgtbx, uctbx # u = uctbx.unit_cell((20,30,39, 73,93,99)) # op = sgtbx.change_of_basis_op('y-x/2,-2/3*z+2/3*y,3*x').inverse() # print(u.change_basis(cb_op=op).parameters()) expected = (117.9468784563987, 25.977921933207348, 20.0, 130.5, 107.65517573180257, 82.63132106791868) assert_almost_equal_seq(self, uc2.parameters, expected) uc3 = uc2.changed_basis_forward(op, set_images=True) assert_almost_equal_seq(self, uc3.parameters, uc.parameters)
def get_phase_restrictions(H, spacegroup): """ Return phase restrictions for Miller indices in a given space group. If there are no phase restrictions, an empty list is returned for that Miller index. If a given Miller index is systematically absent an empty list is also returned. Parameters ---------- H : array n x 3 array of Miller indices spacegroup : gemmi.SpaceGroup Space group for determining phase restrictions Returns ------- restrictions : list of lists List of lists of phase restrictions for each Miller index. An empty list is returned for Miller indices without phase restrictions """ from reciprocalspaceship.utils.asu import is_absent, is_centric from reciprocalspaceship.utils.symop import apply_to_hkl, phase_shift friedel_op = gemmi.Op("-x,-y,-z") #Grabs all the non-identity symops ops = spacegroup.operations().sym_ops[1:] restrictions = [[]] * len(H) #This is the case for P1 if len(ops) == 0: return restrictions #Phase restrictions only apply to centrics. We'll also ignore any absent refls mask = (is_centric(H, spacegroup)) & (~is_absent(H, spacegroup)) idx = np.where(mask)[0] h = H[mask, :] hits = np.column_stack([ np.all(apply_to_hkl(h, op) == apply_to_hkl(h, friedel_op), axis=1) for op in ops ]) hits[np.cumsum(hits, axis=-1) > 1] = False #Remove duplicate hits shifts = np.column_stack([np.rad2deg(phase_shift(h, op)) for op in ops]) shifts = shifts[np.arange(len(hits)), hits.argmax(-1)] restriction = np.column_stack((shifts / 2., 180. + shifts / 2.)) restriction = canonicalize_phases(restriction) restriction.sort(-1) for i, _ in np.argwhere(hits): restrictions[idx[i]] = restriction[i].tolist() return restrictions
def parse_syminfo(path): data = [] cur = None with open(path) as f: for line in f: line = line.strip() if not line or line[0] == '#': continue #print('"%s"' % line) if line == 'begin_spacegroup': assert cur is None, line cur = {'symops': [], 'cenops': []} continue assert cur is not None, line if line == 'end_spacegroup': for must_have in ['basisop', 'ccp4', 'number', 'hall', 'xhm']: assert must_have in cur, must_have for must_have_list in ['symops', 'cenops']: assert len(cur[must_have_list]) > 0 data.append(cur) cur = None elif line.startswith('number '): cur['number'] = int(line[6:]) elif line.startswith('basisop '): cur['basisop'] = line[8:] elif line.startswith('symbol ccp4 '): cur['ccp4'] = int(line[12:]) elif line.startswith('symbol xHM '): cur['xhm'] = line[11:].strip(" '").replace(' :', ':') elif line.startswith('symbol Hall '): cur['hall'] = line[12:].strip(" '") elif line.startswith('symop '): cur['symops'].append(gemmi.Op(line[6:])) elif line.startswith('cenop '): cur['cenops'].append(gemmi.Op(line[6:])) return data
def apply_symop(self, symop, inplace=False): """ Apply symmetry operation to all reflections in DataSet object. Parameters ---------- symop : str, gemmi.Op Gemmi symmetry operation or string representing symmetry op inplace : bool Whether to return a new DataFrame or make the change in place """ if isinstance(symop, str): symop = gemmi.Op(symop) elif not isinstance(symop, gemmi.Op): raise ValueError(f"Provided symop is not of type gemmi.Op") dataset = self # Handle phase flips associated with Friedel operator if symop.det_rot() < 0: phic = -1 else: phic = 1 # Apply symop to generate new HKL indices and phase shifts H = dataset.get_hkls() hkl = apply_to_hkl(H, symop) phase_shifts = phase_shift(H, symop) dataset[['H', 'K', 'L']] = hkl dataset[['H', 'K', 'L']] = dataset[['H', 'K', 'L']].astype(rs.HKLIndexDtype()) # Shift phases according to symop for key in dataset.get_phase_keys(): dataset[key] += np.rad2deg(phase_shifts) dataset[key] *= phic dataset[key] = utils.canonicalize_phases(dataset[key], deg=True) for key in dataset.get_complex_keys(): dataset[key] *= np.exp(1j * phase_shifts) if symop.det_rot() < 0: dataset[key] = np.conjugate(dataset[key]) return dataset
def get_phase_restrictions(H, spacegroup): """ Return phase restrictions for Miller indices in a given space group. If there are no phase restrictions, an empty list is returned for that Miller index. If a given Miller index is systematically absent an empty list is also returned. Parameters ---------- H : array n x 3 array of Miller indices spacegroup : gemmi.SpaceGroup Space group for determining phase restrictions Returns ------- restrictions : list of lists List of lists of phase restrictions for each Miller index. An empty list is returned for Miller indices without phase restrictions """ from reciprocalspaceship.utils.structurefactors import is_centric, is_absent restrictions = [] for h in H: if not is_centric([h], spacegroup)[0] or is_absent([h], spacegroup)[0]: restrictions.append([]) else: friedelop = gemmi.Op("-x,-y,-z") hit = False for op in spacegroup.operations().sym_ops[1:]: if op.apply_to_hkl(h) == friedelop.apply_to_hkl(h): shift = np.rad2deg(op.phase_shift(h)) restriction = np.array([shift / 2, 180 + (shift / 2)]) restriction = canonicalize_phases(restriction) restriction.sort() restrictions.append(restriction.tolist()) hit = True break # Handle [0, 0, 0] in P1 if not hit: restrictions.append([]) return restrictions
def test_apply_symop_hkl(data_fmodel, inplace, op): """ Test DataSet.apply_symop() using fmodel dataset. This test is purely for the HKL indices, but will not explicitly test phase shift for cases other than pure rotations. """ copy = data_fmodel.copy() if isinstance(op, (gemmi.Op, str)): if isinstance(op, str): op = gemmi.Op(op) result = data_fmodel.apply_symop(op, inplace=inplace) expectedH = [op.apply_to_hkl(h) for h in copy.get_hkls()] expectedH = np.array(expectedH) assert np.array_equal(result["FMODEL"].to_numpy(), copy["FMODEL"].to_numpy()) assert np.array_equal(result.get_hkls(), expectedH) # Confirm Miller indices are still correct dtype temp = result.reset_index() assert isinstance(temp.H.dtype, rs.HKLIndexDtype) assert isinstance(temp.K.dtype, rs.HKLIndexDtype) assert isinstance(temp.L.dtype, rs.HKLIndexDtype) # Confirm copy when desired if inplace: assert id(result) == id(data_fmodel) else: assert id(result) != id(data_fmodel) else: with pytest.raises(ValueError): result = data_fmodel.apply_symop(op, inplace=inplace)
def is_centrosymm(self) -> bool: """ Whether a structuere is centro symmetric or not. """ ops = gemmi.GroupOps([gemmi.Op(o) for o in self.symmops]) return ops.is_centric()
def stack_anomalous(self, plus_labels=None, minus_labels=None): """ Convert data from two-column anomalous format to one-column format. Intensities, structure factor amplitudes, or other data are converted from separate columns corresponding to a single Miller index to the same data column at different rows indexed by the Friedel-plus or Friedel-minus Miller index. This method will return a DataSet with, at most, twice as many rows as the original -- one row for each Friedel pair. In most cases, the resulting DataSet will be smaller, because centric reflections will not be stacked. For a merged DataSet, this has the effect of mapping reflections from the positive reciprocal space ASU to the positive and negative reciprocal space ASU, for Friedel-plus and Friedel-minus reflections, respectively. Notes ----- - A ValueError is raised if invoked with an unmerged DataSet - It is assumed that Friedel-plus column labels are suffixed with (+), and that Friedel-minus column labels are suffixed with (-) - Corresponding column labels are expected to be given in the same order Parameters ---------- plus_labels: str or list-like Column label or list of column labels of data associated with Friedel-plus reflection (Defaults to columns suffixed with "(+)") minus_labels: str or list-like Column label or list of column labels of data associated with Friedel-minus reflection (Defaults to columns suffixed with "(-)") Returns ------- DataSet See Also -------- DataSet.unstack_anomalous : Opposite of stack_anomalous """ if not self.merged: raise ValueError( "DataSet.stack_anomalous() cannot be called with unmerged data" ) # Default behavior: Use labels suffixed with "(+)" or "(-)" if (plus_labels is None and minus_labels is None): plus_labels = [l for l in self.columns if "(+)" in l] minus_labels = [l for l in self.columns if "(-)" in l] # Validate column labels if isinstance(plus_labels, str) and isinstance(minus_labels, str): plus_labels = [plus_labels] minus_labels = [minus_labels] elif (isinstance(plus_labels, list) and isinstance(minus_labels, list)): if len(plus_labels) != len(minus_labels): raise ValueError( f"plus_labels: {plus_labels} and minus_labels: " f"{minus_labels} do not have same length.") else: raise ValueError( f"plus_labels and minus_labels must have same type " f"and be str or list: plus_labels is type " f"{type(plus_labels)} and minus_labe is type " f"{type(minus_labels)}.") for plus, minus in zip(plus_labels, minus_labels): if self[plus].dtype != self[minus].dtype: raise ValueError(f"Corresponding labels in {plus_labels} and " f"{minus_labels} are not the same dtype: " f"{self[plus].dtype} and {self[minus].dtype}") # Map Friedel reflections to +/- ASU centrics = self.label_centrics()["CENTRIC"] dataset_plus = self.drop(columns=minus_labels) dataset_minus = self.loc[~centrics].drop(columns=plus_labels) dataset_minus.apply_symop(gemmi.Op("-x,-y,-z"), inplace=True) # Rename columns and update dtypes new_labels = [l.rstrip("(+)") for l in plus_labels] column_mapping_plus = dict(zip(plus_labels, new_labels)) column_mapping_minus = dict(zip(minus_labels, new_labels)) dataset_plus.rename(columns=column_mapping_plus, inplace=True) dataset_minus.rename(columns=column_mapping_minus, inplace=True) F = dataset_plus.append(dataset_minus) for label in new_labels: F[label] = F[label].from_friedel_dtype() return F
# Test inplace if inplace: assert id(result) == id(data_fmodel) else: assert id(result) != id(data_fmodel) # Test labels if return_labels: assert len(labels) == bins @pytest.mark.parametrize("inplace", [True, False]) @pytest.mark.parametrize("op", [ "-x,-y,-z", gemmi.Op("x,y,z"), gemmi.Op("-x,-y,-z"), gemmi.Op("x,y,-z"), gemmi.Op("x,-y,-z"), gemmi.Op("-x,y,-z"), 5, ]) def test_apply_symop_hkl(data_fmodel, inplace, op): """ Test DataSet.apply_symop() using fmodel dataset. This test is purely for the HKL indices, but will not explicitly test phase shift for cases other than pure rotations. """ copy = data_fmodel.copy()
np.cos(np.deg2rad(friedel.loc[H.tolist(), "PHIFMODEL"].to_numpy())), np.cos( np.deg2rad( -1 * friedel.loc[(-1 * H).tolist(), "PHIFMODEL"].to_numpy())), atol=1e-5, ) assert np.allclose( friedel.loc[H.tolist(), "sf"].to_numpy(), np.conjugate(friedel.loc[(-1 * H).tolist(), "sf"].to_numpy()), ) @pytest.mark.parametrize( "op", [ gemmi.Op("x,y,z+1/4"), gemmi.Op("x,y+1/4,z"), gemmi.Op("x+1/4,y,z"), gemmi.Op("x+1/4,y+1/4,z"), gemmi.Op("x+1/4,y,z+1/4"), gemmi.Op("x,y+1/4,z+1/4"), gemmi.Op("x+1/4,y+1/4,z+1/4"), gemmi.Op("x,y+1/4,z-1/4"), gemmi.Op("x-3/4,y+1/4,z-1/4"), gemmi.Op("x-3/4,y+1/4,z-3/4"), gemmi.Op("x-3/4,y+3/4,z-3/4"), gemmi.Op("x,y,z+1/3"), gemmi.Op("x,y+1/3,z"), gemmi.Op("x+1/3,y,z"), gemmi.Op("x+1/3,y+1/3,z"), gemmi.Op("x,y+1/3,z+1/3"),