Chem-Structure-Parser

 view release on metacpan or  search on metacpan

t/real_cif.t  view on Meta::CPAN


# Windows has no HOME, and a smoker runs with warnings fatal, so reading it
# to build the maintainer's default paths is what failed the file there rather
# than skipping it.  USERPROFILE is the Windows spelling; with neither set
# there is no default path to try, and the entry drops out.
my $home = defined $ENV{HOME} ? $ENV{HOME} : $ENV{USERPROFILE};

# canon_charges() -- both readers' charges in one spelling, in place.
#
# The two formats write a formal charge differently and each reader reports
# what its own format wrote: a PDB file saying "1-" reads back as '1-', and the
# mmCIF rendering of that same charge, -1, reads back as '1-' too.  That is the
# conversion working.  But the archive holds PDB files that write the mmCIF
# spelling in the PDB column -- 4byf has "-1" in columns 79-80 -- and there the
# PDB reader passes through what it found while the mmCIF reader normalises, so
# the same charge comes back spelled two ways.
#
# Both are right.  So the comparison asks whether it is the same charge rather
# than whether it is the same string, and a wrong charge -- 1- against 2-, or
# 1- against 1+ -- still fails.  In place because $info is dropped straight
# after, and copying every atom to rewrite one field of it would cost more than
# the comparison.
sub canon_charges {
	my ($i) = @_;
	for my $cid (@{ $i->{chain_order} }) {
		my $c = $i->{chains}{$cid};
		for my $rk (@{ $c->{residue_order} }) {
			my $r = $c->{residues}{$rk};
			for my $an (@{ $r->{atom_order} }) {
				my $a = $r->{atoms}{$an};
				my $q = $a->{charge};
				next unless defined $q && length $q;
				# sign and magnitude, whichever order the field put them in.
				# Zero has no sign: 4iu3 writes "0-" in columns 79-80, and a
				# charge of minus nothing is a charge of nothing.
				my ($sign, $mag) =
					  $q =~ /\A(\d)([-+])\z/ ? ($2, $1)
					: $q =~ /\A([-+])(\d)\z/ ? ($1, $2)
					: $q =~ /\A(\d)\z/       ? ('+', $1)
					:                          (undef, undef);
				next unless defined $mag;              # not a charge; leave it be
				$a->{charge} = $mag == 0 ? '0' : "$sign$mag";
			}
		}
	}
	return $i;
}

# the coordinate half, which is what has to survive the change of format
sub coords {
	my ($i) = @_;
	canon_charges($i);
	my %s = %{ $i->{stats} };
	delete $s{n_lines};    # an mmCIF row and a PDB record are not the same line
	my %c;
	for my $cid (keys %{ $i->{chains} }) {
		my %x = %{ $i->{chains}{$cid} };
		# what _chain_stats() folded in from the header, which the conversion
		# below does not carry across and which t/cif.t tests on its own
		delete @x{qw(seqres seqres_length n_missing mol_id molecule organism
		             fragment ec dbref)};
		$c{$cid} = \%x;
	}
	return { chains => \%c, chain_order => $i->{chain_order}, stats => \%s };
}

# --- consistency, for a structure with nothing to compare it against -------
sub adds_up {
	my ($info, $name) = @_;
	my $atoms = 0;
	$atoms += $info->{chains}{$_}{n_atoms} for @{ $info->{chain_order} };
	return "the chains hold $atoms atoms but the file had $info->{stats}{n_atoms}"
		if $atoms != $info->{stats}{n_atoms};
	for my $cid (@{ $info->{chain_order} }) {
		my $c = $info->{chains}{$cid};
		my ($n, $poly) = (0, 0);
		for my $rk (@{ $c->{residue_order} }) {
			my $r = $c->{residues}{$rk};
			$n += $r->{n_atoms};
			$poly++ if $r->{type} eq 'amino_acid' || $r->{type} eq 'nucleotide';
			return "chain $cid residue $rk has no atoms" unless $r->{n_atoms};
			return "chain $cid residue $rk is keyed wrong" unless $r->{key} eq $rk;
		}
		return "chain $cid: residues hold $n atoms, the chain says $c->{n_atoms}"
			if $n != $c->{n_atoms};
		return "chain $cid: $poly polymer residues but a sequence of "
		       . length($c->{sequence})
			if length($c->{sequence}) != $poly;
		return "chain $cid: $c->{n_residues} residues but "
		       . scalar(@{ $c->{residue_order} }) . ' in the order'
			if $c->{n_residues} != @{ $c->{residue_order} };
	}

	# and the header, for the files that have one.  The .cif files a
	# simulation writes carry no header at all, so this says nothing about
	# them; point STRUCTURE_INFO_TEST_CIF_DIR at a directory of archive
	# entries and it is the half of the reader that gets exercised.
	for my $k (qw(resolution r_work r_free temperature ph)) {
		next unless defined $info->{$k};
		return "$k is '$info->{$k}', which is not a number"
			unless $info->{$k} =~ /\A-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\z/;
	}
	for my $k (qw(keywords experiment authors helix sheet ssbond link cispep)) {
		return "$k is not an array reference" unless ref $info->{$k} eq 'ARRAY';
	}
	for my $k (qw(header seqres het remarks modres dbref cryst1 journal)) {
		return "$k is not a hash reference" unless ref $info->{$k} eq 'HASH';
	}
	for my $cid (keys %{ $info->{seqres} }) {
		my $s = $info->{seqres}{$cid};
		return "seqres $cid has no sequence" unless defined $s->{sequence};
		return "seqres $cid: length $s->{length} but a sequence of "
		       . length($s->{sequence})
			if defined $s->{length} && @{ $s->{residues} }
			&& $s->{length} != length $s->{sequence};
	}
	return undef;
}

#---------------------------------------------------------------------------
# 1. real .cif files



( run in 1.052 second using v1.01-cache-2.11-cpan-364913b4093 )