Skip to content

Commit e700c91

Browse files
Run 3D classification using multiple initial models (#264)
Optionally send relion class3d the initial model star file instead of a single model. Expected behaviour: * If no model supplied the default is to use the star file (multiple_initial_models is true) * Send star file to murfey instead of model mrc * If model is user-supplied use that instead --------- Co-authored-by: Daniel Hatton <daniel.hatton@diamond.ac.uk>
1 parent 793baec commit e700c91

3 files changed

Lines changed: 434 additions & 65 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ murfey_db = [
7070
torch = [
7171
"membrain-seg",
7272
"topaz-em",
73+
"torch==2.11.0",
7374
]
7475
[project.urls]
7576
Bug-Tracker = "https://github.com/DiamondLightSource/cryoem-services/issues"

src/cryoemservices/wrappers/class3d_wrapper.py

Lines changed: 109 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class Class3DParameters(BaseModel):
5656
initial_model_offset_range: float = 6
5757
initial_model_offset_step: float = 2
5858
start_initial_model_C1: bool = True
59+
multiple_initial_models: bool = True
5960
dont_combine_weights_via_disc: bool = True
6061
preread_images: bool = True
6162
scratch_dir: str | None = None
@@ -155,12 +156,20 @@ def run_initial_model(
155156
initial_model_command, cwd=str(project_dir), capture_output=True
156157
)
157158

159+
if initial_model_params.multiple_initial_models:
160+
ini_model_file = (
161+
job_dir
162+
/ f"run_it{initial_model_params.initial_model_iterations:03}_model.star"
163+
)
164+
else:
165+
ini_model_file = job_dir / "initial_model.mrc"
166+
158167
# Register the initial model job with the node creator
159168
logger.info("Sending relion.initialmodel (model) to node creator")
160169
node_creator_parameters_refine: dict = {
161170
"job_type": "relion.initialmodel",
162171
"input_file": f"{project_dir}/{particles_file}",
163-
"output_file": f"{job_dir}/initial_model.mrc",
172+
"output_file": str(ini_model_file),
164173
"relion_options": dict(initial_model_params.relion_options),
165174
"command": " ".join(initial_model_command),
166175
"stdout": result.stdout.decode("utf8", "replace"),
@@ -177,57 +186,89 @@ def run_initial_model(
177186
)
178187
return "", []
179188

180-
ini_model_file = job_dir / "initial_model.mrc"
181-
align_symmetry_command = [
182-
"relion_align_symmetry",
183-
"--i",
184-
str(
185-
job_dir.relative_to(project_dir)
186-
/ f"run_it{initial_model_params.initial_model_iterations:03}_model.star"
187-
),
188-
"--o",
189-
f"{ini_model_file.relative_to(project_dir)}",
190-
"--sym",
191-
initial_model_params.symmetry,
192-
"--apply_sym",
193-
"--select_largest_class",
194-
"--pipeline_control",
195-
f"{job_dir.relative_to(project_dir)}/",
196-
]
189+
if (
190+
initial_model_params.multiple_initial_models
191+
and initial_model_params.symmetry != "C1"
192+
):
193+
for model in ini_model_file.parent.glob(
194+
f"{ini_model_file.stem.replace('_model', '')}_class*.mrc"
195+
):
196+
align_symmetry_command = [
197+
"relion_align_symmetry",
198+
"--i",
199+
str(model.relative_to(project_dir)),
200+
"--o",
201+
str(model.relative_to(project_dir)),
202+
"--sym",
203+
initial_model_params.symmetry,
204+
"--apply_sym",
205+
"--pipeline_control",
206+
f"{job_dir.relative_to(project_dir)}/",
207+
]
197208

198-
# Run symmetry alignment and confirm it ran successfully
199-
logger.info("Running symmetry alignment")
200-
result = subprocess.run(
201-
align_symmetry_command, cwd=str(project_dir), capture_output=True
202-
)
209+
# Run symmetry alignment and confirm it ran successfully
210+
logger.info(f"Running symmetry alignment for {model.name}")
211+
result = subprocess.run(
212+
align_symmetry_command, cwd=str(project_dir), capture_output=True
213+
)
214+
if result.returncode:
215+
logger.error(
216+
f"Relion initial model symmetry alignment "
217+
f"failed with exitcode {result.returncode}:\n"
218+
+ result.stderr.decode("utf8", "replace")
219+
)
203220

204-
# Register the initial model job with the node creator
205-
logger.info("Sending relion.initialmodel (alignment) to node creator")
206-
node_creator_parameters_symmetry: dict = {
207-
"job_type": "relion.initialmodel",
208-
"input_file": f"{project_dir}/{particles_file}",
209-
"output_file": f"{job_dir}/initial_model.mrc",
210-
"relion_options": dict(initial_model_params.relion_options),
211-
"command": " ".join(align_symmetry_command),
212-
"stdout": result.stdout.decode("utf8", "replace"),
213-
"stderr": result.stderr.decode("utf8", "replace"),
214-
"success": (result.returncode == 0),
215-
}
216-
send_to_rabbitmq("node_creator", node_creator_parameters_symmetry)
221+
elif not initial_model_params.multiple_initial_models:
222+
align_symmetry_command = [
223+
"relion_align_symmetry",
224+
"--i",
225+
str(
226+
job_dir.relative_to(project_dir)
227+
/ f"run_it{initial_model_params.initial_model_iterations:03}_model.star"
228+
),
229+
"--o",
230+
f"{ini_model_file.relative_to(project_dir)}",
231+
"--sym",
232+
initial_model_params.symmetry,
233+
"--apply_sym",
234+
"--select_largest_class",
235+
"--pipeline_control",
236+
f"{job_dir.relative_to(project_dir)}/",
237+
]
217238

218-
# End here if the command failed
219-
if result.returncode:
220-
logger.error(
221-
f"Relion initial model symmetry alignment "
222-
f"failed with exitcode {result.returncode}:\n"
223-
+ result.stderr.decode("utf8", "replace")
239+
# Run symmetry alignment and confirm it ran successfully
240+
logger.info("Running symmetry alignment")
241+
result = subprocess.run(
242+
align_symmetry_command, cwd=str(project_dir), capture_output=True
224243
)
225-
return "", []
244+
245+
# Register the initial model job with the node creator
246+
logger.info("Sending relion.initialmodel (alignment) to node creator")
247+
node_creator_parameters_symmetry: dict = {
248+
"job_type": "relion.initialmodel",
249+
"input_file": f"{project_dir}/{particles_file}",
250+
"output_file": f"{job_dir}/initial_model.mrc",
251+
"relion_options": dict(initial_model_params.relion_options),
252+
"command": " ".join(align_symmetry_command),
253+
"stdout": result.stdout.decode("utf8", "replace"),
254+
"stderr": result.stderr.decode("utf8", "replace"),
255+
"success": (result.returncode == 0),
256+
}
257+
send_to_rabbitmq("node_creator", node_creator_parameters_symmetry)
258+
259+
# End here if the command failed
260+
if result.returncode:
261+
logger.error(
262+
f"Relion initial model symmetry alignment "
263+
f"failed with exitcode {result.returncode}:\n"
264+
+ result.stderr.decode("utf8", "replace")
265+
)
266+
return "", []
226267

227268
# Send Murfey the location of the initial model
228269
murfey_params = {
229270
"register": "save_initial_model",
230-
"initial_model": f"{job_dir}/initial_model.mrc",
271+
"initial_model": str(ini_model_file),
231272
}
232273
send_to_rabbitmq("murfey_feedback", murfey_params)
233274

@@ -248,38 +289,41 @@ def run_initial_model(
248289
"Will send best initial model to ispyb with "
249290
f"resolution {resolution} and {number_of_particles} particles"
250291
)
251-
ini_ispyb_parameters = [
252-
{
253-
"ispyb_command": "buffer",
254-
"buffer_lookup": {
255-
"particle_classification_id": class_uuids_dict[class_uuids_keys[0]]
256-
},
257-
"buffer_command": {"ispyb_command": "insert_cryoem_initial_model"},
258-
"number_of_particles": number_of_particles,
259-
"resolution": resolution,
260-
"store_result": "ispyb_initial_model_id",
261-
}
262-
]
263-
for i in range(1, initial_model_params.class3d_nr_classes):
264-
# Insert initial model for every class, sending model id each time
292+
ini_ispyb_parameters: list[dict] = []
293+
for i in range(initial_model_params.class3d_nr_classes):
294+
# Insert initial model for every class,
265295
ini_ispyb_parameters.append(
266296
{
267297
"ispyb_command": "buffer",
268298
"buffer_lookup": {
269299
"particle_classification_id": class_uuids_dict[class_uuids_keys[i]]
270300
},
271301
"buffer_command": {"ispyb_command": "insert_cryoem_initial_model"},
272-
"number_of_particles": number_of_particles,
273-
"resolution": resolution,
274-
"cryoem_initial_model_id": "$ispyb_initial_model_id",
275302
}
276303
)
277304
for i in range(initial_model_params.class3d_nr_classes):
278305
# Add resolution to every model if it is finite
279-
if np.isfinite(float(resolution)):
280-
ini_ispyb_parameters[i]["resolution"] = resolution
306+
if initial_model_params.multiple_initial_models:
307+
ini_ispyb_parameters[i]["resolution"] = (
308+
model_resolutions[i]
309+
if np.isfinite(float(model_resolutions[i]))
310+
else 0.0
311+
)
312+
ini_ispyb_parameters[i]["number_of_particles"] = (
313+
float(model_scores[i]) * initial_model_params.batch_size
314+
)
281315
else:
282-
ini_ispyb_parameters[i]["resolution"] = 0.0
316+
ini_ispyb_parameters[i]["resolution"] = (
317+
resolution if np.isfinite(float(resolution)) else 0.0
318+
)
319+
ini_ispyb_parameters[i]["number_of_particles"] = number_of_particles
320+
# Set model id for the first class, sending model id each time afterwards
321+
if i == 0:
322+
ini_ispyb_parameters[i]["store_result"] = "ispyb_initial_model_id"
323+
else:
324+
ini_ispyb_parameters[i]["cryoem_initial_model_id"] = (
325+
"$ispyb_initial_model_id"
326+
)
283327

284328
logger.info("Running 3D classification using new initial model")
285329
return f"{ini_model_file}", ini_ispyb_parameters
@@ -500,7 +544,7 @@ def run_class3d(class3d_params: Class3DParameters, send_to_rabbitmq: Callable) -
500544
"class_distribution": classes_loop[class_id, 1],
501545
"rotation_accuracy": classes_loop[class_id, 2],
502546
"translation_accuracy": classes_loop[class_id, 3],
503-
"angular_efficiency": class_efficiencies[class_id],
547+
"angular_efficiency": float(class_efficiencies[class_id]),
504548
"suggested_tilt": 0 if class_efficiencies[class_id] > 0.65 else 30,
505549
}
506550
if job_is_rerun:

0 commit comments

Comments
 (0)