Skip to content
Open
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ add_library(copick
src/version.cpp
src/util/escape.cpp
src/models/validate.cpp
src/models/filament.cpp
src/models/array.cpp
src/io/json.cpp
)
Expand Down
1 change: 1 addition & 0 deletions include/copick/copick.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "copick/errors.h"
#include "copick/escape.h"
#include "copick/features.h"
#include "copick/filament.h"
#include "copick/fwd.h"
#include "copick/geometry.h"
#include "copick/mesh.h"
Expand Down
35 changes: 35 additions & 0 deletions include/copick/filament.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Filament objects: a pickable object whose metadata carries a filament spec under
// metadata["copick"]["filament"] (copick >= 1.28). Its picks are points along filaments,
// grouped by instance_id (the filament id, from 1; 0 = unassigned) and ordered along each
// filament.
//
// PUBLIC HEADER — must compile under -std=c++11.
#ifndef COPICK_FILAMENT_H
#define COPICK_FILAMENT_H

#include "copick/export.h"
#include "copick/optional.h"
#include "copick/types.h"

namespace copick {

/// A filament object's spec. (copick.models.FilamentSpec)
struct FilamentSpec {
optional<bool> polar; ///< the structure has a polarity (microtubules: true)
optional<double> helical_rise_a; ///< helical rise in angstrom, > 0 (descriptive)
optional<double> helical_twist_deg; ///< helical twist in degrees (descriptive)
};

/// The filament spec of `obj`, or no value when `obj` is not a filament: its metadata has no
/// "copick" object, or that object's "filament" is absent or null. Unknown keys in the spec
/// are ignored. Throws ValidationError when the metadata is not valid JSON, or the spec is
/// not an object of the fields above, has helical_rise_a <= 0, or belongs to an object that
/// is not a particle — the cases copick refuses.
COPICK_API optional<FilamentSpec> filament(const PickableObject& obj);

/// Whether `obj` is a filament object (has a filament spec). Throws as filament().
COPICK_API bool is_filament(const PickableObject& obj);

} // namespace copick

#endif // COPICK_FILAMENT_H
7 changes: 6 additions & 1 deletion include/copick/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#define COPICK_TYPES_H

#include <array>
#include <cstdint>
#include <string>
#include <vector>

Expand Down Expand Up @@ -43,10 +44,14 @@ struct Location {

/// A picked point: location + orientation + score + instance id.
/// (copick.models.CopickPoint)
///
/// The particle centre is location + the transformation's translation (column 3), both in
/// angstrom in the tomogram frame. instance_id is 64-bit, as copick's Python int and JSON
/// integers are; for points along a filament it is the filament id (from 1; 0 = unassigned).
struct Point {
Location location;
Matrix4 transformation = identity_matrix4();
int instance_id = 0;
std::int64_t instance_id = 0;
double score = 1.0;
};

Expand Down
3 changes: 2 additions & 1 deletion src/io/dto.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <rfl/json.hpp>

#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
Expand Down Expand Up @@ -145,7 +146,7 @@ struct LocationDTO {
struct PointDTO {
LocationDTO location;
std::optional<std::array<std::array<double, 4>, 4>> transformation_;
std::optional<int> instance_id;
std::optional<std::int64_t> instance_id;
std::optional<double> score;
};

Expand Down
70 changes: 70 additions & 0 deletions src/models/filament.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include "copick/filament.h"

#include <rfl.hpp>
#include <rfl/json.hpp>

#include <optional>
#include <string>

#include "copick/errors.h"

namespace copick {
namespace {

// The namespace and the spec are read as generic JSON first, so a "copick" value that is not
// an object (which copick leaves alone) is told apart from a malformed spec (which it refuses).
struct MetadataDTO {
std::optional<rfl::Generic> copick;
};

struct NamespaceDTO {
std::optional<rfl::Generic> filament;
};

struct FilamentSpecDTO {
std::optional<bool> polar;
std::optional<double> helical_rise_a;
std::optional<double> helical_twist_deg;
};

} // namespace

optional<FilamentSpec> filament(const PickableObject& obj) {
const std::string metadata = obj.metadata.empty() ? "{}" : obj.metadata;
const auto meta = rfl::json::read<MetadataDTO>(metadata);
if (!meta) {
throw ValidationError("Invalid metadata of object '" + obj.name + "': " + meta.error().what());
}
if (!meta.value().copick) return optional<FilamentSpec>();
const auto ns = rfl::json::read<NamespaceDTO>(rfl::json::write(*meta.value().copick));
if (!ns || !ns.value().filament) return optional<FilamentSpec>();

const std::string spec_json = rfl::json::write(*ns.value().filament);
if (spec_json == "null") return optional<FilamentSpec>();
const auto spec = rfl::json::read<FilamentSpecDTO>(spec_json);
if (!spec) {
throw ValidationError("Invalid filament spec of object '" + obj.name +
"': " + spec.error().what());
}
if (!obj.is_particle) {
throw ValidationError("Object '" + obj.name +
"' has a filament spec but is not a particle (is_particle=false).");
}
const FilamentSpecDTO& d = spec.value();
if (d.helical_rise_a && !(*d.helical_rise_a > 0.0)) {
throw ValidationError("Filament spec of object '" + obj.name +
"': helical_rise_a must be > 0.");
}

FilamentSpec out;
if (d.polar) out.polar = *d.polar;
if (d.helical_rise_a) out.helical_rise_a = *d.helical_rise_a;
if (d.helical_twist_deg) out.helical_twist_deg = *d.helical_twist_deg;
return out;
}

bool is_filament(const PickableObject& obj) {
return filament(obj).has_value();
}

} // namespace copick
3 changes: 2 additions & 1 deletion tests/cpp/annotation_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Built only under COPICK_ENABLE_ZARR.
#include <gtest/gtest.h>

#include <cstdint>
#include <string>
#include <vector>

Expand Down Expand Up @@ -30,7 +31,7 @@ std::string config_for(const std::string& root_url) {
R"(],"overlay_root":")" + root_url + R"("})";
}

copick::Point make_point(double x, double y, double z, int instance) {
copick::Point make_point(double x, double y, double z, std::int64_t instance) {
copick::Point p;
p.location.x = x;
p.location.y = y;
Expand Down
19 changes: 19 additions & 0 deletions tests/cpp/json_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,22 @@ TEST(PicksJson, RoundTripKeepsTransformationKey) {
EXPECT_DOUBLE_EQ(again.points[0].location.y, 200.0);
EXPECT_DOUBLE_EQ(again.points[0].transformation[1][3], 20.0);
}

TEST(PicksJson, InstanceIdsAre64Bit) {
// Python ints are unbounded; a 32-bit field silently truncated ids >= 2^31.
const std::string json = R"JSON({
"pickable_object_name": "microtubule", "user_id": "tracer", "session_id": "1",
"points": [
{"location": {"x": 0.0, "y": 0.0, "z": 0.0}, "instance_id": 4294967296},
{"location": {"x": 1.0, "y": 0.0, "z": 0.0}, "instance_id": 9007199254740993}
]
})JSON";
copick::CopickPicksFile f = copick::picks_from_json(json);
ASSERT_EQ(f.points.size(), 2u);
EXPECT_EQ(f.points[0].instance_id, INT64_C(4294967296));
EXPECT_EQ(f.points[1].instance_id, INT64_C(9007199254740993));

copick::CopickPicksFile back = copick::picks_from_json(copick::picks_to_json(f));
EXPECT_EQ(back.points[0].instance_id, INT64_C(4294967296));
EXPECT_EQ(back.points[1].instance_id, INT64_C(9007199254740993));
}
42 changes: 42 additions & 0 deletions tests/cpp/object_models_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "copick/config.h"
#include "copick/errors.h"
#include "copick/filament.h"
#include "copick/types.h"
#include "copick/validate.h"

Expand Down Expand Up @@ -91,3 +92,44 @@ TEST(Point, LastRowValidation) {
p.transformation[3] = {{1.0, 0.0, 0.0, 1.0}};
EXPECT_THROW(copick::validate(p), copick::ValidationError);
}

// --- Filament spec (metadata["copick"]["filament"]) -----------------------------

TEST(Filament, SpecIsReadFromMetadata) {
copick::PickableObject obj = make_valid();
obj.metadata =
R"({"copick": {"filament": {"polar": true, "helical_rise_a": 82, "future_key": 1}}, "other": 2})";
const copick::optional<copick::FilamentSpec> spec = copick::filament(obj);
ASSERT_TRUE(spec.has_value());
const copick::FilamentSpec& s = *spec;
EXPECT_TRUE(s.polar.has_value() && *s.polar);
EXPECT_DOUBLE_EQ(*s.helical_rise_a, 82.0);
EXPECT_FALSE(s.helical_twist_deg.has_value());
EXPECT_TRUE(copick::is_filament(obj));

obj.metadata = R"({"copick": {"filament": {}}})";
EXPECT_TRUE(copick::is_filament(obj));
}

TEST(Filament, AbsentNullOrForeignNamespaceIsNotAFilament) {
copick::PickableObject obj = make_valid();
for (const char* metadata :
{"{}", "", R"({"copick": {}})", R"({"copick": {"filament": null}})", R"({"copick": null})",
R"({"copick": "not ours"})", R"({"copick": [1, 2]})"}) {
obj.metadata = metadata;
EXPECT_FALSE(copick::is_filament(obj)) << metadata;
}
}

TEST(Filament, InvalidSpecThrows) {
copick::PickableObject obj = make_valid();
for (const char* metadata :
{R"({"copick": {"filament": true}})", R"({"copick": {"filament": {"polar": "yes"}}})",
R"({"copick": {"filament": {"helical_rise_a": 0}}})", "not json"}) {
obj.metadata = metadata;
EXPECT_THROW(copick::filament(obj), copick::ValidationError) << metadata;
}
obj.metadata = R"({"copick": {"filament": {}}})";
obj.is_particle = false;
EXPECT_THROW(copick::filament(obj), copick::ValidationError);
}
1 change: 1 addition & 0 deletions tests/header_guard/cxx11_include_all.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "copick/escape.h"
#include "copick/export.h"
#include "copick/features.h"
#include "copick/filament.h"
#include "copick/fwd.h"
#include "copick/geometry.h"
#include "copick/mesh.h"
Expand Down
Loading