Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
**/target/
/.vscode/
*.log
# Heaptrack profiler output must stay local.
heaptrack.*
/.temp
# codex superpower plugin
docs/superpowers
Expand All @@ -10,3 +12,4 @@ docs/superpowers
.cargo/
.codex/
.agents/
.commandcode/
13 changes: 7 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ repository = "https://github.com/chitin-dev/chitin"

[workspace.dependencies]
# Kit and the direct GPUI imports must use the same WGPU backend revision.
gpui = { package = "gpui-ce", git = "https://github.com/chitin-dev/gpui-wgpu", rev = "3679d575d3f912efb3b956684898bdab64efc65c" }
gpui-kit = { git = "https://github.com/chitin-dev/gpui-kit", rev = "05c6b2d9c5d97e327c904d480f1fd5b05ade0cdf" }
gpui = { package = "gpui-ce", git = "https://github.com/chitin-dev/gpui-wgpu", rev = "d92ff407d66a52795f70fb6657a4bfa653c3d0d5" }
gpui-kit = { git = "https://github.com/chitin-dev/gpui-kit", rev = "be0403f152159195b57e9a6a0ccf59694a0542e9" }

[workspace.lints.rust]
unsafe_code = "forbid"
Expand Down
1 change: 1 addition & 0 deletions assets/icons/download.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions assets/icons/entry-chains.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/icons/entry-ligands.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions assets/icons/entry-residues.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/icons/entry-structure.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/icons/entry-water.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions assets/icons/surface-implicit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 3 additions & 5 deletions assets/icons/surface-msms.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions crates/chitin-bio/src/structure/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ pub(crate) fn build_structure(
strict: bool,
) -> Result<StructureParseResult, StructureBuildError> {
let StructureInput {
experimental_methods,
resolution,
classification,
identifier,
unit_cell,
Expand All @@ -155,6 +157,8 @@ pub(crate) fn build_structure(
};

builder.structure.metadata.classification = classification;
builder.structure.metadata.experimental_methods = experimental_methods;
builder.structure.metadata.resolution = resolution;
builder.structure.metadata.identifier = identifier;
builder.structure.metadata.unit_cell = unit_cell;
builder.structure.metadata.symmetry = symmetry;
Expand Down
72 changes: 65 additions & 7 deletions crates/chitin-bio/src/structure/mmcif/categories/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Crystallographic unit-cell and space-group metadata projection.
//! Experimental, resolution, and crystallographic metadata projection.

use crate::structure::mmcif::cif::CifDocument;
use crate::structure::mmcif::schema::{Cell, Symmetry as SymmetryCategory};
Expand All @@ -7,26 +7,59 @@ use crate::structure::{MmcifParseError, Symmetry, UnitCell};

use super::required;

/// Projects crystallographic metadata from the generic CIF document into the
/// format-neutral structure input.
/// Projects experimental and crystallographic metadata from the CIF document
/// into the format-neutral structure input.
///
/// The two categories are optional in an mmCIF structure. When present,
/// `_cell` is validated as a complete six-parameter unit cell before it is
/// stored, while `_symmetry` preserves whichever space-group identifiers are
/// All categories are optional. Experimental methods are deduplicated, and the
/// smallest positive reported refinement or reconstruction resolution is kept.
/// When present, `_cell` is validated as a complete six-parameter unit cell
/// before it is stored, while `_symmetry` preserves whichever identifiers are
/// available. This projection intentionally records metadata only; it does
/// not expand symmetry mates or transform fractional coordinates.
///
/// # Parameters
///
/// * `document` contains the syntax-level data blocks and category values.
/// * `input` receives validated unit-cell and space-group metadata.
/// * `input` receives source identity, experimental methods, resolution,
/// validated unit-cell parameters, and space-group metadata.
///
/// # Returns
///
/// `Ok(())` when absent metadata is skipped or all present values are valid.
/// Returns [`MmcifParseError::InvalidField`] for malformed numeric values or
/// geometrically invalid unit-cell parameters.
pub(crate) fn parse(document: &CifDocument, input: &mut StructureInput) -> Result<(), MmcifParseError> {
use crate::structure::mmcif::category::CategoryView;
if let Some(category) = CategoryView::from_document(document, "entry") {
input.identifier = category
.rows()
.find_map(|row| row.optional_text(&["_entry.id"]).map(str::to_owned));
}
if let Some(category) = CategoryView::from_document(document, "exptl") {
for row in category.rows() {
if let Some(method) = row.optional_text(&["_exptl.method"])
&& !input.experimental_methods.iter().any(|s| s == method)
{
input.experimental_methods.push(method.to_owned());
}
}
}
// Structures may contain several refinement/reconstruction rows; retain the
// highest reported resolving power (smallest positive value in ångströms).
for (category, field) in [
("refine", "_refine.ls_d_res_high"),
("em_3d_reconstruction", "_em_3d_reconstruction.resolution"),
] {
if let Some(category) = CategoryView::from_document(document, category) {
for row in category.rows() {
if let Some(value) = row.optional_f32(&[field], field)?
&& value > 0.
{
input.resolution = Some(input.resolution.map_or(value, |current| current.min(value)));
}
}
}
}
parse_unit_cell(document, input)?;
parse_symmetry(document, input)
}
Expand Down Expand Up @@ -154,6 +187,31 @@ mod tests {
use super::validate_cell_parameters;
use crate::structure::MmcifParseError;

#[test]
fn experimental_metadata_should_support_diffraction_and_reconstruction() -> Result<(), Box<dyn std::error::Error>> {
for (text, expected) in [
(
"data_test\n_entry.id 4HHB\n_exptl.method 'X-RAY DIFFRACTION'\n_refine.ls_d_res_high 1.8\n",
Some(1.8),
),
(
"data_test\n_exptl.method 'ELECTRON MICROSCOPY'\n_em_3d_reconstruction.resolution 2.6\n",
Some(2.6),
),
(
"data_test\n_exptl.method 'SOLUTION NMR'\n_refine.ls_d_res_high ?\n",
None,
),
] {
let document = crate::structure::mmcif::cif::CifParser::parse(text)?;
let mut input = crate::structure::projection::StructureInput::default();
super::parse(&document, &mut input)?;
assert_eq!(input.resolution, expected);
assert_eq!(input.experimental_methods.len(), 1);
}
Ok(())
}

#[test]
fn zero_cell_angle_should_be_rejected() {
let error = match validate_cell_parameters(3, [10.0, 10.0, 10.0], [0.0, 90.0, 90.0]) {
Expand Down
4 changes: 4 additions & 0 deletions crates/chitin-bio/src/structure/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ pub struct SecondaryRange {
/// Metadata shared by all models in a structure.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct StructureMetadata {
/// Experimental methods declared by the source file.
pub experimental_methods: Vec<String>,
/// Reported diffraction or reconstruction resolution in ångströms.
pub resolution: Option<f32>,
/// Optional PDB HEADER classification.
pub classification: Option<String>,
/// Optional four-character source identifier.
Expand Down
4 changes: 4 additions & 0 deletions crates/chitin-bio/src/structure/pdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ mod tests {
fn projects_pdb_compnd_seqres_and_cryst1_metadata() {
let input = concat!(
"HEADER TEST STRUCTURE 1ABC\n",
"EXPDTA X-RAY DIFFRACTION\n",
"REMARK 2 RESOLUTION. 1.80 ANGSTROMS.\n",
"COMPND 1 MOL_ID: 1;\n",
"COMPND 2 MOLECULE: TEST PROTEIN;\n",
"COMPND 3 CHAIN: A;\n",
Expand All @@ -210,6 +212,8 @@ mod tests {
.parse_bytes(input.as_bytes())
.unwrap_or_else(|error| panic!("PDB metadata fixture should parse: {error}"));
assert_eq!(parsed.structure.polymer_entities.len(), 1);
assert_eq!(parsed.structure.metadata.experimental_methods, ["X-RAY DIFFRACTION"]);
assert_eq!(parsed.structure.metadata.resolution, Some(1.8));
assert_eq!(parsed.structure.polymer_entities[0].sequence.len(), 3);
assert_eq!(parsed.structure.chains[0].entity_id.as_deref(), Some("1"));
assert_eq!(
Expand Down
21 changes: 21 additions & 0 deletions crates/chitin-bio/src/structure/pdb/projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ impl PdbProjection {
line,
} => match kind {
PdbProjectionRecordKind::Compnd => self.project_compnd(&line, line_number)?,
PdbProjectionRecordKind::Expdta => {
for method in field(&line, 10, 80).split(';').map(str::trim).filter(|s| !s.is_empty()) {
if !self.input.experimental_methods.iter().any(|s| s == method) {
self.input.experimental_methods.push(method.to_owned());
}
}
}
PdbProjectionRecordKind::Seqres => self.project_seqres(&line, line_number)?,
PdbProjectionRecordKind::Remark => self.project_remark(&line, line_number)?,
PdbProjectionRecordKind::Cryst1 => self.project_cryst1(&line, line_number)?,
Expand Down Expand Up @@ -220,6 +227,20 @@ impl PdbProjection {
/// Dispatches supported REMARK records to PDB-specific projection logic.
fn project_remark(&mut self, line: &str, line_number: usize) -> Result<(), PdbParseError> {
match field(line, 7, 10).trim() {
"2" => {
// REMARK 2 also contains free-text declarations such as NOT APPLICABLE.
// Preserve only finite positive resolutions; text is not a parse failure.
if let Some(value) = field(line, 10, 80).trim().strip_prefix("RESOLUTION.")
&& let Some(resolution) = value
.split_whitespace()
.next()
.and_then(|s| s.parse::<f32>().ok())
.filter(|value| value.is_finite() && *value > 0.)
{
self.input.resolution = Some(resolution);
}
Ok(())
}
"465" => self.project_missing_residue(line),
"350" => self.project_assembly_remark(line, line_number),
_ => Ok(()),
Expand Down
3 changes: 3 additions & 0 deletions crates/chitin-bio/src/structure/pdb/records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub(super) enum PdbRecord {
/// PDB records whose meaning requires cross-record projection state.
#[derive(Debug, Clone, Copy)]
pub(super) enum PdbProjectionRecordKind {
/// Experimental method declaration.
Expdta,
/// Molecular entity declaration.
Compnd,
/// Declared polymer sequence.
Expand Down Expand Up @@ -176,6 +178,7 @@ pub(super) fn parse_record(line: &str, line_number: usize) -> Result<PdbRecord,
"HELIX" => Ok(PdbRecord::Secondary(parse_helix(line, line_number)?)),
"SHEET" => Ok(PdbRecord::Secondary(parse_sheet(line, line_number)?)),
"COMPND" => Ok(projection_record(PdbProjectionRecordKind::Compnd, line, line_number)),
"EXPDTA" => Ok(projection_record(PdbProjectionRecordKind::Expdta, line, line_number)),
"SEQRES" => Ok(projection_record(PdbProjectionRecordKind::Seqres, line, line_number)),
"REMARK" => Ok(projection_record(PdbProjectionRecordKind::Remark, line, line_number)),
"CRYST1" => Ok(projection_record(PdbProjectionRecordKind::Cryst1, line, line_number)),
Expand Down
4 changes: 4 additions & 0 deletions crates/chitin-bio/src/structure/projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ use super::model::{
/// models, and validates the final [`super::Structure`].
#[derive(Debug, Default)]
pub(crate) struct StructureInput {
/// Source-declared experimental methods before format-neutral construction.
pub(crate) experimental_methods: Vec<String>,
/// Optional reported diffraction or reconstruction resolution in ångströms.
pub(crate) resolution: Option<f32>,
/// Optional source classification or descriptive header.
pub(crate) classification: Option<String>,
/// Optional source structure identifier.
Expand Down
Loading
Loading