From c027a12179f5654be7fc477d884645853bbbc03e Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Fri, 25 Aug 2023 13:54:23 -0400 Subject: [PATCH 01/26] Simplify prefix parsing for multi-domain projects Co-authored-by: Jake Wagoner --- shapeworks_cloud/core/tasks.py | 9 +++------ swcc/swcc/models/constants.py | 8 -------- swcc/swcc/models/project.py | 28 ++++++++++------------------ 3 files changed, 13 insertions(+), 32 deletions(-) diff --git a/shapeworks_cloud/core/tasks.py b/shapeworks_cloud/core/tasks.py index e800290a..a72bd665 100644 --- a/shapeworks_cloud/core/tasks.py +++ b/shapeworks_cloud/core/tasks.py @@ -177,12 +177,9 @@ def post_command_function(project, download_dir, result_data, project_filename): if len(prefixes) > 0: prefix = prefixes[0] anatomy_id = 'anatomy' + key.replace(prefix, '') - if prefix in ['mesh', 'segmentation', 'image', 'contour']: - prefix = 'shape' - if prefix in ['shape', 'groomed']: - if anatomy_id not in row: - row[anatomy_id] = {} - row[anatomy_id][prefix] = entry[key].replace('../', '').replace('./', '') + if anatomy_id not in row: + row[anatomy_id] = {} + row[anatomy_id][prefix] = entry[key].replace('../', '').replace('./', '') for anatomy_data in row.values(): if 'groomed' not in anatomy_data: diff --git a/swcc/swcc/models/constants.py b/swcc/swcc/models/constants.py index d4b739be..c2d1d50d 100644 --- a/swcc/swcc/models/constants.py +++ b/swcc/swcc/models/constants.py @@ -1,15 +1,7 @@ expected_key_prefixes = [ - 'name', 'shape', 'mesh', 'segmentation', 'contour', 'image', - 'groomed', - 'local', - 'world', - 'alignment', - 'procrustes', - 'landmarks', - 'constraints', ] diff --git a/swcc/swcc/models/project.py b/swcc/swcc/models/project.py index b8fa7efa..e4c933c0 100644 --- a/swcc/swcc/models/project.py +++ b/swcc/swcc/models/project.py @@ -97,25 +97,17 @@ def interpret_data(self, input_data): name=entry.get('name'), groups=groups_dict, dataset=self.project.dataset ).create() - entry_values: Dict = {p: [] for p in expected_key_prefixes} - entry_values['anatomy_ids'] = [] - for key in entry.keys(): - if key != 'name': - prefixes = [p for p in expected_key_prefixes if key.startswith(p)] - if len(prefixes) > 0: - entry_values[prefixes[0]].append(entry[key]) - anatomy_id = 'anatomy' + key.replace(prefixes[0], '').replace( - '_particles', '' - ).replace('_file', '') - if anatomy_id not in entry_values['anatomy_ids']: - entry_values['anatomy_ids'].append(anatomy_id) objects_by_domain = {} - for index, anatomy_id in enumerate(entry_values['anatomy_ids']): - objects_by_domain[anatomy_id] = { - k: v[index] if len(v) > index else v[0] - for k, v in entry_values.items() - if len(v) > 0 - } + for key in entry.keys(): + prefixes = [p for p in expected_key_prefixes if key.startswith(p)] + if len(prefixes) > 0: + prefix = prefixes[0] + anatomy_id = 'anatomy' + key.replace(prefix, '') + if anatomy_id not in objects_by_domain: + objects_by_domain[anatomy_id] = {} + objects_by_domain[anatomy_id][prefix] = ( + entry[key].replace('../', '').replace('./', '') + ) output_data.append( [ subject, From 510cd2712f8eb35b0dcfd972ff18e753a7a319d9 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Fri, 25 Aug 2023 13:54:23 -0400 Subject: [PATCH 02/26] Simplify prefix parsing for multi-domain projects Co-authored-by: Jake Wagoner --- shapeworks_cloud/core/tasks.py | 9 +++------ swcc/swcc/models/constants.py | 8 -------- swcc/swcc/models/project.py | 28 ++++++++++------------------ 3 files changed, 13 insertions(+), 32 deletions(-) diff --git a/shapeworks_cloud/core/tasks.py b/shapeworks_cloud/core/tasks.py index e800290a..a72bd665 100644 --- a/shapeworks_cloud/core/tasks.py +++ b/shapeworks_cloud/core/tasks.py @@ -177,12 +177,9 @@ def post_command_function(project, download_dir, result_data, project_filename): if len(prefixes) > 0: prefix = prefixes[0] anatomy_id = 'anatomy' + key.replace(prefix, '') - if prefix in ['mesh', 'segmentation', 'image', 'contour']: - prefix = 'shape' - if prefix in ['shape', 'groomed']: - if anatomy_id not in row: - row[anatomy_id] = {} - row[anatomy_id][prefix] = entry[key].replace('../', '').replace('./', '') + if anatomy_id not in row: + row[anatomy_id] = {} + row[anatomy_id][prefix] = entry[key].replace('../', '').replace('./', '') for anatomy_data in row.values(): if 'groomed' not in anatomy_data: diff --git a/swcc/swcc/models/constants.py b/swcc/swcc/models/constants.py index d4b739be..c2d1d50d 100644 --- a/swcc/swcc/models/constants.py +++ b/swcc/swcc/models/constants.py @@ -1,15 +1,7 @@ expected_key_prefixes = [ - 'name', 'shape', 'mesh', 'segmentation', 'contour', 'image', - 'groomed', - 'local', - 'world', - 'alignment', - 'procrustes', - 'landmarks', - 'constraints', ] diff --git a/swcc/swcc/models/project.py b/swcc/swcc/models/project.py index b8fa7efa..e4c933c0 100644 --- a/swcc/swcc/models/project.py +++ b/swcc/swcc/models/project.py @@ -97,25 +97,17 @@ def interpret_data(self, input_data): name=entry.get('name'), groups=groups_dict, dataset=self.project.dataset ).create() - entry_values: Dict = {p: [] for p in expected_key_prefixes} - entry_values['anatomy_ids'] = [] - for key in entry.keys(): - if key != 'name': - prefixes = [p for p in expected_key_prefixes if key.startswith(p)] - if len(prefixes) > 0: - entry_values[prefixes[0]].append(entry[key]) - anatomy_id = 'anatomy' + key.replace(prefixes[0], '').replace( - '_particles', '' - ).replace('_file', '') - if anatomy_id not in entry_values['anatomy_ids']: - entry_values['anatomy_ids'].append(anatomy_id) objects_by_domain = {} - for index, anatomy_id in enumerate(entry_values['anatomy_ids']): - objects_by_domain[anatomy_id] = { - k: v[index] if len(v) > index else v[0] - for k, v in entry_values.items() - if len(v) > 0 - } + for key in entry.keys(): + prefixes = [p for p in expected_key_prefixes if key.startswith(p)] + if len(prefixes) > 0: + prefix = prefixes[0] + anatomy_id = 'anatomy' + key.replace(prefix, '') + if anatomy_id not in objects_by_domain: + objects_by_domain[anatomy_id] = {} + objects_by_domain[anatomy_id][prefix] = ( + entry[key].replace('../', '').replace('./', '') + ) output_data.append( [ subject, From 27acfcec49d7f28ad57d7301cb3d65842b4865ff Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 30 Aug 2023 09:39:21 -0400 Subject: [PATCH 03/26] Don't skip over other prefixes completely; objects should still be made if there is a subject to match --- swcc/swcc/models/constants.py | 15 +++++++++++++++ swcc/swcc/models/project.py | 12 +++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/swcc/swcc/models/constants.py b/swcc/swcc/models/constants.py index c2d1d50d..88e5c115 100644 --- a/swcc/swcc/models/constants.py +++ b/swcc/swcc/models/constants.py @@ -1,7 +1,22 @@ +required_key_prefixes = [ + 'shape', + 'mesh', + 'segmentation', + 'contour', + 'image', +] + expected_key_prefixes = [ 'shape', 'mesh', 'segmentation', 'contour', 'image', + 'groomed', + 'local', + 'world', + 'alignment', + 'procrustes', + 'landmarks', + 'constraints', ] diff --git a/swcc/swcc/models/project.py b/swcc/swcc/models/project.py index e4c933c0..c9afc6c3 100644 --- a/swcc/swcc/models/project.py +++ b/swcc/swcc/models/project.py @@ -24,7 +24,7 @@ from ..api import current_session from .api_model import ApiModel -from .constants import expected_key_prefixes +from .constants import expected_key_prefixes, required_key_prefixes from .dataset import Dataset from .file_type import FileType from .other_models import ( @@ -102,9 +102,15 @@ def interpret_data(self, input_data): prefixes = [p for p in expected_key_prefixes if key.startswith(p)] if len(prefixes) > 0: prefix = prefixes[0] - anatomy_id = 'anatomy' + key.replace(prefix, '') + anatomy_id = 'anatomy' + key.replace(prefix, '').replace('_file', '') + # Only create a new domain object if a shape exists for that suffix if anatomy_id not in objects_by_domain: - objects_by_domain[anatomy_id] = {} + if prefix in required_key_prefixes: + objects_by_domain[anatomy_id] = {} + else: + raise ValueError( + f'No shape exists for {anatomy_id}. Cannot create {key}.' + ) objects_by_domain[anatomy_id][prefix] = ( entry[key].replace('../', '').replace('./', '') ) From 93666b348297bd5c6d2ed6224aeb761d5e580791 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Fri, 1 Sep 2023 12:22:57 -0400 Subject: [PATCH 04/26] Fix prefix parsing again --- swcc/swcc/models/project.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/swcc/swcc/models/project.py b/swcc/swcc/models/project.py index c9afc6c3..c5e77b1c 100644 --- a/swcc/swcc/models/project.py +++ b/swcc/swcc/models/project.py @@ -5,6 +5,7 @@ from tempfile import TemporaryDirectory import requests +import warnings try: from typing import Any, Dict, Iterator, Literal, Optional, Union @@ -102,15 +103,19 @@ def interpret_data(self, input_data): prefixes = [p for p in expected_key_prefixes if key.startswith(p)] if len(prefixes) > 0: prefix = prefixes[0] - anatomy_id = 'anatomy' + key.replace(prefix, '').replace('_file', '') + anatomy_id = 'anatomy' + key + anatomy_id = ( + anatomy_id.replace(prefix, '') + .replace('_file', '') + .replace('_particles', '') + ) # Only create a new domain object if a shape exists for that suffix if anatomy_id not in objects_by_domain: if prefix in required_key_prefixes: objects_by_domain[anatomy_id] = {} else: - raise ValueError( - f'No shape exists for {anatomy_id}. Cannot create {key}.' - ) + warnings.warn(f'No shape exists for {anatomy_id}. Cannot create {key}.') + continue objects_by_domain[anatomy_id][prefix] = ( entry[key].replace('../', '').replace('./', '') ) From ed4e0a27b8a31fe3c20ae3ce8f836dbd2ad9827c Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Fri, 1 Sep 2023 12:51:36 -0400 Subject: [PATCH 05/26] Fix lint & type tests --- swcc/swcc/models/project.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/swcc/swcc/models/project.py b/swcc/swcc/models/project.py index c5e77b1c..bb77cfda 100644 --- a/swcc/swcc/models/project.py +++ b/swcc/swcc/models/project.py @@ -8,12 +8,13 @@ import warnings try: - from typing import Any, Dict, Iterator, Literal, Optional, Union + from typing import Any, Dict, Iterator, Literal, List, Optional, Union except ImportError: from typing import ( Any, Dict, Iterator, + List, Optional, Union, ) @@ -98,7 +99,7 @@ def interpret_data(self, input_data): name=entry.get('name'), groups=groups_dict, dataset=self.project.dataset ).create() - objects_by_domain = {} + objects_by_domain: Dict[str, List[Dict]] = {} for key in entry.keys(): prefixes = [p for p in expected_key_prefixes if key.startswith(p)] if len(prefixes) > 0: From e43c4f3f25b183a9c02a0c93214c381285de3f26 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Sat, 9 Sep 2023 12:26:23 -0400 Subject: [PATCH 06/26] Remove old layer upon spawning a task rerun --- web/shapeworks/src/store/methods.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/web/shapeworks/src/store/methods.ts b/web/shapeworks/src/store/methods.ts index 69061952..4dc376e1 100644 --- a/web/shapeworks/src/store/methods.ts +++ b/web/shapeworks/src/store/methods.ts @@ -141,10 +141,19 @@ export async function spawnJob(action: string, payload: Record): Pr if (!projectId) return undefined switch (action) { case 'groom': + layersShown.value = layersShown.value.filter( + (l) => l !== 'Groomed' + ) return (await groomProject(projectId, payload))?.data case 'optimize': + layersShown.value = layersShown.value.filter( + (l) => l !== 'Particles' + ) return (await optimizeProject(projectId, payload))?.data case 'analyze': + layersShown.value = layersShown.value.filter( + (l) => l !== 'Reconstructed' + ) return (await analyzeProject(projectId, payload as AnalysisParams))?.data default: break; @@ -224,7 +233,9 @@ export async function fetchJobResults(taskName: string) { ([cachedLabel]) => !cachedLabel.includes(layerName) ) ) - if (!layersShown.value.includes(layerName)) layersShown.value.push(layerName) + if (!layersShown.value.includes(layerName)) { + layersShown.value = [...layersShown.value, layerName] + } } } From 1a309e0f64e8c50d7a2153fb823337d1e2b19a20 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Sun, 10 Sep 2023 04:16:59 -0400 Subject: [PATCH 07/26] Fix missing particle files after grooming (prevent cascading deletions) --- .../0035_protect_from_cascading_deletion.py | 56 +++++++++++++++++++ shapeworks_cloud/core/models.py | 30 +++++++--- 2 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 shapeworks_cloud/core/migrations/0035_protect_from_cascading_deletion.py diff --git a/shapeworks_cloud/core/migrations/0035_protect_from_cascading_deletion.py b/shapeworks_cloud/core/migrations/0035_protect_from_cascading_deletion.py new file mode 100644 index 00000000..2f425a89 --- /dev/null +++ b/shapeworks_cloud/core/migrations/0035_protect_from_cascading_deletion.py @@ -0,0 +1,56 @@ +# Generated by Django 3.2.20 on 2023-09-10 04:58 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0034_good_bad_particles'), + ] + + operations = [ + migrations.AlterField( + model_name='constraints', + name='optimized_particles', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='constraints', to='core.optimizedparticles'), + ), + migrations.AlterField( + model_name='dataset', + name='creator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='groomedmesh', + name='mesh', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='groomed', to='core.mesh'), + ), + migrations.AlterField( + model_name='groomedsegmentation', + name='segmentation', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='groomed', to='core.segmentation'), + ), + migrations.AlterField( + model_name='optimizedparticles', + name='groomed_mesh', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.groomedmesh'), + ), + migrations.AlterField( + model_name='optimizedparticles', + name='groomed_segmentation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.groomedsegmentation'), + ), + migrations.AlterField( + model_name='project', + name='creator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='reconstructedsample', + name='particles', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reconstructed_samples', to='core.optimizedparticles'), + ), + ] diff --git a/shapeworks_cloud/core/models.py b/shapeworks_cloud/core/models.py index 6f638e3e..45d43273 100644 --- a/shapeworks_cloud/core/models.py +++ b/shapeworks_cloud/core/models.py @@ -10,7 +10,7 @@ class Dataset(TimeStampedModel, models.Model): name = models.CharField(max_length=255, unique=True) private = models.BooleanField(default=False) - creator = models.ForeignKey(User, on_delete=models.PROTECT, null=True) + creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) thumbnail = S3FileField(null=True, blank=True) license = models.TextField() description = models.TextField() @@ -109,13 +109,17 @@ class Project(TimeStampedModel, models.Model): name = models.CharField(max_length=255) private = models.BooleanField(default=False) readonly = models.BooleanField(default=False) - creator = models.ForeignKey(User, on_delete=models.PROTECT, null=True) + creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) thumbnail = S3FileField(null=True, blank=True) keywords = models.CharField(max_length=255, blank=True, default='') description = models.TextField(blank=True, default='') dataset = models.ForeignKey(Dataset, on_delete=models.CASCADE, related_name='projects') - last_cached_analysis = models.ForeignKey(CachedAnalysis, on_delete=models.SET_NULL, null=True) landmarks_info = models.JSONField(default=list, null=True) + last_cached_analysis = models.ForeignKey( + CachedAnalysis, + on_delete=models.SET_NULL, + null=True, + ) def create_new_file(self): file_contents = { @@ -194,8 +198,9 @@ class GroomedSegmentation(TimeStampedModel, models.Model): segmentation = models.ForeignKey( Segmentation, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, related_name='groomed', + null=True, ) project = models.ForeignKey( @@ -213,8 +218,9 @@ class GroomedMesh(TimeStampedModel, models.Model): mesh = models.ForeignKey( Mesh, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, related_name='groomed', + null=True, ) project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='groomed_meshes') @@ -232,14 +238,14 @@ class OptimizedParticles(TimeStampedModel, models.Model): groomed_segmentation = models.ForeignKey( GroomedSegmentation, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, related_name='+', blank=True, null=True, ) groomed_mesh = models.ForeignKey( GroomedMesh, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, related_name='+', blank=True, null=True, @@ -260,7 +266,10 @@ class Constraints(TimeStampedModel, models.Model): subject = models.ForeignKey(Subject, on_delete=models.CASCADE, related_name='constraints') anatomy_type = models.CharField(max_length=255) optimized_particles = models.ForeignKey( - OptimizedParticles, on_delete=models.CASCADE, related_name='constraints', null=True + OptimizedParticles, + on_delete=models.SET_NULL, + related_name='constraints', + null=True, ) @@ -270,7 +279,10 @@ class ReconstructedSample(TimeStampedModel, models.Model): Project, on_delete=models.CASCADE, related_name='reconstructed_samples' ) particles = models.ForeignKey( - OptimizedParticles, on_delete=models.CASCADE, related_name='reconstructed_samples' + OptimizedParticles, + on_delete=models.SET_NULL, + related_name='reconstructed_samples', + null=True, ) From 0f27136fceef2509236b807870e6af044f4c1d57 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Sun, 10 Sep 2023 04:38:41 -0400 Subject: [PATCH 08/26] Fix number of particles not working on multi domain cases --- shapeworks_cloud/core/tasks.py | 59 ++++++++++++++++------------------ swcc/swcc/models/project.py | 4 +++ swcc/swcc/models/subject.py | 9 ++++++ 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/shapeworks_cloud/core/tasks.py b/shapeworks_cloud/core/tasks.py index a72bd665..98684f9b 100644 --- a/shapeworks_cloud/core/tasks.py +++ b/shapeworks_cloud/core/tasks.py @@ -10,7 +10,6 @@ from django.contrib.auth.models import User from django.core.files.base import ContentFile from django.db.models import Q -import pandas from rest_framework.authtoken.models import Token from shapeworks_cloud.core import models @@ -28,10 +27,9 @@ def parse_progress(xml_string): return 0 -def edit_swproj_section(filename, section_name, new_df): +def edit_swproj_section(filename, section_name, new_contents): with open(filename, 'r') as f: data = json.load(f) - new_contents = {item['key']: item['value'] for item in new_df.to_dict(orient='records')} if section_name == 'groom': data[section_name] = {} data[section_name]['shape'] = new_contents @@ -48,28 +46,28 @@ def edit_swproj_section(filename, section_name, new_df): json.dump(data, f) -def interpret_form_df(df, command): - if command == 'groom' and df['key'].str.contains('anisotropic_').any(): - # consolidate anisotropic values to one row - anisotropic_values = { - axis: str(df.loc[df['key'] == 'anisotropic_' + axis].iloc[0]['value']) - for axis in ['x', 'y', 'z'] - } - df_filter = df['key'].map(lambda key: 'anisotropic_' not in key) - df = df[df_filter] - return pandas.concat( - [ - df, - pandas.DataFrame.from_dict( - { - 'key': ['spacing'], - 'value': [' '.join(anisotropic_values.values())], - } - ), - ] - ) - else: - return df +def interpret_form_data(data, command, swcc_project): + anisotropic_values = [] + del_keys = [] + for key, value in data.items(): + if 'anisotropic' in key: + anisotropic_values.append(value) + del_keys.append(key) + + for del_key in del_keys: + del data[del_key] + + if command == 'groom' and len(anisotropic_values) > 0: + data['spacing'] = ' '.join(anisotropic_values) + elif command == 'optimize': + num_particles = data.get('number_of_particles') + if num_particles: + max_num_domains = max(s.num_domains for s in swcc_project.subjects) + data['number_of_particles'] = " ".join( + str(num_particles) for i in range(max_num_domains) + ) + + return data def run_shapeworks_command( @@ -93,22 +91,19 @@ def run_shapeworks_command( session.set_token(token.key) project = models.Project.objects.get(id=project_id) project_filename = project.file.name.split('/')[-1] - SWCCProject.from_id(project.id).download(download_dir) + swcc_project = SWCCProject.from_id(project.id) + swcc_project.download(download_dir) pre_command_function() progress.update_percentage(10) if form_data: # write the form data to the project file - form_df = pandas.DataFrame( - list(form_data.items()), - columns=['key', 'value'], - ) - form_df = interpret_form_df(form_df, command) + form_data = interpret_form_data(form_data, command, swcc_project) edit_swproj_section( Path(download_dir, project_filename), command, - form_df, + form_data, ) # perform command diff --git a/swcc/swcc/models/project.py b/swcc/swcc/models/project.py index bb77cfda..f5e7de40 100644 --- a/swcc/swcc/models/project.py +++ b/swcc/swcc/models/project.py @@ -326,6 +326,10 @@ class Project(ApiModel): def get_file_io(self): return ProjectFileIO(project=self) + @property + def subjects(self) -> Iterator[Subject]: + return Subject.list(project=self) + @property def groomed_segmentations(self) -> Iterator[GroomedSegmentation]: self.assert_remote() diff --git a/swcc/swcc/models/subject.py b/swcc/swcc/models/subject.py index 1fc3a18f..a22dca08 100644 --- a/swcc/swcc/models/subject.py +++ b/swcc/swcc/models/subject.py @@ -14,6 +14,15 @@ class Subject(ApiModel): dataset: Dataset groups: Optional[Dict[str, str]] + @property + def num_domains(self) -> int: + return ( + len(list(self.segmentations)) + + len(list(self.meshes)) + + len(list(self.contours)) + + len(list(self.images)) + ) + @property def segmentations(self) -> Iterator[Segmentation]: return Segmentation.list(subject=self) From 3432b00710c1d91d914d72b6a0c1c9a50d151374 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Sun, 10 Sep 2023 05:14:05 -0400 Subject: [PATCH 09/26] Lint / Type fixes --- .../0035_protect_from_cascading_deletion.py | 53 +++++++++++++++---- shapeworks_cloud/core/models.py | 2 + shapeworks_cloud/core/tasks.py | 6 +-- swcc/swcc/models/project.py | 12 +++-- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/shapeworks_cloud/core/migrations/0035_protect_from_cascading_deletion.py b/shapeworks_cloud/core/migrations/0035_protect_from_cascading_deletion.py index 2f425a89..6f3da210 100644 --- a/shapeworks_cloud/core/migrations/0035_protect_from_cascading_deletion.py +++ b/shapeworks_cloud/core/migrations/0035_protect_from_cascading_deletion.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('core', '0034_good_bad_particles'), @@ -16,41 +15,77 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='constraints', name='optimized_particles', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='constraints', to='core.optimizedparticles'), + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='constraints', + to='core.optimizedparticles', + ), ), migrations.AlterField( model_name='dataset', name='creator', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( model_name='groomedmesh', name='mesh', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='groomed', to='core.mesh'), + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='groomed', + to='core.mesh', + ), ), migrations.AlterField( model_name='groomedsegmentation', name='segmentation', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='groomed', to='core.segmentation'), + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='groomed', + to='core.segmentation', + ), ), migrations.AlterField( model_name='optimizedparticles', name='groomed_mesh', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.groomedmesh'), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='+', + to='core.groomedmesh', + ), ), migrations.AlterField( model_name='optimizedparticles', name='groomed_segmentation', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.groomedsegmentation'), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='+', + to='core.groomedsegmentation', + ), ), migrations.AlterField( model_name='project', name='creator', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( model_name='reconstructedsample', name='particles', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reconstructed_samples', to='core.optimizedparticles'), + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='reconstructed_samples', + to='core.optimizedparticles', + ), ), ] diff --git a/shapeworks_cloud/core/models.py b/shapeworks_cloud/core/models.py index 45d43273..5b615bfc 100644 --- a/shapeworks_cloud/core/models.py +++ b/shapeworks_cloud/core/models.py @@ -155,12 +155,14 @@ def get_download_paths(self): 'groomed': [ (gm.mesh.anatomy_type, gm.file) for gm in GroomedMesh.objects.filter(project=self, mesh__subject=subject) + if gm.mesh ] + [ (gs.segmentation.anatomy_type, gs.file) for gs in GroomedSegmentation.objects.filter( project=self, segmentation__subject=subject ) + if gs.segmentation ], 'local': [(p.anatomy_type, p.local) for p in particles], 'world': [(p.anatomy_type, p.world) for p in particles], diff --git a/shapeworks_cloud/core/tasks.py b/shapeworks_cloud/core/tasks.py index 98684f9b..af217256 100644 --- a/shapeworks_cloud/core/tasks.py +++ b/shapeworks_cloud/core/tasks.py @@ -63,7 +63,7 @@ def interpret_form_data(data, command, swcc_project): num_particles = data.get('number_of_particles') if num_particles: max_num_domains = max(s.num_domains for s in swcc_project.subjects) - data['number_of_particles'] = " ".join( + data['number_of_particles'] = ' '.join( str(num_particles) for i in range(max_num_domains) ) @@ -254,9 +254,9 @@ def post_command_function(project, download_dir, result_data, project_filename): target_mesh = project_groomed_meshes.filter( file__endswith=groomed_filename, ).first() - if target_mesh: + if target_mesh and target_mesh.mesh: subject = target_mesh.mesh.subject - elif target_segmentation: + elif target_segmentation and target_segmentation.segmentation: subject = target_segmentation.segmentation.subject result_particles_object = models.OptimizedParticles.objects.create( groomed_segmentation=target_segmentation, diff --git a/swcc/swcc/models/project.py b/swcc/swcc/models/project.py index f5e7de40..fe652f3f 100644 --- a/swcc/swcc/models/project.py +++ b/swcc/swcc/models/project.py @@ -3,18 +3,17 @@ import json from pathlib import Path from tempfile import TemporaryDirectory +import warnings import requests -import warnings try: - from typing import Any, Dict, Iterator, Literal, List, Optional, Union + from typing import Any, Dict, Iterator, Literal, Optional, Union except ImportError: from typing import ( Any, Dict, Iterator, - List, Optional, Union, ) @@ -99,7 +98,7 @@ def interpret_data(self, input_data): name=entry.get('name'), groups=groups_dict, dataset=self.project.dataset ).create() - objects_by_domain: Dict[str, List[Dict]] = {} + objects_by_domain: Dict[str, Dict] = {} for key in entry.keys(): prefixes = [p for p in expected_key_prefixes if key.startswith(p)] if len(prefixes) > 0: @@ -115,7 +114,10 @@ def interpret_data(self, input_data): if prefix in required_key_prefixes: objects_by_domain[anatomy_id] = {} else: - warnings.warn(f'No shape exists for {anatomy_id}. Cannot create {key}.') + warnings.warn( + f'No shape exists for {anatomy_id}. Cannot create {key}.', + stacklevel=2, + ) continue objects_by_domain[anatomy_id][prefix] = ( entry[key].replace('../', '').replace('./', '') From 279d23584406553f831eb7bf6dd67128e30f07cf Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Thu, 14 Sep 2023 16:31:00 -0400 Subject: [PATCH 10/26] Fix only one ReconstructedSample appearing; both were created but were associated with the same particles object --- shapeworks_cloud/core/tasks.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shapeworks_cloud/core/tasks.py b/shapeworks_cloud/core/tasks.py index af217256..a888a161 100644 --- a/shapeworks_cloud/core/tasks.py +++ b/shapeworks_cloud/core/tasks.py @@ -320,16 +320,16 @@ def post_command_function(project, download_dir, result_data, project_filename): project_data = json.load(pf)['data'] for i, sample in enumerate(project_data): reconstructed_filenames = result_data['reconstructed_samples'][i] - particles = ( - models.OptimizedParticles.objects.filter(project=project) - .filter( + subject_particles = list( + models.OptimizedParticles.objects.filter(project=project).filter( Q(groomed_mesh__mesh__subject__name=sample['name']) | Q(groomed_segmentation__segmentation__subject__name=sample['name']) ) - .first() ) - for reconstructed_filename in reconstructed_filenames: - reconstructed = models.ReconstructedSample(project=project, particles=particles) + for j, reconstructed_filename in enumerate(reconstructed_filenames): + reconstructed = models.ReconstructedSample( + project=project, particles=subject_particles[j] + ) reconstructed.file.save( reconstructed_filename, open(Path(download_dir, reconstructed_filename), 'rb'), From d13a613170c7f9e81c9ee7d5f1fe259ae0383dcb Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Fri, 22 Sep 2023 14:38:33 -0400 Subject: [PATCH 11/26] Move Subject `num_domains` to serializer instead of SWCC representation Co-authored-by: Jake Wagoner --- shapeworks_cloud/core/models.py | 4 ++-- shapeworks_cloud/core/serializers.py | 10 ++++++++++ swcc/swcc/models/subject.py | 10 +--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/shapeworks_cloud/core/models.py b/shapeworks_cloud/core/models.py index 5b615bfc..f523a4c4 100644 --- a/shapeworks_cloud/core/models.py +++ b/shapeworks_cloud/core/models.py @@ -164,8 +164,8 @@ def get_download_paths(self): ) if gs.segmentation ], - 'local': [(p.anatomy_type, p.local) for p in particles], - 'world': [(p.anatomy_type, p.world) for p in particles], + 'local': [(p.anatomy_type, p.local) for p in particles if p.local], + 'world': [(p.anatomy_type, p.world) for p in particles if p.world], } related_files['shape'] = ( related_files['mesh'] diff --git a/shapeworks_cloud/core/serializers.py b/shapeworks_cloud/core/serializers.py index cff33cd1..c59798bf 100644 --- a/shapeworks_cloud/core/serializers.py +++ b/shapeworks_cloud/core/serializers.py @@ -112,6 +112,16 @@ class Meta: class SubjectSerializer(serializers.ModelSerializer): + num_domains = serializers.SerializerMethodField('get_num_domains') + + def get_num_domains(self, obj): + return ( + len(list(obj.segmentations.all())) + + len(list(obj.meshes.all())) + + len(list(obj.contours.all())) + + len(list(obj.images.all())) + ) + class Meta: model = models.Subject fields = '__all__' diff --git a/swcc/swcc/models/subject.py b/swcc/swcc/models/subject.py index a22dca08..12a7d979 100644 --- a/swcc/swcc/models/subject.py +++ b/swcc/swcc/models/subject.py @@ -13,15 +13,7 @@ class Subject(ApiModel): name: NonEmptyString dataset: Dataset groups: Optional[Dict[str, str]] - - @property - def num_domains(self) -> int: - return ( - len(list(self.segmentations)) - + len(list(self.meshes)) - + len(list(self.contours)) - + len(list(self.images)) - ) + num_domains: int @property def segmentations(self) -> Iterator[Segmentation]: From 07bf5ae504c27ce6f896196b03b2b766e1882300 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Fri, 22 Sep 2023 14:50:32 -0400 Subject: [PATCH 12/26] Add reset function for web client state --- web/shapeworks/src/store/methods.ts | 43 +++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/web/shapeworks/src/store/methods.ts b/web/shapeworks/src/store/methods.ts index 4dc376e1..35882f85 100644 --- a/web/shapeworks/src/store/methods.ts +++ b/web/shapeworks/src/store/methods.ts @@ -18,7 +18,17 @@ import { meanAnalysisFileParticles, allDatasets, goodBadAngles, - landmarkColorList, landmarkInfo, + landmarkColorList, + landmarkInfo, + analysisExpandedTab, + selectedDataObjects, + particleSize, + analysisFileShown, + currentAnalysisFileParticles, + goodBadMaxAngle, + showDifferenceFromMeanMode, + showGoodBadParticlesMode, + analysisAnimate, } from "."; import imageReader from "@/reader/image"; import pointsReader from "@/reader/points"; @@ -39,6 +49,32 @@ import { layers, COLORS } from "./constants"; import { getDistance, hexToRgb } from "@/helper"; import router from "@/router"; +export const resetState = () => { + selectedDataObjects.value = []; + layersShown.value = ['Original']; + landmarkInfo.value = undefined; + landmarkColorList.value = []; + currentTasks.value = {}; + jobProgressPoll.value = undefined; + particleSize.value = 2; + analysis.value = undefined; + analysisExpandedTab.value = 0; + analysisFileShown.value = undefined; + currentAnalysisFileParticles.value = undefined; + meanAnalysisFileParticles.value = undefined; + goodBadAngles.value = undefined; + goodBadMaxAngle.value = 45; + showGoodBadParticlesMode.value = false; + showDifferenceFromMeanMode.value = false; + cachedMarchingCubes.value = {}; + cachedParticleComparisonVectors.value = {}; + cachedParticleComparisonColors.value = {}; + landmarkInfo.value = undefined; + landmarkColorList.value = []; + analysisExpandedTab.value = 0; + analysisAnimate.value = false; +} + export const loadDataset = async (datasetId: number) => { // Only reload if something has changed if (selectedDataset.value?.id != datasetId) { @@ -79,10 +115,10 @@ export const loadProjectsForDataset = async (datasetId: number) => { export const selectProject = (projectId: number | undefined) => { if (projectId) { + resetState(); selectedProject.value = allProjectsForDataset.value.find( (project: Project) => project.id == projectId, ) - layersShown.value = ["Original"] getLandmarks(); } } @@ -233,7 +269,8 @@ export async function fetchJobResults(taskName: string) { ([cachedLabel]) => !cachedLabel.includes(layerName) ) ) - if (!layersShown.value.includes(layerName)) { + const layer = layers.value.find((l) => l.name === layerName) + if (layer?.available() && !layersShown.value.includes(layerName)) { layersShown.value = [...layersShown.value, layerName] } } From f03a6537cb465fc053f4808622268097a65e4b29 Mon Sep 17 00:00:00 2001 From: Jake Wagoner Date: Mon, 25 Sep 2023 14:51:36 -0600 Subject: [PATCH 13/26] Fix groom and optimization not saving --- swcc/swcc/models/constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/swcc/swcc/models/constants.py b/swcc/swcc/models/constants.py index 98460c10..08c6cd88 100644 --- a/swcc/swcc/models/constants.py +++ b/swcc/swcc/models/constants.py @@ -12,4 +12,8 @@ 'segmentation', 'contour', 'image', + 'groomed', + 'local', + 'world', + 'alignment', ] From 6f8a7671433fbc287de773dbc1bcd909049912c9 Mon Sep 17 00:00:00 2001 From: Jake Wagoner Date: Mon, 25 Sep 2023 14:52:07 -0600 Subject: [PATCH 14/26] Update analysis mean shape to be list for multidomain --- .../migrations/0036_analysis_multi_domain.py | 35 +++++++++++++++++++ shapeworks_cloud/core/models.py | 8 +++-- shapeworks_cloud/core/rest.py | 5 +++ shapeworks_cloud/core/serializers.py | 7 ++++ shapeworks_cloud/core/signals.py | 11 +++++- shapeworks_cloud/core/tasks.py | 2 ++ shapeworks_cloud/urls.py | 5 +++ swcc/swcc/models/other_models.py | 10 ++++-- swcc/swcc/models/project.py | 29 +++++++++------ .../src/components/Analysis/AnalysisTab.vue | 2 -- .../src/components/Analysis/Groups.vue | 2 +- .../src/components/Analysis/PCA.vue | 11 +++--- web/shapeworks/src/store/index.ts | 6 ++-- web/shapeworks/src/store/methods.ts | 6 ++-- web/shapeworks/src/types/index.ts | 8 +++-- web/shapeworks/src/views/Main.vue | 20 +++++------ 16 files changed, 125 insertions(+), 42 deletions(-) create mode 100644 shapeworks_cloud/core/migrations/0036_analysis_multi_domain.py diff --git a/shapeworks_cloud/core/migrations/0036_analysis_multi_domain.py b/shapeworks_cloud/core/migrations/0036_analysis_multi_domain.py new file mode 100644 index 00000000..0819d1ef --- /dev/null +++ b/shapeworks_cloud/core/migrations/0036_analysis_multi_domain.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.21 on 2023-09-25 20:42 + +from django.db import migrations, models +import s3_file_field.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0035_protect_from_cascading_deletion'), + ] + + operations = [ + migrations.CreateModel( + name='CachedAnalysisMeanShape', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', s3_file_field.fields.S3FileField()), + ('particles', s3_file_field.fields.S3FileField(null=True)), + ], + ), + migrations.RemoveField( + model_name='cachedanalysis', + name='mean_particles', + ), + migrations.RemoveField( + model_name='cachedanalysis', + name='mean_shape', + ), + migrations.AddField( + model_name='cachedanalysis', + name='mean_shapes', + field=models.ManyToManyField(to='core.CachedAnalysisMeanShape'), + ), + ] diff --git a/shapeworks_cloud/core/models.py b/shapeworks_cloud/core/models.py index f523a4c4..d410e945 100644 --- a/shapeworks_cloud/core/models.py +++ b/shapeworks_cloud/core/models.py @@ -95,9 +95,13 @@ class CachedAnalysisMode(models.Model): pca_values = models.ManyToManyField(CachedAnalysisModePCA) +class CachedAnalysisMeanShape(models.Model): + file = S3FileField() + particles = S3FileField(null=True) + + class CachedAnalysis(TimeStampedModel, models.Model): - mean_shape = S3FileField() - mean_particles = S3FileField(null=True) + mean_shapes = models.ManyToManyField(CachedAnalysisMeanShape) modes = models.ManyToManyField(CachedAnalysisMode) charts = models.JSONField() groups = models.ManyToManyField(CachedAnalysisGroup, blank=True) diff --git a/shapeworks_cloud/core/rest.py b/shapeworks_cloud/core/rest.py index 81dbf32d..7e6011fe 100644 --- a/shapeworks_cloud/core/rest.py +++ b/shapeworks_cloud/core/rest.py @@ -460,6 +460,11 @@ class CachedAnalysisGroupViewSet(BaseViewSet): serializer_class = serializers.CachedAnalysisGroupSerializer +class CachedAnalysisMeanShapeViewSet(BaseViewSet): + queryset = models.CachedAnalysisMeanShape.objects.all() + serializer_class = serializers.CachedAnalysisMeanShapeSerializer + + class ReconstructedSampleViewSet( GenericViewSet, mixins.ListModelMixin, diff --git a/shapeworks_cloud/core/serializers.py b/shapeworks_cloud/core/serializers.py index c59798bf..aff6859f 100644 --- a/shapeworks_cloud/core/serializers.py +++ b/shapeworks_cloud/core/serializers.py @@ -18,6 +18,12 @@ class Meta: fields = '__all__' +class CachedAnalysisMeanShapeSerializer(serializers.ModelSerializer): + class Meta: + model = models.CachedAnalysisMeanShape + fields = '__all__' + + class CachedAnalysisModePCASerializer(serializers.ModelSerializer): class Meta: model = models.CachedAnalysisModePCA @@ -55,6 +61,7 @@ class Meta: class CachedAnalysisReadSerializer(serializers.ModelSerializer): modes = CachedAnalysisModeReadSerializer(many=True) groups = CachedAnalysisGroupSerializer(many=True) + mean_shapes = CachedAnalysisMeanShapeSerializer(many=True) class Meta: model = models.CachedAnalysis diff --git a/shapeworks_cloud/core/signals.py b/shapeworks_cloud/core/signals.py index 27e6259c..c7b4733b 100644 --- a/shapeworks_cloud/core/signals.py +++ b/shapeworks_cloud/core/signals.py @@ -1,7 +1,14 @@ from django.db.models.signals import pre_delete from django.dispatch import receiver -from .models import CachedAnalysis, CachedAnalysisMode, CachedAnalysisModePCA, Project +from .models import ( + CachedAnalysis, + CachedAnalysisGroup, + CachedAnalysisMeanShape, + CachedAnalysisMode, + CachedAnalysisModePCA, + Project, +) @receiver(pre_delete, sender=Project) @@ -11,3 +18,5 @@ def delete_cached_analysis(sender, instance, using, **kwargs): ).delete() CachedAnalysisMode.objects.filter(cachedanalysis__project=instance).delete() CachedAnalysis.objects.filter(project=instance).delete() + CachedAnalysisGroup.objects.filter(project=instance).delete() + CachedAnalysisMeanShape.objects.filter(project=instance).delete() diff --git a/shapeworks_cloud/core/tasks.py b/shapeworks_cloud/core/tasks.py index a888a161..bc7ab183 100644 --- a/shapeworks_cloud/core/tasks.py +++ b/shapeworks_cloud/core/tasks.py @@ -306,6 +306,8 @@ def pre_command_function(): cachedanalysismode__cachedanalysis__project=project ).delete() models.CachedAnalysisMode.objects.filter(cachedanalysis__project=project).delete() + models.CachedAnalysisGroup.objects.filter(cachedanalysis__project=project).delete() + models.CachedAnalysisMeanShape.objects.filter(cachedanalysis__project=project).delete() models.CachedAnalysis.objects.filter(project=project).delete() def post_command_function(project, download_dir, result_data, project_filename): diff --git a/shapeworks_cloud/urls.py b/shapeworks_cloud/urls.py index 51344c9a..23daeb07 100644 --- a/shapeworks_cloud/urls.py +++ b/shapeworks_cloud/urls.py @@ -41,6 +41,11 @@ rest.CachedAnalysisGroupViewSet, basename='cached_analysis_group', ) +router.register( + 'cached-analysis-mean-shape', + rest.CachedAnalysisMeanShapeViewSet, + basename='cached_analysis_mean_shape', +) router.register( 'reconstructed-samples', rest.ReconstructedSampleViewSet, basename='reconstructed_sample' ) diff --git a/swcc/swcc/models/other_models.py b/swcc/swcc/models/other_models.py index b6b3ab95..f35d9f50 100644 --- a/swcc/swcc/models/other_models.py +++ b/swcc/swcc/models/other_models.py @@ -131,11 +131,17 @@ class CachedAnalysisMode(ApiModel): pca_values: List[CachedAnalysisModePCA] +class CachedAnalysisMeanShape(ApiModel): + _endpoint = 'cached-analysis-mean-shape' + + file: FileType[Literal['core.CachedAnalysisMeanShape.file']] + particles: FileType[Literal['core.CachedAnalysisMeanShape.particles']] + + class CachedAnalysis(ApiModel): _endpoint = 'cached-analysis' - mean_shape: FileType[Literal['core.CachedAnalysis.mean_shape']] - mean_particles: FileType[Literal['core.CachedAnalysis.mean_particles']] + mean_shapes: List[CachedAnalysisMeanShape] modes: List[CachedAnalysisMode] charts: List[dict] groups: Optional[List[CachedAnalysisGroup]] diff --git a/swcc/swcc/models/project.py b/swcc/swcc/models/project.py index fe652f3f..440f0cb4 100644 --- a/swcc/swcc/models/project.py +++ b/swcc/swcc/models/project.py @@ -33,6 +33,7 @@ CachedAnalysisGroup, CachedAnalysisMode, CachedAnalysisModePCA, + CachedAnalysisMeanShape, Constraints, Contour, GroomedMesh, @@ -241,12 +242,23 @@ def load_analysis_from_json(self, file_path): analysis_file_location = project_root / Path(file_path) contents = json.load(open(analysis_file_location)) if contents['mean'] and contents['mean']['meshes']: - mean_shape_path = contents['mean']['meshes'][0] - mean_particles_path = None + mean_shapes_cache = [] + mean_shapes = [] + for mean_shape in contents['mean']['meshes']: + mean_shapes.append(analysis_file_location.parent / Path(mean_shape)) + if 'particle_files' in contents['mean']: - mean_particles_path = contents['mean']['particle_files'][0] - if 'particles' in contents['mean']: - mean_particles_path = contents['mean']['particles'][0] + mean_particles = [] + for mean_particle_path in contents['mean']['particle_files']: + mean_particles.append(analysis_file_location.parent / Path(mean_particle_path)) + + for i in range(len(mean_shapes)): + cams = CachedAnalysisMeanShape( + file=mean_shapes[i], + particles=mean_particles[i] if mean_particles else None, + ).create() + mean_shapes_cache.append(cams) + modes = [] for mode in contents['modes']: pca_values = [] @@ -277,10 +289,6 @@ def load_analysis_from_json(self, file_path): modes.append(cam) if len(modes) > 0: - mean_particles = None - if mean_particles_path: - mean_particles = analysis_file_location.parent / Path(mean_particles_path) - groups_cache = [] if contents['groups']: for group in contents['groups']: @@ -301,8 +309,7 @@ def load_analysis_from_json(self, file_path): groups_cache.append(cag) return CachedAnalysis( - mean_shape=analysis_file_location.parent / Path(mean_shape_path), - mean_particles=mean_particles, + mean_shapes=mean_shapes_cache, modes=modes, charts=contents['charts'], groups=groups_cache, diff --git a/web/shapeworks/src/components/Analysis/AnalysisTab.vue b/web/shapeworks/src/components/Analysis/AnalysisTab.vue index c1cc0913..e1d764bd 100644 --- a/web/shapeworks/src/components/Analysis/AnalysisTab.vue +++ b/web/shapeworks/src/components/Analysis/AnalysisTab.vue @@ -1,7 +1,6 @@