diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cef17d..dc756b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,12 @@ and the project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] ### Added -- (Add upcoming features here.) +- **Fraction of Native Contacts (Q-Value) Analysis**: New `qvalue` module implementing the Best-Hummer-Eaton Q metric for measuring protein folding state and native structure retention. + - User-configurable parameters: `reference_frame`, `beta_const`, `lambda_const`, `native_cutoff` + - Automatic native contact identification and metadata reporting + - Integration with multi-analysis orchestrator and slide generation + - Full CLI support with `fastmda qvalue` command +- New features since `v1.0.0`. ### Changed - (Add behavior/CLI/API changes here.) ### Fixed diff --git a/docs/source/analysis/qvalue.rst b/docs/source/analysis/qvalue.rst index 66f8dd1..3d42e8b 100644 --- a/docs/source/analysis/qvalue.rst +++ b/docs/source/analysis/qvalue.rst @@ -117,6 +117,7 @@ Outputs - ``qvalue.dat`` — Q-values (one per frame, range [0, 1]) - ``qvalue_stats.dat`` — mean and standard deviation when ``compute_stat=True`` +- ``qvalue_contact_list.dat`` — residue index pairs (1-based) used as native contacts - ``qvalue.png`` — Line plot of Q-value vs. frame with metadata annotation - ``qvalue_metadata.json`` — Complete metadata including: diff --git a/src/fastmdanalysis/analysis/qvalue.py b/src/fastmdanalysis/analysis/qvalue.py index c2ecb2e..1ee4d0d 100644 --- a/src/fastmdanalysis/analysis/qvalue.py +++ b/src/fastmdanalysis/analysis/qvalue.py @@ -236,6 +236,14 @@ def run(self) -> Dict[str, np.ndarray]: r = md.compute_distances(self.traj, native_contacts) # shape (N_frames, N_contacts) r0 = md.compute_distances(self.traj[self.reference_frame], native_contacts)[0] # shape (N_contacts,) + # Build residue-pair contact list (1-based residue indices) + contact_pairs = [] + for atom_i, atom_j in native_contacts: + res_i = self.traj.topology.atom(atom_i).residue.index + 1 + res_j = self.traj.topology.atom(atom_j).residue.index + 1 + contact_pairs.append((res_i, res_j)) + contact_pairs = np.asarray(contact_pairs, dtype=int) + # Compute Q-values using Best-Hummer-Eaton formula logger.debug("Computing Q-values...") q_values = np.mean( @@ -244,7 +252,10 @@ def run(self) -> Dict[str, np.ndarray]: ) self.data = np.asarray(q_values, dtype=float).reshape(-1, 1) - self.results = {"qvalue": self.data} + self.results = { + "qvalue": self.data, + "contact_list": contact_pairs, + } if self.compute_stat: mean_val = float(np.nanmean(q_values)) @@ -256,10 +267,15 @@ def run(self) -> Dict[str, np.ndarray]: header="mean std", fmt="%.6f", ) - # Save data logger.info("Saving Q-value data...") self._save_data(self.data, "qvalue", header="qvalue", fmt="%.6f") + self._save_data( + contact_pairs, + "qvalue_contact_list", + header="residue_i residue_j (1-based)", + fmt="%d", + ) # Save metadata self._save_metadata() @@ -341,7 +357,6 @@ def plot(self, data: Optional[np.ndarray] = None, **kwargs): line_kwargs = {"linestyle": linestyle, "marker": marker} if color is not None: line_kwargs["color"] = color - ax.plot(x, y, **line_kwargs) if self.compute_stat: mean_val = float(np.nanmean(y)) diff --git a/tests/test_qvalue.py b/tests/test_qvalue.py index 7287c1f..b2bdde2 100644 --- a/tests/test_qvalue.py +++ b/tests/test_qvalue.py @@ -42,6 +42,10 @@ def test_qvalue_metadata(fastmda, tmp_path): metadata_path = output_dir / "qvalue_metadata.json" assert metadata_path.exists() + # Contact list exists + contact_path = output_dir / "qvalue_contact_list.dat" + assert contact_path.exists() + # Load and validate metadata with open(metadata_path, "r") as f: metadata = json.load(f)